feat: Dashboard redesign, plan permissions, and help docs improvements
Major updates including: - Customizable dashboard with drag-and-drop widget grid layout - Plan-based feature locking for plugins and tasks - Comprehensive help documentation updates across all pages - Plugin seeding in deployment process for all tenants - Permission synchronization system for subscription plans - QuotaOverageModal component and enhanced UX flows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# Help Documentation Implementation Plan
|
||||
|
||||
## Overview
|
||||
This plan covers creating comprehensive help documentation for the SmoothSchedule business dashboard, adding contextual help buttons to each page, and creating a monolithic help document.
|
||||
|
||||
## Phase 1: Create Plugin Page First (User Request)
|
||||
|
||||
### Task 1.1: Create CreatePlugin.tsx Page
|
||||
- Create `/frontend/src/pages/CreatePlugin.tsx`
|
||||
- Features:
|
||||
- Name, description, short description fields
|
||||
- Category dropdown (EMAIL, REPORTS, CUSTOMER, BOOKING, INTEGRATION, AUTOMATION, OTHER)
|
||||
- Plugin code editor with syntax highlighting (using same Prism setup as HelpPluginDocs)
|
||||
- Template variables preview (auto-extracted from code)
|
||||
- Version field (default 1.0.0)
|
||||
- Logo URL field (optional)
|
||||
- Save as Private / Submit to Marketplace options
|
||||
- Visibility selector (PRIVATE, PUBLIC)
|
||||
- Uses API endpoint: `POST /api/plugin-templates/`
|
||||
- Plan feature gate: `can_create_plugins`
|
||||
|
||||
### Task 1.2: Add Route for CreatePlugin
|
||||
- Add lazy import: `const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin'));`
|
||||
- Add route: `/plugins/create` pointing to CreatePlugin component
|
||||
|
||||
## Phase 2: Create Reusable HelpButton Component
|
||||
|
||||
### Task 2.1: Create HelpButton Component
|
||||
- Create `/frontend/src/components/HelpButton.tsx`
|
||||
- Props: `helpPath: string` (route to help page)
|
||||
- Renders: HelpCircle icon button at fixed position (top-right of page)
|
||||
- Styling: Circular button with question mark icon, tooltip on hover
|
||||
- Uses Link from react-router-dom to navigate to help page
|
||||
|
||||
## Phase 3: Create Individual Help Pages
|
||||
|
||||
### 3.1 Core Pages Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Dashboard | HelpDashboard.tsx | /help/dashboard |
|
||||
| Scheduler | HelpScheduler.tsx | /help/scheduler |
|
||||
| Tasks | HelpTasks.tsx | /help/tasks |
|
||||
|
||||
### 3.2 Manage Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Customers | HelpCustomers.tsx | /help/customers |
|
||||
| Services | HelpServices.tsx | /help/services |
|
||||
| Resources | HelpResources.tsx | /help/resources |
|
||||
| Staff | HelpStaff.tsx | /help/staff |
|
||||
|
||||
### 3.3 Communicate Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Messages | HelpMessages.tsx | /help/messages |
|
||||
| Tickets | HelpTicketing.tsx (exists) | /help/ticketing |
|
||||
|
||||
### 3.4 Money Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Payments | HelpPayments.tsx | /help/payments |
|
||||
|
||||
### 3.5 Extend Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Plugins | HelpPluginsOverview.tsx | /help/plugins-overview |
|
||||
| Plugin Marketplace | (link to existing HelpPluginDocs) | /help/plugins |
|
||||
| My Plugins | HelpMyPlugins.tsx | /help/my-plugins |
|
||||
| Create Plugin | HelpCreatePlugin.tsx | /help/create-plugin |
|
||||
|
||||
### 3.6 Settings Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| General | HelpSettingsGeneral.tsx | /help/settings/general |
|
||||
| Resource Types | HelpSettingsResourceTypes.tsx | /help/settings/resource-types |
|
||||
| Booking | HelpSettingsBooking.tsx | /help/settings/booking |
|
||||
| Appearance | HelpSettingsAppearance.tsx | /help/settings/appearance |
|
||||
| Email Templates | HelpSettingsEmailTemplates.tsx | /help/settings/email-templates |
|
||||
| Custom Domains | HelpSettingsCustomDomains.tsx | /help/settings/custom-domains |
|
||||
| API & Webhooks | HelpSettingsApi.tsx | /help/settings/api |
|
||||
| Authentication | HelpSettingsAuth.tsx | /help/settings/authentication |
|
||||
| Email Setup | HelpEmailSettings.tsx (exists) | /help/email-settings |
|
||||
| SMS & Calling | HelpSettingsSmsCalling.tsx | /help/settings/sms-calling |
|
||||
| Plan & Billing | HelpSettingsBilling.tsx | /help/settings/billing |
|
||||
| Quota Management | HelpSettingsQuota.tsx | /help/settings/quota |
|
||||
|
||||
## Phase 4: Add HelpButton to Each Page
|
||||
|
||||
Add the HelpButton component to the top-right of each dashboard page, linking to its corresponding help page.
|
||||
|
||||
## Phase 5: Update HelpPluginDocs
|
||||
|
||||
### Task 5.1: Review and Update Plugin Documentation
|
||||
- Verify plugin documentation matches current codebase
|
||||
- Add section for "Creating Custom Plugins"
|
||||
- Add links to API documentation
|
||||
- Ensure examples work with current API
|
||||
|
||||
## Phase 6: Create Monolithic Help Document
|
||||
|
||||
### Task 6.1: Create HelpGuideComplete.tsx
|
||||
- Compile all help content into single comprehensive page
|
||||
- Table of contents with anchor links
|
||||
- Searchable content
|
||||
- Organized by sections (Core, Manage, Communicate, Money, Extend, Settings)
|
||||
|
||||
### Task 6.2: Update HelpGuide.tsx
|
||||
- Replace "Coming Soon" with actual compiled documentation
|
||||
- Or redirect to HelpGuideComplete
|
||||
|
||||
## Phase 7: Register All Routes
|
||||
|
||||
Add all new help page routes to App.tsx in the business dashboard section.
|
||||
|
||||
## Help Page Template Structure
|
||||
|
||||
Each help page should follow this structure:
|
||||
```tsx
|
||||
- Header with icon and title
|
||||
- Overview/Introduction
|
||||
- Key Features section
|
||||
- How to Use section (step-by-step)
|
||||
- Benefits section
|
||||
- Tips & Best Practices
|
||||
- Related Features (links to other help pages)
|
||||
- Need More Help? (link to support/tickets)
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Create CreatePlugin.tsx page and route
|
||||
2. Create HelpButton component
|
||||
3. Create help pages for core pages (Dashboard, Scheduler, Tasks)
|
||||
4. Create help pages for Manage section
|
||||
5. Create help pages for Communicate section
|
||||
6. Create help pages for Money section
|
||||
7. Create help pages for Extend section (including plugin docs update)
|
||||
8. Create help pages for Settings section
|
||||
9. Add HelpButton to all pages
|
||||
10. Create monolithic help document
|
||||
11. Test all help pages and navigation
|
||||
|
||||
## Files to Create
|
||||
|
||||
### New Components
|
||||
- `/frontend/src/components/HelpButton.tsx`
|
||||
|
||||
### New Pages
|
||||
- `/frontend/src/pages/CreatePlugin.tsx`
|
||||
- `/frontend/src/pages/help/HelpDashboard.tsx`
|
||||
- `/frontend/src/pages/help/HelpScheduler.tsx`
|
||||
- `/frontend/src/pages/help/HelpTasks.tsx`
|
||||
- `/frontend/src/pages/help/HelpCustomers.tsx`
|
||||
- `/frontend/src/pages/help/HelpServices.tsx`
|
||||
- `/frontend/src/pages/help/HelpResources.tsx`
|
||||
- `/frontend/src/pages/help/HelpStaff.tsx`
|
||||
- `/frontend/src/pages/help/HelpMessages.tsx`
|
||||
- `/frontend/src/pages/help/HelpPayments.tsx`
|
||||
- `/frontend/src/pages/help/HelpPluginsOverview.tsx`
|
||||
- `/frontend/src/pages/help/HelpMyPlugins.tsx`
|
||||
- `/frontend/src/pages/help/HelpCreatePlugin.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsGeneral.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsResourceTypes.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsBooking.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsAppearance.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsEmailTemplates.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsCustomDomains.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsApi.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsAuth.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsSmsCalling.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsBilling.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsQuota.tsx`
|
||||
- `/frontend/src/pages/help/HelpGuideComplete.tsx`
|
||||
|
||||
### Files to Modify
|
||||
- `/frontend/src/App.tsx` - Add routes
|
||||
- `/frontend/src/pages/HelpPluginDocs.tsx` - Update with current codebase info
|
||||
- `/frontend/src/pages/HelpGuide.tsx` - Replace Coming Soon
|
||||
- All dashboard pages - Add HelpButton component
|
||||
@@ -98,6 +98,17 @@ docker compose -f docker-compose.production.yml exec -T django sh -c 'export DAT
|
||||
echo ">>> Collecting static files..."
|
||||
docker compose -f docker-compose.production.yml exec -T django sh -c 'export DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} && python manage.py collectstatic --noinput'
|
||||
|
||||
echo ">>> Seeding/updating platform plugins for all tenants..."
|
||||
docker compose -f docker-compose.production.yml exec -T django python -c "
|
||||
from django_tenants.utils import get_tenant_model
|
||||
from django.core.management import call_command
|
||||
Tenant = get_tenant_model()
|
||||
for tenant in Tenant.objects.exclude(schema_name='public'):
|
||||
print(f' Seeding plugins for {tenant.schema_name}...')
|
||||
call_command('tenant_command', 'seed_platform_plugins', schema=tenant.schema_name, verbosity=0)
|
||||
print(' Done!')
|
||||
"
|
||||
|
||||
echo ">>> Checking container status..."
|
||||
docker compose -f docker-compose.production.yml ps
|
||||
|
||||
|
||||
Generated
+68
@@ -15,6 +15,7 @@
|
||||
"@stripe/react-stripe-js": "^5.4.1",
|
||||
"@stripe/stripe-js": "^8.5.3",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -25,6 +26,7 @@
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
@@ -1960,6 +1962,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-grid-layout": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.6.tgz",
|
||||
"integrity": "sha512-Cw7+sb3yyjtmxwwJiXtEXcu5h4cgs+sCGkHwHXsFmPyV30bf14LeD/fa2LwQovuD2HWxCcjIdNhDlcYGj95qGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
@@ -2922,6 +2933,12 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
|
||||
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -4376,6 +4393,38 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
||||
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-grid-layout": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.2.tgz",
|
||||
"integrity": "sha512-vT7xmQqszTT+sQw/LfisrEO4le1EPNnSEMVHy6sBZyzS3yGkMywdOd+5iEFFwQwt0NSaGkxuRmYwa1JsP6OJdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"fast-equals": "^4.0.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-resizable": "^3.0.5",
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
@@ -4477,6 +4526,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
|
||||
"integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "15.x",
|
||||
"react-draggable": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||
@@ -4603,6 +4665,12 @@
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@stripe/react-stripe-js": "^5.4.1",
|
||||
"@stripe/stripe-js": "^8.5.3",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -21,6 +22,7 @@
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
|
||||
@@ -65,7 +65,7 @@ const Tickets = React.lazy(() => import('./pages/Tickets')); // Import Tickets p
|
||||
const HelpGuide = React.lazy(() => import('./pages/HelpGuide')); // Import Platform Guide page
|
||||
const HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing
|
||||
const HelpApiDocs = React.lazy(() => import('./pages/HelpApiDocs')); // Import API documentation page
|
||||
const HelpPluginDocs = React.lazy(() => import('./pages/HelpPluginDocs')); // Import Plugin documentation page
|
||||
const HelpPluginDocs = React.lazy(() => import('./pages/help/HelpPluginDocs')); // Import Plugin documentation page
|
||||
const HelpEmailSettings = React.lazy(() => import('./pages/HelpEmailSettings')); // Import Email settings help page
|
||||
|
||||
// Import new help pages
|
||||
@@ -79,7 +79,6 @@ const HelpStaff = React.lazy(() => import('./pages/help/HelpStaff'));
|
||||
const HelpMessages = React.lazy(() => import('./pages/help/HelpMessages'));
|
||||
const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments'));
|
||||
const HelpPlugins = React.lazy(() => import('./pages/help/HelpPlugins'));
|
||||
const HelpCreatePlugin = React.lazy(() => import('./pages/help/HelpCreatePlugin'));
|
||||
const HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral'));
|
||||
const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes'));
|
||||
const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking'));
|
||||
@@ -90,6 +89,7 @@ const HelpSettingsApi = React.lazy(() => import('./pages/help/HelpSettingsApi'))
|
||||
const HelpSettingsAuth = React.lazy(() => import('./pages/help/HelpSettingsAuth'));
|
||||
const HelpSettingsBilling = React.lazy(() => import('./pages/help/HelpSettingsBilling'));
|
||||
const HelpSettingsQuota = React.lazy(() => import('./pages/help/HelpSettingsQuota'));
|
||||
const HelpComprehensive = React.lazy(() => import('./pages/help/HelpComprehensive'));
|
||||
const PlatformSupport = React.lazy(() => import('./pages/PlatformSupport')); // Import Platform Support page (for businesses to contact SmoothSchedule)
|
||||
const PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); // Import Plugin Marketplace page
|
||||
const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins page
|
||||
@@ -613,6 +613,7 @@ const AppContent: React.FC = () => {
|
||||
/>
|
||||
<Route path="/scheduler" element={<Scheduler />} />
|
||||
<Route path="/tickets" element={<Tickets />} />
|
||||
<Route path="/help" element={<HelpComprehensive />} />
|
||||
<Route path="/help/guide" element={<HelpGuide />} />
|
||||
<Route path="/help/ticketing" element={<HelpTicketing />} />
|
||||
<Route path="/help/api" element={<HelpApiDocs />} />
|
||||
@@ -629,7 +630,6 @@ const AppContent: React.FC = () => {
|
||||
<Route path="/help/messages" element={<HelpMessages />} />
|
||||
<Route path="/help/payments" element={<HelpPayments />} />
|
||||
<Route path="/help/plugins" element={<HelpPlugins />} />
|
||||
<Route path="/help/plugins/create" element={<HelpCreatePlugin />} />
|
||||
<Route path="/help/settings/general" element={<HelpSettingsGeneral />} />
|
||||
<Route path="/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
|
||||
<Route path="/help/settings/booking" element={<HelpSettingsBooking />} />
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* QuotaOverageModal Component
|
||||
*
|
||||
* Modal that appears on login/masquerade when the tenant has exceeded quotas.
|
||||
* Shows warning about grace period and what will happen when it expires.
|
||||
* Uses sessionStorage to only show once per session.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
AlertTriangle,
|
||||
X,
|
||||
Clock,
|
||||
Archive,
|
||||
ChevronRight,
|
||||
Users,
|
||||
Layers,
|
||||
Briefcase,
|
||||
Mail,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { QuotaOverage } from '../api/auth';
|
||||
|
||||
interface QuotaOverageModalProps {
|
||||
overages: QuotaOverage[];
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const QUOTA_ICONS: Record<string, React.ReactNode> = {
|
||||
'MAX_ADDITIONAL_USERS': <Users className="w-5 h-5" />,
|
||||
'MAX_RESOURCES': <Layers className="w-5 h-5" />,
|
||||
'MAX_SERVICES': <Briefcase className="w-5 h-5" />,
|
||||
'MAX_EMAIL_TEMPLATES': <Mail className="w-5 h-5" />,
|
||||
'MAX_AUTOMATED_TASKS': <Zap className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const SESSION_STORAGE_KEY = 'quota_overage_modal_dismissed';
|
||||
|
||||
const QuotaOverageModal: React.FC<QuotaOverageModalProps> = ({ overages, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if already dismissed this session
|
||||
const dismissed = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
||||
if (!dismissed && overages && overages.length > 0) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, [overages]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, 'true');
|
||||
setIsVisible(false);
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
if (!isVisible || !overages || overages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the most urgent overage (least days remaining)
|
||||
const mostUrgent = overages.reduce((prev, curr) =>
|
||||
curr.days_remaining < prev.days_remaining ? curr : prev
|
||||
);
|
||||
|
||||
const isCritical = mostUrgent.days_remaining <= 1;
|
||||
const isUrgent = mostUrgent.days_remaining <= 7;
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-lg w-full overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className={`px-6 py-4 ${
|
||||
isCritical
|
||||
? 'bg-red-600'
|
||||
: isUrgent
|
||||
? 'bg-amber-500'
|
||||
: 'bg-amber-100 dark:bg-amber-900/30'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-full ${
|
||||
isCritical || isUrgent
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-200 dark:bg-amber-800'
|
||||
}`}>
|
||||
<AlertTriangle className={`w-6 h-6 ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white'
|
||||
: 'text-amber-700 dark:text-amber-300'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`text-lg font-bold ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white'
|
||||
: 'text-amber-900 dark:text-amber-100'
|
||||
}`}>
|
||||
{isCritical
|
||||
? t('quota.modal.titleCritical', 'Action Required Immediately!')
|
||||
: isUrgent
|
||||
? t('quota.modal.titleUrgent', 'Action Required Soon')
|
||||
: t('quota.modal.title', 'Quota Exceeded')
|
||||
}
|
||||
</h2>
|
||||
<p className={`text-sm ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white/90'
|
||||
: 'text-amber-700 dark:text-amber-200'
|
||||
}`}>
|
||||
{mostUrgent.days_remaining <= 0
|
||||
? t('quota.modal.subtitleExpired', 'Grace period has expired')
|
||||
: mostUrgent.days_remaining === 1
|
||||
? t('quota.modal.subtitleOneDay', '1 day remaining')
|
||||
: t('quota.modal.subtitle', '{{days}} days remaining', { days: mostUrgent.days_remaining })
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isCritical || isUrgent
|
||||
? 'hover:bg-white/20 text-white'
|
||||
: 'hover:bg-amber-200 dark:hover:bg-amber-800 text-amber-700 dark:text-amber-300'
|
||||
}`}
|
||||
aria-label={t('common.close', 'Close')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-5 space-y-5">
|
||||
{/* Main message */}
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-gray-500 dark:text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="font-medium mb-1">
|
||||
{t('quota.modal.gracePeriodEnds', 'Grace period ends on {{date}}', {
|
||||
date: formatDate(mostUrgent.grace_period_ends_at)
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('quota.modal.explanation',
|
||||
'Your account has exceeded its plan limits. Please remove or archive excess items before the grace period ends, or they will be automatically archived.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overage list */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{t('quota.modal.overagesTitle', 'Items Over Quota')}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{overages.map((overage) => (
|
||||
<div
|
||||
key={overage.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${
|
||||
overage.days_remaining <= 1
|
||||
? 'border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20'
|
||||
: overage.days_remaining <= 7
|
||||
? 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
overage.days_remaining <= 1
|
||||
? 'bg-red-100 dark:bg-red-800/50 text-red-600 dark:text-red-400'
|
||||
: overage.days_remaining <= 7
|
||||
? 'bg-amber-100 dark:bg-amber-800/50 text-amber-600 dark:text-amber-400'
|
||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300'
|
||||
}`}>
|
||||
{QUOTA_ICONS[overage.quota_type] || <Layers className="w-5 h-5" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{overage.display_name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('quota.modal.usageInfo', '{{current}} used / {{limit}} allowed', {
|
||||
current: overage.current_usage,
|
||||
limit: overage.allowed_limit
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`font-bold ${
|
||||
overage.days_remaining <= 1
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: overage.days_remaining <= 7
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-gray-600 dark:text-gray-300'
|
||||
}`}>
|
||||
+{overage.overage_amount}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('quota.modal.overLimit', 'over limit')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* What happens section */}
|
||||
<div className="flex items-start gap-3 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<Archive className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">
|
||||
{t('quota.modal.whatHappens', 'What happens if I don\'t take action?')}
|
||||
</p>
|
||||
<p>
|
||||
{t('quota.modal.autoArchiveExplanation',
|
||||
'After the grace period ends, the oldest items over your limit will be automatically archived. Archived items remain in your account but cannot be used until you upgrade or remove other items.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between gap-3">
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
{t('quota.modal.dismissButton', 'Remind Me Later')}
|
||||
</button>
|
||||
<Link
|
||||
to="/settings/quota"
|
||||
onClick={handleDismiss}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{t('quota.modal.manageButton', 'Manage Quota')}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuotaOverageModal;
|
||||
|
||||
/**
|
||||
* Clear the session storage dismissal flag
|
||||
* Call this when user logs out or masquerade changes
|
||||
*/
|
||||
export const resetQuotaOverageModalDismissal = () => {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||
};
|
||||
@@ -119,6 +119,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
icon={Clock}
|
||||
label={t('nav.tasks', 'Tasks')}
|
||||
isCollapsed={isCollapsed}
|
||||
locked={!canUse('plugins') || !canUse('tasks')}
|
||||
/>
|
||||
</SidebarSection>
|
||||
|
||||
@@ -193,7 +194,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
{canViewAdminPages && (
|
||||
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
|
||||
<SidebarItem
|
||||
to="/plugins/marketplace"
|
||||
to="/plugins/my-plugins"
|
||||
icon={Plug}
|
||||
label={t('nav.plugins', 'Plugins')}
|
||||
isCollapsed={isCollapsed}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Users, User } from 'lucide-react';
|
||||
import { Appointment, Resource } from '../../types';
|
||||
import { startOfWeek, endOfWeek, isWithinInterval } from 'date-fns';
|
||||
|
||||
interface CapacityWidgetProps {
|
||||
appointments: Appointment[];
|
||||
resources: Resource[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const CapacityWidget: React.FC<CapacityWidgetProps> = ({
|
||||
appointments,
|
||||
resources,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const capacityData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||
const weekEnd = endOfWeek(now, { weekStartsOn: 1 });
|
||||
|
||||
// Calculate for each resource
|
||||
const resourceStats = resources.map((resource) => {
|
||||
// Filter appointments for this resource this week
|
||||
const resourceAppointments = appointments.filter(
|
||||
(appt) =>
|
||||
appt.resourceId === resource.id &&
|
||||
isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }) &&
|
||||
appt.status !== 'CANCELLED'
|
||||
);
|
||||
|
||||
// Calculate total booked minutes
|
||||
const bookedMinutes = resourceAppointments.reduce(
|
||||
(sum, appt) => sum + appt.durationMinutes,
|
||||
0
|
||||
);
|
||||
|
||||
// Assume 8 hours/day, 5 days/week = 2400 minutes capacity
|
||||
const totalCapacityMinutes = 8 * 60 * 5;
|
||||
const utilization = Math.min((bookedMinutes / totalCapacityMinutes) * 100, 100);
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
utilization: Math.round(utilization),
|
||||
bookedHours: Math.round(bookedMinutes / 60),
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate overall utilization
|
||||
const totalBooked = resourceStats.reduce((sum, r) => sum + r.bookedHours, 0);
|
||||
const totalCapacity = resources.length * 40; // 40 hours/week per resource
|
||||
const overallUtilization = totalCapacity > 0 ? Math.round((totalBooked / totalCapacity) * 100) : 0;
|
||||
|
||||
return {
|
||||
overall: overallUtilization,
|
||||
resources: resourceStats.sort((a, b) => b.utilization - a.utilization),
|
||||
};
|
||||
}, [appointments, resources]);
|
||||
|
||||
const getUtilizationColor = (utilization: number) => {
|
||||
if (utilization >= 80) return 'bg-green-500';
|
||||
if (utilization >= 50) return 'bg-yellow-500';
|
||||
return 'bg-gray-300 dark:bg-gray-600';
|
||||
};
|
||||
|
||||
const getUtilizationTextColor = (utilization: number) => {
|
||||
if (utilization >= 80) return 'text-green-600 dark:text-green-400';
|
||||
if (utilization >= 50) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-gray-500 dark:text-gray-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center justify-between mb-3 ${isEditing ? 'pl-5' : ''}`}>
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
Capacity This Week
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users size={14} className="text-gray-400" />
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{capacityData.overall}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{capacityData.resources.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<Users size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No resources configured</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 grid grid-cols-2 gap-2 auto-rows-min">
|
||||
{capacityData.resources.map((resource) => (
|
||||
<div
|
||||
key={resource.id}
|
||||
className="p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<User size={12} className="text-gray-400 flex-shrink-0" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-300 truncate">
|
||||
{resource.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getUtilizationColor(resource.utilization)} transition-all duration-300`}
|
||||
style={{ width: `${resource.utilization}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold ${getUtilizationTextColor(resource.utilization)}`}>
|
||||
{resource.utilization}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapacityWidget;
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
} from 'recharts';
|
||||
import { GripVertical, X } from 'lucide-react';
|
||||
|
||||
interface ChartData {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ChartWidgetProps {
|
||||
title: string;
|
||||
data: ChartData[];
|
||||
type: 'bar' | 'line';
|
||||
color?: string;
|
||||
valuePrefix?: string;
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const ChartWidget: React.FC<ChartWidgetProps> = ({
|
||||
title,
|
||||
data,
|
||||
type,
|
||||
color = '#3b82f6',
|
||||
valuePrefix = '',
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const formatValue = (value: number) => `${valuePrefix}${value}`;
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{type === 'bar' ? (
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tickFormatter={formatValue} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(107, 114, 128, 0.1)' }}
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
formatter={(value: number) => [formatValue(value), title]}
|
||||
/>
|
||||
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
) : (
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
formatter={(value: number) => [value, title]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="value" stroke={color} strokeWidth={3} dot={{ r: 4, fill: color }} />
|
||||
</LineChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartWidget;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Users, UserPlus, UserCheck } from 'lucide-react';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { Customer } from '../../types';
|
||||
|
||||
interface CustomerBreakdownWidgetProps {
|
||||
customers: Customer[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
|
||||
customers,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const breakdownData = useMemo(() => {
|
||||
// Customers with lastVisit are returning, without are new
|
||||
const returning = customers.filter((c) => c.lastVisit !== null).length;
|
||||
const newCustomers = customers.filter((c) => c.lastVisit === null).length;
|
||||
const total = customers.length;
|
||||
|
||||
return {
|
||||
new: newCustomers,
|
||||
returning,
|
||||
total,
|
||||
newPercentage: total > 0 ? Math.round((newCustomers / total) * 100) : 0,
|
||||
returningPercentage: total > 0 ? Math.round((returning / total) * 100) : 0,
|
||||
chartData: [
|
||||
{ name: 'New', value: newCustomers, color: '#8b5cf6' },
|
||||
{ name: 'Returning', value: returning, color: '#10b981' },
|
||||
],
|
||||
};
|
||||
}, [customers]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-base font-semibold text-gray-900 dark:text-white mb-2 ${isEditing ? 'pl-5' : ''}`}>
|
||||
Customers This Month
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 flex items-center gap-3 min-h-0">
|
||||
{/* Pie Chart */}
|
||||
<div className="w-20 h-20 flex-shrink-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={breakdownData.chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={20}
|
||||
outerRadius={35}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{breakdownData.chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 rounded-lg bg-purple-100 dark:bg-purple-900/30">
|
||||
<UserPlus size={12} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">New</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{breakdownData.new}{' '}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
({breakdownData.newPercentage}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 rounded-lg bg-green-100 dark:bg-green-900/30">
|
||||
<UserCheck size={12} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Returning</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{breakdownData.returning}{' '}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
({breakdownData.returningPercentage}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-gray-100 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
||||
<Users size={12} />
|
||||
<span>Total Customers</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{breakdownData.total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerBreakdownWidget;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp, TrendingDown, Minus, GripVertical, X } from 'lucide-react';
|
||||
|
||||
interface GrowthData {
|
||||
weekly: { value: number; change: number };
|
||||
monthly: { value: number; change: number };
|
||||
}
|
||||
|
||||
interface MetricWidgetProps {
|
||||
title: string;
|
||||
value: number | string;
|
||||
growth: GrowthData;
|
||||
icon?: React.ReactNode;
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const MetricWidget: React.FC<MetricWidgetProps> = ({
|
||||
title,
|
||||
value,
|
||||
growth,
|
||||
icon,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const formatChange = (change: number) => {
|
||||
if (change === 0) return '0%';
|
||||
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const getTrendIcon = (change: number) => {
|
||||
if (change > 0) return <TrendingUp size={12} className="mr-1" />;
|
||||
if (change < 0) return <TrendingDown size={12} className="mr-1" />;
|
||||
return <Minus size={12} className="mr-1" />;
|
||||
};
|
||||
|
||||
const getTrendClass = (change: number) => {
|
||||
if (change > 0) return 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400';
|
||||
if (change < 0) return 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400';
|
||||
return 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={isEditing ? 'pl-5' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{icon && <span className="text-brand-500">{icon}</span>}
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
|
||||
{value}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.weekly.change)}`}>
|
||||
{getTrendIcon(growth.weekly.change)}
|
||||
{formatChange(growth.weekly.change)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.monthly.change)}`}>
|
||||
{getTrendIcon(growth.monthly.change)}
|
||||
{formatChange(growth.monthly.change)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricWidget;
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, UserX, TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
import { Appointment } from '../../types';
|
||||
import { subDays, subMonths, isAfter } from 'date-fns';
|
||||
|
||||
interface NoShowRateWidgetProps {
|
||||
appointments: Appointment[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const NoShowRateWidget: React.FC<NoShowRateWidgetProps> = ({
|
||||
appointments,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const noShowData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = subDays(now, 7);
|
||||
const twoWeeksAgo = subDays(now, 14);
|
||||
const oneMonthAgo = subMonths(now, 1);
|
||||
const twoMonthsAgo = subMonths(now, 2);
|
||||
|
||||
// Calculate rates for different periods
|
||||
const calculateRate = (appts: Appointment[]) => {
|
||||
const completed = appts.filter(
|
||||
(a) => a.status === 'COMPLETED' || a.status === 'NO_SHOW' || a.status === 'CANCELLED'
|
||||
);
|
||||
const noShows = completed.filter((a) => a.status === 'NO_SHOW');
|
||||
return completed.length > 0 ? (noShows.length / completed.length) * 100 : 0;
|
||||
};
|
||||
|
||||
// Current week
|
||||
const thisWeekAppts = appointments.filter((a) => isAfter(new Date(a.startTime), oneWeekAgo));
|
||||
const currentWeekRate = calculateRate(thisWeekAppts);
|
||||
|
||||
// Last week
|
||||
const lastWeekAppts = appointments.filter(
|
||||
(a) => isAfter(new Date(a.startTime), twoWeeksAgo) && !isAfter(new Date(a.startTime), oneWeekAgo)
|
||||
);
|
||||
const lastWeekRate = calculateRate(lastWeekAppts);
|
||||
|
||||
// Current month
|
||||
const thisMonthAppts = appointments.filter((a) => isAfter(new Date(a.startTime), oneMonthAgo));
|
||||
const currentMonthRate = calculateRate(thisMonthAppts);
|
||||
|
||||
// Last month
|
||||
const lastMonthAppts = appointments.filter(
|
||||
(a) => isAfter(new Date(a.startTime), twoMonthsAgo) && !isAfter(new Date(a.startTime), oneMonthAgo)
|
||||
);
|
||||
const lastMonthRate = calculateRate(lastMonthAppts);
|
||||
|
||||
// Calculate changes (negative is good for no-show rate)
|
||||
const weeklyChange = lastWeekRate !== 0 ? ((currentWeekRate - lastWeekRate) / lastWeekRate) * 100 : 0;
|
||||
const monthlyChange = lastMonthRate !== 0 ? ((currentMonthRate - lastMonthRate) / lastMonthRate) * 100 : 0;
|
||||
|
||||
// Count total no-shows this month
|
||||
const noShowsThisMonth = thisMonthAppts.filter((a) => a.status === 'NO_SHOW').length;
|
||||
|
||||
return {
|
||||
currentRate: currentMonthRate,
|
||||
noShowCount: noShowsThisMonth,
|
||||
weeklyChange,
|
||||
monthlyChange,
|
||||
};
|
||||
}, [appointments]);
|
||||
|
||||
const formatChange = (change: number) => {
|
||||
if (change === 0) return '0%';
|
||||
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
// For no-show rate, down is good (green), up is bad (red)
|
||||
const getTrendIcon = (change: number) => {
|
||||
if (change < 0) return <TrendingDown size={12} className="mr-1" />;
|
||||
if (change > 0) return <TrendingUp size={12} className="mr-1" />;
|
||||
return <Minus size={12} className="mr-1" />;
|
||||
};
|
||||
|
||||
const getTrendClass = (change: number) => {
|
||||
if (change < 0) return 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400';
|
||||
if (change > 0) return 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400';
|
||||
return 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300';
|
||||
};
|
||||
|
||||
const getRateColor = (rate: number) => {
|
||||
if (rate <= 5) return 'text-green-600 dark:text-green-400';
|
||||
if (rate <= 10) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={isEditing ? 'pl-5' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<UserX size={18} className="text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">No-Show Rate</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className={`text-2xl font-bold ${getRateColor(noShowData.currentRate)}`}>
|
||||
{noShowData.currentRate.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({noShowData.noShowCount} this month)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs mt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.weeklyChange)}`}>
|
||||
{getTrendIcon(noShowData.weeklyChange)}
|
||||
{formatChange(noShowData.weeklyChange)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.monthlyChange)}`}>
|
||||
{getTrendIcon(noShowData.monthlyChange)}
|
||||
{formatChange(noShowData.monthlyChange)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoShowRateWidget;
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GripVertical, X, AlertCircle, Clock, ChevronRight } from 'lucide-react';
|
||||
import { Ticket } from '../../types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
interface OpenTicketsWidgetProps {
|
||||
tickets: Ticket[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
|
||||
tickets,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const openTickets = tickets.filter(t => t.status === 'open' || t.status === 'in_progress');
|
||||
const urgentCount = openTickets.filter(t => t.priority === 'urgent' || t.isOverdue).length;
|
||||
|
||||
const getPriorityColor = (priority: string, isOverdue?: boolean) => {
|
||||
if (isOverdue) return 'text-red-600 dark:text-red-400';
|
||||
switch (priority) {
|
||||
case 'urgent': return 'text-red-600 dark:text-red-400';
|
||||
case 'high': return 'text-orange-600 dark:text-orange-400';
|
||||
case 'medium': return 'text-yellow-600 dark:text-yellow-400';
|
||||
default: return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBg = (priority: string, isOverdue?: boolean) => {
|
||||
if (isOverdue) return 'bg-red-50 dark:bg-red-900/20';
|
||||
switch (priority) {
|
||||
case 'urgent': return 'bg-red-50 dark:bg-red-900/20';
|
||||
case 'high': return 'bg-orange-50 dark:bg-orange-900/20';
|
||||
case 'medium': return 'bg-yellow-50 dark:bg-yellow-900/20';
|
||||
default: return 'bg-gray-50 dark:bg-gray-700/50';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center justify-between mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Open Tickets
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{urgentCount > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400">
|
||||
<AlertCircle size={12} />
|
||||
{urgentCount} urgent
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{openTickets.length} open
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{openTickets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<AlertCircle size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No open tickets</p>
|
||||
</div>
|
||||
) : (
|
||||
openTickets.slice(0, 5).map((ticket) => (
|
||||
<Link
|
||||
key={ticket.id}
|
||||
to="/tickets"
|
||||
className={`block p-3 rounded-lg ${getPriorityBg(ticket.priority, ticket.isOverdue)} hover:opacity-80 transition-opacity`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{ticket.subject}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className={getPriorityColor(ticket.priority, ticket.isOverdue)}>
|
||||
{ticket.isOverdue ? 'Overdue' : ticket.priority}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={10} />
|
||||
{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-400 flex-shrink-0" />
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openTickets.length > 5 && (
|
||||
<Link
|
||||
to="/tickets"
|
||||
className="mt-3 text-sm text-brand-600 dark:text-brand-400 hover:underline text-center"
|
||||
>
|
||||
View all {openTickets.length} tickets
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenTicketsWidget;
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Calendar, UserPlus, XCircle, CheckCircle, DollarSign } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Appointment, Customer } from '../../types';
|
||||
|
||||
interface ActivityItem {
|
||||
id: string;
|
||||
type: 'booking' | 'cancellation' | 'completion' | 'new_customer' | 'payment';
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
icon: React.ReactNode;
|
||||
iconBg: string;
|
||||
}
|
||||
|
||||
interface RecentActivityWidgetProps {
|
||||
appointments: Appointment[];
|
||||
customers: Customer[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
|
||||
appointments,
|
||||
customers,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const activities = useMemo(() => {
|
||||
const items: ActivityItem[] = [];
|
||||
|
||||
// Add appointments as activity
|
||||
appointments.forEach((appt) => {
|
||||
const timestamp = new Date(appt.startTime);
|
||||
|
||||
if (appt.status === 'CONFIRMED' || appt.status === 'PENDING') {
|
||||
items.push({
|
||||
id: `booking-${appt.id}`,
|
||||
type: 'booking',
|
||||
title: 'New Booking',
|
||||
description: `${appt.customerName} booked an appointment`,
|
||||
timestamp,
|
||||
icon: <Calendar size={14} />,
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
|
||||
});
|
||||
} else if (appt.status === 'CANCELLED') {
|
||||
items.push({
|
||||
id: `cancel-${appt.id}`,
|
||||
type: 'cancellation',
|
||||
title: 'Cancellation',
|
||||
description: `${appt.customerName} cancelled their appointment`,
|
||||
timestamp,
|
||||
icon: <XCircle size={14} />,
|
||||
iconBg: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
||||
});
|
||||
} else if (appt.status === 'COMPLETED') {
|
||||
items.push({
|
||||
id: `complete-${appt.id}`,
|
||||
type: 'completion',
|
||||
title: 'Completed',
|
||||
description: `${appt.customerName}'s appointment completed`,
|
||||
timestamp,
|
||||
icon: <CheckCircle size={14} />,
|
||||
iconBg: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add recent customers (those with no lastVisit are new)
|
||||
customers
|
||||
.filter(c => !c.lastVisit)
|
||||
.slice(0, 5)
|
||||
.forEach((customer) => {
|
||||
items.push({
|
||||
id: `customer-${customer.id}`,
|
||||
type: 'new_customer',
|
||||
title: 'New Customer',
|
||||
description: `${customer.name} signed up`,
|
||||
timestamp: new Date(), // Approximate - would need createdAt field
|
||||
icon: <UserPlus size={14} />,
|
||||
iconBg: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by timestamp descending
|
||||
items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return items.slice(0, 10);
|
||||
}, [appointments, customers]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
Recent Activity
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activities.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<Calendar size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No recent activity</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{activities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3">
|
||||
<div className={`p-1.5 rounded-lg ${activity.iconBg} flex-shrink-0`}>
|
||||
{activity.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{activity.title}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{activity.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
|
||||
{formatDistanceToNow(activity.timestamp, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentActivityWidget;
|
||||
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { X, Plus, Check, LayoutDashboard, BarChart2, Ticket, Activity, Users, UserX, PieChart } from 'lucide-react';
|
||||
import { WIDGET_DEFINITIONS, WidgetType } from './types';
|
||||
|
||||
interface WidgetConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
activeWidgets: string[];
|
||||
onToggleWidget: (widgetId: string) => void;
|
||||
onResetLayout: () => void;
|
||||
}
|
||||
|
||||
const WIDGET_ICONS: Record<WidgetType, React.ReactNode> = {
|
||||
'appointments-metric': <LayoutDashboard size={18} />,
|
||||
'customers-metric': <Users size={18} />,
|
||||
'services-metric': <LayoutDashboard size={18} />,
|
||||
'resources-metric': <LayoutDashboard size={18} />,
|
||||
'revenue-chart': <BarChart2 size={18} />,
|
||||
'appointments-chart': <BarChart2 size={18} />,
|
||||
'open-tickets': <Ticket size={18} />,
|
||||
'recent-activity': <Activity size={18} />,
|
||||
'capacity-utilization': <Users size={18} />,
|
||||
'no-show-rate': <UserX size={18} />,
|
||||
'customer-breakdown': <PieChart size={18} />,
|
||||
};
|
||||
|
||||
const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
activeWidgets,
|
||||
onToggleWidget,
|
||||
onResetLayout,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const widgets = Object.values(WIDGET_DEFINITIONS);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Configure Dashboard Widgets
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
Select which widgets to show on your dashboard. You can drag widgets to reposition them.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{widgets.map((widget) => {
|
||||
const isActive = activeWidgets.includes(widget.id);
|
||||
return (
|
||||
<button
|
||||
key={widget.id}
|
||||
onClick={() => onToggleWidget(widget.id)}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors text-left ${
|
||||
isActive
|
||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`p-2 rounded-lg ${
|
||||
isActive
|
||||
? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{WIDGET_ICONS[widget.type]}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
isActive
|
||||
? 'text-brand-700 dark:text-brand-300'
|
||||
: 'text-gray-900 dark:text-white'
|
||||
}`}
|
||||
>
|
||||
{widget.title}
|
||||
</p>
|
||||
{isActive && (
|
||||
<Check size={14} className="text-brand-600 dark:text-brand-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{widget.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={onResetLayout}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetConfigModal;
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './types';
|
||||
export { default as MetricWidget } from './MetricWidget';
|
||||
export { default as ChartWidget } from './ChartWidget';
|
||||
export { default as OpenTicketsWidget } from './OpenTicketsWidget';
|
||||
export { default as RecentActivityWidget } from './RecentActivityWidget';
|
||||
export { default as CapacityWidget } from './CapacityWidget';
|
||||
export { default as NoShowRateWidget } from './NoShowRateWidget';
|
||||
export { default as CustomerBreakdownWidget } from './CustomerBreakdownWidget';
|
||||
export { default as WidgetConfigModal } from './WidgetConfigModal';
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
export type WidgetType =
|
||||
| 'appointments-metric'
|
||||
| 'customers-metric'
|
||||
| 'services-metric'
|
||||
| 'resources-metric'
|
||||
| 'revenue-chart'
|
||||
| 'appointments-chart'
|
||||
| 'open-tickets'
|
||||
| 'recent-activity'
|
||||
| 'capacity-utilization'
|
||||
| 'no-show-rate'
|
||||
| 'customer-breakdown';
|
||||
|
||||
export interface WidgetConfig {
|
||||
id: string;
|
||||
type: WidgetType;
|
||||
title: string;
|
||||
description: string;
|
||||
defaultSize: { w: number; h: number };
|
||||
minSize?: { w: number; h: number };
|
||||
}
|
||||
|
||||
export interface DashboardLayout {
|
||||
widgets: string[]; // Widget IDs that are visible
|
||||
layout: Layout[];
|
||||
}
|
||||
|
||||
// Widget definitions with metadata
|
||||
export const WIDGET_DEFINITIONS: Record<WidgetType, WidgetConfig> = {
|
||||
'appointments-metric': {
|
||||
id: 'appointments-metric',
|
||||
type: 'appointments-metric',
|
||||
title: 'Total Appointments',
|
||||
description: 'Shows appointment count with weekly and monthly growth',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'customers-metric': {
|
||||
id: 'customers-metric',
|
||||
type: 'customers-metric',
|
||||
title: 'Active Customers',
|
||||
description: 'Shows customer count with weekly and monthly growth',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'services-metric': {
|
||||
id: 'services-metric',
|
||||
type: 'services-metric',
|
||||
title: 'Services',
|
||||
description: 'Shows number of services offered',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'resources-metric': {
|
||||
id: 'resources-metric',
|
||||
type: 'resources-metric',
|
||||
title: 'Resources',
|
||||
description: 'Shows number of resources available',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'revenue-chart': {
|
||||
id: 'revenue-chart',
|
||||
type: 'revenue-chart',
|
||||
title: 'Revenue',
|
||||
description: 'Weekly revenue bar chart',
|
||||
defaultSize: { w: 6, h: 4 },
|
||||
minSize: { w: 4, h: 3 },
|
||||
},
|
||||
'appointments-chart': {
|
||||
id: 'appointments-chart',
|
||||
type: 'appointments-chart',
|
||||
title: 'Appointments Trend',
|
||||
description: 'Weekly appointments line chart',
|
||||
defaultSize: { w: 6, h: 4 },
|
||||
minSize: { w: 4, h: 3 },
|
||||
},
|
||||
'open-tickets': {
|
||||
id: 'open-tickets',
|
||||
type: 'open-tickets',
|
||||
title: 'Open Tickets',
|
||||
description: 'Shows open support tickets requiring attention',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'recent-activity': {
|
||||
id: 'recent-activity',
|
||||
type: 'recent-activity',
|
||||
title: 'Recent Activity',
|
||||
description: 'Timeline of recent business events',
|
||||
defaultSize: { w: 4, h: 5 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'capacity-utilization': {
|
||||
id: 'capacity-utilization',
|
||||
type: 'capacity-utilization',
|
||||
title: 'Capacity Utilization',
|
||||
description: 'Shows how booked your resources are this week',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'no-show-rate': {
|
||||
id: 'no-show-rate',
|
||||
type: 'no-show-rate',
|
||||
title: 'No-Show Rate',
|
||||
description: 'Percentage of appointments marked as no-show',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'customer-breakdown': {
|
||||
id: 'customer-breakdown',
|
||||
type: 'customer-breakdown',
|
||||
title: 'New vs Returning',
|
||||
description: 'Customer breakdown this month',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
// Default layout for new users
|
||||
export const DEFAULT_LAYOUT: DashboardLayout = {
|
||||
widgets: [
|
||||
'appointments-metric',
|
||||
'customers-metric',
|
||||
'no-show-rate',
|
||||
'revenue-chart',
|
||||
'appointments-chart',
|
||||
'open-tickets',
|
||||
'recent-activity',
|
||||
'capacity-utilization',
|
||||
'customer-breakdown',
|
||||
],
|
||||
layout: [
|
||||
{ i: 'appointments-metric', x: 0, y: 0, w: 4, h: 2 },
|
||||
{ i: 'customers-metric', x: 4, y: 0, w: 4, h: 2 },
|
||||
{ i: 'no-show-rate', x: 8, y: 0, w: 4, h: 2 },
|
||||
{ i: 'revenue-chart', x: 0, y: 2, w: 6, h: 4 },
|
||||
{ i: 'appointments-chart', x: 6, y: 2, w: 6, h: 4 },
|
||||
{ i: 'open-tickets', x: 0, y: 6, w: 4, h: 4 },
|
||||
{ i: 'recent-activity', x: 4, y: 6, w: 4, h: 4 },
|
||||
{ i: 'capacity-utilization', x: 8, y: 6, w: 4, h: 4 },
|
||||
{ i: 'customer-breakdown', x: 0, y: 10, w: 4, h: 4 },
|
||||
],
|
||||
};
|
||||
@@ -84,6 +84,7 @@ export const FEATURE_NAMES: Record<FeatureKey, string> = {
|
||||
white_label: 'White Label',
|
||||
custom_oauth: 'Custom OAuth',
|
||||
plugins: 'Custom Plugins',
|
||||
tasks: 'Scheduled Tasks',
|
||||
export_data: 'Data Export',
|
||||
video_conferencing: 'Video Conferencing',
|
||||
two_factor_auth: 'Two-Factor Authentication',
|
||||
@@ -103,6 +104,7 @@ export const FEATURE_DESCRIPTIONS: Record<FeatureKey, string> = {
|
||||
white_label: 'Remove SmoothSchedule branding and use your own',
|
||||
custom_oauth: 'Configure your own OAuth credentials for social login',
|
||||
plugins: 'Create custom plugins to extend functionality',
|
||||
tasks: 'Create scheduled tasks to automate plugin execution',
|
||||
export_data: 'Export your data to CSV or other formats',
|
||||
video_conferencing: 'Add video conferencing links to appointments',
|
||||
two_factor_auth: 'Require two-factor authentication for enhanced security',
|
||||
|
||||
@@ -243,3 +243,15 @@ export const useSyncPlansWithStripe = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to sync a plan's permissions to all tenants on that plan
|
||||
*/
|
||||
export const useSyncPlanToTenants = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (planId: number) => {
|
||||
const { data } = await apiClient.post(`/platform/subscription-plans/${planId}/sync_tenants/`);
|
||||
return data as { message: string; tenant_count: number };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Hook to scroll to top on route changes
|
||||
* Should be used in layout components to ensure scroll restoration
|
||||
* works consistently across all routes
|
||||
*
|
||||
* @param containerRef - Optional ref to a scrollable container element.
|
||||
* If provided, scrolls that element instead of window.
|
||||
*/
|
||||
export function useScrollToTop() {
|
||||
export function useScrollToTop(containerRef?: RefObject<HTMLElement | null>) {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
if (containerRef?.current) {
|
||||
containerRef.current.scrollTo(0, 0);
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [pathname, containerRef]);
|
||||
}
|
||||
|
||||
@@ -33,3 +33,96 @@ body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* React Grid Layout Dashboard Styling */
|
||||
.layout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.widget-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.widget-container > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Drag handle styling */
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* React Grid Layout overrides */
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .react-grid-item > .react-resizable-handle::after {
|
||||
border-right-color: rgba(255, 255, 255, 0.3);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ const BusinessLayoutContent: React.FC<BusinessLayoutProps> = ({ business, user,
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Fetch ticket data when modal is opened from notification
|
||||
const { data: ticketFromNotification } = useTicket(ticketModalId || undefined);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Outlet, Link, useNavigate } from 'react-router-dom';
|
||||
import { User, Business } from '../types';
|
||||
import { LayoutDashboard, CalendarPlus, CreditCard, HelpCircle, Sun, Moon } from 'lucide-react';
|
||||
@@ -18,7 +18,8 @@ interface CustomerLayoutProps {
|
||||
|
||||
const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMode, toggleTheme }) => {
|
||||
const navigate = useNavigate();
|
||||
useScrollToTop();
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Masquerade logic
|
||||
const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
|
||||
@@ -116,7 +117,7 @@ const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMod
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-y-auto">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Outlet context={{ business, user }} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Moon, Sun, Bell, Globe, Menu } from 'lucide-react';
|
||||
import { User } from '../types';
|
||||
@@ -16,8 +16,9 @@ interface ManagerLayoutProps {
|
||||
const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleTheme, onSignOut }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
return (
|
||||
<div className="flex h-full bg-gray-100 dark:bg-gray-900">
|
||||
@@ -63,7 +64,7 @@ const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleThe
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
||||
import { User } from '../types';
|
||||
@@ -21,8 +21,9 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [ticketModalId, setTicketModalId] = useState<string | null>(null);
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Fetch ticket data when modal is opened from notification
|
||||
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
||||
@@ -83,7 +84,7 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+383
-144
@@ -1,28 +1,31 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line
|
||||
} from 'recharts';
|
||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
import GridLayout, { Layout } from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { Settings, Calendar, Users, Briefcase, ClipboardList, Edit2, Check } from 'lucide-react';
|
||||
import { useServices } from '../hooks/useServices';
|
||||
import { useResources } from '../hooks/useResources';
|
||||
import { useAppointments } from '../hooks/useAppointments';
|
||||
import { useCustomers } from '../hooks/useCustomers';
|
||||
import { useTickets } from '../hooks/useTickets';
|
||||
import { subDays, subMonths, isAfter, startOfWeek, endOfWeek, isWithinInterval } from 'date-fns';
|
||||
import {
|
||||
MetricWidget,
|
||||
ChartWidget,
|
||||
OpenTicketsWidget,
|
||||
RecentActivityWidget,
|
||||
CapacityWidget,
|
||||
NoShowRateWidget,
|
||||
CustomerBreakdownWidget,
|
||||
WidgetConfigModal,
|
||||
WIDGET_DEFINITIONS,
|
||||
DEFAULT_LAYOUT,
|
||||
DashboardLayout,
|
||||
WidgetType,
|
||||
} from '../components/dashboard';
|
||||
|
||||
interface Metric {
|
||||
label: string;
|
||||
value: string;
|
||||
trend: 'up' | 'down' | 'neutral';
|
||||
change: string;
|
||||
}
|
||||
const STORAGE_KEY = 'dashboard_layout';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -30,76 +33,311 @@ const Dashboard: React.FC = () => {
|
||||
const { data: resources, isLoading: resourcesLoading } = useResources();
|
||||
const { data: appointments, isLoading: appointmentsLoading } = useAppointments();
|
||||
const { data: customers, isLoading: customersLoading } = useCustomers();
|
||||
const { data: tickets, isLoading: ticketsLoading } = useTickets();
|
||||
|
||||
const isLoading = servicesLoading || resourcesLoading || appointmentsLoading || customersLoading;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [dashboardLayout, setDashboardLayout] = useState<DashboardLayout>(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
return JSON.parse(saved);
|
||||
} catch {
|
||||
return DEFAULT_LAYOUT;
|
||||
}
|
||||
}
|
||||
return DEFAULT_LAYOUT;
|
||||
});
|
||||
|
||||
// Calculate metrics from real data
|
||||
const metrics: Metric[] = useMemo(() => {
|
||||
// Save layout to localStorage when it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(dashboardLayout));
|
||||
}, [dashboardLayout]);
|
||||
|
||||
const isLoading = servicesLoading || resourcesLoading || appointmentsLoading || customersLoading || ticketsLoading;
|
||||
|
||||
// Calculate growth percentages
|
||||
const calculateGrowth = useCallback((
|
||||
items: any[],
|
||||
dateField: string,
|
||||
filterFn?: (item: any) => boolean
|
||||
) => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = subDays(now, 7);
|
||||
const twoWeeksAgo = subDays(now, 14);
|
||||
const oneMonthAgo = subMonths(now, 1);
|
||||
const twoMonthsAgo = subMonths(now, 2);
|
||||
|
||||
const filteredItems = filterFn ? items.filter(filterFn) : items;
|
||||
|
||||
const thisWeek = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, oneWeekAgo);
|
||||
}).length;
|
||||
|
||||
const lastWeek = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, twoWeeksAgo) && !isAfter(date, oneWeekAgo);
|
||||
}).length;
|
||||
|
||||
const thisMonth = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, oneMonthAgo);
|
||||
}).length;
|
||||
|
||||
const lastMonth = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, twoMonthsAgo) && !isAfter(date, oneMonthAgo);
|
||||
}).length;
|
||||
|
||||
const weeklyChange = lastWeek !== 0 ? ((thisWeek - lastWeek) / lastWeek) * 100 : (thisWeek > 0 ? 100 : 0);
|
||||
const monthlyChange = lastMonth !== 0 ? ((thisMonth - lastMonth) / lastMonth) * 100 : (thisMonth > 0 ? 100 : 0);
|
||||
|
||||
return {
|
||||
weekly: { value: thisWeek, change: weeklyChange },
|
||||
monthly: { value: thisMonth, change: monthlyChange },
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Calculate metrics with real growth data
|
||||
const metrics = useMemo(() => {
|
||||
if (!appointments || !customers || !services || !resources) {
|
||||
return [
|
||||
{ label: t('dashboard.totalAppointments'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('customers.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('services.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('resources.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
];
|
||||
return {
|
||||
appointments: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||
customers: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||
services: { count: 0 },
|
||||
resources: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const activeCustomers = customers.filter(c => c.status === 'Active').length;
|
||||
const activeCustomers = customers.filter(c => c.status === 'Active');
|
||||
|
||||
return [
|
||||
{ label: t('dashboard.totalAppointments'), value: appointments.length.toString(), trend: 'up', change: '+12%' },
|
||||
{ label: t('customers.title'), value: activeCustomers.toString(), trend: 'up', change: '+8%' },
|
||||
{ label: t('services.title'), value: services.length.toString(), trend: 'neutral', change: '0%' },
|
||||
{ label: t('resources.title'), value: resources.length.toString(), trend: 'up', change: '+3%' },
|
||||
];
|
||||
}, [appointments, customers, services, resources, t]);
|
||||
return {
|
||||
appointments: {
|
||||
count: appointments.length,
|
||||
growth: calculateGrowth(appointments, 'startTime'),
|
||||
},
|
||||
customers: {
|
||||
count: activeCustomers.length,
|
||||
growth: calculateGrowth(customers, 'lastVisit', c => c.status === 'Active' && c.lastVisit),
|
||||
},
|
||||
services: { count: services.length },
|
||||
resources: { count: resources.length },
|
||||
};
|
||||
}, [appointments, customers, services, resources, calculateGrowth]);
|
||||
|
||||
// Calculate weekly data from appointments
|
||||
// Calculate weekly chart data
|
||||
const weeklyData = useMemo(() => {
|
||||
if (!appointments) {
|
||||
return [
|
||||
{ name: 'Mon', revenue: 0, appointments: 0 },
|
||||
{ name: 'Tue', revenue: 0, appointments: 0 },
|
||||
{ name: 'Wed', revenue: 0, appointments: 0 },
|
||||
{ name: 'Thu', revenue: 0, appointments: 0 },
|
||||
{ name: 'Fri', revenue: 0, appointments: 0 },
|
||||
{ name: 'Sat', revenue: 0, appointments: 0 },
|
||||
{ name: 'Sun', revenue: 0, appointments: 0 },
|
||||
];
|
||||
return { revenue: [], appointments: [] };
|
||||
}
|
||||
|
||||
// Group appointments by day of week
|
||||
const dayMap: { [key: string]: { revenue: number; count: number } } = {
|
||||
'Mon': { revenue: 0, count: 0 },
|
||||
'Tue': { revenue: 0, count: 0 },
|
||||
'Wed': { revenue: 0, count: 0 },
|
||||
'Thu': { revenue: 0, count: 0 },
|
||||
'Fri': { revenue: 0, count: 0 },
|
||||
'Sat': { revenue: 0, count: 0 },
|
||||
'Sun': { revenue: 0, count: 0 },
|
||||
const now = new Date();
|
||||
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||
const weekEnd = endOfWeek(now, { weekStartsOn: 1 });
|
||||
|
||||
const dayMap: Record<string, { revenue: number; count: number }> = {
|
||||
Mon: { revenue: 0, count: 0 },
|
||||
Tue: { revenue: 0, count: 0 },
|
||||
Wed: { revenue: 0, count: 0 },
|
||||
Thu: { revenue: 0, count: 0 },
|
||||
Fri: { revenue: 0, count: 0 },
|
||||
Sat: { revenue: 0, count: 0 },
|
||||
Sun: { revenue: 0, count: 0 },
|
||||
};
|
||||
|
||||
appointments.forEach(appt => {
|
||||
const date = new Date(appt.startTime);
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
appointments
|
||||
.filter(appt => isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }))
|
||||
.forEach(appt => {
|
||||
const date = new Date(appt.startTime);
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
|
||||
dayMap[dayName].count++;
|
||||
// Use price from appointment or default to 0
|
||||
dayMap[dayName].revenue += appt.price || 0;
|
||||
});
|
||||
dayMap[dayName].count++;
|
||||
dayMap[dayName].revenue += (appt as any).price || 0;
|
||||
});
|
||||
|
||||
return [
|
||||
{ name: 'Mon', revenue: dayMap['Mon'].revenue, appointments: dayMap['Mon'].count },
|
||||
{ name: 'Tue', revenue: dayMap['Tue'].revenue, appointments: dayMap['Tue'].count },
|
||||
{ name: 'Wed', revenue: dayMap['Wed'].revenue, appointments: dayMap['Wed'].count },
|
||||
{ name: 'Thu', revenue: dayMap['Thu'].revenue, appointments: dayMap['Thu'].count },
|
||||
{ name: 'Fri', revenue: dayMap['Fri'].revenue, appointments: dayMap['Fri'].count },
|
||||
{ name: 'Sat', revenue: dayMap['Sat'].revenue, appointments: dayMap['Sat'].count },
|
||||
{ name: 'Sun', revenue: dayMap['Sun'].revenue, appointments: dayMap['Sun'].count },
|
||||
];
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return {
|
||||
revenue: days.map(day => ({ name: day, value: dayMap[day].revenue })),
|
||||
appointments: days.map(day => ({ name: day, value: dayMap[day].count })),
|
||||
};
|
||||
}, [appointments]);
|
||||
|
||||
// Handle layout change
|
||||
const onLayoutChange = useCallback((newLayout: Layout[]) => {
|
||||
setDashboardLayout(prev => ({
|
||||
...prev,
|
||||
layout: newLayout,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Toggle widget visibility
|
||||
const toggleWidget = useCallback((widgetId: string) => {
|
||||
setDashboardLayout(prev => {
|
||||
const isActive = prev.widgets.includes(widgetId);
|
||||
if (isActive) {
|
||||
return {
|
||||
widgets: prev.widgets.filter(id => id !== widgetId),
|
||||
layout: prev.layout.filter(l => l.i !== widgetId),
|
||||
};
|
||||
} else {
|
||||
const widgetDef = WIDGET_DEFINITIONS[widgetId as WidgetType];
|
||||
const maxY = Math.max(0, ...prev.layout.map(l => l.y + l.h));
|
||||
return {
|
||||
widgets: [...prev.widgets, widgetId],
|
||||
layout: [
|
||||
...prev.layout,
|
||||
{
|
||||
i: widgetId,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: widgetDef.defaultSize.w,
|
||||
h: widgetDef.defaultSize.h,
|
||||
minW: widgetDef.minSize?.w,
|
||||
minH: widgetDef.minSize?.h,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Remove widget
|
||||
const removeWidget = useCallback((widgetId: string) => {
|
||||
setDashboardLayout(prev => ({
|
||||
widgets: prev.widgets.filter(id => id !== widgetId),
|
||||
layout: prev.layout.filter(l => l.i !== widgetId),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reset to default layout
|
||||
const resetLayout = useCallback(() => {
|
||||
setDashboardLayout(DEFAULT_LAYOUT);
|
||||
}, []);
|
||||
|
||||
// Render individual widget
|
||||
const renderWidget = useCallback((widgetId: string) => {
|
||||
const widgetProps = {
|
||||
isEditing,
|
||||
onRemove: () => removeWidget(widgetId),
|
||||
};
|
||||
|
||||
switch (widgetId) {
|
||||
case 'appointments-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.totalAppointments')}
|
||||
value={metrics.appointments.count}
|
||||
growth={metrics.appointments.growth}
|
||||
icon={<Calendar size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'customers-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('customers.title')}
|
||||
value={metrics.customers.count}
|
||||
growth={metrics.customers.growth}
|
||||
icon={<Users size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'services-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('services.title')}
|
||||
value={metrics.services.count}
|
||||
growth={{ weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } }}
|
||||
icon={<Briefcase size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'resources-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('resources.title')}
|
||||
value={metrics.resources.count}
|
||||
growth={{ weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } }}
|
||||
icon={<ClipboardList size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'revenue-chart':
|
||||
return (
|
||||
<ChartWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.totalRevenue')}
|
||||
data={weeklyData.revenue}
|
||||
type="bar"
|
||||
color="#3b82f6"
|
||||
valuePrefix="$"
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'appointments-chart':
|
||||
return (
|
||||
<ChartWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.upcomingAppointments')}
|
||||
data={weeklyData.appointments}
|
||||
type="line"
|
||||
color="#10b981"
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'open-tickets':
|
||||
return (
|
||||
<OpenTicketsWidget
|
||||
key={widgetId}
|
||||
tickets={tickets || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'recent-activity':
|
||||
return (
|
||||
<RecentActivityWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
customers={customers || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'capacity-utilization':
|
||||
return (
|
||||
<CapacityWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
resources={resources || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'no-show-rate':
|
||||
return (
|
||||
<NoShowRateWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'customer-breakdown':
|
||||
return (
|
||||
<CustomerBreakdownWidget
|
||||
key={widgetId}
|
||||
customers={customers || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [t, metrics, weeklyData, tickets, appointments, customers, resources, isEditing, removeWidget]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
@@ -119,85 +357,86 @@ const Dashboard: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Get layout with min sizes
|
||||
const layoutWithConstraints = dashboardLayout.layout.map(l => {
|
||||
const widgetDef = WIDGET_DEFINITIONS[l.i as WidgetType];
|
||||
return {
|
||||
...l,
|
||||
minW: widgetDef?.minSize?.w || 2,
|
||||
minH: widgetDef?.minSize?.h || 2,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
|
||||
<div className="p-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
|
||||
isEditing
|
||||
? 'bg-brand-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isEditing ? <Check size={16} /> : <Edit2 size={16} />}
|
||||
<span className="text-sm">{isEditing ? 'Done' : 'Edit Layout'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfig(true)}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<Settings size={16} />
|
||||
<span className="text-sm">Widgets</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index} className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{metric.label}</p>
|
||||
<div className="flex items-baseline gap-2 mt-2">
|
||||
<span className="text-2xl font-bold text-gray-900 dark:text-white">{metric.value}</span>
|
||||
<span className={`flex items-center text-xs font-medium px-2 py-0.5 rounded-full ${
|
||||
metric.trend === 'up' ? 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400' :
|
||||
metric.trend === 'down' ? 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400' : 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}>
|
||||
{metric.trend === 'up' && <TrendingUp size={12} className="mr-1" />}
|
||||
{metric.trend === 'down' && <TrendingDown size={12} className="mr-1" />}
|
||||
{metric.trend === 'neutral' && <Minus size={12} className="mr-1" />}
|
||||
{metric.change}
|
||||
</span>
|
||||
{/* Edit mode hint */}
|
||||
{isEditing && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
Drag widgets to reposition them. Drag the corner to resize. Hover over a widget and click the X to remove it.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid Layout */}
|
||||
<div className="max-w-[1200px] mx-auto">
|
||||
<GridLayout
|
||||
className="layout"
|
||||
layout={layoutWithConstraints}
|
||||
cols={12}
|
||||
rowHeight={60}
|
||||
width={1200}
|
||||
isDraggable={isEditing}
|
||||
isResizable={isEditing}
|
||||
onLayoutChange={onLayoutChange}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
>
|
||||
{dashboardLayout.widgets.map(widgetId => (
|
||||
<div key={widgetId} className="widget-container">
|
||||
{renderWidget(widgetId)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</GridLayout>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* Revenue Chart */}
|
||||
<div className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<h3 className="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{t('dashboard.totalRevenue')}</h3>
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={weeklyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<YAxis axisLine={false} tickLine={false} tickFormatter={(value) => `$${value}`} tick={{ fill: '#9CA3AF' }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(107, 114, 128, 0.1)' }}
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="revenue" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Appointments Chart - Full Width */}
|
||||
<div className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<h3 className="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{t('dashboard.upcomingAppointments')}</h3>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={weeklyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
/>
|
||||
<Line type="monotone" dataKey="appointments" stroke="#10b981" strokeWidth={3} dot={{ r: 4, fill: '#10b981' }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/* Widget Config Modal */}
|
||||
<WidgetConfigModal
|
||||
isOpen={showConfig}
|
||||
onClose={() => setShowConfig(false)}
|
||||
activeWidgets={dashboardLayout.widgets}
|
||||
onToggleWidget={toggleWidget}
|
||||
onResetLayout={resetLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
|
||||
@@ -77,9 +77,7 @@ const HelpGuide: React.FC = () => {
|
||||
title: 'Extend',
|
||||
description: 'Add functionality with plugins',
|
||||
links: [
|
||||
{ label: 'Plugins Overview', path: '/help/plugins', icon: <Puzzle size={18} /> },
|
||||
{ label: 'Creating Plugins', path: '/help/plugins/create', icon: <Puzzle size={18} /> },
|
||||
{ label: 'Plugin Documentation', path: '/help/plugins/docs', icon: <BookOpen size={18} /> },
|
||||
{ label: 'Plugins', path: '/help/plugins', icon: <Puzzle size={18} /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import api from '../api/client';
|
||||
import { PluginInstallation, PluginCategory } from '../types';
|
||||
import EmailTemplateSelector from '../components/EmailTemplateSelector';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Category icon mapping
|
||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||
@@ -60,6 +62,11 @@ const MyPlugins: React.FC = () => {
|
||||
const [review, setReview] = useState('');
|
||||
const [configValues, setConfigValues] = useState<Record<string, any>>({});
|
||||
|
||||
// Check plan permissions
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const isLocked = !hasPluginsFeature;
|
||||
|
||||
// Fetch installed plugins
|
||||
const { data: plugins = [], isLoading, error } = useQuery<PluginInstallation[]>({
|
||||
queryKey: ['plugin-installations'],
|
||||
@@ -227,7 +234,7 @@ const MyPlugins: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -250,6 +257,7 @@ const MyPlugins: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -763,6 +771,7 @@ const MyPlugins: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
const [editDateTime, setEditDateTime] = useState('');
|
||||
const [editResource, setEditResource] = useState('');
|
||||
const [editDuration, setEditDuration] = useState(0);
|
||||
const [editStatus, setEditStatus] = useState<AppointmentStatus>('CONFIRMED');
|
||||
|
||||
// Filter state
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
@@ -113,9 +114,17 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
// Update edit state when selected appointment changes
|
||||
useEffect(() => {
|
||||
if (selectedAppointment) {
|
||||
setEditDateTime(new Date(selectedAppointment.startTime).toISOString().slice(0, 16));
|
||||
// Format date in local time for datetime-local input (toISOString uses UTC)
|
||||
const date = new Date(selectedAppointment.startTime);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
setEditDateTime(`${year}-${month}-${day}T${hours}:${minutes}`);
|
||||
setEditResource(selectedAppointment.resourceId || '');
|
||||
setEditDuration(selectedAppointment.durationMinutes);
|
||||
setEditStatus(selectedAppointment.status);
|
||||
}
|
||||
}, [selectedAppointment]);
|
||||
|
||||
@@ -551,7 +560,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
|
||||
const getStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
||||
if (status === 'COMPLETED') return 'bg-green-100 border-green-500 text-green-900 dark:bg-green-900/50 dark:border-green-500 dark:text-green-200';
|
||||
if (status === 'NO_SHOW') return 'bg-gray-100 border-gray-400 text-gray-600 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400';
|
||||
if (status === 'NO_SHOW') return 'bg-orange-100 border-orange-500 text-orange-900 dark:bg-orange-900/50 dark:border-orange-500 dark:text-orange-200';
|
||||
if (status === 'CANCELLED') return 'bg-gray-100 border-gray-400 text-gray-500 opacity-75 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400';
|
||||
const now = new Date();
|
||||
if (now > endTime) return 'bg-red-100 border-red-500 text-red-900 dark:bg-red-900/50 dark:border-red-500 dark:text-red-200';
|
||||
@@ -562,7 +571,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
// Simplified status colors for month view (no border classes)
|
||||
const getMonthStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
||||
if (status === 'COMPLETED') return 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800/50';
|
||||
if (status === 'NO_SHOW') return 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600';
|
||||
if (status === 'NO_SHOW') return 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-200 hover:bg-orange-200 dark:hover:bg-orange-800/50';
|
||||
if (status === 'CANCELLED') return 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 opacity-75 hover:bg-gray-200 dark:hover:bg-gray-600';
|
||||
const now = new Date();
|
||||
if (now > endTime) return 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800/50';
|
||||
@@ -823,6 +832,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
const updates: any = {
|
||||
startTime: new Date(editDateTime),
|
||||
durationMinutes: validDuration,
|
||||
status: editStatus,
|
||||
};
|
||||
|
||||
if (editResource) {
|
||||
@@ -1112,9 +1122,9 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
<div className={`w-2 h-2 rounded-full ml-auto ${
|
||||
status === 'COMPLETED' ? 'bg-green-500' :
|
||||
status === 'CANCELLED' ? 'bg-gray-400' :
|
||||
status === 'NO_SHOW' ? 'bg-gray-400' :
|
||||
status === 'NO_SHOW' ? 'bg-orange-500' :
|
||||
status === 'CONFIRMED' ? 'bg-blue-500' :
|
||||
'bg-orange-400'
|
||||
'bg-yellow-400'
|
||||
}`}></div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1719,8 +1729,18 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-white">{services.find(s => s.id === selectedAppointment.serviceId)?.name}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Status</p>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-white capitalize">{selectedAppointment.status.toLowerCase().replace('_', ' ')}</p>
|
||||
<label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 block">Status</label>
|
||||
<select
|
||||
value={editStatus}
|
||||
onChange={(e) => setEditStatus(e.target.value as AppointmentStatus)}
|
||||
className="w-full px-2 py-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-sm font-semibold text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
<option value="PENDING">Pending</option>
|
||||
<option value="CONFIRMED">Confirmed</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="CANCELLED">Cancelled</option>
|
||||
<option value="NO_SHOW">No Show</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import api from '../api/client';
|
||||
import { PluginTemplate, PluginCategory } from '../types';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Category icon mapping
|
||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||
@@ -91,6 +93,11 @@ const PluginMarketplace: React.FC = () => {
|
||||
const [showWhatsNextModal, setShowWhatsNextModal] = useState(false);
|
||||
const [installedPluginId, setInstalledPluginId] = useState<string | null>(null);
|
||||
|
||||
// Check plan permissions
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const isLocked = !hasPluginsFeature;
|
||||
|
||||
// Fetch marketplace plugins
|
||||
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
|
||||
queryKey: ['plugin-templates', 'marketplace'],
|
||||
@@ -206,7 +213,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -229,6 +236,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -702,6 +710,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import toast from 'react-hot-toast';
|
||||
import CreateTaskModal from '../components/CreateTaskModal';
|
||||
import EditTaskModal from '../components/EditTaskModal';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Types
|
||||
interface ScheduledTask {
|
||||
@@ -95,6 +97,12 @@ const Tasks: React.FC = () => {
|
||||
const [editingTask, setEditingTask] = useState<ScheduledTask | null>(null);
|
||||
const [editingEventAutomation, setEditingEventAutomation] = useState<GlobalEventPlugin | null>(null);
|
||||
|
||||
// Check plan permissions - tasks requires both plugins AND tasks features
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const hasTasksFeature = canUse('tasks');
|
||||
const isLocked = !hasPluginsFeature || !hasTasksFeature;
|
||||
|
||||
// Fetch scheduled tasks
|
||||
const { data: scheduledTasks = [], isLoading: tasksLoading } = useQuery<ScheduledTask[]>({
|
||||
queryKey: ['scheduled-tasks'],
|
||||
@@ -246,7 +254,7 @@ const Tasks: React.FC = () => {
|
||||
return AlertCircle;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
@@ -255,6 +263,7 @@ const Tasks: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="tasks" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -588,6 +597,7 @@ const Tasks: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,829 @@
|
||||
/**
|
||||
* Comprehensive Help Guide - Monolithic Documentation
|
||||
*
|
||||
* Complete documentation for SmoothSchedule in a single scrollable page.
|
||||
* Includes all features: Dashboard, Scheduler, Services, Resources, Customers, Staff, and Settings.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, BookOpen, LayoutDashboard, Calendar, Briefcase, Users, UserCog,
|
||||
ClipboardList, Settings, ChevronRight, HelpCircle, CheckCircle, AlertCircle,
|
||||
Clock, Eye, Palette, Link2, Mail, Globe, CreditCard, Zap, Search, Filter,
|
||||
Plus, Edit, Trash2, ArrowUpDown, GripVertical, Image, Save, ExternalLink,
|
||||
MessageSquare, Tag, UserPlus, Shield, Copy, Layers, Play, Pause, Puzzle,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TocSubItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface TocItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
subItems?: TocSubItem[];
|
||||
}
|
||||
|
||||
const HelpComprehensive: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [expandedItems, setExpandedItems] = React.useState<string[]>(['getting-started', 'settings']);
|
||||
|
||||
// Table of contents items with sub-items
|
||||
const tocItems: TocItem[] = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
label: 'Getting Started',
|
||||
icon: <Zap size={16} />,
|
||||
subItems: [
|
||||
{ label: 'Services Setup', href: '/help/services' },
|
||||
{ label: 'Resources Setup', href: '/help/resources' },
|
||||
{ label: 'Branding', href: '/help/settings/appearance' },
|
||||
{ label: 'Booking URL', href: '/help/settings/booking' },
|
||||
{ label: 'Scheduler', href: '/help/scheduler' },
|
||||
],
|
||||
},
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} /> },
|
||||
{ id: 'scheduler', label: 'Scheduler', icon: <Calendar size={16} /> },
|
||||
{ id: 'services', label: 'Services', icon: <Briefcase size={16} /> },
|
||||
{ id: 'resources', label: 'Resources', icon: <ClipboardList size={16} /> },
|
||||
{ id: 'customers', label: 'Customers', icon: <Users size={16} /> },
|
||||
{ id: 'staff', label: 'Staff', icon: <UserCog size={16} /> },
|
||||
{ id: 'plugins', label: 'Plugins', icon: <Puzzle size={16} /> },
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
icon: <Settings size={16} />,
|
||||
subItems: [
|
||||
{ label: 'Resource Types', href: '/help/settings/resource-types' },
|
||||
{ label: 'Email Settings', href: '/help/settings/email' },
|
||||
{ label: 'Custom Domains', href: '/help/settings/domains' },
|
||||
{ label: 'Billing', href: '/help/settings/billing' },
|
||||
{ label: 'API Settings', href: '/help/settings/api' },
|
||||
{ label: 'Authentication', href: '/help/settings/auth' },
|
||||
{ label: 'Usage & Quota', href: '/help/settings/quota' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedItems((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
{/* Fixed Header */}
|
||||
<div className="sticky top-0 z-20 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400">
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">SmoothSchedule Complete Guide</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/tickets" className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400">
|
||||
Contact Support
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 py-8 flex gap-8">
|
||||
{/* Sidebar Table of Contents */}
|
||||
<nav className="hidden lg:block w-64 shrink-0">
|
||||
<div className="sticky top-24">
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">Contents</h2>
|
||||
<ul className="space-y-1">
|
||||
{tocItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-left"
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
{item.subItems && (
|
||||
<button
|
||||
onClick={() => toggleExpanded(item.id)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className={`transition-transform ${expandedItems.includes(item.id) ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{item.subItems && expandedItems.includes(item.id) && (
|
||||
<ul className="ml-6 mt-1 space-y-1 border-l border-gray-200 dark:border-gray-700 pl-2">
|
||||
{item.subItems.map((subItem) => (
|
||||
<li key={subItem.href}>
|
||||
<Link
|
||||
to={subItem.href}
|
||||
onClick={scrollToTop}
|
||||
className="block px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
||||
>
|
||||
{subItem.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 min-w-0">
|
||||
{/* Introduction */}
|
||||
<section className="mb-12">
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Welcome to SmoothSchedule</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
SmoothSchedule is a complete scheduling platform designed to help businesses manage appointments,
|
||||
customers, staff, and services. This comprehensive guide covers everything you need to know to
|
||||
get the most out of the platform.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Use the table of contents on the left to jump to specific sections, or scroll through the entire guide.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* GETTING STARTED */}
|
||||
{/* ============================================== */}
|
||||
<section id="getting-started" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
|
||||
<Zap size={20} className="text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Getting Started</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Quick Setup Checklist</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Follow these steps to get your scheduling system up and running:
|
||||
</p>
|
||||
<ol className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">1</span>
|
||||
<div>
|
||||
<Link to="/help/services" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Set up your Services
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Define what you offer - consultations, appointments, classes, etc. Include names, durations, and prices.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">2</span>
|
||||
<div>
|
||||
<Link to="/help/resources" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Add your Resources
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Create staff members, rooms, or equipment that can be booked. Set their availability schedules.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">3</span>
|
||||
<div>
|
||||
<Link to="/help/settings/appearance" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Configure your Branding
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Upload your logo and set your brand colors so customers recognize your business.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">4</span>
|
||||
<div>
|
||||
<Link to="/help/settings/booking" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Share your Booking URL
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Copy your booking URL from Settings → Booking and share it with customers.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">5</span>
|
||||
<div>
|
||||
<Link to="/help/scheduler" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Start Managing Appointments
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Use the Scheduler to view, create, and manage bookings as they come in.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* DASHBOARD */}
|
||||
{/* ============================================== */}
|
||||
<section id="dashboard" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<LayoutDashboard size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Dashboard provides an at-a-glance overview of your business performance. It displays key metrics
|
||||
and charts to help you understand how your scheduling business is doing.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Metrics</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Total Appointments</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Number of bookings in the system</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Active Customers</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customers with Active status</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Services</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Total number of services offered</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Resources</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Staff, rooms, and equipment available</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Charts</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Revenue Chart:</strong> Bar chart showing daily revenue by day of week</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Appointments Chart:</strong> Line chart showing appointment volume by day</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SCHEDULER */}
|
||||
{/* ============================================== */}
|
||||
<section id="scheduler" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
||||
<Calendar size={20} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Scheduler</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Scheduler is the heart of SmoothSchedule. It provides a visual calendar interface for managing
|
||||
all your appointments with full drag-and-drop support.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Interface Layout</h3>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Left Sidebar - Pending Appointments</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Unscheduled appointments waiting to be placed on the calendar. Drag them onto available time slots.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Calendar size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Center - Calendar View</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Main calendar showing appointments organized by resource in columns. Switch between day, 3-day, week, and month views.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Edit size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Right Sidebar - Appointment Details</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Click any appointment to view/edit details, add notes, change status, or send reminders.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Drag & Drop:</strong> Move appointments between time slots and resources</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Resize:</strong> Drag appointment edges to change duration</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Quick Create:</strong> Double-click any empty slot to create a new appointment</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Resource Filtering:</strong> Toggle which resources are visible in the calendar</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Status Colors:</strong> Appointments are color-coded by status (confirmed, pending, cancelled)</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Appointment Statuses</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Pending</span>
|
||||
</div>
|
||||
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-300">Confirmed</span>
|
||||
</div>
|
||||
<div className="p-2 bg-red-50 dark:bg-red-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">Cancelled</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Completed</span>
|
||||
</div>
|
||||
<div className="p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-yellow-700 dark:text-yellow-300">No-Show</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SERVICES */}
|
||||
{/* ============================================== */}
|
||||
<section id="services" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<Briefcase size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Services</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Services define what customers can book with you. Each service has a name, duration, price, and
|
||||
description. The Services page uses a two-column layout: an editable list on the left and a
|
||||
customer preview on the right.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Service Properties</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Name</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">The service title shown to customers</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Duration</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">How long the appointment takes (in minutes)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Price</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Cost of the service (displayed to customers)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Description</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Details about what the service includes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<GripVertical size={16} className="text-brand-500" />
|
||||
<span><strong>Drag to Reorder:</strong> Change the display order by dragging services up/down</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Image size={16} className="text-brand-500" />
|
||||
<span><strong>Photo Gallery:</strong> Add, reorder, and remove images for each service</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Eye size={16} className="text-brand-500" />
|
||||
<span><strong>Live Preview:</strong> See how customers will view your service in real-time</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Plus size={16} className="text-brand-500" />
|
||||
<span><strong>Quick Add:</strong> Create new services with the Add Service button</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* RESOURCES */}
|
||||
{/* ============================================== */}
|
||||
<section id="resources" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
||||
<ClipboardList size={20} className="text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Resources</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Resources are the things that get booked - staff members, rooms, equipment, or any other bookable
|
||||
entity. Each resource appears as a column in the scheduler calendar.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Resource Types</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<UserCog size={24} className="text-blue-600 dark:text-blue-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Staff</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">People who provide services (employees, contractors, etc.)</p>
|
||||
</div>
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<Layers size={24} className="text-green-600 dark:text-green-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Room</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Physical spaces (meeting rooms, studios, treatment rooms)</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<ClipboardList size={24} className="text-purple-600 dark:text-purple-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Equipment</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Physical items (cameras, projectors, vehicles)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Staff Autocomplete:</strong> When creating staff resources, link to existing staff members</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Multilane Mode:</strong> Enable for resources that can handle multiple concurrent bookings</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>View Calendar:</strong> Click the calendar icon to see a resource's schedule</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Table Actions:</strong> Edit or delete resources from the actions column</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* CUSTOMERS */}
|
||||
{/* ============================================== */}
|
||||
<section id="customers" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-pink-100 dark:bg-pink-900/30 flex items-center justify-center">
|
||||
<Users size={20} className="text-pink-600 dark:text-pink-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Customers</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Customers page lets you manage all the people who book appointments with your business.
|
||||
Track their information, booking history, and status.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Customer Statuses</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<h4 className="font-medium text-green-700 dark:text-green-300 text-sm">Active</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer can book appointments normally</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-700 dark:text-gray-300 text-sm">Inactive</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer record is dormant</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<h4 className="font-medium text-red-700 dark:text-red-300 text-sm">Blocked</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer cannot make new bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<Search size={16} className="text-brand-500" />
|
||||
<span><strong>Search:</strong> Find customers by name, email, or phone</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Filter size={16} className="text-brand-500" />
|
||||
<span><strong>Filter:</strong> Filter by status (Active, Inactive, Blocked)</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={16} className="text-brand-500" />
|
||||
<span><strong>Tags:</strong> Organize customers with custom tags (VIP, New, etc.)</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<ArrowUpDown size={16} className="text-brand-500" />
|
||||
<span><strong>Sorting:</strong> Click column headers to sort the table</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Eye size={18} className="text-purple-500" /> Masquerading
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Use the Masquerade feature to see exactly what a customer sees when they log in. This is helpful
|
||||
for walking customers through tasks or troubleshooting issues. Click the eye icon in a customer's
|
||||
row to start masquerading.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* STAFF */}
|
||||
{/* ============================================== */}
|
||||
<section id="staff" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-teal-100 dark:bg-teal-900/30 flex items-center justify-center">
|
||||
<UserCog size={20} className="text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Staff</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Staff page lets you manage team members who help run your business. Invite new staff,
|
||||
assign roles, and control what each person can access.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Staff Roles</h3>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<Shield size={18} className="text-yellow-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Owner</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Full access to everything including billing and settings. Cannot be removed.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<Shield size={18} className="text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Manager</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Can manage staff, customers, services, and appointments. No billing access.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<Shield size={18} className="text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Staff</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Basic access. Can view scheduler and manage own appointments if bookable.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Inviting Staff</h3>
|
||||
<ol className="space-y-2 text-sm text-gray-600 dark:text-gray-300 list-decimal list-inside mb-6">
|
||||
<li>Click the <strong>Invite Staff</strong> button</li>
|
||||
<li>Enter their email address</li>
|
||||
<li>Select a role (Manager or Staff)</li>
|
||||
<li>Click <strong>Send Invitation</strong></li>
|
||||
<li>They'll receive an email with a link to join</li>
|
||||
</ol>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Make Bookable</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
The "Make Bookable" option creates a bookable resource for a staff member. When enabled, they
|
||||
appear as a column in the scheduler and customers can book appointments with them directly.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* PLUGINS */}
|
||||
{/* ============================================== */}
|
||||
<section id="plugins" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
|
||||
<Puzzle size={20} className="text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Plugins</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Plugins extend SmoothSchedule with custom automation and integrations. Browse the marketplace
|
||||
for pre-built plugins or create your own using our scripting language.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">What Plugins Can Do</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Send Emails:</strong> Automated reminders, confirmations, and follow-ups</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Webhooks:</strong> Integrate with external services when events occur</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Reports:</strong> Generate and email business reports on a schedule</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Cleanup:</strong> Automatically archive old data or manage records</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Plugin Types</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Marketplace Plugins</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Pre-built plugins available to install immediately. Browse, install, and configure with a few clicks.</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Custom Plugins</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Create your own plugins using our scripting language. Full control over logic and triggers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Triggers</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Plugins can be triggered in various ways:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">Before Event</span>
|
||||
</div>
|
||||
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-green-700 dark:text-green-300">At Start</span>
|
||||
</div>
|
||||
<div className="p-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">After End</span>
|
||||
</div>
|
||||
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-purple-700 dark:text-purple-300">On Status Change</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Learn More</h3>
|
||||
<Link to="/help/plugins/docs" onClick={scrollToTop} className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<Puzzle size={24} className="text-indigo-500" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Plugin Documentation</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Complete guide to creating and using plugins, including API reference and examples</p>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SETTINGS */}
|
||||
{/* ============================================== */}
|
||||
<section id="settings" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||||
<Settings size={20} className="text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Settings is where business owners configure their scheduling platform. Most settings are
|
||||
owner-only and affect how your business operates.
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Owner Access Required:</strong> Only business owners can access most settings pages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* General Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">General Settings</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Configure your business name, timezone, and contact information.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Business Name:</strong> Your company name displayed throughout the app</li>
|
||||
<li>• <strong>Subdomain:</strong> Your booking URL (read-only after creation)</li>
|
||||
<li>• <strong>Timezone:</strong> Business operating timezone</li>
|
||||
<li>• <strong>Time Display Mode:</strong> Show times in business timezone or viewer's timezone</li>
|
||||
<li>• <strong>Contact Email/Phone:</strong> How customers can reach you</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Booking Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Booking Settings</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Your booking URL and post-booking redirect configuration.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Booking URL:</strong> The link customers use to book (copy/share it)</li>
|
||||
<li>• <strong>Return URL:</strong> Where to redirect customers after booking (optional)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Branding Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Branding (Appearance)</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Customize your business appearance with logos and colors.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Website Logo:</strong> Appears in sidebar and booking pages (500×500px recommended)</li>
|
||||
<li>• <strong>Email Logo:</strong> Appears in email notifications (600×200px recommended)</li>
|
||||
<li>• <strong>Display Mode:</strong> Text Only, Logo Only, or Logo and Text</li>
|
||||
<li>• <strong>Color Palettes:</strong> 10 preset palettes to choose from</li>
|
||||
<li>• <strong>Custom Colors:</strong> Set your own primary and secondary colors</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Other Settings */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Other Settings</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Link to="/help/settings/resource-types" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Resource Types</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Configure staff, room, equipment types</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/email" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Email Templates</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customize email notifications</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/domains" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Custom Domains</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Use your own domain for booking</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/billing" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Billing</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Manage subscription and payments</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/api" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">API Settings</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">API keys and webhooks</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/quota" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Usage & Quota</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Track usage and limits</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Help Footer */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Can't find what you're looking for? Our support team is ready to help.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpComprehensive;
|
||||
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* Help Create Plugin Page
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Code, FileCode, Play, Settings, Eye,
|
||||
CheckCircle, ChevronRight, HelpCircle, Puzzle, BookOpen,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpCreatePlugin: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6">
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<Code size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Creating Plugins Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Build custom plugins for your business</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Code size={20} className="text-brand-500" /> Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Create custom plugins to automate workflows, generate reports, send notifications, and more. Plugins use AI-powered prompts that can access your business data to perform specific actions.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
No coding experience required - simply describe what you want the plugin to do using natural language prompts.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<FileCode size={20} className="text-brand-500" /> Plugin Components
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Settings size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Basic Information</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Name, description, category, and version for your plugin</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Code size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Plugin Code</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">AI prompt instructions that define plugin behavior</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Eye size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Visibility</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Private (only you) or Public (share with community)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Play size={20} className="text-brand-500" /> Template Variables
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Use these special variables in your plugin code to access dynamic data:
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg font-mono">
|
||||
<code className="text-brand-600 dark:text-brand-400">{"{{PROMPT}}"}</code>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- User input when running the plugin</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg font-mono">
|
||||
<code className="text-brand-600 dark:text-brand-400">{"{{CONTEXT}}"}</code>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Additional context data from the system</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg font-mono">
|
||||
<code className="text-brand-600 dark:text-brand-400">{"{{DATE}}"}</code>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Current date for time-based operations</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Puzzle size={20} className="text-brand-500" /> Plugin Categories
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Scheduling</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Appointment automation</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Communication</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Messages and notifications</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Analytics</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Reports and insights</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Integration</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">External service connections</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Automation</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Workflow automation</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Other</span>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Custom functionality</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Best Practices
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Be Specific:</strong> Write clear, detailed prompts for better results</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Test First:</strong> Run plugins in test mode before production use</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Version Control:</strong> Update version numbers when making changes</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Document:</strong> Add clear descriptions for easy understanding</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Guides</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/plugins" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Puzzle size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Plugins Overview</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/plugins/docs" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<BookOpen size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Plugin Documentation</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpCreatePlugin;
|
||||
@@ -19,6 +19,13 @@ import {
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
Settings,
|
||||
Edit2,
|
||||
GripVertical,
|
||||
Ticket,
|
||||
Activity,
|
||||
UserX,
|
||||
PieChart,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpDashboard: React.FC = () => {
|
||||
@@ -47,7 +54,7 @@ const HelpDashboard: React.FC = () => {
|
||||
Dashboard Guide
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Your business at a glance
|
||||
Your customizable business command center
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,89 +68,202 @@ const HelpDashboard: React.FC = () => {
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Dashboard is your command center for understanding your business performance. It provides real-time insights into appointments, customers, services, and resources, helping you make informed decisions quickly.
|
||||
The Dashboard is your fully customizable command center for understanding your business performance. It provides real-time insights into appointments, customers, capacity, tickets, and more.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
All metrics update automatically as you add new appointments, register customers, and manage your business operations.
|
||||
You can personalize your dashboard by adding, removing, repositioning, and resizing widgets to focus on the metrics that matter most to you. Your layout is automatically saved.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Key Metrics Section */}
|
||||
{/* Customization Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<TrendingUp size={20} className="text-brand-500" />
|
||||
Key Metrics
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Dashboard displays four primary metrics that give you an instant snapshot of your business:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Calendar size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Total Appointments</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
The number of appointments in your system. Shows trend compared to previous period.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Users size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Active Customers</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Customers with "Active" status who can book appointments with your business.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Briefcase size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Services</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
The total number of services you offer. Click to manage your service catalog.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<ClipboardList size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Resources</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Staff, rooms, and equipment available for booking. Essential for scheduling.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Charts Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 size={20} className="text-brand-500" />
|
||||
Analytics Charts
|
||||
<Settings size={20} className="text-brand-500" />
|
||||
Customizing Your Dashboard
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Weekly Revenue Chart</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
A bar chart showing revenue by day of the week. Helps identify your busiest and most profitable days. Calculated from appointment prices.
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Edit2 size={16} className="text-blue-500" />
|
||||
Edit Layout Mode
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
Click the <strong>"Edit Layout"</strong> button to enter edit mode. While in edit mode you can:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 ml-4 space-y-1">
|
||||
<li className="flex items-center gap-2">
|
||||
<GripVertical size={14} className="text-gray-400" />
|
||||
<strong>Drag widgets</strong> to reposition them on the dashboard
|
||||
</li>
|
||||
<li>• <strong>Drag corners</strong> to resize widgets larger or smaller</li>
|
||||
<li>• <strong>Click the X</strong> on any widget to remove it</li>
|
||||
</ul>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
Click <strong>"Done"</strong> when finished editing.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Weekly Appointments Chart</h4>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Settings size={16} className="text-purple-500" />
|
||||
Widget Configuration
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
A line chart showing appointment counts by day. Useful for staffing decisions and understanding demand patterns throughout the week.
|
||||
Click the <strong>"Widgets"</strong> button to open the widget configuration panel. Here you can toggle widgets on/off and click <strong>"Reset to Default"</strong> to restore the original layout.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Available Widgets Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<LayoutDashboard size={20} className="text-brand-500" />
|
||||
Available Widgets
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Choose from a variety of widgets to build your perfect dashboard:
|
||||
</p>
|
||||
|
||||
{/* Metric Widgets */}
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Metric Widgets</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Calendar size={18} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Total Appointments</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Count with weekly and monthly growth trends
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Users size={18} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Active Customers</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Active customer count with growth trends
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<UserX size={18} className="text-red-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">No-Show Rate</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Percentage of missed appointments with trends
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart Widgets */}
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Chart Widgets</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<BarChart3 size={18} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Weekly Revenue</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Bar chart of revenue by day of week
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<TrendingUp size={18} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Appointments Trend</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Line chart of appointments by day
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity Widgets */}
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Activity & Status Widgets</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Ticket size={18} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Open Tickets</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Support tickets requiring attention
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Activity size={18} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Recent Activity</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Timeline of recent business events
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Users size={18} className="text-cyan-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Capacity Utilization</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Shows booking percentage for each resource
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<PieChart size={18} className="text-violet-500 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Customer Breakdown</h5>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
New vs returning customers this month
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Growth Metrics Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<TrendingUp size={20} className="text-brand-500" />
|
||||
Understanding Growth Metrics
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Metric widgets display both <strong>weekly</strong> and <strong>monthly</strong> growth percentages:
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 text-xs rounded">
|
||||
+12%
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<strong>Green with +</strong> indicates growth compared to the previous period
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="px-2 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 text-xs rounded">
|
||||
-8%
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<strong>Red with -</strong> indicates decline compared to the previous period
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||
Weekly compares this week to last week. Monthly compares this month to last month.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
@@ -155,25 +275,31 @@ const HelpDashboard: React.FC = () => {
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Quick Overview:</strong> See your entire business performance in seconds
|
||||
<strong>Fully Customizable:</strong> Arrange widgets exactly how you want them
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Trend Tracking:</strong> Understand if your business is growing with percentage changes
|
||||
<strong>Persistent Layout:</strong> Your dashboard layout is saved automatically
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Data-Driven Decisions:</strong> Use charts to identify patterns and optimize operations
|
||||
<strong>Real-Time Data:</strong> All metrics update automatically as your data changes
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Real-Time Updates:</strong> All metrics refresh automatically as your data changes
|
||||
<strong>Trend Tracking:</strong> Weekly and monthly growth percentages help you spot patterns
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Resource Insights:</strong> See capacity utilization for each team member
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,52 +1,87 @@
|
||||
/**
|
||||
* Help Plugins Overview Page
|
||||
* Help Plugins Page
|
||||
*
|
||||
* User-friendly help documentation for Plugins.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Puzzle, Store, Code, Zap, Settings,
|
||||
CheckCircle, ChevronRight, HelpCircle, Shield, Download,
|
||||
ArrowLeft,
|
||||
Puzzle,
|
||||
Store,
|
||||
Code,
|
||||
Zap,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
Play,
|
||||
Pause,
|
||||
Settings,
|
||||
Mail,
|
||||
BookOpen,
|
||||
Calendar,
|
||||
ListTodo,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpPlugins: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6">
|
||||
<ArrowLeft size={20} /> Back
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
{t('common.back', 'Back')}
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<Puzzle size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Plugins Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Extend your scheduling platform</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Plugins Guide
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Automate your business with powerful plugins
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Puzzle size={20} className="text-brand-500" /> Overview
|
||||
<Puzzle size={20} className="text-brand-500" />
|
||||
Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Plugins extend the functionality of your scheduling platform. Browse the marketplace for ready-made solutions, or create custom plugins tailored to your specific business needs.
|
||||
Plugins extend your scheduling platform with automation capabilities. Send reminder emails,
|
||||
generate reports, track no-shows, and create custom workflows - all running automatically
|
||||
on schedules you define.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Plugins can automate tasks, integrate with external services, add new features, and customize how your scheduling system works.
|
||||
Browse the marketplace for ready-made plugins, or create your own custom automations
|
||||
using our simple scripting language.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Plugin Areas Section */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Settings size={20} className="text-brand-500" /> Plugin Areas
|
||||
<Settings size={20} className="text-brand-500" />
|
||||
Plugin Areas
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -54,7 +89,7 @@ const HelpPlugins: React.FC = () => {
|
||||
<Store size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Marketplace</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Browse and install plugins from the community</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Browse and install pre-built plugins from our library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
@@ -68,23 +103,65 @@ const HelpPlugins: React.FC = () => {
|
||||
<Code size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Create Plugin</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Build custom plugins with our development tools</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Build custom plugins with our scripting tools</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Zap size={20} className="text-orange-500 mt-0.5" />
|
||||
<ListTodo size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Plugin Actions</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Configure how plugins interact with your system</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Tasks</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">View and manage scheduled plugin executions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What Plugins Can Do */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Download size={20} className="text-brand-500" /> Getting Started
|
||||
<Zap size={20} className="text-brand-500" />
|
||||
What Plugins Can Do
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail size={18} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Send Emails</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Automated reminders, follow-ups, and notifications to customers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Calendar size={18} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Manage Appointments</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Query, filter, and process appointment data automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock size={18} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Run on Schedules</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Execute hourly, daily, weekly, or on custom cron schedules</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Zap size={18} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">External Integrations</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Connect to approved external APIs for advanced workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Getting Started */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Play size={20} className="text-brand-500" />
|
||||
Getting Started
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<ol className="space-y-4">
|
||||
@@ -92,81 +169,101 @@ const HelpPlugins: React.FC = () => {
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">1</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Browse Marketplace</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Explore available plugins by category or search for specific functionality.</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Go to Plugins → Marketplace to explore available plugins.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">2</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Install Plugin</h4>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Install a Plugin</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any plugin to add it to your account.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Configure Settings</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Set up plugin options and connect any required integrations.</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Configure & Schedule</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Set up plugin options and choose when it should run.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Run Plugin</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Execute plugins on-demand or set up automatic triggers.</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Monitor Tasks</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">View execution history and logs in the Tasks page.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Task Management */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Benefits
|
||||
<ListTodo size={20} className="text-brand-500" />
|
||||
Managing Tasks
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Automation:</strong> Automate repetitive tasks and workflows</span>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
When you install a plugin, it creates a scheduled task that runs automatically. Use the Tasks page to:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<Play size={16} className="text-green-500" />
|
||||
<span><strong>Run manually:</strong> Execute a task immediately without waiting for the schedule</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Integration:</strong> Connect with external services and tools</span>
|
||||
<li className="flex items-center gap-2">
|
||||
<Pause size={16} className="text-yellow-500" />
|
||||
<span><strong>Pause/Resume:</strong> Temporarily stop a task without deleting it</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Customization:</strong> Tailor functionality to your needs</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300"><strong>Community:</strong> Access plugins built by other users</span>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-blue-500" />
|
||||
<span><strong>View logs:</strong> See execution history and any errors</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Developer Documentation Link */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Guides</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/plugins/create" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Code size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Creating Plugins</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/plugins/docs" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Shield size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Plugin Documentation</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<BookOpen size={24} className="text-brand-600 dark:text-brand-400 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Creating Custom Plugins
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Want to build your own automations? Our comprehensive developer documentation covers
|
||||
the scripting language, available API methods, and example code.
|
||||
</p>
|
||||
<Link
|
||||
to="/help/plugins/docs"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
View Developer Docs
|
||||
<ChevronRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Need More Help */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Need More Help?
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Our support team is ready to help with any questions about plugins.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -259,9 +259,9 @@ const HelpScheduler: React.FC = () => {
|
||||
<span className="font-medium text-green-900 dark:text-green-200">Green - Completed</span>
|
||||
<span className="text-sm text-green-700 dark:text-green-300">Successfully finished</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-100 dark:bg-gray-700 border-l-4 border-gray-400 rounded">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Gray - No-show</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Customer didn't arrive</span>
|
||||
<div className="flex items-center gap-3 p-3 bg-orange-100 dark:bg-orange-900/50 border-l-4 border-orange-500 rounded">
|
||||
<span className="font-medium text-orange-900 dark:text-orange-200">Orange - No-show</span>
|
||||
<span className="text-sm text-orange-700 dark:text-orange-300">Customer didn't arrive</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-100 dark:bg-gray-700 border-l-4 border-gray-400 rounded opacity-75">
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">Gray (faded) - Cancelled</span>
|
||||
@@ -542,9 +542,9 @@ const HelpScheduler: React.FC = () => {
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<CheckCircle size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Update Status</h4>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Change Status</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Mark as completed, no-show, or cancelled
|
||||
Use the status dropdown to change between Pending, Confirmed, Completed, No-show, or Cancelled
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,12 +560,6 @@ const HelpScheduler: React.FC = () => {
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Drag from where you grab:</strong> When dragging, the appointment time adjusts based on where you clicked on it, making repositioning intuitive
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
/**
|
||||
* Help Settings API Page
|
||||
*
|
||||
* Documentation for managing API tokens for third-party integrations.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Code, Key, Shield, Zap, FileText,
|
||||
CheckCircle, ChevronRight, HelpCircle, Lock,
|
||||
ArrowLeft,
|
||||
Key,
|
||||
Shield,
|
||||
Plus,
|
||||
Copy,
|
||||
Trash2,
|
||||
Clock,
|
||||
Eye,
|
||||
EyeOff,
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
AlertTriangle,
|
||||
Lock,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpSettingsApi: React.FC = () => {
|
||||
@@ -14,144 +29,364 @@ const HelpSettingsApi: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6">
|
||||
<ArrowLeft size={20} /> Back
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<Code size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<Key size={24} className="text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">API Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Integrate with external systems</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
API Settings Guide
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Manage API tokens for integrations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Code size={20} className="text-brand-500" /> Overview
|
||||
<Key size={20} className="text-brand-500" />
|
||||
Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The API allows you to programmatically access your scheduling data. Create integrations with your website, mobile app, or other business systems.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
API access requires a Business plan or higher.
|
||||
API Settings allows you to create and manage API tokens for integrating your scheduling
|
||||
system with external applications. These tokens provide secure, controlled access to your
|
||||
business data through our REST API.
|
||||
</p>
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<Lock size={18} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
API access requires a plan with the <strong>api_access</strong> feature. Only the business
|
||||
owner can manage API tokens.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Creating API Tokens */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Key size={20} className="text-brand-500" /> API Keys
|
||||
<Plus size={20} className="text-brand-500" />
|
||||
Creating API Tokens
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Key size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Generate Keys</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Create new API keys for authentication</p>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Click the <strong>New Token</strong> button to create a new API token. You'll need to configure:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Token Name */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Token Name</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Give your token a descriptive name to identify its purpose, like "Website Integration"
|
||||
or "Mobile App".
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Lock size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Key Permissions</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Set read/write access levels per key</p>
|
||||
</div>
|
||||
|
||||
{/* Permission Presets */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Permission Presets</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Choose from predefined permission sets:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<Eye size={14} className="text-blue-500 mt-1 flex-shrink-0" />
|
||||
<span><strong>Read Only</strong> - Can only view data, no modifications</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Shield size={14} className="text-green-500 mt-1 flex-shrink-0" />
|
||||
<span><strong>Full Access</strong> - Complete read and write access</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Settings size={14} className="text-purple-500 mt-1 flex-shrink-0" />
|
||||
<span><strong>Custom</strong> - Select individual permissions</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Shield size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Revoke Keys</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Disable compromised or unused keys</p>
|
||||
</div>
|
||||
|
||||
{/* Individual Permissions */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Individual Permissions</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Expand "Show individual permissions" for granular control over what the token can access:
|
||||
appointments, customers, services, resources, and more.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Zap size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Rate Limits</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">View your API usage and limits</p>
|
||||
</div>
|
||||
|
||||
{/* Expiration */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Clock size={16} className="text-orange-500" />
|
||||
Expiration
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
Set when the token should expire:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 grid grid-cols-2 gap-2">
|
||||
<li>Never expires</li>
|
||||
<li>7 days</li>
|
||||
<li>30 days</li>
|
||||
<li>90 days</li>
|
||||
<li>1 year</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Token Security */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<FileText size={20} className="text-brand-500" /> Available Endpoints
|
||||
<AlertTriangle size={20} className="text-brand-500" />
|
||||
Token Security
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="space-y-3 font-mono text-sm">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/appointments</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/customers</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/services</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/resources</span>
|
||||
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 rounded-xl border border-yellow-200 dark:border-yellow-800 p-6">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle size={24} className="text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium text-yellow-800 dark:text-yellow-200 mb-1">
|
||||
Important: Copy Your Token Immediately
|
||||
</h4>
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
When a token is created, you'll see the full token <strong>only once</strong>. Copy it
|
||||
immediately and store it securely. You will not be able to view it again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">See full documentation for complete endpoint list.</p>
|
||||
<div className="space-y-2 text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<p className="flex items-center gap-2">
|
||||
<Copy size={14} className="flex-shrink-0" />
|
||||
Use the Copy button to copy the token to your clipboard
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<EyeOff size={14} className="flex-shrink-0" />
|
||||
Toggle visibility with the eye icon to verify the token
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Managing Tokens */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Security Best Practices
|
||||
<Shield size={20} className="text-brand-500" />
|
||||
Managing Existing Tokens
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Your tokens are organized into <strong>Active Tokens</strong> and <strong>Revoked Tokens</strong>.
|
||||
Click on any token to expand and view its details.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Token Information */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Token Information</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||
Each token displays:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 ml-4 list-disc">
|
||||
<li>Token name and status (Active, Expired, or Revoked)</li>
|
||||
<li>Key prefix (first few characters of the token)</li>
|
||||
<li>Created date and who created it</li>
|
||||
<li>Last used date</li>
|
||||
<li>Expiration date</li>
|
||||
<li>Assigned permissions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Revoking Tokens */}
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<h4 className="font-medium text-red-800 dark:text-red-200 mb-2 flex items-center gap-2">
|
||||
<Trash2 size={16} />
|
||||
Revoking Tokens
|
||||
</h4>
|
||||
<p className="text-sm text-red-700 dark:text-red-300 mb-2">
|
||||
Click the trash icon to revoke a token. This action:
|
||||
</p>
|
||||
<ul className="text-sm text-red-700 dark:text-red-300 space-y-1 ml-4 list-disc">
|
||||
<li><strong>Cannot be undone</strong></li>
|
||||
<li>Immediately disables API access for that token</li>
|
||||
<li>Any applications using the token will lose access instantly</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Best Practices */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" />
|
||||
Best Practices
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Never share API keys in public code repositories</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>One token per integration</strong> - Create separate tokens for each application
|
||||
or service that needs API access
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Use separate keys for different integrations</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Minimum permissions</strong> - Grant only the permissions each integration needs
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Rotate keys periodically for security</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Set expiration dates</strong> - Use expiring tokens for temporary integrations
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Grant minimum required permissions</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Never share tokens publicly</strong> - Don't commit tokens to version control
|
||||
or share them in public channels
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Rotate tokens regularly</strong> - Create new tokens and revoke old ones periodically
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Monitor last used dates</strong> - Revoke tokens that haven't been used recently
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Step by Step */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Step-by-Step: Create an API Token
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<ol className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
1
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Go to <strong>Settings > API Settings</strong>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
2
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Click the <strong>New Token</strong> button
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
3
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Enter a descriptive <strong>Token Name</strong> (e.g., "Website Booking Integration")
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
4
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Select a <strong>Permission Preset</strong> or customize individual permissions
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
5
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Choose an <strong>Expiration</strong> period if desired
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
6
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Click <strong>Create Token</strong>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
7
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>Immediately copy the token</strong> and store it securely - you won't see it again!
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Features */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Related Features
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/plugins" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Zap size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Plugins Guide</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/auth" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Link
|
||||
to="/help/settings/auth"
|
||||
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
|
||||
>
|
||||
<Shield size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Authentication Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/help/plugins"
|
||||
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
|
||||
>
|
||||
<Settings size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Plugins Guide</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Need Help */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Need More Help?
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Our support team is ready to help with API integration questions.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* Help Settings Appearance Page
|
||||
* Help Settings Appearance (Branding) Page
|
||||
*
|
||||
* Comprehensive documentation for the Branding Settings page.
|
||||
* Documents: Logo uploads (website/email), display modes, color palettes, custom colors with live preview.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Palette, Image, Type, Layout, Eye,
|
||||
CheckCircle, ChevronRight, HelpCircle, Sun, Moon,
|
||||
ArrowLeft, Palette, Image, Upload, Eye, Save,
|
||||
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Sparkles,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpSettingsAppearance: React.FC = () => {
|
||||
@@ -18,134 +21,365 @@ const HelpSettingsAppearance: React.FC = () => {
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<Palette size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<Palette size={24} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Appearance Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Customize your brand and look</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Branding Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Customize your logos and brand colors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Palette size={20} className="text-brand-500" /> Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Appearance Settings let you customize how your booking page looks to customers. Upload your logo, set brand colors, and create a cohesive visual experience that matches your brand identity.
|
||||
The Branding Settings page lets you customize how your business appears throughout SmoothSchedule.
|
||||
Upload your logos, choose from preset color palettes, or define custom brand colors. Changes
|
||||
preview instantly so you can see exactly how they'll look before saving.
|
||||
</p>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Owner Access Required:</strong> Only business owners can modify branding settings.
|
||||
This feature may be limited based on your subscription plan.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Brand Logos */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Image size={20} className="text-brand-500" /> Branding Options
|
||||
<Image size={20} className="text-brand-500" /> Brand Logos
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Image size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Logo</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Upload your business logo for the booking page</p>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
You can upload two different logos for different contexts. PNG images with transparent
|
||||
backgrounds are recommended for the best appearance across light and dark themes.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Website Logo */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Image size={18} className="text-blue-500" /> Website Logo
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
This logo appears in the sidebar navigation and on customer-facing booking pages.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Recommended Size:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-2">500×500 pixels</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Supported Formats:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-2">PNG, JPEG, SVG</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Image size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Favicon</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Small icon shown in browser tabs</p>
|
||||
|
||||
{/* Email Logo */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Image size={18} className="text-green-500" /> Email Logo
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
This logo appears at the top of all email notifications sent to customers (booking
|
||||
confirmations, reminders, etc.). A wider format works best for email headers.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Recommended Size:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-2">600×200 pixels (wide)</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Supported Formats:</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-2">PNG, JPEG, SVG</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload Instructions */}
|
||||
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Uploading a Logo</h4>
|
||||
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
|
||||
<li>Click the <strong>Upload</strong> button next to the logo type</li>
|
||||
<li>Select an image file from your computer</li>
|
||||
<li>The logo preview will update immediately</li>
|
||||
<li>Click <strong>Save Changes</strong> to apply the logo</li>
|
||||
</ol>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-3">
|
||||
To remove a logo, click the × button on the preview image, then save.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Logo Display Mode */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Eye size={20} className="text-brand-500" /> Logo Display Mode
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Control how your website logo is displayed in the sidebar and on booking pages.
|
||||
This setting only affects the website logo, not the email logo.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Palette size={20} className="text-purple-500 mt-0.5" />
|
||||
<div className="w-8 h-8 rounded bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold text-gray-500 dark:text-gray-400">
|
||||
Aa
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Brand Color</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Primary color used throughout the interface</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Text Only</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Display only your business name in text. Good if you don't have a logo or prefer a clean look.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Type size={20} className="text-orange-500 mt-0.5" />
|
||||
<div className="w-8 h-8 rounded bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center">
|
||||
<Image size={16} className="text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Custom CSS</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Advanced styling with custom CSS (Pro plan)</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Logo Only</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Display only your uploaded logo image without text. Best for recognizable brand marks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="w-8 h-8 rounded bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<Image size={12} className="text-brand-500" />
|
||||
<span className="text-[8px] font-bold text-brand-600">+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Logo and Text</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Display both your logo and business name together. Ideal for full brand recognition.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Color Palettes */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Layout size={20} className="text-brand-500" /> Theme Options
|
||||
<Palette size={20} className="text-purple-500" /> Color Palettes
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Sun size={20} className="text-yellow-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Light Mode</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Clean, bright interface for daytime use</p>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Choose from 10 professionally designed color palettes. Each palette includes a primary
|
||||
and secondary color that work well together. Click any palette to instantly preview how
|
||||
it looks throughout the interface.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
|
||||
{[
|
||||
{ name: 'Ocean Blue', colors: ['#2563eb', '#0ea5e9'] },
|
||||
{ name: 'Sky Blue', colors: ['#0ea5e9', '#38bdf8'] },
|
||||
{ name: 'Mint Green', colors: ['#10b981', '#34d399'] },
|
||||
{ name: 'Coral Reef', colors: ['#f97316', '#fb923c'] },
|
||||
{ name: 'Lavender', colors: ['#a78bfa', '#c4b5fd'] },
|
||||
{ name: 'Rose Pink', colors: ['#ec4899', '#f472b6'] },
|
||||
{ name: 'Forest Green', colors: ['#059669', '#10b981'] },
|
||||
{ name: 'Royal Purple', colors: ['#7c3aed', '#a78bfa'] },
|
||||
{ name: 'Slate Gray', colors: ['#475569', '#64748b'] },
|
||||
{ name: 'Crimson Red', colors: ['#dc2626', '#ef4444'] },
|
||||
].map((palette) => (
|
||||
<div key={palette.name} className="text-center">
|
||||
<div
|
||||
className="h-6 rounded-md mb-1"
|
||||
style={{ background: `linear-gradient(to right, ${palette.colors[0]}, ${palette.colors[1]})` }}
|
||||
/>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">{palette.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Moon size={20} className="text-indigo-500 mt-0.5" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles size={18} className="text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Dark Mode</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Easy on the eyes in low-light conditions</p>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Live Preview</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
When you click a palette, the interface instantly updates to show those colors.
|
||||
If you navigate away without saving, colors revert to your saved settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Custom Colors */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Palette size={20} className="text-brand-500" /> Custom Colors
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
If the preset palettes don't match your brand, you can define your own colors using
|
||||
the color picker or by entering hex color codes directly.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Primary Color</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Used for buttons, links, and interactive elements. This is your main brand color.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Secondary Color</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Used for accents, hover states, and gradients. Should complement the primary color.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Setting Custom Colors</h4>
|
||||
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
|
||||
<li>Click the color swatch to open the color picker</li>
|
||||
<li>Select a color visually or enter a hex code (e.g., #3b82f6)</li>
|
||||
<li>The preview bar shows the gradient of your primary and secondary colors</li>
|
||||
<li>Click <strong>Save Changes</strong> to apply your custom colors</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Saving Changes */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Save size={20} className="text-brand-500" /> Saving Changes
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
All branding changes (logos, display mode, and colors) are saved together when you
|
||||
click the Save Changes button at the bottom of the page.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<CheckCircle size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Changes Save Together</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Logos, display mode, and colors are all saved in one action
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<AlertCircle size={18} className="text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Unsaved Changes Revert</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
If you leave the page without saving, colors revert to your last saved settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tips */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Tips
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<div className="bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Use a high-resolution logo (at least 200x200 pixels)</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Use PNG images with transparent backgrounds for logos to look best on both light and dark themes
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Choose brand colors that provide good contrast for readability</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Test color readability by previewing your booking page in both light and dark modes
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Test your booking page in both light and dark modes</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Choose colors that provide good contrast for text readability and accessibility
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Consider using your existing brand colors from your website or marketing materials
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Features */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Settings</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Eye size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">General Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/booking" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Upload size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Booking Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/email" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Image size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Email Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/domains" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Layout size={20} className="text-brand-500" />
|
||||
<Palette size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Custom Domains</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Help Footer */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Our support team is ready to help with any branding questions.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
/**
|
||||
* Help Settings Authentication Page
|
||||
*
|
||||
* Documentation for configuring OAuth providers and social login for customers.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Shield, Key, Lock, Users, Smartphone,
|
||||
CheckCircle, ChevronRight, HelpCircle, LogIn,
|
||||
ArrowLeft,
|
||||
Lock,
|
||||
Users,
|
||||
Key,
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
Save,
|
||||
Eye,
|
||||
EyeOff,
|
||||
AlertCircle,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpSettingsAuth: React.FC = () => {
|
||||
@@ -14,137 +26,328 @@ const HelpSettingsAuth: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6">
|
||||
<ArrowLeft size={20} /> Back
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<Shield size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<Lock size={24} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Authentication Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Configure login and security</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Authentication Settings Guide
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Configure social login for customers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Shield size={20} className="text-brand-500" /> Overview
|
||||
<Lock size={20} className="text-brand-500" />
|
||||
Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Authentication Settings control how users log in to your system. Configure security policies, enable two-factor authentication, and manage social login options.
|
||||
Authentication Settings allows you to configure how customers can sign in to your booking
|
||||
system. Enable social login providers like Google, Apple, and Facebook to make it easier
|
||||
for customers to create accounts and book appointments.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Lock size={20} className="text-brand-500" /> Security Features
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Smartphone size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Two-Factor Auth (2FA)</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Require additional verification for logins</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Key size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Password Policies</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Set minimum password requirements</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Lock size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Session Timeout</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Auto-logout after inactivity</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Shield size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Login Attempts</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Lock accounts after failed attempts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||
<Lock size={18} className="text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-purple-800 dark:text-purple-200">
|
||||
OAuth settings require a plan with the <strong>custom_oauth</strong> feature. Only the
|
||||
business owner can configure authentication settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Social Login */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<LogIn size={20} className="text-brand-500" /> Login Options
|
||||
<Users size={20} className="text-brand-500" />
|
||||
Social Login Providers
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Enable or disable social login providers that customers can use to sign in. Click on a
|
||||
provider to toggle it on or off.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🔍</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Google</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🍎</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Apple</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">📘</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Facebook</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">💼</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">LinkedIn</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🪧</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Microsoft</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🐦</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">X (Twitter)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🎮</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Twitch</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Check size={18} className="text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Enabled providers show a checkmark badge. Click <strong>Save</strong> after making changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* OAuth Settings */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Key size={20} className="text-brand-500" />
|
||||
OAuth Settings
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Configure additional options for OAuth authentication:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Key size={20} className="text-gray-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Email & Password</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Traditional login with email and password</p>
|
||||
{/* Allow Registration */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Allow OAuth Registration</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
When enabled, new customers can create accounts using OAuth providers. When disabled,
|
||||
only existing customers can sign in via OAuth.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Users size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Social Login</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sign in with Google, Apple, or other providers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Shield size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">SSO (Single Sign-On)</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Enterprise integration with SAML/OIDC</p>
|
||||
|
||||
{/* Auto-link by Email */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Auto-link by Email</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
When enabled, if a customer signs in with OAuth and their email matches an existing
|
||||
account, the accounts are automatically linked. This lets customers sign in with
|
||||
multiple methods.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<Save size={18} className="text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
Click the <strong>Save</strong> button to apply your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Custom OAuth Credentials */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Security Tips
|
||||
<Key size={20} className="text-brand-500" />
|
||||
Custom OAuth Credentials
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-4">
|
||||
<AlertCircle size={18} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
This section only appears if your platform administrator has granted permission to
|
||||
manage OAuth credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
By default, your business uses the platform's shared OAuth credentials. If you want
|
||||
complete branding control, you can use your own OAuth app credentials.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Toggle Custom Credentials */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Use Custom Credentials</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Toggle this on to use your own OAuth credentials instead of the platform's shared
|
||||
credentials. When using custom credentials, the OAuth consent screen will show your
|
||||
app name and branding.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Provider Credentials */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Provider Credentials</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Expand each provider to enter your credentials:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-2 ml-4 list-disc">
|
||||
<li><strong>Client ID</strong> - Your OAuth app's client identifier</li>
|
||||
<li><strong>Client Secret</strong> - Your OAuth app's secret key</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Provider-specific Fields */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Provider-Specific Fields</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Some providers require additional information:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-2 ml-4 list-disc">
|
||||
<li><strong>Apple</strong> - Team ID and Key ID</li>
|
||||
<li><strong>Microsoft</strong> - Tenant ID (or "common" for multi-tenant)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Eye size={18} className="text-gray-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Client secrets are hidden by default. Click the <EyeOff size={14} className="inline" /> icon
|
||||
to reveal them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Step by Step */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Step-by-Step: Enable Social Login
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<ol className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
1
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Go to <strong>Settings > Authentication</strong>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
2
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
In the <strong>Social Login</strong> section, click on the providers you want to enable
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
3
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Configure <strong>Allow OAuth Registration</strong> if you want new customers to sign up
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
4
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Enable <strong>Auto-link by Email</strong> to let customers use multiple sign-in methods
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||
5
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Click <strong>Save</strong> to apply your changes
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tips */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" />
|
||||
Tips
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Enable 2FA for all staff accounts</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Enable Google and Apple at minimum - these are the most popular sign-in methods
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Use strong, unique passwords for each service</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Keep <strong>Auto-link by Email</strong> enabled to prevent duplicate accounts
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Review login activity regularly</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Only enable <strong>Allow OAuth Registration</strong> if you want new customers to create
|
||||
accounts via OAuth
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Set appropriate session timeouts</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Custom OAuth credentials are recommended for businesses that want their own branding
|
||||
on the sign-in consent screen
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Features */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Related Features
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/staff" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Link
|
||||
to="/help/customers"
|
||||
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
|
||||
>
|
||||
<Users size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Staff Guide</span>
|
||||
<span className="text-gray-900 dark:text-white">Customers Guide</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/api" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Link
|
||||
to="/help/settings/api"
|
||||
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
|
||||
>
|
||||
<Key size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">API Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
@@ -152,11 +355,21 @@ const HelpSettingsAuth: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Need Help */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Need More Help?
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Our support team is ready to help with authentication configuration.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
/**
|
||||
* Help Settings Billing Page
|
||||
*
|
||||
* Comprehensive documentation for the Plan & Billing Settings page.
|
||||
* Documents: Current plan, subscriptions, add-ons, payment methods, invoices, upgrade flow.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, CreditCard, FileText, Clock, TrendingUp, Shield,
|
||||
CheckCircle, ChevronRight, HelpCircle, DollarSign,
|
||||
ArrowLeft, CreditCard, Crown, Package, Wallet, FileText,
|
||||
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Calendar,
|
||||
Plus, Trash2, Star, RotateCcw, X, Zap, ArrowRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpSettingsBilling: React.FC = () => {
|
||||
@@ -18,135 +22,351 @@ const HelpSettingsBilling: React.FC = () => {
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
<CreditCard size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
|
||||
<CreditCard size={24} className="text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Billing Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Manage your subscription and payments</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Plan & Billing Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Manage your subscription, add-ons, and payment methods</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CreditCard size={20} className="text-brand-500" /> Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Billing Settings let you manage your subscription plan, update payment methods, view invoices, and monitor your usage. Keep your account in good standing to maintain access to all features.
|
||||
The Plan & Billing page is where you manage your SmoothSchedule subscription. View your current plan,
|
||||
upgrade or change plans, add feature add-ons, manage payment methods, and access your billing history.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<DollarSign size={20} className="text-brand-500" /> Subscription Management
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<TrendingUp size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Current Plan</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">View your active subscription tier and features</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<CreditCard size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Payment Method</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Update credit card or payment details</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<FileText size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Invoices</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Download past invoices and receipts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Billing Cycle</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">View next billing date and renewal info</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Owner Access Required:</strong> Only business owners can access billing settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Current Plan */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Shield size={20} className="text-brand-500" /> Plan Features
|
||||
<Crown size={20} className="text-amber-500" /> Current Plan
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Different plans unlock different features:
|
||||
The Current Plan section shows your active subscription tier, its monthly price, and included features.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Free</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Basic scheduling, limited resources</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Pro</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- More resources, custom branding, payments</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Business</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Unlimited resources, API access, priority support</span>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">What You'll See</h4>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<li>• <strong>Plan Name:</strong> Your current tier (Free, Starter, Professional, Enterprise)</li>
|
||||
<li>• <strong>Monthly Price:</strong> What you pay per month (or "Contact Us" for Enterprise)</li>
|
||||
<li>• <strong>Key Features:</strong> List of what's included in your plan</li>
|
||||
<li>• <strong>Upgrade Button:</strong> Opens the plan selection modal</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Active Subscriptions */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Calendar size={20} className="text-blue-500" /> Active Subscriptions
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
This section lists all your active subscriptions, including your main plan and any add-ons you've purchased.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Subscription Details */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">For Each Subscription</h4>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<li>• <strong>Plan Name:</strong> The subscription's name</li>
|
||||
<li>• <strong>Type Badge:</strong> "Plan" for main subscription, "Add-on" for extras</li>
|
||||
<li>• <strong>Price & Interval:</strong> Amount charged and billing frequency (monthly/yearly)</li>
|
||||
<li>• <strong>Next Billing Date:</strong> When you'll be charged next</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Cancellation States */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<X size={16} className="text-red-500" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Cancel Subscription</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Cancels your subscription. You can choose to cancel at period end (keeps access until then) or immediately.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<RotateCcw size={16} className="text-green-500" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Reactivate</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
If you've cancelled but your subscription hasn't ended yet, you can reactivate it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cancelling Status */}
|
||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm mb-1">Cancelling Status</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
If you cancelled at period end, the subscription shows a "Cancelling" badge and displays when it will end.
|
||||
You can still reactivate before that date.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Available Add-ons */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Package size={20} className="text-purple-500" /> Available Add-ons
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Add-ons let you enhance your subscription with additional features beyond what's included in your base plan.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">How Add-ons Work</h4>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<li>• Each add-on has its own monthly price</li>
|
||||
<li>• Click "Add" to purchase - you'll be taken to Stripe checkout</li>
|
||||
<li>• Add-ons appear in your Active Subscriptions once purchased</li>
|
||||
<li>• Cancel anytime just like your main subscription</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Wallet */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Wallet size={20} className="text-blue-500" /> Wallet
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Wallet section provides a quick link to manage your communication credits (SMS & Calling).
|
||||
Credits are used for sending SMS reminders and making calls through the platform.
|
||||
</p>
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
To manage credits, top up your balance, or configure auto-reload, go to{' '}
|
||||
<strong>Settings → SMS & Calling</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Payment Methods */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CreditCard size={20} className="text-purple-500" /> Payment Methods
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Manage the credit cards used for your subscriptions and credit purchases.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Card Display */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Each Card Shows</h4>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<li>• <strong>Card Brand:</strong> Visa, Mastercard, Amex, etc.</li>
|
||||
<li>• <strong>Last 4 Digits:</strong> •••• 4242</li>
|
||||
<li>• <strong>Expiration:</strong> Month/Year the card expires</li>
|
||||
<li>• <strong>Default Badge:</strong> Shows which card is used for automatic payments</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Plus size={16} className="text-brand-500" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Add Card</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Opens Stripe to securely add a new card
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Star size={16} className="text-amber-500" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Set Default</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Make a card the default for payments
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Trash2 size={16} className="text-red-500" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Remove</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Delete a card from your account
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Billing History */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<FileText size={20} className="text-gray-500" /> Billing History
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
View and download invoices for all your past payments. Invoices are generated automatically
|
||||
after each successful charge.
|
||||
</p>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Each invoice includes the date, amount, what was purchased, and a download link for your records.
|
||||
Use these for expense tracking, tax purposes, or reimbursement requests.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Upgrading Your Plan */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<ArrowRight size={20} className="text-brand-500" /> Upgrading Your Plan
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Click the "Upgrade Plan" button to see all available plans and change your subscription.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">How to Upgrade</h4>
|
||||
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
|
||||
<li>Click <strong>Upgrade Plan</strong> in the Current Plan section</li>
|
||||
<li>Review the available plans and their features</li>
|
||||
<li>Click <strong>Upgrade</strong> on your desired plan</li>
|
||||
<li>Complete checkout through Stripe</li>
|
||||
<li>Your new plan activates immediately</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Plan Comparison Note */}
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<Zap size={18} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Transaction Fees</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Higher tier plans include lower transaction fees on payment processing.
|
||||
Enterprise plans offer custom pricing - contact support for details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tips */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-brand-500" /> Tips
|
||||
</h2>
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<div className="bg-gradient-to-r from-brand-50 to-emerald-50 dark:from-brand-900/20 dark:to-emerald-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Keep payment methods up to date to avoid service interruption</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Keep payment methods up to date to avoid service interruption
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Annual billing saves money compared to monthly</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
If you cancel at period end, you can reactivate anytime before it expires
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Download invoices for your accounting records</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Download invoices regularly for your accounting records
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Add-ons are billed separately and can be cancelled independently
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Features */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Settings</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Link to="/help/settings/quota" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<TrendingUp size={20} className="text-brand-500" />
|
||||
<Calendar size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Usage & Quota</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/payments" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<DollarSign size={20} className="text-brand-500" />
|
||||
<CreditCard size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Payments Guide</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<CheckCircle size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">General Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
<Link to="/help/settings/appearance" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
|
||||
<Crown size={20} className="text-brand-500" />
|
||||
<span className="text-gray-900 dark:text-white">Branding Settings</span>
|
||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Help Footer */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
||||
<button onClick={() => navigate('/tickets')} className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">Contact Support</button>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Our support team is ready to help with billing questions.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* Help Settings Booking Page
|
||||
*
|
||||
* Comprehensive documentation for the Booking Settings page.
|
||||
* Documents: Booking URL display/sharing, Return URL configuration.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, Calendar, Clock, Bell, Shield, CreditCard,
|
||||
CheckCircle, ChevronRight, HelpCircle, AlertCircle,
|
||||
ArrowLeft, Calendar, Link2, ExternalLink, Copy, Share2,
|
||||
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Globe,
|
||||
} from 'lucide-react';
|
||||
|
||||
const HelpSettingsBooking: React.FC = () => {
|
||||
@@ -18,6 +21,7 @@ const HelpSettingsBooking: React.FC = () => {
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||
@@ -25,141 +29,286 @@ const HelpSettingsBooking: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Booking Settings Guide</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Configure how customers book appointments</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Configure your booking URL and customer redirect</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Calendar size={20} className="text-brand-500" /> Overview
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Booking Settings control how customers interact with your scheduling system. Configure booking windows, cancellation policies, confirmation requirements, and more.
|
||||
The Booking Settings page is where you find your public booking URL to share with customers
|
||||
and configure where customers go after completing a booking. This is an owner-only section
|
||||
that controls how customers access your scheduling system.
|
||||
</p>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Owner Access Required:</strong> Only business owners can view and modify booking settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Your Booking URL */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Clock size={20} className="text-brand-500" /> Time Settings
|
||||
<Link2 size={20} className="text-brand-500" /> Your Booking URL
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Booking Window</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">How far in advance customers can book</p>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Your booking URL is the public link where customers can view your services and book appointments.
|
||||
It's based on your business subdomain and is automatically generated when you create your business.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* URL Format */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">URL Format</h4>
|
||||
<code className="text-sm font-mono text-brand-600 dark:text-brand-400">
|
||||
https://[your-subdomain].smoothschedule.com
|
||||
</code>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
Your subdomain was set when you created your business and cannot be changed.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<AlertCircle size={20} className="text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Minimum Notice</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Minimum time before appointment for booking</p>
|
||||
|
||||
{/* Actions Available */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Copy size={20} className="text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Copy to Clipboard</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Click the copy icon to copy the full URL to your clipboard for easy sharing
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock size={20} className="text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Buffer Time</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Gap between consecutive appointments</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Calendar size={20} className="text-purple-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Time Slots</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Interval between available booking times</p>
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<ExternalLink size={20} className="text-green-500 mt-0.5" />
|
||||