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:
179
PLAN_HELP_DOCS.md
Normal file
179
PLAN_HELP_DOCS.md
Normal file
@@ -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
|
||||||
11
deploy.sh
11
deploy.sh
@@ -98,6 +98,17 @@ docker compose -f docker-compose.production.yml exec -T django sh -c 'export DAT
|
|||||||
echo ">>> Collecting static files..."
|
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'
|
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..."
|
echo ">>> Checking container status..."
|
||||||
docker compose -f docker-compose.production.yml ps
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
|
||||||
|
|||||||
68
frontend/package-lock.json
generated
68
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@stripe/react-stripe-js": "^5.4.1",
|
"@stripe/react-stripe-js": "^5.4.1",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
|
"@types/react-grid-layout": "^1.3.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-grid-layout": "^1.5.2",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-i18next": "^16.3.5",
|
"react-i18next": "^16.3.5",
|
||||||
"react-phone-number-input": "^3.4.14",
|
"react-phone-number-input": "^3.4.14",
|
||||||
@@ -1960,6 +1962,15 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/@types/react-syntax-highlighter": {
|
||||||
"version": "15.5.13",
|
"version": "15.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
"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==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"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"
|
"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": {
|
"node_modules/react-hot-toast": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||||
@@ -4477,6 +4526,19 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-router": {
|
||||||
"version": "7.9.6",
|
"version": "7.9.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
"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==",
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"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/react-stripe-js": "^5.4.1",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@tanstack/react-query": "^5.90.10",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
|
"@types/react-grid-layout": "^1.3.6",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-grid-layout": "^1.5.2",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-i18next": "^16.3.5",
|
"react-i18next": "^16.3.5",
|
||||||
"react-phone-number-input": "^3.4.14",
|
"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 HelpGuide = React.lazy(() => import('./pages/HelpGuide')); // Import Platform Guide page
|
||||||
const HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing
|
const HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing
|
||||||
const HelpApiDocs = React.lazy(() => import('./pages/HelpApiDocs')); // Import API documentation page
|
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
|
const HelpEmailSettings = React.lazy(() => import('./pages/HelpEmailSettings')); // Import Email settings help page
|
||||||
|
|
||||||
// Import new help pages
|
// 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 HelpMessages = React.lazy(() => import('./pages/help/HelpMessages'));
|
||||||
const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments'));
|
const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments'));
|
||||||
const HelpPlugins = React.lazy(() => import('./pages/help/HelpPlugins'));
|
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 HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral'));
|
||||||
const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes'));
|
const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes'));
|
||||||
const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking'));
|
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 HelpSettingsAuth = React.lazy(() => import('./pages/help/HelpSettingsAuth'));
|
||||||
const HelpSettingsBilling = React.lazy(() => import('./pages/help/HelpSettingsBilling'));
|
const HelpSettingsBilling = React.lazy(() => import('./pages/help/HelpSettingsBilling'));
|
||||||
const HelpSettingsQuota = React.lazy(() => import('./pages/help/HelpSettingsQuota'));
|
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 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 PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); // Import Plugin Marketplace page
|
||||||
const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins 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="/scheduler" element={<Scheduler />} />
|
||||||
<Route path="/tickets" element={<Tickets />} />
|
<Route path="/tickets" element={<Tickets />} />
|
||||||
|
<Route path="/help" element={<HelpComprehensive />} />
|
||||||
<Route path="/help/guide" element={<HelpGuide />} />
|
<Route path="/help/guide" element={<HelpGuide />} />
|
||||||
<Route path="/help/ticketing" element={<HelpTicketing />} />
|
<Route path="/help/ticketing" element={<HelpTicketing />} />
|
||||||
<Route path="/help/api" element={<HelpApiDocs />} />
|
<Route path="/help/api" element={<HelpApiDocs />} />
|
||||||
@@ -629,7 +630,6 @@ const AppContent: React.FC = () => {
|
|||||||
<Route path="/help/messages" element={<HelpMessages />} />
|
<Route path="/help/messages" element={<HelpMessages />} />
|
||||||
<Route path="/help/payments" element={<HelpPayments />} />
|
<Route path="/help/payments" element={<HelpPayments />} />
|
||||||
<Route path="/help/plugins" element={<HelpPlugins />} />
|
<Route path="/help/plugins" element={<HelpPlugins />} />
|
||||||
<Route path="/help/plugins/create" element={<HelpCreatePlugin />} />
|
|
||||||
<Route path="/help/settings/general" element={<HelpSettingsGeneral />} />
|
<Route path="/help/settings/general" element={<HelpSettingsGeneral />} />
|
||||||
<Route path="/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
|
<Route path="/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
|
||||||
<Route path="/help/settings/booking" element={<HelpSettingsBooking />} />
|
<Route path="/help/settings/booking" element={<HelpSettingsBooking />} />
|
||||||
|
|||||||
268
frontend/src/components/QuotaOverageModal.tsx
Normal file
268
frontend/src/components/QuotaOverageModal.tsx
Normal file
@@ -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}
|
icon={Clock}
|
||||||
label={t('nav.tasks', 'Tasks')}
|
label={t('nav.tasks', 'Tasks')}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
|
locked={!canUse('plugins') || !canUse('tasks')}
|
||||||
/>
|
/>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
|||||||
{canViewAdminPages && (
|
{canViewAdminPages && (
|
||||||
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
|
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="/plugins/marketplace"
|
to="/plugins/my-plugins"
|
||||||
icon={Plug}
|
icon={Plug}
|
||||||
label={t('nav.plugins', 'Plugins')}
|
label={t('nav.plugins', 'Plugins')}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
|
|||||||
140
frontend/src/components/dashboard/CapacityWidget.tsx
Normal file
140
frontend/src/components/dashboard/CapacityWidget.tsx
Normal file
@@ -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;
|
||||||
105
frontend/src/components/dashboard/ChartWidget.tsx
Normal file
105
frontend/src/components/dashboard/ChartWidget.tsx
Normal file
@@ -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;
|
||||||
134
frontend/src/components/dashboard/CustomerBreakdownWidget.tsx
Normal file
134
frontend/src/components/dashboard/CustomerBreakdownWidget.tsx
Normal file
@@ -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;
|
||||||
90
frontend/src/components/dashboard/MetricWidget.tsx
Normal file
90
frontend/src/components/dashboard/MetricWidget.tsx
Normal file
@@ -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;
|
||||||
144
frontend/src/components/dashboard/NoShowRateWidget.tsx
Normal file
144
frontend/src/components/dashboard/NoShowRateWidget.tsx
Normal file
@@ -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;
|
||||||
121
frontend/src/components/dashboard/OpenTicketsWidget.tsx
Normal file
121
frontend/src/components/dashboard/OpenTicketsWidget.tsx
Normal file
@@ -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;
|
||||||
144
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
144
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
@@ -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;
|
||||||
131
frontend/src/components/dashboard/WidgetConfigModal.tsx
Normal file
131
frontend/src/components/dashboard/WidgetConfigModal.tsx
Normal file
@@ -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;
|
||||||
9
frontend/src/components/dashboard/index.ts
Normal file
9
frontend/src/components/dashboard/index.ts
Normal file
@@ -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';
|
||||||
146
frontend/src/components/dashboard/types.ts
Normal file
146
frontend/src/components/dashboard/types.ts
Normal file
@@ -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',
|
white_label: 'White Label',
|
||||||
custom_oauth: 'Custom OAuth',
|
custom_oauth: 'Custom OAuth',
|
||||||
plugins: 'Custom Plugins',
|
plugins: 'Custom Plugins',
|
||||||
|
tasks: 'Scheduled Tasks',
|
||||||
export_data: 'Data Export',
|
export_data: 'Data Export',
|
||||||
video_conferencing: 'Video Conferencing',
|
video_conferencing: 'Video Conferencing',
|
||||||
two_factor_auth: 'Two-Factor Authentication',
|
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',
|
white_label: 'Remove SmoothSchedule branding and use your own',
|
||||||
custom_oauth: 'Configure your own OAuth credentials for social login',
|
custom_oauth: 'Configure your own OAuth credentials for social login',
|
||||||
plugins: 'Create custom plugins to extend functionality',
|
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',
|
export_data: 'Export your data to CSV or other formats',
|
||||||
video_conferencing: 'Add video conferencing links to appointments',
|
video_conferencing: 'Add video conferencing links to appointments',
|
||||||
two_factor_auth: 'Require two-factor authentication for enhanced security',
|
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';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to scroll to top on route changes
|
* Hook to scroll to top on route changes
|
||||||
* Should be used in layout components to ensure scroll restoration
|
* Should be used in layout components to ensure scroll restoration
|
||||||
* works consistently across all routes
|
* 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();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
if (containerRef?.current) {
|
||||||
}, [pathname]);
|
containerRef.current.scrollTo(0, 0);
|
||||||
|
} else {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
}, [pathname, containerRef]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,3 +33,96 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
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 [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useScrollToTop();
|
useScrollToTop(mainContentRef);
|
||||||
|
|
||||||
// Fetch ticket data when modal is opened from notification
|
// Fetch ticket data when modal is opened from notification
|
||||||
const { data: ticketFromNotification } = useTicket(ticketModalId || undefined);
|
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 { Outlet, Link, useNavigate } from 'react-router-dom';
|
||||||
import { User, Business } from '../types';
|
import { User, Business } from '../types';
|
||||||
import { LayoutDashboard, CalendarPlus, CreditCard, HelpCircle, Sun, Moon } from 'lucide-react';
|
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 CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMode, toggleTheme }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useScrollToTop();
|
const mainContentRef = useRef<HTMLElement>(null);
|
||||||
|
useScrollToTop(mainContentRef);
|
||||||
|
|
||||||
// Masquerade logic
|
// Masquerade logic
|
||||||
const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
|
const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
|
||||||
@@ -116,7 +117,7 @@ const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMod
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<Outlet context={{ business, user }} />
|
<Outlet context={{ business, user }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Moon, Sun, Bell, Globe, Menu } from 'lucide-react';
|
import { Moon, Sun, Bell, Globe, Menu } from 'lucide-react';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
@@ -16,8 +16,9 @@ interface ManagerLayoutProps {
|
|||||||
const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleTheme, onSignOut }) => {
|
const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleTheme, onSignOut }) => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const mainContentRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
useScrollToTop();
|
useScrollToTop(mainContentRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full bg-gray-100 dark:bg-gray-900">
|
<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>
|
</div>
|
||||||
</header>
|
</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 />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
@@ -21,8 +21,9 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [ticketModalId, setTicketModalId] = useState<string | null>(null);
|
const [ticketModalId, setTicketModalId] = useState<string | null>(null);
|
||||||
|
const mainContentRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
useScrollToTop();
|
useScrollToTop(mainContentRef);
|
||||||
|
|
||||||
// Fetch ticket data when modal is opened from notification
|
// Fetch ticket data when modal is opened from notification
|
||||||
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
||||||
@@ -83,7 +84,7 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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 />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import GridLayout, { Layout } from 'react-grid-layout';
|
||||||
BarChart,
|
import 'react-grid-layout/css/styles.css';
|
||||||
Bar,
|
import 'react-resizable/css/styles.css';
|
||||||
XAxis,
|
import { Settings, Calendar, Users, Briefcase, ClipboardList, Edit2, Check } from 'lucide-react';
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
LineChart,
|
|
||||||
Line
|
|
||||||
} from 'recharts';
|
|
||||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
|
||||||
import { useServices } from '../hooks/useServices';
|
import { useServices } from '../hooks/useServices';
|
||||||
import { useResources } from '../hooks/useResources';
|
import { useResources } from '../hooks/useResources';
|
||||||
import { useAppointments } from '../hooks/useAppointments';
|
import { useAppointments } from '../hooks/useAppointments';
|
||||||
import { useCustomers } from '../hooks/useCustomers';
|
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 {
|
const STORAGE_KEY = 'dashboard_layout';
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
trend: 'up' | 'down' | 'neutral';
|
|
||||||
change: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -30,76 +33,311 @@ const Dashboard: React.FC = () => {
|
|||||||
const { data: resources, isLoading: resourcesLoading } = useResources();
|
const { data: resources, isLoading: resourcesLoading } = useResources();
|
||||||
const { data: appointments, isLoading: appointmentsLoading } = useAppointments();
|
const { data: appointments, isLoading: appointmentsLoading } = useAppointments();
|
||||||
const { data: customers, isLoading: customersLoading } = useCustomers();
|
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
|
// Save layout to localStorage when it changes
|
||||||
const metrics: Metric[] = useMemo(() => {
|
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) {
|
if (!appointments || !customers || !services || !resources) {
|
||||||
return [
|
return {
|
||||||
{ label: t('dashboard.totalAppointments'), value: '0', trend: 'neutral', change: '0%' },
|
appointments: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||||
{ label: t('customers.title'), value: '0', trend: 'neutral', change: '0%' },
|
customers: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||||
{ label: t('services.title'), value: '0', trend: 'neutral', change: '0%' },
|
services: { count: 0 },
|
||||||
{ label: t('resources.title'), value: '0', trend: 'neutral', change: '0%' },
|
resources: { count: 0 },
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeCustomers = customers.filter(c => c.status === 'Active').length;
|
const activeCustomers = customers.filter(c => c.status === 'Active');
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{ label: t('dashboard.totalAppointments'), value: appointments.length.toString(), trend: 'up', change: '+12%' },
|
appointments: {
|
||||||
{ label: t('customers.title'), value: activeCustomers.toString(), trend: 'up', change: '+8%' },
|
count: appointments.length,
|
||||||
{ label: t('services.title'), value: services.length.toString(), trend: 'neutral', change: '0%' },
|
growth: calculateGrowth(appointments, 'startTime'),
|
||||||
{ label: t('resources.title'), value: resources.length.toString(), trend: 'up', change: '+3%' },
|
},
|
||||||
];
|
customers: {
|
||||||
}, [appointments, customers, services, resources, t]);
|
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(() => {
|
const weeklyData = useMemo(() => {
|
||||||
if (!appointments) {
|
if (!appointments) {
|
||||||
return [
|
return { revenue: [], appointments: [] };
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group appointments by day of week
|
const now = new Date();
|
||||||
const dayMap: { [key: string]: { revenue: number; count: number } } = {
|
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||||
'Mon': { revenue: 0, count: 0 },
|
const weekEnd = endOfWeek(now, { weekStartsOn: 1 });
|
||||||
'Tue': { revenue: 0, count: 0 },
|
|
||||||
'Wed': { revenue: 0, count: 0 },
|
const dayMap: Record<string, { revenue: number; count: number }> = {
|
||||||
'Thu': { revenue: 0, count: 0 },
|
Mon: { revenue: 0, count: 0 },
|
||||||
'Fri': { revenue: 0, count: 0 },
|
Tue: { revenue: 0, count: 0 },
|
||||||
'Sat': { revenue: 0, count: 0 },
|
Wed: { revenue: 0, count: 0 },
|
||||||
'Sun': { 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 => {
|
appointments
|
||||||
const date = new Date(appt.startTime);
|
.filter(appt => isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }))
|
||||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
.forEach(appt => {
|
||||||
const dayName = dayNames[date.getDay()];
|
const date = new Date(appt.startTime);
|
||||||
|
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
const dayName = dayNames[date.getDay()];
|
||||||
|
|
||||||
dayMap[dayName].count++;
|
dayMap[dayName].count++;
|
||||||
// Use price from appointment or default to 0
|
dayMap[dayName].revenue += (appt as any).price || 0;
|
||||||
dayMap[dayName].revenue += appt.price || 0;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
{ name: 'Mon', revenue: dayMap['Mon'].revenue, appointments: dayMap['Mon'].count },
|
return {
|
||||||
{ name: 'Tue', revenue: dayMap['Tue'].revenue, appointments: dayMap['Tue'].count },
|
revenue: days.map(day => ({ name: day, value: dayMap[day].revenue })),
|
||||||
{ name: 'Wed', revenue: dayMap['Wed'].revenue, appointments: dayMap['Wed'].count },
|
appointments: days.map(day => ({ name: day, value: dayMap[day].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 },
|
|
||||||
];
|
|
||||||
}, [appointments]);
|
}, [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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8">
|
<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 (
|
return (
|
||||||
<div className="p-8 space-y-8">
|
<div className="p-8 space-y-6">
|
||||||
<div>
|
{/* Header */}
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
|
<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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
{/* Edit mode hint */}
|
||||||
{metrics.map((metric, index) => (
|
{isEditing && (
|
||||||
<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">
|
<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">
|
||||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{metric.label}</p>
|
Drag widgets to reposition them. Drag the corner to resize. Hover over a widget and click the X to remove it.
|
||||||
<div className="flex items-baseline gap-2 mt-2">
|
</div>
|
||||||
<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' :
|
{/* Grid Layout */}
|
||||||
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'
|
<div className="max-w-[1200px] mx-auto">
|
||||||
}`}>
|
<GridLayout
|
||||||
{metric.trend === 'up' && <TrendingUp size={12} className="mr-1" />}
|
className="layout"
|
||||||
{metric.trend === 'down' && <TrendingDown size={12} className="mr-1" />}
|
layout={layoutWithConstraints}
|
||||||
{metric.trend === 'neutral' && <Minus size={12} className="mr-1" />}
|
cols={12}
|
||||||
{metric.change}
|
rowHeight={60}
|
||||||
</span>
|
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>
|
||||||
</div>
|
))}
|
||||||
))}
|
</GridLayout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6">
|
{/* Widget Config Modal */}
|
||||||
{/* Revenue Chart */}
|
<WidgetConfigModal
|
||||||
<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">
|
isOpen={showConfig}
|
||||||
<h3 className="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{t('dashboard.totalRevenue')}</h3>
|
onClose={() => setShowConfig(false)}
|
||||||
<div className="h-80">
|
activeWidgets={dashboardLayout.widgets}
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
onToggleWidget={toggleWidget}
|
||||||
<BarChart data={weeklyData}>
|
onResetLayout={resetLayout}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ const HelpGuide: React.FC = () => {
|
|||||||
title: 'Extend',
|
title: 'Extend',
|
||||||
description: 'Add functionality with plugins',
|
description: 'Add functionality with plugins',
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Plugins Overview', path: '/help/plugins', icon: <Puzzle size={18} /> },
|
{ label: 'Plugins', 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} /> },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
import { PluginInstallation, PluginCategory } from '../types';
|
import { PluginInstallation, PluginCategory } from '../types';
|
||||||
import EmailTemplateSelector from '../components/EmailTemplateSelector';
|
import EmailTemplateSelector from '../components/EmailTemplateSelector';
|
||||||
|
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||||
|
import { LockedSection } from '../components/UpgradePrompt';
|
||||||
|
|
||||||
// Category icon mapping
|
// Category icon mapping
|
||||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||||
@@ -60,6 +62,11 @@ const MyPlugins: React.FC = () => {
|
|||||||
const [review, setReview] = useState('');
|
const [review, setReview] = useState('');
|
||||||
const [configValues, setConfigValues] = useState<Record<string, any>>({});
|
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
|
// Fetch installed plugins
|
||||||
const { data: plugins = [], isLoading, error } = useQuery<PluginInstallation[]>({
|
const { data: plugins = [], isLoading, error } = useQuery<PluginInstallation[]>({
|
||||||
queryKey: ['plugin-installations'],
|
queryKey: ['plugin-installations'],
|
||||||
@@ -227,7 +234,7 @@ const MyPlugins: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || permissionsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -250,6 +257,7 @@ const MyPlugins: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -763,6 +771,7 @@ const MyPlugins: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</LockedSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
|||||||
const [editDateTime, setEditDateTime] = useState('');
|
const [editDateTime, setEditDateTime] = useState('');
|
||||||
const [editResource, setEditResource] = useState('');
|
const [editResource, setEditResource] = useState('');
|
||||||
const [editDuration, setEditDuration] = useState(0);
|
const [editDuration, setEditDuration] = useState(0);
|
||||||
|
const [editStatus, setEditStatus] = useState<AppointmentStatus>('CONFIRMED');
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||||
@@ -113,9 +114,17 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
|||||||
// Update edit state when selected appointment changes
|
// Update edit state when selected appointment changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedAppointment) {
|
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 || '');
|
setEditResource(selectedAppointment.resourceId || '');
|
||||||
setEditDuration(selectedAppointment.durationMinutes);
|
setEditDuration(selectedAppointment.durationMinutes);
|
||||||
|
setEditStatus(selectedAppointment.status);
|
||||||
}
|
}
|
||||||
}, [selectedAppointment]);
|
}, [selectedAppointment]);
|
||||||
|
|
||||||
@@ -551,7 +560,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
|||||||
|
|
||||||
const getStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
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 === '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';
|
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();
|
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';
|
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)
|
// Simplified status colors for month view (no border classes)
|
||||||
const getMonthStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
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 === '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';
|
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();
|
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';
|
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 = {
|
const updates: any = {
|
||||||
startTime: new Date(editDateTime),
|
startTime: new Date(editDateTime),
|
||||||
durationMinutes: validDuration,
|
durationMinutes: validDuration,
|
||||||
|
status: editStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editResource) {
|
if (editResource) {
|
||||||
@@ -1112,9 +1122,9 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
|||||||
<div className={`w-2 h-2 rounded-full ml-auto ${
|
<div className={`w-2 h-2 rounded-full ml-auto ${
|
||||||
status === 'COMPLETED' ? 'bg-green-500' :
|
status === 'COMPLETED' ? 'bg-green-500' :
|
||||||
status === 'CANCELLED' ? 'bg-gray-400' :
|
status === 'CANCELLED' ? 'bg-gray-400' :
|
||||||
status === 'NO_SHOW' ? 'bg-gray-400' :
|
status === 'NO_SHOW' ? 'bg-orange-500' :
|
||||||
status === 'CONFIRMED' ? 'bg-blue-500' :
|
status === 'CONFIRMED' ? 'bg-blue-500' :
|
||||||
'bg-orange-400'
|
'bg-yellow-400'
|
||||||
}`}></div>
|
}`}></div>
|
||||||
</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>
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">{services.find(s => s.id === selectedAppointment.serviceId)?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<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>
|
<label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 block">Status</label>
|
||||||
<p className="text-sm font-semibold text-gray-900 dark:text-white capitalize">{selectedAppointment.status.toLowerCase().replace('_', ' ')}</p>
|
<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>
|
||||||
</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 { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
import { PluginTemplate, PluginCategory } from '../types';
|
import { PluginTemplate, PluginCategory } from '../types';
|
||||||
|
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||||
|
import { LockedSection } from '../components/UpgradePrompt';
|
||||||
|
|
||||||
// Category icon mapping
|
// Category icon mapping
|
||||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||||
@@ -91,6 +93,11 @@ const PluginMarketplace: React.FC = () => {
|
|||||||
const [showWhatsNextModal, setShowWhatsNextModal] = useState(false);
|
const [showWhatsNextModal, setShowWhatsNextModal] = useState(false);
|
||||||
const [installedPluginId, setInstalledPluginId] = useState<string | null>(null);
|
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
|
// Fetch marketplace plugins
|
||||||
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
|
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
|
||||||
queryKey: ['plugin-templates', 'marketplace'],
|
queryKey: ['plugin-templates', 'marketplace'],
|
||||||
@@ -206,7 +213,7 @@ const PluginMarketplace: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || permissionsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -229,6 +236,7 @@ const PluginMarketplace: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -702,6 +710,7 @@ const PluginMarketplace: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</LockedSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import CreateTaskModal from '../components/CreateTaskModal';
|
import CreateTaskModal from '../components/CreateTaskModal';
|
||||||
import EditTaskModal from '../components/EditTaskModal';
|
import EditTaskModal from '../components/EditTaskModal';
|
||||||
|
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||||
|
import { LockedSection } from '../components/UpgradePrompt';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface ScheduledTask {
|
interface ScheduledTask {
|
||||||
@@ -95,6 +97,12 @@ const Tasks: React.FC = () => {
|
|||||||
const [editingTask, setEditingTask] = useState<ScheduledTask | null>(null);
|
const [editingTask, setEditingTask] = useState<ScheduledTask | null>(null);
|
||||||
const [editingEventAutomation, setEditingEventAutomation] = useState<GlobalEventPlugin | 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
|
// Fetch scheduled tasks
|
||||||
const { data: scheduledTasks = [], isLoading: tasksLoading } = useQuery<ScheduledTask[]>({
|
const { data: scheduledTasks = [], isLoading: tasksLoading } = useQuery<ScheduledTask[]>({
|
||||||
queryKey: ['scheduled-tasks'],
|
queryKey: ['scheduled-tasks'],
|
||||||
@@ -246,7 +254,7 @@ const Tasks: React.FC = () => {
|
|||||||
return AlertCircle;
|
return AlertCircle;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || permissionsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-96">
|
<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>
|
<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 (
|
return (
|
||||||
|
<LockedSection feature="tasks" isLocked={isLocked} variant="overlay">
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
@@ -588,6 +597,7 @@ const Tasks: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</LockedSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
829
frontend/src/pages/help/HelpComprehensive.tsx
Normal file
829
frontend/src/pages/help/HelpComprehensive.tsx
Normal file
@@ -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,
|
CheckCircle,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Settings,
|
||||||
|
Edit2,
|
||||||
|
GripVertical,
|
||||||
|
Ticket,
|
||||||
|
Activity,
|
||||||
|
UserX,
|
||||||
|
PieChart,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpDashboard: React.FC = () => {
|
const HelpDashboard: React.FC = () => {
|
||||||
@@ -47,7 +54,7 @@ const HelpDashboard: React.FC = () => {
|
|||||||
Dashboard Guide
|
Dashboard Guide
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
Your business at a glance
|
Your customizable business command center
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,89 +68,202 @@ const HelpDashboard: React.FC = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Key Metrics Section */}
|
{/* Customization Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" />
|
<Settings size={20} className="text-brand-500" />
|
||||||
Key Metrics
|
Customizing Your Dashboard
|
||||||
</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
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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 className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Weekly Revenue Chart</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<Edit2 size={16} className="text-blue-500" />
|
||||||
A bar chart showing revenue by day of the week. Helps identify your busiest and most profitable days. Calculated from appointment prices.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 */}
|
{/* Benefits Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,52 +1,87 @@
|
|||||||
/**
|
/**
|
||||||
* Help Plugins Overview Page
|
* Help Plugins Page
|
||||||
|
*
|
||||||
|
* User-friendly help documentation for Plugins.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Puzzle, Store, Code, Zap, Settings,
|
ArrowLeft,
|
||||||
CheckCircle, ChevronRight, HelpCircle, Shield, Download,
|
Puzzle,
|
||||||
|
Store,
|
||||||
|
Code,
|
||||||
|
Zap,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Settings,
|
||||||
|
Mail,
|
||||||
|
BookOpen,
|
||||||
|
Calendar,
|
||||||
|
ListTodo,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpPlugins: React.FC = () => {
|
const HelpPlugins: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* Back Button */}
|
||||||
<ArrowLeft size={20} /> Back
|
<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>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<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" />
|
<Puzzle size={24} className="text-brand-600 dark:text-brand-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Plugins Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Extend your scheduling platform</p>
|
Plugins Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Automate your business with powerful plugins
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Plugin Areas Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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="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" />
|
<Store size={20} className="text-blue-500 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Marketplace</h4>
|
<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>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<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" />
|
<Code size={20} className="text-purple-500 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Create Plugin</h4>
|
<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>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<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>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Plugin Actions</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Tasks</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Configure how plugins interact with your system</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">View and manage scheduled plugin executions</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* What Plugins Can Do */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
<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>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Browse Marketplace</h4>
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">2</span>
|
||||||
<div>
|
<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>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any plugin to add it to your account.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Configure Settings</h4>
|
<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 connect any required integrations.</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Set up plugin options and choose when it should run.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Run Plugin</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Monitor Tasks</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Execute plugins on-demand or set up automatic triggers.</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">View execution history and logs in the Tasks page.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Task Management */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</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-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
<ul className="space-y-3">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<li className="flex items-start gap-2">
|
When you install a plugin, it creates a scheduled task that runs automatically. Use the Tasks page to:
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
</p>
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Automation:</strong> Automate repetitive tasks and workflows</span>
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<Pause size={16} className="text-yellow-500" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Integration:</strong> Connect with external services and tools</span>
|
<span><strong>Pause/Resume:</strong> Temporarily stop a task without deleting it</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<CheckCircle size={16} className="text-blue-500" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Customization:</strong> Tailor functionality to your needs</span>
|
<span><strong>View logs:</strong> See execution history and any errors</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Developer Documentation Link */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Guides</h2>
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="flex items-start 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">
|
<BookOpen size={24} className="text-brand-600 dark:text-brand-400 mt-1" />
|
||||||
<Code size={20} className="text-brand-500" />
|
<div className="flex-1">
|
||||||
<span className="text-gray-900 dark:text-white">Creating Plugins</span>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
Creating Custom Plugins
|
||||||
</Link>
|
</h3>
|
||||||
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<Shield size={20} className="text-brand-500" />
|
Want to build your own automations? Our comprehensive developer documentation covers
|
||||||
<span className="text-gray-900 dark:text-white">Plugin Documentation</span>
|
the scripting language, available API methods, and example code.
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
</p>
|
||||||
</Link>
|
<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>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
Need More Help?
|
||||||
<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>
|
||||||
|
<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>
|
</section>
|
||||||
</div>
|
</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="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>
|
<span className="text-sm text-green-700 dark:text-green-300">Successfully finished</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-100 dark:bg-gray-700 border-l-4 border-gray-400 rounded">
|
<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-gray-700 dark:text-gray-300">Gray - No-show</span>
|
<span className="font-medium text-orange-900 dark:text-orange-200">Orange - No-show</span>
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400">Customer didn't arrive</span>
|
<span className="text-sm text-orange-700 dark:text-orange-300">Customer didn't arrive</span>
|
||||||
</div>
|
</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">
|
<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>
|
<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">
|
<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" />
|
<CheckCircle size={20} className="text-orange-500 mt-0.5" />
|
||||||
<div>
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,12 +560,6 @@ const HelpScheduler: React.FC = () => {
|
|||||||
</h2>
|
</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-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">
|
<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">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings API Page
|
* Help Settings API Page
|
||||||
|
*
|
||||||
|
* Documentation for managing API tokens for third-party integrations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Code, Key, Shield, Zap, FileText,
|
ArrowLeft,
|
||||||
CheckCircle, ChevronRight, HelpCircle, Lock,
|
Key,
|
||||||
|
Shield,
|
||||||
|
Plus,
|
||||||
|
Copy,
|
||||||
|
Trash2,
|
||||||
|
Clock,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
Lock,
|
||||||
|
Settings,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsApi: React.FC = () => {
|
const HelpSettingsApi: React.FC = () => {
|
||||||
@@ -14,144 +29,364 @@ const HelpSettingsApi: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* Back Button */}
|
||||||
<ArrowLeft size={20} /> Back
|
<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>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||||
<Code size={24} className="text-brand-600 dark:text-brand-400" />
|
<Key size={24} className="text-amber-600 dark:text-amber-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">API Settings Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Integrate with external systems</p>
|
API Settings Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Manage API tokens for integrations
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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.
|
API Settings allows you to create and manage API tokens for integrating your scheduling
|
||||||
</p>
|
system with external applications. These tokens provide secure, controlled access to your
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
business data through our REST API.
|
||||||
API access requires a Business plan or higher.
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Creating API Tokens */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Click the <strong>New Token</strong> button to create a new API token. You'll need to configure:
|
||||||
<Key size={20} className="text-blue-500 mt-0.5" />
|
</p>
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Generate Keys</h4>
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Create new API keys for authentication</p>
|
{/* Token Name */}
|
||||||
</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">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>
|
||||||
<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" />
|
{/* Permission Presets */}
|
||||||
<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">Key Permissions</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Permission Presets</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Set read/write access levels per key</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||||
</div>
|
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>
|
||||||
<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" />
|
{/* Individual Permissions */}
|
||||||
<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">Revoke Keys</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Individual Permissions</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Disable compromised or unused keys</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
</div>
|
Expand "Show individual permissions" for granular control over what the token can access:
|
||||||
|
appointments, customers, services, resources, and more.
|
||||||
|
</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" />
|
{/* Expiration */}
|
||||||
<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">Rate Limits</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">View your API usage and limits</p>
|
<Clock size={16} className="text-orange-500" />
|
||||||
</div>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Token Security */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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="space-y-3 font-mono text-sm">
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<AlertTriangle size={24} className="text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
|
||||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/appointments</span>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-yellow-800 dark:text-yellow-200 mb-1">
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Important: Copy Your Token Immediately
|
||||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/customers</span>
|
</h4>
|
||||||
</div>
|
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
When a token is created, you'll see the full token <strong>only once</strong>. Copy it
|
||||||
<span className="text-green-600">GET</span> <span className="text-gray-600 dark:text-gray-300">/api/services</span>
|
immediately and store it securely. You will not be able to view it again.
|
||||||
</div>
|
</p>
|
||||||
<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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Managing Tokens */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Step by Step */}
|
||||||
<section className="mb-10">
|
<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">
|
<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">
|
<Link
|
||||||
<Zap size={20} className="text-brand-500" />
|
to="/help/settings/auth"
|
||||||
<span className="text-gray-900 dark:text-white">Plugins Guide</span>
|
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"
|
||||||
<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">
|
|
||||||
<Shield size={20} className="text-brand-500" />
|
<Shield size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Authentication Settings</span>
|
<span className="text-gray-900 dark:text-white">Authentication Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
Need More Help?
|
||||||
<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>
|
||||||
|
<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>
|
</section>
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Palette, Image, Type, Layout, Eye,
|
ArrowLeft, Palette, Image, Upload, Eye, Save,
|
||||||
CheckCircle, ChevronRight, HelpCircle, Sun, Moon,
|
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsAppearance: React.FC = () => {
|
const HelpSettingsAppearance: React.FC = () => {
|
||||||
@@ -18,134 +21,365 @@ const HelpSettingsAppearance: React.FC = () => {
|
|||||||
<ArrowLeft size={20} /> Back
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<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-brand-600 dark:text-brand-400" />
|
<Palette size={24} className="text-purple-600 dark:text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Appearance Settings Guide</h1>
|
<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 brand and look</p>
|
<p className="text-gray-500 dark:text-gray-400">Customize your logos and brand colors</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<Palette size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Brand Logos */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
You can upload two different logos for different contexts. PNG images with transparent
|
||||||
<Image size={20} className="text-blue-500 mt-0.5" />
|
backgrounds are recommended for the best appearance across light and dark themes.
|
||||||
<div>
|
</p>
|
||||||
<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>
|
<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>
|
</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" />
|
{/* Email Logo */}
|
||||||
<div>
|
<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">Favicon</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Small icon shown in browser tabs</p>
|
<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>
|
||||||
</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">
|
<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>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Brand Color</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Text Only</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Primary color used throughout the interface</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<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>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Custom CSS</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Logo Only</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Advanced styling with custom CSS (Pro plan)</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Color Palettes */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Choose from 10 professionally designed color palettes. Each palette includes a primary
|
||||||
<Sun size={20} className="text-yellow-500 mt-0.5" />
|
and secondary color that work well together. Click any palette to instantly preview how
|
||||||
<div>
|
it looks throughout the interface.
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Light Mode</h4>
|
</p>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Clean, bright interface for daytime use</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>
|
))}
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
</div>
|
||||||
<Moon size={20} className="text-indigo-500 mt-0.5" />
|
|
||||||
|
<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>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Dark Mode</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Live Preview</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Easy on the eyes in low-light conditions</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<CheckCircle size={20} className="text-brand-500" /> Tips
|
||||||
</h2>
|
</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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<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">
|
<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" />
|
<Eye size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">General Settings</span>
|
<span className="text-gray-900 dark:text-white">General Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<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>
|
<span className="text-gray-900 dark:text-white">Custom Domains</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Authentication Page
|
* Help Settings Authentication Page
|
||||||
|
*
|
||||||
|
* Documentation for configuring OAuth providers and social login for customers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Shield, Key, Lock, Users, Smartphone,
|
ArrowLeft,
|
||||||
CheckCircle, ChevronRight, HelpCircle, LogIn,
|
Lock,
|
||||||
|
Users,
|
||||||
|
Key,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
Save,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
AlertCircle,
|
||||||
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsAuth: React.FC = () => {
|
const HelpSettingsAuth: React.FC = () => {
|
||||||
@@ -14,137 +26,328 @@ const HelpSettingsAuth: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* Back Button */}
|
||||||
<ArrowLeft size={20} /> Back
|
<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>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||||
<Shield size={24} className="text-brand-600 dark:text-brand-400" />
|
<Lock size={24} className="text-purple-600 dark:text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Authentication Settings Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Configure login and security</p>
|
Authentication Settings Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Configure social login for customers
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
</p>
|
||||||
</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">
|
||||||
</section>
|
<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">
|
||||||
<section className="mb-10">
|
OAuth settings require a plan with the <strong>custom_oauth</strong> feature. Only the
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
business owner can configure authentication settings.
|
||||||
<Lock size={20} className="text-brand-500" /> Security Features
|
</p>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Social Login */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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="space-y-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
{/* Allow Registration */}
|
||||||
<Key size={20} className="text-gray-500 mt-0.5" />
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Email & Password</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Traditional login with email and password</p>
|
<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>
|
</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" />
|
{/* Auto-link by Email */}
|
||||||
<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">Social Login</h4>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sign in with Google, Apple, or other providers</p>
|
<div>
|
||||||
</div>
|
<h4 className="font-medium text-gray-900 dark:text-white">Auto-link by Email</h4>
|
||||||
</div>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
When enabled, if a customer signs in with OAuth and their email matches an existing
|
||||||
<Shield size={20} className="text-purple-500 mt-0.5" />
|
account, the accounts are automatically linked. This lets customers sign in with
|
||||||
<div>
|
multiple methods.
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">SSO (Single Sign-On)</h4>
|
</p>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Enterprise integration with SAML/OIDC</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Custom OAuth Credentials */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<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" />
|
<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" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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" />
|
<Key size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">API Settings</span>
|
<span className="text-gray-900 dark:text-white">API Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
@@ -152,11 +355,21 @@ const HelpSettingsAuth: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
Need More Help?
|
||||||
<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>
|
||||||
|
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Billing Page
|
* 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 React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, CreditCard, FileText, Clock, TrendingUp, Shield,
|
ArrowLeft, CreditCard, Crown, Package, Wallet, FileText,
|
||||||
CheckCircle, ChevronRight, HelpCircle, DollarSign,
|
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Calendar,
|
||||||
|
Plus, Trash2, Star, RotateCcw, X, Zap, ArrowRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsBilling: React.FC = () => {
|
const HelpSettingsBilling: React.FC = () => {
|
||||||
@@ -18,135 +22,351 @@ const HelpSettingsBilling: React.FC = () => {
|
|||||||
<ArrowLeft size={20} /> Back
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<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-brand-600 dark:text-brand-400" />
|
<CreditCard size={24} className="text-emerald-600 dark:text-emerald-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Billing Settings Guide</h1>
|
<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 and payments</p>
|
<p className="text-gray-500 dark:text-gray-400">Manage your subscription, add-ons, and payment methods</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<CreditCard size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
</section>
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||||
<section className="mb-10">
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<strong>Owner Access Required:</strong> Only business owners can access billing settings.
|
||||||
<DollarSign size={20} className="text-brand-500" /> Subscription Management
|
</p>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Current Plan */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||||
<span className="font-medium text-gray-900 dark:text-white">Free</span>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2">What You'll See</h4>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Basic scheduling, limited resources</span>
|
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
</div>
|
<li>• <strong>Plan Name:</strong> Your current tier (Free, Starter, Professional, Enterprise)</li>
|
||||||
<div className="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
<li>• <strong>Monthly Price:</strong> What you pay per month (or "Contact Us" for Enterprise)</li>
|
||||||
<span className="font-medium text-gray-900 dark:text-white">Pro</span>
|
<li>• <strong>Key Features:</strong> List of what's included in your plan</li>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">- More resources, custom branding, payments</span>
|
<li>• <strong>Upgrade Button:</strong> Opens the plan selection modal</li>
|
||||||
</div>
|
</ul>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<CheckCircle size={20} className="text-brand-500" /> Tips
|
||||||
</h2>
|
</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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<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">
|
<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>
|
<span className="text-gray-900 dark:text-white">Usage & Quota</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<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>
|
<span className="text-gray-900 dark:text-white">Payments Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Booking Page
|
* Help Settings Booking Page
|
||||||
|
*
|
||||||
|
* Comprehensive documentation for the Booking Settings page.
|
||||||
|
* Documents: Booking URL display/sharing, Return URL configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Calendar, Clock, Bell, Shield, CreditCard,
|
ArrowLeft, Calendar, Link2, ExternalLink, Copy, Share2,
|
||||||
CheckCircle, ChevronRight, HelpCircle, AlertCircle,
|
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Globe,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsBooking: React.FC = () => {
|
const HelpSettingsBooking: React.FC = () => {
|
||||||
@@ -18,6 +21,7 @@ const HelpSettingsBooking: React.FC = () => {
|
|||||||
<ArrowLeft size={20} /> Back
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Booking Settings Guide</h1>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<Calendar size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<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>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Your Booking URL */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Your booking URL is the public link where customers can view your services and book appointments.
|
||||||
<Clock size={20} className="text-blue-500 mt-0.5" />
|
It's based on your business subdomain and is automatically generated when you create your business.
|
||||||
<div>
|
</p>
|
||||||
<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 className="space-y-4">
|
||||||
</div>
|
{/* 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>
|
||||||
<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" />
|
{/* Actions Available */}
|
||||||
<div>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Minimum Notice</h4>
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Minimum time before appointment for booking</p>
|
<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>
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<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" />
|
||||||
<Clock size={20} className="text-green-500 mt-0.5" />
|
<div>
|
||||||
<div>
|
<h4 className="font-medium text-gray-900 dark:text-white">Open Booking Page</h4>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Buffer Time</h4>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Gap between consecutive appointments</p>
|
Click the external link icon to open your booking page in a new tab
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Sharing Your Booking URL */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" /> Policies
|
<Share2 size={20} className="text-brand-500" /> Sharing Your Booking URL
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
||||||
|
Once you've copied your booking URL, you can share it in many ways to help customers find you:
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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">
|
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Shield size={20} className="text-red-500 mt-0.5" />
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Website</h4>
|
||||||
<div>
|
<p className="text-xs text-gray-500 dark:text-gray-400">Add a "Book Now" button linking to your URL</p>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Cancellation Policy</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">How late customers can cancel without penalty</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Bell size={20} className="text-blue-500 mt-0.5" />
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Email Signature</h4>
|
||||||
<div>
|
<p className="text-xs text-gray-500 dark:text-gray-400">Include your booking link in every email</p>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Confirmation Required</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Require staff to confirm new bookings</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<CreditCard size={20} className="text-green-500 mt-0.5" />
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Social Media</h4>
|
||||||
<div>
|
<p className="text-xs text-gray-500 dark:text-gray-400">Add to your bio on Instagram, Facebook, etc.</p>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Deposit Required</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Collect payment upfront for bookings</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<CheckCircle size={20} className="text-purple-500 mt-0.5" />
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Business Cards</h4>
|
||||||
<div>
|
<p className="text-xs text-gray-500 dark:text-gray-400">Print a QR code or short URL on cards</p>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Auto-Confirm</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Automatically confirm bookings instantly</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Return URL */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<ExternalLink size={20} className="text-green-500" /> Return URL
|
||||||
|
</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 Return URL controls where customers are redirected after successfully completing a booking.
|
||||||
|
This is optional but useful for providing a seamless experience that connects back to your website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* How It Works */}
|
||||||
|
<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">How It Works</h4>
|
||||||
|
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-2 list-decimal list-inside">
|
||||||
|
<li>Customer completes their booking on your SmoothSchedule page</li>
|
||||||
|
<li>If a Return URL is set, they're automatically redirected to that page</li>
|
||||||
|
<li>If no Return URL is set, they stay on the booking confirmation page</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Example Uses */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Common Uses</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<CheckCircle size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Thank You Page</h5>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Redirect to a custom thank you page on your website with additional info
|
||||||
|
</p>
|
||||||
|
<code className="text-xs text-brand-600 dark:text-brand-400 mt-1 block">
|
||||||
|
https://yourbusiness.com/thank-you
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<CheckCircle size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Preparation Instructions</h5>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Send customers to a page with info about what to bring or how to prepare
|
||||||
|
</p>
|
||||||
|
<code className="text-xs text-brand-600 dark:text-brand-400 mt-1 block">
|
||||||
|
https://yourbusiness.com/appointment-prep
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<CheckCircle size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Homepage</h5>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Simply return customers to your main website
|
||||||
|
</p>
|
||||||
|
<code className="text-xs text-brand-600 dark:text-brand-400 mt-1 block">
|
||||||
|
https://yourbusiness.com
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Setting the Return URL */}
|
||||||
|
<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 the Return URL</h4>
|
||||||
|
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
|
||||||
|
<li>Enter the full URL including https://</li>
|
||||||
|
<li>Click the Save button</li>
|
||||||
|
<li>A confirmation toast will appear when saved successfully</li>
|
||||||
|
</ol>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
To remove the Return URL and keep customers on the confirmation page, clear the field and save.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Custom Domains */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<Globe size={20} className="text-purple-500" /> Want Your Own Domain?
|
||||||
|
</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 you'd prefer customers to book at your own domain (like book.yourbusiness.com instead of
|
||||||
|
yourbusiness.smoothschedule.com), you can set up a custom domain.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/settings/custom-domains"
|
||||||
|
className="inline-flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 font-medium"
|
||||||
|
>
|
||||||
|
Set up a custom domain <ChevronRight size={16} />
|
||||||
|
</Link>
|
||||||
|
</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>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Set minimum notice time to avoid last-minute bookings</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Test your booking URL by opening it in an incognito/private browser window to see what customers see
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Use buffer time if services need cleanup or preparation</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Make sure your Return URL is accessible and mobile-friendly before setting it
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Consider deposits for high-value services to reduce no-shows</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Use a URL shortener if you need a simpler link for print materials
|
||||||
|
</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">
|
||||||
|
The confirmation page shows booking details, so leaving customers there is often fine
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Link to="/help/scheduler" 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/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">
|
||||||
<Calendar size={20} className="text-brand-500" />
|
<Calendar size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
|
<span className="text-gray-900 dark:text-white">General Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<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">
|
||||||
<CreditCard size={20} className="text-brand-500" />
|
<Share2 size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Payments Guide</span>
|
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
|
||||||
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
|
</Link>
|
||||||
|
<Link to="/help/services" 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">
|
||||||
|
<Calendar size={20} className="text-brand-500" />
|
||||||
|
<span className="text-gray-900 dark:text-white">Services Guide</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">
|
||||||
|
<Globe 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" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
Our support team is ready to help with any questions about your booking setup.
|
||||||
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Custom Domains Page
|
* Help Settings Custom Domains Page
|
||||||
|
*
|
||||||
|
* Documentation for managing custom domains and domain purchases.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Globe, Shield, Link as LinkIcon, CheckCircle,
|
ArrowLeft,
|
||||||
ChevronRight, HelpCircle, Settings, AlertCircle,
|
Globe,
|
||||||
|
Copy,
|
||||||
|
Star,
|
||||||
|
Trash2,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
ShoppingCart,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
Lock,
|
||||||
|
Settings,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsDomains: React.FC = () => {
|
const HelpSettingsDomains: React.FC = () => {
|
||||||
@@ -14,151 +27,379 @@ const HelpSettingsDomains: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* Back Button */}
|
||||||
<ArrowLeft size={20} /> Back
|
<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>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
|
||||||
<Globe size={24} className="text-brand-600 dark:text-brand-400" />
|
<Globe size={24} className="text-indigo-600 dark:text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Custom Domains Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Use your own domain for booking</p>
|
Custom Domains Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Use your own domain for booking pages
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Globe size={20} className="text-brand-500" /> Overview
|
<Globe size={20} className="text-brand-500" />
|
||||||
|
Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
Custom Domains let you use your own domain name (like booking.yourcompany.com) instead of the default subdomain. This creates a more professional, branded experience for your customers.
|
Custom Domains lets you use your own domain (like <code className="px-1 bg-gray-100 dark:bg-gray-700 rounded">booking.yourcompany.com</code>)
|
||||||
</p>
|
for your booking pages. You can connect a domain you already own, or purchase a new one directly through the platform.
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
Custom domains are available on Pro and Business plans.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-800 rounded-lg">
|
||||||
|
<Lock size={18} className="text-indigo-600 dark:text-indigo-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-indigo-800 dark:text-indigo-200">
|
||||||
|
Custom domains require a plan with the <strong>custom_domain</strong> feature. Only the
|
||||||
|
business owner can manage custom domains.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Bring Your Own Domain */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" /> Setup Process
|
<Globe size={20} className="text-brand-500" />
|
||||||
|
Bring Your Own Domain
|
||||||
|
</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">
|
||||||
|
Connect a domain you already own by following these steps:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Add Your Domain</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Enter your domain in the input field (e.g., <code className="px-1 bg-gray-100 dark:bg-gray-700 rounded">booking.yourdomain.com</code>)
|
||||||
|
and click <strong>Add</strong>.
|
||||||
|
</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">Add DNS TXT Record</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Your domain will show as <strong>Pending</strong> with DNS instructions. Add the TXT record
|
||||||
|
to your DNS provider:
|
||||||
|
</p>
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg text-sm">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-amber-700 dark:text-amber-300 font-medium">Name:</span>
|
||||||
|
<code className="px-1 bg-white dark:bg-gray-800 rounded text-gray-900 dark:text-white">
|
||||||
|
_smoothschedule-verify
|
||||||
|
</code>
|
||||||
|
<Copy size={12} className="text-amber-600 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-amber-700 dark:text-amber-300 font-medium">Value:</span>
|
||||||
|
<code className="px-1 bg-white dark:bg-gray-800 rounded text-gray-900 dark:text-white">
|
||||||
|
verify=abc123...
|
||||||
|
</code>
|
||||||
|
<Copy size={12} className="text-amber-600 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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">Verify Domain</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Click the <RefreshCw size={14} className="inline text-brand-500" /> verify button.
|
||||||
|
Once verified, the status changes to <strong>Verified</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<AlertCircle size={14} className="inline text-amber-500 mr-1" />
|
||||||
|
DNS changes can take up to 48 hours to propagate worldwide.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Managing Domains */}
|
||||||
|
<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" />
|
||||||
|
Managing Your Domains
|
||||||
|
</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 domain list shows all connected domains with their status and actions:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Domain Status */}
|
||||||
|
<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">Domain Status</h4>
|
||||||
|
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-2">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded">
|
||||||
|
Verified
|
||||||
|
</span>
|
||||||
|
<span>Domain ownership confirmed, ready to use</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300 rounded">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
<span>Waiting for DNS verification</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-300 rounded flex items-center gap-1">
|
||||||
|
<Star size={10} className="fill-current" /> Primary
|
||||||
|
</span>
|
||||||
|
<span>The main domain used for your booking pages</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<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">Available Actions</h4>
|
||||||
|
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-2">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<RefreshCw size={16} className="text-brand-500" />
|
||||||
|
<span><strong>Verify</strong> - Check DNS configuration and verify ownership</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<Star size={16} className="text-indigo-500" />
|
||||||
|
<span><strong>Set Primary</strong> - Make this your main booking domain</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<Trash2 size={16} className="text-red-500" />
|
||||||
|
<span><strong>Delete</strong> - Remove domain from your account</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Domain Purchase */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<ShoppingCart size={20} className="text-brand-500" />
|
||||||
|
Purchase a Domain
|
||||||
|
</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">
|
||||||
|
Don't have a domain? You can search for and register a new domain directly from this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
|
<h4 className="font-medium text-green-800 dark:text-green-200 mb-2">
|
||||||
|
How Domain Purchase Works
|
||||||
|
</h4>
|
||||||
|
<ol className="text-sm text-green-700 dark:text-green-300 space-y-1 list-decimal list-inside">
|
||||||
|
<li>Enter the domain name you want to register</li>
|
||||||
|
<li>See availability and pricing</li>
|
||||||
|
<li>Complete payment to register the domain</li>
|
||||||
|
<li>Domain is automatically connected to your account</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Purchased domains are registered in your name and include automatic SSL certificates.
|
||||||
|
Domain pricing varies by extension (.com, .net, etc.).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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: Connect Your Domain
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<ol className="space-y-4">
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||||
<div>
|
1
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Add Domain</h4>
|
</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Enter your custom domain in the settings</p>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
</div>
|
Go to <strong>Settings > Custom Domains</strong>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||||
<div>
|
2
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Configure DNS</h4>
|
</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Add CNAME record pointing to our servers</p>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
</div>
|
In the <strong>Bring Your Own Domain</strong> section, enter your domain
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||||
<div>
|
3
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Verify</h4>
|
</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Click verify to confirm DNS is configured</p>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
</div>
|
Click <strong>Add</strong> to add the domain
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<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="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
|
||||||
<div>
|
4
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">SSL Certificate</h4>
|
</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">We automatically provision an SSL certificate</p>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
</div>
|
Copy the <strong>DNS TXT record</strong> details shown
|
||||||
|
</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">
|
||||||
|
Log into your domain registrar and add the <strong>TXT record</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">
|
||||||
|
6
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Wait for DNS propagation (usually a few minutes, up to 48 hours)
|
||||||
|
</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">
|
||||||
|
Click the <RefreshCw size={14} className="inline" /> <strong>Verify</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">
|
||||||
|
8
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Once verified, click <Star size={14} className="inline" /> to <strong>Set as Primary</strong> if desired
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Tips */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<AlertCircle size={20} className="text-brand-500" /> DNS Configuration
|
<CheckCircle size={20} className="text-brand-500" />
|
||||||
</h2>
|
Tips
|
||||||
<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 this CNAME record to your DNS provider:
|
|
||||||
</p>
|
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg font-mono text-sm">
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-500">Type:</span>
|
|
||||||
<div className="text-gray-900 dark:text-white">CNAME</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-500">Name:</span>
|
|
||||||
<div className="text-gray-900 dark:text-white">booking</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-500">Value:</span>
|
|
||||||
<div className="text-gray-900 dark:text-white">cname.smoothschedule.com</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
|
|
||||||
DNS changes can take up to 48 hours to propagate.
|
|
||||||
</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">
|
|
||||||
<CheckCircle size={20} className="text-brand-500" /> Benefits
|
|
||||||
</h2>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Professional:</strong> Use your own branded domain</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Use a subdomain like <code className="px-1 bg-white/50 dark:bg-gray-800/50 rounded">booking.yourcompany.com</code>
|
||||||
|
rather than your main domain
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Trust:</strong> Customers see your domain, not ours</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
If verification fails, double-check the TXT record name and value match exactly
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>SEO:</strong> Build search rankings on your domain</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Use an online DNS lookup tool to check if your TXT record is published
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>SSL:</strong> Automatic HTTPS encryption included</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Set your most important domain as <strong>Primary</strong> - it will be used in booking links
|
||||||
|
</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">
|
||||||
|
You can connect multiple domains and switch between them as needed
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<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">
|
<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"
|
||||||
|
>
|
||||||
<Settings size={20} className="text-brand-500" />
|
<Settings size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">General Settings</span>
|
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<Link
|
||||||
<LinkIcon size={20} className="text-brand-500" />
|
to="/help/settings/booking"
|
||||||
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
|
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"
|
||||||
|
>
|
||||||
|
<Globe 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" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
Need More Help?
|
||||||
<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>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Our support team is ready to help with domain 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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Email Templates Page
|
* Help Settings Email Page
|
||||||
|
*
|
||||||
|
* Comprehensive documentation for the Email Setup Settings page.
|
||||||
|
* Documents: Ticket email addresses for receiving and sending support tickets via IMAP/SMTP.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Mail, FileText, Edit, Eye, Bell,
|
ArrowLeft, Mail, Plus, Edit, Trash2, Star, TestTube,
|
||||||
CheckCircle, ChevronRight, HelpCircle, Code,
|
RefreshCw, CheckCircle, ChevronRight, HelpCircle, AlertCircle, XCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsEmail: React.FC = () => {
|
const HelpSettingsEmail: React.FC = () => {
|
||||||
@@ -18,138 +21,320 @@ const HelpSettingsEmail: React.FC = () => {
|
|||||||
<ArrowLeft size={20} /> Back
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||||
<Mail size={24} className="text-brand-600 dark:text-brand-400" />
|
<Mail size={24} className="text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Email Templates Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Email Setup Guide</h1>
|
||||||
<p className="text-gray-500 dark:text-gray-400">Customize automated emails</p>
|
<p className="text-gray-500 dark:text-gray-400">Configure email addresses for ticket support</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Mail size={20} className="text-brand-500" /> Overview
|
<Mail size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
Email Templates let you customize the automated emails sent to customers. Personalize confirmation messages, reminders, and notifications to match your brand voice.
|
Email Setup lets you configure email addresses for your ticketing system. When customers email
|
||||||
|
these addresses, their messages automatically become support tickets. You can also send ticket
|
||||||
|
responses back via these email addresses.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
</section>
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||||
<section className="mb-10">
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<strong>Owner Access Required:</strong> Only business owners can configure ticket email addresses.
|
||||||
<FileText size={20} className="text-brand-500" /> Template Types
|
</p>
|
||||||
</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">
|
|
||||||
<CheckCircle size={20} className="text-green-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Booking Confirmation</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sent when appointment is booked</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
||||||
<Bell size={20} className="text-blue-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Appointment Reminder</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sent before the appointment</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
||||||
<Edit size={20} className="text-purple-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Reschedule Notice</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sent when appointment is changed</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
||||||
<Mail size={20} className="text-orange-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Cancellation</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Sent when appointment is cancelled</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Email Address List */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" /> Template Variables
|
<Mail size={20} className="text-brand-500" /> Your Email Addresses
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
Use these variables in your templates to include dynamic content:
|
Each configured email address appears as a card showing:
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3 font-mono text-sm">
|
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="space-y-4">
|
||||||
<code className="text-brand-600">{"{{customer_name}}"}</code> - Customer's full name
|
<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 Email Address</h4>
|
||||||
|
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
|
<li>• <strong>Display Name:</strong> A friendly name for the address (e.g., "Support", "Sales")</li>
|
||||||
|
<li>• <strong>Email Address:</strong> The actual email (e.g., support@yourbusiness.com)</li>
|
||||||
|
<li>• <strong>Color:</strong> Left border color for visual identification</li>
|
||||||
|
<li>• <strong>Emails Processed:</strong> How many emails have been converted to tickets</li>
|
||||||
|
<li>• <strong>Last Checked:</strong> When emails were last fetched</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
||||||
<code className="text-brand-600">{"{{service_name}}"}</code> - Name of the booked service
|
{/* Status Badges */}
|
||||||
</div>
|
<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="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||||
<code className="text-brand-600">{"{{appointment_date}}"}</code> - Date of appointment
|
<div className="flex items-center gap-2 mb-1">
|
||||||
</div>
|
<Star size={14} className="text-yellow-600" />
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<span className="text-sm font-medium text-yellow-800 dark:text-yellow-300">Default</span>
|
||||||
<code className="text-brand-600">{"{{appointment_time}}"}</code> - Time of appointment
|
</div>
|
||||||
</div>
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Primary address for outgoing ticket emails
|
||||||
<code className="text-brand-600">{"{{business_name}}"}</code> - Your business name
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<CheckCircle size={14} className="text-green-600" />
|
||||||
|
<span className="text-sm font-medium text-green-800 dark:text-green-300">Active</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
Address is being monitored for new emails
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<XCircle size={14} className="text-gray-500" />
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Inactive</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
Address is configured but not checking
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<CheckCircle size={20} className="text-brand-500" /> Available Actions
|
||||||
|
</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">
|
||||||
|
Each email address has action buttons for testing and management:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<Star size={16} className="text-yellow-500" />
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Set as Default</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Make this the default address for sending outgoing ticket emails
|
||||||
|
</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">
|
||||||
|
<TestTube size={16} className="text-blue-500" />
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Test IMAP</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Test the incoming email (IMAP) connection to verify configuration
|
||||||
|
</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">
|
||||||
|
<RefreshCw size={16} className="text-green-500" />
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Fetch Emails Now</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Manually check for new emails and create tickets 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">
|
||||||
|
<Edit size={16} className="text-brand-500" />
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Edit</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Modify the email address settings, IMAP/SMTP configuration
|
||||||
|
</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">Delete</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Remove the email address (with confirmation prompt)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Adding an Email Address */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<Plus size={20} className="text-green-500" /> Adding an Email Address
|
||||||
|
</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 "Add Email Address" button to configure a new ticket email. You'll need IMAP and SMTP
|
||||||
|
credentials from your email provider.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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-3">Required Information</h4>
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">Display Name</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Friendly name like "Support" or "Sales Inquiries"</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">Email Address</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">The full email address (e.g., support@example.com)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">IMAP Settings</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Server, port, username, and password for receiving emails</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">SMTP Settings</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Server, port, username, and password for sending emails</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">Color</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Choose a color to identify this address in the ticket list</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 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 mb-2">Where to Find IMAP/SMTP Settings</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Your email provider (Gmail, Outlook, etc.) has documentation with IMAP and SMTP server details.
|
||||||
|
Search for "[your provider] IMAP settings" or check your email account settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* How It Works */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<Mail size={20} className="text-brand-500" /> How Email-to-Ticket Works
|
||||||
|
</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-4">
|
||||||
|
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center font-medium">1</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Customer Sends Email</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">A customer emails your configured address (e.g., support@yourbusiness.com)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center font-medium">2</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">System Fetches Email</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">SmoothSchedule checks the mailbox via IMAP and retrieves new messages</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center font-medium">3</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Ticket Created</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">A new ticket is created with the email content and sender information</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center font-medium">4</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">You Respond</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">When you reply to the ticket, your response is sent via SMTP back to the customer</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>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Keep emails concise and include only essential information</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Use "Test IMAP" after setup to verify your credentials are correct
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Use preview mode to test templates before activating</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Consider using different email addresses for different departments (support, sales, billing)
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Include clear calls to action (cancel, reschedule links)</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Set a meaningful default address - this is what customers see when you reply
|
||||||
|
</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">
|
||||||
|
Assign different colors to make it easy to identify which department a ticket came from
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<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">
|
<Link to="/help/ticketing" 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" />
|
<Mail size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
|
<span className="text-gray-900 dark:text-white">Ticketing Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<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">
|
||||||
<Bell size={20} className="text-brand-500" />
|
<CheckCircle size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Booking Settings</span>
|
<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">
|
||||||
|
<Star 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>
|
||||||
|
<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">
|
||||||
|
<Mail size={20} className="text-brand-500" />
|
||||||
|
<span className="text-gray-900 dark:text-white">Staff Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
Our support team is ready to help with email setup 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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings General Page
|
* Help Settings General Page
|
||||||
|
*
|
||||||
|
* Comprehensive help documentation for General Settings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Settings, Building, Globe, Clock, MapPin,
|
ArrowLeft,
|
||||||
CheckCircle, ChevronRight, HelpCircle,
|
Settings,
|
||||||
|
Building2,
|
||||||
|
Globe,
|
||||||
|
Clock,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Save,
|
||||||
|
Shield,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsGeneral: React.FC = () => {
|
const HelpSettingsGeneral: React.FC = () => {
|
||||||
@@ -14,114 +27,299 @@ const HelpSettingsGeneral: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* 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
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
|
||||||
<Settings size={24} className="text-brand-600 dark:text-brand-400" />
|
<Building2 size={24} className="text-brand-600 dark:text-brand-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">General Settings Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">General Settings Guide</h1>
|
||||||
<p className="text-gray-500 dark:text-gray-400">Configure your business basics</p>
|
<p className="text-gray-500 dark:text-gray-400">Configure your business identity, timezone, and contact information</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" /> Overview
|
<Settings size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
General Settings contain the core configuration for your business. Set your business name, contact information, timezone, and other fundamental settings that affect your entire account.
|
General Settings contain the foundational configuration for your business. These settings affect how your business appears to customers, how appointment times are displayed, and how customers can contact you.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
The page is organized into three sections: Business Identity, Timezone Settings, and Contact Information. Changes are saved when you click the "Save Changes" button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Owner Access Note */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-xl border border-amber-200 dark:border-amber-800">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Shield size={20} className="text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-amber-800 dark:text-amber-200">Owner Access Required</h4>
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||||
|
Only business owners can access and modify General Settings. Managers and staff members will see a message indicating they don't have permission to change these settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Business Identity Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Building size={20} className="text-brand-500" /> Business Information
|
<Building2 size={20} className="text-brand-500" /> Business Identity
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Define how your business is identified in the system:
|
||||||
|
</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">
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Building size={20} className="text-blue-500 mt-0.5" />
|
<Building2 size={20} className="text-blue-500 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Business Name</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Business Name</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Your company name displayed to customers</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your company name displayed in the sidebar, emails, booking pages, and customer-facing interfaces. This can be changed at any time.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Globe size={20} className="text-green-500 mt-0.5" />
|
<Globe size={20} className="text-green-500 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Subdomain</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Subdomain</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Your unique URL for customer booking</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your unique URL: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-600 rounded text-xs">yourname.smoothschedule.com</code>.
|
||||||
|
This is your booking page URL that customers use to schedule appointments.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 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-2">
|
||||||
|
<AlertCircle size={16} className="text-blue-600 dark:text-blue-400 mt-0.5 shrink-0" />
|
||||||
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Note:</strong> Your subdomain is read-only and cannot be changed directly. Contact support if you need to change your subdomain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Timezone Settings Section */}
|
||||||
|
<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" /> Timezone 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 how appointment times are calculated and displayed:
|
||||||
|
</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">
|
||||||
|
<Globe size={20} className="text-purple-500 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Business Timezone</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Select the timezone where your business operates. All appointment times are stored relative to this timezone.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Includes common timezones for North America, Europe, Asia, and Australia. Examples: Eastern Time (New York), Pacific Time (Los Angeles), London (GMT/BST).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Clock size={20} className="text-purple-500 mt-0.5" />
|
<Clock size={20} className="text-orange-500 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Timezone</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Time Display Mode</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Business timezone for scheduling</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Choose how times are shown to viewers:
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">Business Timezone:</span>
|
||||||
|
All appointment times display in your business timezone regardless of where the viewer is located. Best for local businesses.
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300">Viewer's Local Timezone:</span>
|
||||||
|
Appointment times automatically adapt to each viewer's local timezone. Best for businesses with customers in different time zones.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle size={16} className="text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
||||||
|
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||||
|
<strong>Important:</strong> Changing your business timezone after appointments are created may cause confusion. The appointments themselves remain at the same absolute time, but the displayed time will shift. Always verify existing appointments after a timezone change.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Information Section */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<Mail size={20} className="text-brand-500" /> Contact Information
|
||||||
|
</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">
|
||||||
|
Provide contact details that customers can use to reach you:
|
||||||
|
</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">
|
||||||
|
<Mail size={20} className="text-blue-500 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Contact Email</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
The email address displayed on your booking page and in customer communications. This is where customers will expect to reach you for questions or support.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<MapPin size={20} className="text-orange-500 mt-0.5" />
|
<Phone size={20} className="text-green-500 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Address</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">Phone Number</h4>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Business location and contact details</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your business phone number for customers who prefer to call. Include country code for international businesses.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Saving Changes Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<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">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<Save size={20} className="text-brand-500 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Save Changes Button</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
After making any modifications, click the "Save Changes" button at the bottom of the page. A green confirmation toast appears when changes are saved successfully.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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-amber-500 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Unsaved Changes</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
If you navigate away without saving, your changes will be lost. Always save before leaving the page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<CheckCircle size={20} className="text-brand-500" /> Benefits
|
||||||
</h2>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Set your timezone correctly to ensure appointments display at the right times</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Consistent Branding:</strong> Your business name appears across all customer touchpoints
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Your subdomain cannot be changed after creation - choose wisely</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Accurate Scheduling:</strong> Correct timezone prevents appointment confusion
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Keep contact information up to date for customer communications</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Flexible Display:</strong> Choose between business or viewer timezone based on your customer base
|
||||||
|
</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>Easy Contact:</strong> Clear contact info helps customers reach you quickly
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Settings */}
|
||||||
<section className="mb-10">
|
<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 Settings</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<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">
|
<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"
|
||||||
|
>
|
||||||
<Settings size={20} className="text-brand-500" />
|
<Settings size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
|
<span className="text-gray-900 dark:text-white">Branding Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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">
|
<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"
|
||||||
|
>
|
||||||
|
<Globe 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/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"
|
||||||
|
>
|
||||||
<Globe size={20} className="text-brand-500" />
|
<Globe size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Custom Domains</span>
|
<span className="text-gray-900 dark:text-white">Custom Domains</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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"
|
||||||
|
>
|
||||||
|
<Mail 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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
Our support team is ready to help with any questions about general settings.
|
||||||
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Quota Page
|
* Help Settings Quota Page
|
||||||
|
*
|
||||||
|
* Documentation for managing quota overages and usage limits.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, BarChart2, TrendingUp, AlertTriangle, Users, Calendar,
|
ArrowLeft,
|
||||||
CheckCircle, ChevronRight, HelpCircle, CreditCard,
|
AlertTriangle,
|
||||||
|
Users,
|
||||||
|
Briefcase,
|
||||||
|
Calendar,
|
||||||
|
Archive,
|
||||||
|
Clock,
|
||||||
|
Check,
|
||||||
|
Download,
|
||||||
|
ChevronDown,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronRight,
|
||||||
|
HelpCircle,
|
||||||
|
CreditCard,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsQuota: React.FC = () => {
|
const HelpSettingsQuota: React.FC = () => {
|
||||||
@@ -14,139 +28,387 @@ const HelpSettingsQuota: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
<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">
|
{/* Back Button */}
|
||||||
<ArrowLeft size={20} /> Back
|
<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>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||||
<BarChart2 size={24} className="text-brand-600 dark:text-brand-400" />
|
<AlertTriangle size={24} className="text-amber-600 dark:text-amber-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Usage & Quota Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Monitor your plan limits</p>
|
Quota Management Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Manage account limits and overages
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<BarChart2 size={20} className="text-brand-500" /> Overview
|
<AlertTriangle size={20} className="text-brand-500" />
|
||||||
|
Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
The Usage & Quota page shows your current usage against your plan limits. Monitor resources, appointments, staff members, and other tracked metrics to ensure you stay within your plan.
|
Quota Management allows you to monitor your usage against plan limits and resolve overages.
|
||||||
|
When you exceed your plan's limits, you'll have a grace period to either archive resources
|
||||||
|
or upgrade your plan.
|
||||||
</p>
|
</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">
|
||||||
|
<AlertTriangle 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">
|
||||||
|
Business owners and managers can access quota management. This page is most important
|
||||||
|
when you have active overages that need to be resolved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Current Usage */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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" /> Tracked Metrics
|
<Check size={20} className="text-brand-500" />
|
||||||
|
Current Usage
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
The usage overview shows your current consumption for each quota type:
|
||||||
<Users size={20} className="text-blue-500 mt-0.5" />
|
</p>
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Resources</h4>
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Staff, rooms, and equipment count</p>
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Users size={18} className="text-blue-500" />
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">Additional Users</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Staff members beyond the plan's included users
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Calendar size={20} className="text-green-500 mt-0.5" />
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div>
|
<Briefcase size={18} className="text-purple-500" />
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Appointments</h4>
|
<span className="font-medium text-gray-900 dark:text-white">Resources</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Monthly appointment count</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Total resources (staff, rooms, equipment)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Users size={20} className="text-purple-500 mt-0.5" />
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div>
|
<Calendar size={18} className="text-green-500" />
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Staff Members</h4>
|
<span className="font-medium text-gray-900 dark:text-white">Services</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Team members with login access</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Active services offered to customers
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
</div>
|
||||||
<BarChart2 size={20} className="text-orange-500 mt-0.5" />
|
|
||||||
<div>
|
<div className="mt-4 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Storage</h4>
|
<p className="text-sm text-green-800 dark:text-green-200 flex items-center gap-2">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">File and image storage used</p>
|
<Check size={16} className="flex-shrink-0" />
|
||||||
</div>
|
If all quotas show <strong>current / limit</strong> with current under limit, you're within
|
||||||
|
your plan limits.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Active Overages */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<AlertTriangle size={20} className="text-brand-500" />
|
||||||
|
Active Overages
|
||||||
|
</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">
|
||||||
|
When you exceed a limit, an overage is created with a <strong>grace period</strong>.
|
||||||
|
You must resolve the overage before the grace period ends.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Overage Card Example */}
|
||||||
|
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded-lg">
|
||||||
|
<h4 className="font-medium text-amber-800 dark:text-amber-200 mb-2">
|
||||||
|
Each Overage Shows:
|
||||||
|
</h4>
|
||||||
|
<ul className="text-sm text-amber-700 dark:text-amber-300 space-y-2">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
<span><strong>Quota Type</strong> - Which limit you've exceeded</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-xs bg-amber-100 dark:bg-amber-800 px-1 rounded">5/3</span>
|
||||||
|
<span><strong>Usage</strong> - Current count vs allowed limit</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="text-red-600 font-medium">2 over</span>
|
||||||
|
<span><strong>Overage Amount</strong> - How many items over the limit</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<Clock size={14} />
|
||||||
|
<span><strong>Days Remaining</strong> - Time left to resolve the overage</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grace Period Warning */}
|
||||||
|
<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">
|
||||||
|
<AlertTriangle size={16} />
|
||||||
|
Auto-Archive Warning
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-red-700 dark:text-red-300">
|
||||||
|
After the grace period ends, the system will <strong>automatically archive</strong> the
|
||||||
|
oldest items to bring you back within limits. Archive items yourself to choose which
|
||||||
|
ones to keep.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Resolving Overages */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<AlertTriangle size={20} className="text-brand-500" /> Limit Warnings
|
<Archive size={20} className="text-brand-500" />
|
||||||
|
Resolving Overages
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-center gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
You have two options to resolve an overage:
|
||||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
</p>
|
||||||
<span className="font-medium text-gray-900 dark:text-white">Under 75%</span>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Plenty of room remaining</span>
|
<div className="space-y-4">
|
||||||
|
{/* Option 1: Archive */}
|
||||||
|
<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">
|
||||||
|
<Archive size={18} className="text-amber-500" />
|
||||||
|
Option 1: Archive Items
|
||||||
|
</h4>
|
||||||
|
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-2 list-decimal list-inside">
|
||||||
|
<li>Click on an overage to expand it</li>
|
||||||
|
<li>Select the items you want to archive using the checkboxes</li>
|
||||||
|
<li>Click <strong>Archive Selected</strong></li>
|
||||||
|
<li>Archived items become read-only and cannot be used for new bookings</li>
|
||||||
|
</ol>
|
||||||
|
<div className="mt-3 p-2 bg-amber-50 dark:bg-amber-900/20 rounded text-xs text-amber-700 dark:text-amber-300">
|
||||||
|
<AlertTriangle size={12} className="inline mr-1" />
|
||||||
|
Tip: Archive the items you use least. You can still view archived data.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
{/* Option 2: Upgrade */}
|
||||||
<span className="font-medium text-gray-900 dark:text-white">75% - 90%</span>
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Consider upgrading soon</span>
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||||
</div>
|
<CreditCard size={18} className="text-brand-500" />
|
||||||
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
Option 2: Upgrade Your Plan
|
||||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
</h4>
|
||||||
<span className="font-medium text-gray-900 dark:text-white">Over 90%</span>
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">- Approaching limit, upgrade recommended</span>
|
Click <strong>Upgrade Plan Instead</strong> to increase your limits. This immediately
|
||||||
|
resolves the overage without archiving anything.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/settings/billing"
|
||||||
|
className="inline-flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 hover:underline"
|
||||||
|
>
|
||||||
|
<CreditCard size={14} />
|
||||||
|
Go to Billing Settings
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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: Archive Items to Resolve Overage
|
||||||
|
</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 > Usage & Quota</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">
|
||||||
|
Review the <strong>Active Overages</strong> section
|
||||||
|
</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">
|
||||||
|
Click on an overage to <ChevronDown size={14} className="inline" /> expand it
|
||||||
|
</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">
|
||||||
|
Review the list of items and select which to archive
|
||||||
|
</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>Archive Selected</strong> to archive the selected items
|
||||||
|
</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">
|
||||||
|
Once enough items are archived, the overage is <strong>resolved</strong>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Additional Actions */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<Download size={20} className="text-brand-500" />
|
||||||
|
Additional Actions
|
||||||
|
</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="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">
|
||||||
|
<Download size={16} className="text-gray-500" />
|
||||||
|
Export Data
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Before archiving, you can export your data to keep a backup. This is useful if you
|
||||||
|
need to reference the data later.
|
||||||
|
</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">Already Archived</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
The "Already Archived" section shows items that have been previously archived with
|
||||||
|
their archive dates. These items are read-only but data is preserved.
|
||||||
|
</p>
|
||||||
|
</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>
|
</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-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">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Check usage regularly to avoid unexpected limits</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Act before the grace period ends</strong> - Choose which items to archive rather
|
||||||
|
than letting the system auto-archive the oldest ones
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Remove inactive resources to free up quota</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Archive inactive items first</strong> - Choose resources or services you no longer
|
||||||
|
actively use
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300">Upgrade before hitting limits to avoid disruption</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
<strong>Consider upgrading</strong> - If you need all your current items, upgrading your
|
||||||
|
plan is often simpler than archiving
|
||||||
|
</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>Export data first</strong> - Before archiving important items, export the data
|
||||||
|
as a backup
|
||||||
|
</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>Watch grace period colors</strong> - Red means less than 1 day, amber means less
|
||||||
|
than 7 days remaining
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Link to="/help/settings/billing" 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/billing"
|
||||||
|
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"
|
||||||
|
>
|
||||||
<CreditCard size={20} className="text-brand-500" />
|
<CreditCard size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Billing Settings</span>
|
<span className="text-gray-900 dark:text-white">Billing Settings</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/help/resources" 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
|
||||||
<Users size={20} className="text-brand-500" />
|
to="/help/resources"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Briefcase size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Resources Guide</span>
|
<span className="text-gray-900 dark:text-white">Resources Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">Our support team is ready to help with any questions.</p>
|
Need More Help?
|
||||||
<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>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Our support team is ready to help with quota management 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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Help Settings Resource Types Page
|
* Help Settings Resource Types Page
|
||||||
|
*
|
||||||
|
* Comprehensive documentation for the Resource Types Settings page.
|
||||||
|
* Documents: Creating, editing, and deleting custom resource types with STAFF/OTHER categories.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Layers, Users, Building, Wrench, Plus,
|
ArrowLeft, Layers, Users, Plus, Pencil, Trash2,
|
||||||
CheckCircle, ChevronRight, HelpCircle, Edit, Palette,
|
CheckCircle, ChevronRight, HelpCircle, AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const HelpSettingsResourceTypes: React.FC = () => {
|
const HelpSettingsResourceTypes: React.FC = () => {
|
||||||
@@ -18,144 +21,310 @@ const HelpSettingsResourceTypes: React.FC = () => {
|
|||||||
<ArrowLeft size={20} /> Back
|
<ArrowLeft size={20} /> Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<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">
|
<div className="w-12 h-12 rounded-xl bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
|
||||||
<Layers size={24} className="text-brand-600 dark:text-brand-400" />
|
<Layers size={24} className="text-indigo-600 dark:text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Resource Types Guide</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Resource Types Guide</h1>
|
||||||
<p className="text-gray-500 dark:text-gray-400">Categorize your bookable resources</p>
|
<p className="text-gray-500 dark:text-gray-400">Define custom categories for your resources</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Layers size={20} className="text-brand-500" /> Overview
|
<Layers size={20} className="text-brand-500" /> Overview
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
Resource Types help you categorize different kinds of bookable entities in your business. Create custom types like "Staff", "Rooms", "Equipment", or any category that fits your needs.
|
Resource Types let you define custom categories for your bookable resources. Instead of just
|
||||||
</p>
|
"Staff", "Room", and "Equipment", you can create specific types like "Stylist", "Treatment Room",
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
"Camera", or any category that fits your business needs.
|
||||||
Each resource type can have its own icon and color, making it easy to distinguish between different categories in the scheduler.
|
|
||||||
</p>
|
</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 create and manage resource types.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Resource Type Categories */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Layers size={20} className="text-brand-500" /> Default Types
|
<Layers size={20} className="text-brand-500" /> Categories
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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-3 gap-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
Each resource type belongs to one of two categories, which determines how it behaves in the system:
|
||||||
<Users size={20} className="text-blue-500 mt-0.5" />
|
</p>
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Staff</h4>
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Team members who provide services</p>
|
{/* Staff Category */}
|
||||||
|
<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">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||||
|
<Users size={20} className="text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white">Staff Category</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Requires staff assignment - these resources must be linked to a team member from your Staff page.
|
||||||
|
Use this for types like Stylist, Therapist, Consultant, or any role performed by a person.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
||||||
<Building size={20} className="text-green-500 mt-0.5" />
|
{/* Other Category */}
|
||||||
<div>
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Rooms</h4>
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Physical spaces for appointments</p>
|
<div className="w-10 h-10 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
||||||
</div>
|
<Layers size={20} className="text-gray-600 dark:text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div>
|
||||||
<Wrench size={20} className="text-purple-500 mt-0.5" />
|
<h4 className="font-medium text-gray-900 dark:text-white">Other Category</h4>
|
||||||
<div>
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Equipment</h4>
|
General resources - physical items or spaces that don't require a staff link.
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Tools and machinery</p>
|
Use this for rooms, equipment, vehicles, or any non-person resource.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Your Resource Types List */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
<Plus size={20} className="text-brand-500" /> Managing Types
|
<Layers size={20} className="text-brand-500" /> Your Resource Types
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<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 main section displays all your defined resource types. Each type shows:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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">For Each Type</h4>
|
||||||
|
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
|
<li>• <strong>Icon:</strong> Blue person icon for Staff types, gray layers icon for Other types</li>
|
||||||
|
<li>• <strong>Name:</strong> The type's name (e.g., "Stylist", "Treatment Room")</li>
|
||||||
|
<li>• <strong>Category Label:</strong> "Requires staff assignment" or "General resource"</li>
|
||||||
|
<li>• <strong>Description:</strong> Optional description you provide</li>
|
||||||
|
<li>• <strong>Default Badge:</strong> Shows if this is a system default type (can't be deleted)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<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">
|
||||||
|
<Pencil size={16} className="text-brand-500" />
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Edit</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Click the pencil icon to modify name, description, or category
|
||||||
|
</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">Delete</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Remove custom types (not available for default types)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Creating a Resource Type */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<Plus size={20} className="text-green-500" /> Creating a Resource Type
|
||||||
|
</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 "Add Type" button to create a new resource type. You'll fill out:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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-3">Form Fields</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300 text-sm">Name *</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
The name for this type (e.g., "Stylist", "Treatment Room", "Camera")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300 text-sm">Description</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Optional text to describe what this type represents
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700 dark:text-gray-300 text-sm">Category *</span>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Choose "Staff" if resources need staff assignment, or "Other" for general resources
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
<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">Steps to Create</h4>
|
||||||
|
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
|
||||||
|
<li>Click <strong>Add Type</strong> button</li>
|
||||||
|
<li>Enter the name (required)</li>
|
||||||
|
<li>Add a description (optional)</li>
|
||||||
|
<li>Select the category (Staff or Other)</li>
|
||||||
|
<li>Click <strong>Create</strong> to save</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Use Cases */}
|
||||||
|
<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" /> Example Use Cases
|
||||||
|
</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">
|
||||||
|
Here are some examples of custom resource types for different businesses:
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Plus size={20} className="text-green-500 mt-0.5" />
|
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Hair Salon</h4>
|
||||||
<div>
|
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Create New Type</h4>
|
<li>• Stylist (Staff)</li>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Add custom resource categories for your business</p>
|
<li>• Colorist (Staff)</li>
|
||||||
</div>
|
<li>• Wash Station (Other)</li>
|
||||||
|
<li>• Styling Chair (Other)</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Edit size={20} className="text-blue-500 mt-0.5" />
|
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Medical Clinic</h4>
|
||||||
<div>
|
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Edit Types</h4>
|
<li>• Doctor (Staff)</li>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Modify name, icon, and color of existing types</p>
|
<li>• Nurse (Staff)</li>
|
||||||
</div>
|
<li>• Exam Room (Other)</li>
|
||||||
|
<li>• X-Ray Machine (Other)</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Palette size={20} className="text-purple-500 mt-0.5" />
|
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Photo Studio</h4>
|
||||||
<div>
|
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Custom Colors</h4>
|
<li>• Photographer (Staff)</li>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Assign colors for visual distinction</p>
|
<li>• Assistant (Staff)</li>
|
||||||
</div>
|
<li>• Studio A (Other)</li>
|
||||||
|
<li>• Lighting Kit (Other)</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||||
<Layers size={20} className="text-orange-500 mt-0.5" />
|
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Fitness Center</h4>
|
||||||
<div>
|
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Assign to Resources</h4>
|
<li>• Personal Trainer (Staff)</li>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">Categorize each resource under a type</p>
|
<li>• Yoga Instructor (Staff)</li>
|
||||||
</div>
|
<li>• Training Room (Other)</li>
|
||||||
|
<li>• Spin Bike (Other)</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Tips */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
<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
|
<CheckCircle size={20} className="text-brand-500" /> Tips
|
||||||
</h2>
|
</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-indigo-50 dark:from-brand-900/20 dark:to-indigo-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Organization:</strong> Group resources logically for easier management</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Create specific types rather than generic ones - "Stylist" is better than just "Staff"
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Visual Clarity:</strong> Color-coding helps identify resource types quickly</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Use the Staff category when the resource represents a person who needs a system account
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
|
||||||
<span className="text-gray-700 dark:text-gray-300"><strong>Filtering:</strong> Filter scheduler view by resource type</span>
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
Default types cannot be deleted, but you can create alternatives and use those instead
|
||||||
|
</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 descriptions to help other team members understand what each type is for
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Related Features */}
|
||||||
<section className="mb-10">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Link to="/help/resources" 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/resources" 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" />
|
<Layers size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Resources Guide</span>
|
<span className="text-gray-900 dark:text-white">Resources Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
<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">
|
||||||
|
<Users size={20} className="text-brand-500" />
|
||||||
|
<span className="text-gray-900 dark:text-white">Staff Guide</span>
|
||||||
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
|
</Link>
|
||||||
<Link to="/help/scheduler" 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/scheduler" 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">
|
||||||
<Building size={20} className="text-brand-500" />
|
<Layers size={20} className="text-brand-500" />
|
||||||
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
|
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
|
||||||
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
<ChevronRight size={16} className="text-gray-400 ml-auto" />
|
||||||
</Link>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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" />
|
<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>
|
<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>
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
<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>
|
Our support team is ready to help with resource type 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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
useUpdateSubscriptionPlan,
|
useUpdateSubscriptionPlan,
|
||||||
useDeleteSubscriptionPlan,
|
useDeleteSubscriptionPlan,
|
||||||
useSyncPlansWithStripe,
|
useSyncPlansWithStripe,
|
||||||
|
useSyncPlanToTenants,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionPlanCreate,
|
SubscriptionPlanCreate,
|
||||||
} from '../../hooks/usePlatformSettings';
|
} from '../../hooks/usePlatformSettings';
|
||||||
@@ -527,9 +528,12 @@ const TiersSettingsTab: React.FC = () => {
|
|||||||
const updatePlanMutation = useUpdateSubscriptionPlan();
|
const updatePlanMutation = useUpdateSubscriptionPlan();
|
||||||
const deletePlanMutation = useDeleteSubscriptionPlan();
|
const deletePlanMutation = useDeleteSubscriptionPlan();
|
||||||
const syncMutation = useSyncPlansWithStripe();
|
const syncMutation = useSyncPlansWithStripe();
|
||||||
|
const syncTenantsMutation = useSyncPlanToTenants();
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null);
|
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null);
|
||||||
|
const [showSyncConfirmModal, setShowSyncConfirmModal] = useState(false);
|
||||||
|
const [savedPlanForSync, setSavedPlanForSync] = useState<SubscriptionPlan | null>(null);
|
||||||
|
|
||||||
const handleCreatePlan = () => {
|
const handleCreatePlan = () => {
|
||||||
setEditingPlan(null);
|
setEditingPlan(null);
|
||||||
@@ -550,6 +554,9 @@ const TiersSettingsTab: React.FC = () => {
|
|||||||
const handleSavePlan = async (data: SubscriptionPlanCreate) => {
|
const handleSavePlan = async (data: SubscriptionPlanCreate) => {
|
||||||
if (editingPlan) {
|
if (editingPlan) {
|
||||||
await updatePlanMutation.mutateAsync({ id: editingPlan.id, ...data });
|
await updatePlanMutation.mutateAsync({ id: editingPlan.id, ...data });
|
||||||
|
// After updating an existing plan, ask if they want to sync to tenants
|
||||||
|
setSavedPlanForSync(editingPlan);
|
||||||
|
setShowSyncConfirmModal(true);
|
||||||
} else {
|
} else {
|
||||||
await createPlanMutation.mutateAsync(data);
|
await createPlanMutation.mutateAsync(data);
|
||||||
}
|
}
|
||||||
@@ -557,6 +564,19 @@ const TiersSettingsTab: React.FC = () => {
|
|||||||
setEditingPlan(null);
|
setEditingPlan(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSyncConfirm = async () => {
|
||||||
|
if (savedPlanForSync) {
|
||||||
|
await syncTenantsMutation.mutateAsync(savedPlanForSync.id);
|
||||||
|
}
|
||||||
|
setShowSyncConfirmModal(false);
|
||||||
|
setSavedPlanForSync(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSyncCancel = () => {
|
||||||
|
setShowSyncConfirmModal(false);
|
||||||
|
setSavedPlanForSync(null);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
@@ -672,6 +692,48 @@ const TiersSettingsTab: React.FC = () => {
|
|||||||
isLoading={createPlanMutation.isPending || updatePlanMutation.isPending}
|
isLoading={createPlanMutation.isPending || updatePlanMutation.isPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Sync Confirmation Modal */}
|
||||||
|
{showSyncConfirmModal && savedPlanForSync && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Update All Tenants?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Do you want to sync the updated settings to all tenants currently on the "{savedPlanForSync.name}" plan?
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-500 mt-2">
|
||||||
|
This will update permissions and limits for all businesses on this tier.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-700/50 rounded-b-lg">
|
||||||
|
<button
|
||||||
|
onClick={handleSyncCancel}
|
||||||
|
disabled={syncTenantsMutation.isPending}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
No, Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSyncConfirm}
|
||||||
|
disabled={syncTenantsMutation.isPending}
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{syncTenantsMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Yes, Update All Tenants'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -780,6 +842,8 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
|
|||||||
advanced_reporting: false,
|
advanced_reporting: false,
|
||||||
priority_support: false,
|
priority_support: false,
|
||||||
can_use_custom_domain: false,
|
can_use_custom_domain: false,
|
||||||
|
can_use_plugins: false,
|
||||||
|
can_use_tasks: false,
|
||||||
can_create_plugins: false,
|
can_create_plugins: false,
|
||||||
can_white_label: false,
|
can_white_label: false,
|
||||||
can_api_access: false,
|
can_api_access: false,
|
||||||
@@ -1470,10 +1534,37 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
|
|||||||
<span className="text-sm text-gray-700 dark:text-gray-300">API Access</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">API Access</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.permissions?.can_use_plugins || false}
|
||||||
|
onChange={(e) => {
|
||||||
|
handlePermissionChange('can_use_plugins', e.target.checked);
|
||||||
|
// If disabling plugins, also disable dependent permissions
|
||||||
|
if (!e.target.checked) {
|
||||||
|
handlePermissionChange('can_use_tasks', false);
|
||||||
|
handlePermissionChange('can_create_plugins', false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Use Plugins</span>
|
||||||
|
</label>
|
||||||
|
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.permissions?.can_use_tasks || false}
|
||||||
|
onChange={(e) => handlePermissionChange('can_use_tasks', e.target.checked)}
|
||||||
|
disabled={!formData.permissions?.can_use_plugins}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Scheduled Tasks</span>
|
||||||
|
</label>
|
||||||
|
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer ${formData.permissions?.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'opacity-50 cursor-not-allowed'}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.permissions?.can_create_plugins || false}
|
checked={formData.permissions?.can_create_plugins || false}
|
||||||
onChange={(e) => handlePermissionChange('can_create_plugins', e.target.checked)}
|
onChange={(e) => handlePermissionChange('can_create_plugins', e.target.checked)}
|
||||||
|
disabled={!formData.permissions?.can_use_plugins}
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
||||||
|
|||||||
@@ -69,38 +69,29 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
// Limits
|
// Limits
|
||||||
max_users: 5,
|
max_users: 5,
|
||||||
max_resources: 10,
|
max_resources: 10,
|
||||||
max_services: 0,
|
// Platform Permissions (flat, matching backend model)
|
||||||
max_appointments: 0,
|
|
||||||
max_email_templates: 0,
|
|
||||||
max_automated_tasks: 0,
|
|
||||||
// Platform Permissions
|
|
||||||
can_manage_oauth_credentials: false,
|
can_manage_oauth_credentials: false,
|
||||||
can_accept_payments: false,
|
can_accept_payments: false,
|
||||||
can_use_custom_domain: false,
|
can_use_custom_domain: false,
|
||||||
can_white_label: false,
|
can_white_label: false,
|
||||||
can_api_access: false,
|
can_api_access: false,
|
||||||
// Extended Permissions
|
// Feature permissions (flat, matching backend model)
|
||||||
permissions: {
|
can_add_video_conferencing: false,
|
||||||
// Payments & Revenue
|
can_connect_to_api: false,
|
||||||
can_process_refunds: false,
|
can_book_repeated_events: true,
|
||||||
can_create_packages: false,
|
can_require_2fa: false,
|
||||||
// Communication
|
can_download_logs: false,
|
||||||
sms_reminders: false,
|
can_delete_data: false,
|
||||||
can_use_masked_phone_numbers: false,
|
can_use_sms_reminders: false,
|
||||||
can_use_email_templates: false,
|
can_use_masked_phone_numbers: false,
|
||||||
// Customization
|
can_use_pos: false,
|
||||||
can_customize_booking_page: false,
|
can_use_mobile_app: false,
|
||||||
// Advanced Features
|
can_export_data: false,
|
||||||
advanced_reporting: false,
|
can_use_plugins: true,
|
||||||
can_create_plugins: false,
|
can_use_tasks: true,
|
||||||
can_export_data: false,
|
can_create_plugins: false,
|
||||||
can_use_webhooks: false,
|
can_use_webhooks: false,
|
||||||
calendar_sync: false,
|
can_use_calendar_sync: false,
|
||||||
// Support & Enterprise
|
|
||||||
priority_support: false,
|
|
||||||
dedicated_support: false,
|
|
||||||
sso_enabled: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get tier defaults from subscription plans or fallback to static defaults
|
// Get tier defaults from subscription plans or fallback to static defaults
|
||||||
@@ -122,33 +113,29 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
// Limits
|
// Limits
|
||||||
max_users: plan.limits?.max_users ?? staticDefaults.max_users,
|
max_users: plan.limits?.max_users ?? staticDefaults.max_users,
|
||||||
max_resources: plan.limits?.max_resources ?? staticDefaults.max_resources,
|
max_resources: plan.limits?.max_resources ?? staticDefaults.max_resources,
|
||||||
max_services: plan.limits?.max_services ?? 0,
|
|
||||||
max_appointments: plan.limits?.max_appointments ?? 0,
|
|
||||||
max_email_templates: plan.limits?.max_email_templates ?? 0,
|
|
||||||
max_automated_tasks: plan.limits?.max_automated_tasks ?? 0,
|
|
||||||
// Platform Permissions
|
// Platform Permissions
|
||||||
can_manage_oauth_credentials: plan.permissions?.can_manage_oauth_credentials ?? staticDefaults.can_manage_oauth_credentials,
|
can_manage_oauth_credentials: plan.permissions?.can_manage_oauth_credentials ?? staticDefaults.can_manage_oauth_credentials,
|
||||||
can_accept_payments: plan.permissions?.can_accept_payments ?? staticDefaults.can_accept_payments,
|
can_accept_payments: plan.permissions?.can_accept_payments ?? staticDefaults.can_accept_payments,
|
||||||
can_use_custom_domain: plan.permissions?.can_use_custom_domain ?? staticDefaults.can_use_custom_domain,
|
can_use_custom_domain: plan.permissions?.can_use_custom_domain ?? staticDefaults.can_use_custom_domain,
|
||||||
can_white_label: plan.permissions?.can_white_label ?? staticDefaults.can_white_label,
|
can_white_label: plan.permissions?.can_white_label ?? staticDefaults.can_white_label,
|
||||||
can_api_access: plan.permissions?.can_api_access ?? staticDefaults.can_api_access,
|
can_api_access: plan.permissions?.can_api_access ?? staticDefaults.can_api_access,
|
||||||
// Extended Permissions
|
// Feature permissions (flat, matching backend model)
|
||||||
permissions: {
|
can_add_video_conferencing: plan.permissions?.video_conferencing ?? false,
|
||||||
can_process_refunds: plan.permissions?.can_process_refunds ?? false,
|
can_connect_to_api: plan.permissions?.can_api_access ?? false,
|
||||||
can_create_packages: plan.permissions?.can_create_packages ?? false,
|
can_book_repeated_events: true,
|
||||||
sms_reminders: plan.permissions?.sms_reminders ?? false,
|
can_require_2fa: false,
|
||||||
can_use_masked_phone_numbers: plan.permissions?.can_use_masked_phone_numbers ?? false,
|
can_download_logs: false,
|
||||||
can_use_email_templates: plan.permissions?.can_use_email_templates ?? false,
|
can_delete_data: false,
|
||||||
can_customize_booking_page: plan.permissions?.can_customize_booking_page ?? false,
|
can_use_sms_reminders: plan.permissions?.sms_reminders ?? false,
|
||||||
advanced_reporting: plan.permissions?.advanced_reporting ?? false,
|
can_use_masked_phone_numbers: plan.permissions?.masked_calling ?? false,
|
||||||
can_create_plugins: plan.permissions?.can_create_plugins ?? false,
|
can_use_pos: false,
|
||||||
can_export_data: plan.permissions?.can_export_data ?? false,
|
can_use_mobile_app: false,
|
||||||
can_use_webhooks: plan.permissions?.can_use_webhooks ?? false,
|
can_export_data: plan.permissions?.export_data ?? false,
|
||||||
calendar_sync: plan.permissions?.calendar_sync ?? false,
|
can_use_plugins: plan.permissions?.plugins ?? true,
|
||||||
priority_support: plan.permissions?.priority_support ?? false,
|
can_use_tasks: plan.permissions?.tasks ?? true,
|
||||||
dedicated_support: plan.permissions?.dedicated_support ?? false,
|
can_create_plugins: plan.permissions?.can_create_plugins ?? false,
|
||||||
sso_enabled: plan.permissions?.sso_enabled ?? false,
|
can_use_webhooks: plan.permissions?.webhooks ?? false,
|
||||||
},
|
can_use_calendar_sync: plan.permissions?.calendar_sync ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,26 +143,22 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
const staticDefaults = TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
|
const staticDefaults = TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
|
||||||
return {
|
return {
|
||||||
...staticDefaults,
|
...staticDefaults,
|
||||||
max_services: 0,
|
can_add_video_conferencing: false,
|
||||||
max_appointments: 0,
|
can_connect_to_api: staticDefaults.can_api_access,
|
||||||
max_email_templates: 0,
|
can_book_repeated_events: true,
|
||||||
max_automated_tasks: 0,
|
can_require_2fa: false,
|
||||||
permissions: {
|
can_download_logs: false,
|
||||||
can_process_refunds: false,
|
can_delete_data: false,
|
||||||
can_create_packages: false,
|
can_use_sms_reminders: false,
|
||||||
sms_reminders: false,
|
can_use_masked_phone_numbers: false,
|
||||||
can_use_masked_phone_numbers: false,
|
can_use_pos: false,
|
||||||
can_use_email_templates: false,
|
can_use_mobile_app: false,
|
||||||
can_customize_booking_page: false,
|
can_export_data: false,
|
||||||
advanced_reporting: false,
|
can_use_plugins: true,
|
||||||
can_create_plugins: false,
|
can_use_tasks: true,
|
||||||
can_export_data: false,
|
can_create_plugins: false,
|
||||||
can_use_webhooks: false,
|
can_use_webhooks: false,
|
||||||
calendar_sync: false,
|
can_use_calendar_sync: false,
|
||||||
priority_support: false,
|
|
||||||
dedicated_support: false,
|
|
||||||
sso_enabled: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,6 +184,7 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
// Update form when business changes
|
// Update form when business changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (business) {
|
if (business) {
|
||||||
|
const b = business as any;
|
||||||
setEditForm({
|
setEditForm({
|
||||||
name: business.name,
|
name: business.name,
|
||||||
is_active: business.is_active,
|
is_active: business.is_active,
|
||||||
@@ -208,47 +192,33 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
// Limits
|
// Limits
|
||||||
max_users: business.max_users || 5,
|
max_users: business.max_users || 5,
|
||||||
max_resources: business.max_resources || 10,
|
max_resources: business.max_resources || 10,
|
||||||
max_services: (business as any).max_services || 0,
|
// Platform Permissions (flat, matching backend)
|
||||||
max_appointments: (business as any).max_appointments || 0,
|
|
||||||
max_email_templates: (business as any).max_email_templates || 0,
|
|
||||||
max_automated_tasks: (business as any).max_automated_tasks || 0,
|
|
||||||
// Platform Permissions
|
|
||||||
can_manage_oauth_credentials: business.can_manage_oauth_credentials || false,
|
can_manage_oauth_credentials: business.can_manage_oauth_credentials || false,
|
||||||
can_accept_payments: business.can_accept_payments || false,
|
can_accept_payments: b.can_accept_payments || false,
|
||||||
can_use_custom_domain: business.can_use_custom_domain || false,
|
can_use_custom_domain: b.can_use_custom_domain || false,
|
||||||
can_white_label: business.can_white_label || false,
|
can_white_label: b.can_white_label || false,
|
||||||
can_api_access: business.can_api_access || false,
|
can_api_access: b.can_api_access || false,
|
||||||
// Extended Permissions
|
// Feature permissions (flat, matching backend)
|
||||||
permissions: {
|
can_add_video_conferencing: b.can_add_video_conferencing || false,
|
||||||
can_process_refunds: (business as any).permissions?.can_process_refunds || false,
|
can_connect_to_api: b.can_connect_to_api || false,
|
||||||
can_create_packages: (business as any).permissions?.can_create_packages || false,
|
can_book_repeated_events: b.can_book_repeated_events ?? true,
|
||||||
sms_reminders: (business as any).permissions?.sms_reminders || false,
|
can_require_2fa: b.can_require_2fa || false,
|
||||||
can_use_masked_phone_numbers: (business as any).permissions?.can_use_masked_phone_numbers || false,
|
can_download_logs: b.can_download_logs || false,
|
||||||
can_use_email_templates: (business as any).permissions?.can_use_email_templates || false,
|
can_delete_data: b.can_delete_data || false,
|
||||||
can_customize_booking_page: (business as any).permissions?.can_customize_booking_page || false,
|
can_use_sms_reminders: b.can_use_sms_reminders || false,
|
||||||
advanced_reporting: (business as any).permissions?.advanced_reporting || false,
|
can_use_masked_phone_numbers: b.can_use_masked_phone_numbers || false,
|
||||||
can_create_plugins: (business as any).permissions?.can_create_plugins || false,
|
can_use_pos: b.can_use_pos || false,
|
||||||
can_export_data: (business as any).permissions?.can_export_data || false,
|
can_use_mobile_app: b.can_use_mobile_app || false,
|
||||||
can_use_webhooks: (business as any).permissions?.can_use_webhooks || false,
|
can_export_data: b.can_export_data || false,
|
||||||
calendar_sync: (business as any).permissions?.calendar_sync || false,
|
can_use_plugins: b.can_use_plugins ?? true,
|
||||||
priority_support: (business as any).permissions?.priority_support || false,
|
can_use_tasks: b.can_use_tasks ?? true,
|
||||||
dedicated_support: (business as any).permissions?.dedicated_support || false,
|
can_create_plugins: b.can_create_plugins || false,
|
||||||
sso_enabled: (business as any).permissions?.sso_enabled || false,
|
can_use_webhooks: b.can_use_webhooks || false,
|
||||||
},
|
can_use_calendar_sync: b.can_use_calendar_sync || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [business]);
|
}, [business]);
|
||||||
|
|
||||||
// Helper for permission changes
|
|
||||||
const handlePermissionChange = (key: string, value: boolean) => {
|
|
||||||
setEditForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
permissions: {
|
|
||||||
...prev.permissions,
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSave = () => {
|
const handleEditSave = () => {
|
||||||
if (!business) return;
|
if (!business) return;
|
||||||
@@ -357,7 +327,7 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Use -1 for unlimited. These limits control what this business can create.
|
Use -1 for unlimited. These limits control what this business can create.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
Max Users
|
Max Users
|
||||||
@@ -382,54 +352,6 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Max Services
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
value={editForm.max_services}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, max_services: parseInt(e.target.value) || 0 })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Max Appointments / Month
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
value={editForm.max_appointments}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, max_appointments: parseInt(e.target.value) || 0 })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Max Email Templates
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
value={editForm.max_email_templates}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, max_email_templates: parseInt(e.target.value) || 0 })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Max Automated Tasks
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="-1"
|
|
||||||
value={editForm.max_automated_tasks}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, max_automated_tasks: parseInt(e.target.value) || 0 })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -456,24 +378,6 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Online Payments</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Online Payments</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_process_refunds}
|
|
||||||
onChange={(e) => handlePermissionChange('can_process_refunds', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Process Refunds</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_create_packages}
|
|
||||||
onChange={(e) => handlePermissionChange('can_create_packages', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Service Packages</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -484,8 +388,8 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.sms_reminders}
|
checked={editForm.can_use_sms_reminders}
|
||||||
onChange={(e) => handlePermissionChange('sms_reminders', e.target.checked)}
|
onChange={(e) => setEditForm({ ...editForm, can_use_sms_reminders: e.target.checked })}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">SMS Reminders</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">SMS Reminders</span>
|
||||||
@@ -493,21 +397,12 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.can_use_masked_phone_numbers}
|
checked={editForm.can_use_masked_phone_numbers}
|
||||||
onChange={(e) => handlePermissionChange('can_use_masked_phone_numbers', e.target.checked)}
|
onChange={(e) => setEditForm({ ...editForm, can_use_masked_phone_numbers: e.target.checked })}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Masked Calling</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Masked Calling</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_use_email_templates}
|
|
||||||
onChange={(e) => handlePermissionChange('can_use_email_templates', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Email Templates</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -515,15 +410,6 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Customization</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Customization</h4>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_customize_booking_page}
|
|
||||||
onChange={(e) => handlePermissionChange('can_customize_booking_page', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Custom Booking Page</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -545,19 +431,59 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Features */}
|
{/* Plugins & Automation */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Advanced Features</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Plugins & Automation</h4>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.advanced_reporting}
|
checked={editForm.can_use_plugins}
|
||||||
onChange={(e) => handlePermissionChange('advanced_reporting', e.target.checked)}
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setEditForm(prev => ({
|
||||||
|
...prev,
|
||||||
|
can_use_plugins: checked,
|
||||||
|
// If disabling plugins, also disable tasks and create plugins
|
||||||
|
...(checked ? {} : { can_use_tasks: false, can_create_plugins: false })
|
||||||
|
}));
|
||||||
|
}}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Advanced Analytics</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Use Plugins</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg ${editForm.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm.can_use_tasks}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, can_use_tasks: e.target.checked })}
|
||||||
|
disabled={!editForm.can_use_plugins}
|
||||||
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Scheduled Tasks</span>
|
||||||
|
</label>
|
||||||
|
<label className={`flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg ${editForm.can_use_plugins ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm.can_create_plugins}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, can_create_plugins: e.target.checked })}
|
||||||
|
disabled={!editForm.can_use_plugins}
|
||||||
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{!editForm.can_use_plugins && (
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||||
|
Enable "Use Plugins" to allow Scheduled Tasks and Create Plugins
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Features */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Advanced Features</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -570,26 +496,8 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.can_create_plugins}
|
checked={editForm.can_use_webhooks}
|
||||||
onChange={(e) => handlePermissionChange('can_create_plugins', e.target.checked)}
|
onChange={(e) => setEditForm({ ...editForm, can_use_webhooks: e.target.checked })}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_export_data}
|
|
||||||
onChange={(e) => handlePermissionChange('can_export_data', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Data Export</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.can_use_webhooks}
|
|
||||||
onChange={(e) => handlePermissionChange('can_use_webhooks', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Webhooks</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Webhooks</span>
|
||||||
@@ -597,18 +505,36 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.calendar_sync}
|
checked={editForm.can_use_calendar_sync}
|
||||||
onChange={(e) => handlePermissionChange('calendar_sync', e.target.checked)}
|
onChange={(e) => setEditForm({ ...editForm, can_use_calendar_sync: e.target.checked })}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Calendar Sync</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Calendar Sync</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm.can_export_data}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, can_export_data: e.target.checked })}
|
||||||
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Data Export</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm.can_add_video_conferencing}
|
||||||
|
onChange={(e) => setEditForm({ ...editForm, can_add_video_conferencing: e.target.checked })}
|
||||||
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">Video Conferencing</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Support & Enterprise */}
|
{/* Enterprise */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Support & Enterprise</h4>
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Enterprise</h4>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -622,29 +548,11 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
|||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editForm.permissions.priority_support}
|
checked={editForm.can_require_2fa}
|
||||||
onChange={(e) => handlePermissionChange('priority_support', e.target.checked)}
|
onChange={(e) => setEditForm({ ...editForm, can_require_2fa: e.target.checked })}
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Priority Support</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300">Require 2FA</span>
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.dedicated_support}
|
|
||||||
onChange={(e) => handlePermissionChange('dedicated_support', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Dedicated Support</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editForm.permissions.sso_enabled}
|
|
||||||
onChange={(e) => handlePermissionChange('sso_enabled', e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">SSO / SAML</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface PlanPermissions {
|
|||||||
white_label: boolean;
|
white_label: boolean;
|
||||||
custom_oauth: boolean;
|
custom_oauth: boolean;
|
||||||
plugins: boolean;
|
plugins: boolean;
|
||||||
|
tasks: boolean;
|
||||||
export_data: boolean;
|
export_data: boolean;
|
||||||
video_conferencing: boolean;
|
video_conferencing: boolean;
|
||||||
two_factor_auth: boolean;
|
two_factor_auth: boolean;
|
||||||
|
|||||||
@@ -13,6 +13,4 @@ class CoreConfig(AppConfig):
|
|||||||
"""
|
"""
|
||||||
Import signals and perform app initialization.
|
Import signals and perform app initialization.
|
||||||
"""
|
"""
|
||||||
# Import signals here when needed
|
from . import signals # noqa: F401
|
||||||
# from . import signals
|
|
||||||
pass
|
|
||||||
|
|||||||
23
smoothschedule/core/migrations/0021_add_can_use_plugins.py
Normal file
23
smoothschedule/core/migrations/0021_add_can_use_plugins.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-03 15:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0020_booking_return_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='can_use_plugins',
|
||||||
|
field=models.BooleanField(default=True, help_text='Whether this business can use plugins from the marketplace'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='can_create_plugins',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether this business can create custom plugins for automation (requires can_use_plugins)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
smoothschedule/core/migrations/0022_add_can_use_tasks.py
Normal file
18
smoothschedule/core/migrations/0022_add_can_use_tasks.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-03 15:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0021_add_can_use_plugins'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='can_use_tasks',
|
||||||
|
field=models.BooleanField(default=True, help_text='Whether this business can create scheduled tasks (requires can_use_plugins)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -207,9 +207,17 @@ class Tenant(TenantMixin):
|
|||||||
default=False,
|
default=False,
|
||||||
help_text="Whether this business can export data (appointments, customers, etc.)"
|
help_text="Whether this business can export data (appointments, customers, etc.)"
|
||||||
)
|
)
|
||||||
|
can_use_plugins = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this business can use plugins from the marketplace"
|
||||||
|
)
|
||||||
|
can_use_tasks = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this business can create scheduled tasks (requires can_use_plugins)"
|
||||||
|
)
|
||||||
can_create_plugins = models.BooleanField(
|
can_create_plugins = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text="Whether this business can create custom plugins for automation"
|
help_text="Whether this business can create custom plugins for automation (requires can_use_plugins)"
|
||||||
)
|
)
|
||||||
can_use_webhooks = models.BooleanField(
|
can_use_webhooks = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
67
smoothschedule/core/signals.py
Normal file
67
smoothschedule/core/signals.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Core App Signals
|
||||||
|
|
||||||
|
Handles automatic setup tasks when tenants are created.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender='core.Tenant')
|
||||||
|
def seed_platform_plugins_on_tenant_create(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Seed platform plugins when a new tenant is created.
|
||||||
|
|
||||||
|
This ensures new tenants have access to all marketplace plugins immediately.
|
||||||
|
"""
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip public schema
|
||||||
|
if instance.schema_name == 'public':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Defer the import to avoid circular imports
|
||||||
|
from django.db import connection
|
||||||
|
from django_tenants.utils import schema_context
|
||||||
|
from schedule.models import PluginTemplate
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
logger.info(f"Seeding platform plugins for new tenant: {instance.schema_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with schema_context(instance.schema_name):
|
||||||
|
# Import the plugin definitions from the seed command
|
||||||
|
from schedule.management.commands.seed_platform_plugins import get_platform_plugins
|
||||||
|
|
||||||
|
plugins_data = get_platform_plugins()
|
||||||
|
created_count = 0
|
||||||
|
|
||||||
|
for plugin_data in plugins_data:
|
||||||
|
# Check if plugin already exists by slug
|
||||||
|
if PluginTemplate.objects.filter(slug=plugin_data['slug']).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create the plugin
|
||||||
|
PluginTemplate.objects.create(
|
||||||
|
name=plugin_data['name'],
|
||||||
|
slug=plugin_data['slug'],
|
||||||
|
category=plugin_data['category'],
|
||||||
|
short_description=plugin_data['short_description'],
|
||||||
|
description=plugin_data['description'],
|
||||||
|
plugin_code=plugin_data['plugin_code'],
|
||||||
|
logo_url=plugin_data.get('logo_url', ''),
|
||||||
|
visibility=PluginTemplate.Visibility.PLATFORM,
|
||||||
|
is_approved=True,
|
||||||
|
approved_at=timezone.now(),
|
||||||
|
author_name='Smooth Schedule',
|
||||||
|
license_type='PLATFORM',
|
||||||
|
)
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
logger.info(f"Created {created_count} platform plugins for tenant: {instance.schema_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to seed plugins for tenant {instance.schema_name}: {e}")
|
||||||
@@ -10,6 +10,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from core.permissions import HasFeaturePermission
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from .services import get_stripe_service_for_tenant
|
from .services import get_stripe_service_for_tenant
|
||||||
from .models import TransactionLink
|
from .models import TransactionLink
|
||||||
@@ -517,7 +518,7 @@ class ApiKeysView(APIView):
|
|||||||
GET /payments/api-keys/
|
GET /payments/api-keys/
|
||||||
POST /payments/api-keys/
|
POST /payments/api-keys/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current API key configuration."""
|
"""Get current API key configuration."""
|
||||||
@@ -627,7 +628,7 @@ class ApiKeysValidateView(APIView):
|
|||||||
|
|
||||||
POST /payments/api-keys/validate/
|
POST /payments/api-keys/validate/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Validate keys without saving."""
|
"""Validate keys without saving."""
|
||||||
@@ -672,7 +673,7 @@ class ApiKeysRevalidateView(APIView):
|
|||||||
|
|
||||||
POST /payments/api-keys/revalidate/
|
POST /payments/api-keys/revalidate/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Re-validate stored keys."""
|
"""Re-validate stored keys."""
|
||||||
@@ -721,7 +722,7 @@ class ApiKeysDeleteView(APIView):
|
|||||||
|
|
||||||
DELETE /payments/api-keys/delete/
|
DELETE /payments/api-keys/delete/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
"""Delete stored keys."""
|
"""Delete stored keys."""
|
||||||
@@ -792,7 +793,7 @@ class ConnectOnboardView(APIView):
|
|||||||
|
|
||||||
POST /payments/connect/onboard/
|
POST /payments/connect/onboard/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Start Connect onboarding flow."""
|
"""Start Connect onboarding flow."""
|
||||||
@@ -855,7 +856,7 @@ class ConnectRefreshLinkView(APIView):
|
|||||||
|
|
||||||
POST /payments/connect/refresh-link/
|
POST /payments/connect/refresh-link/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Get a new onboarding link."""
|
"""Get a new onboarding link."""
|
||||||
@@ -905,7 +906,7 @@ class ConnectAccountSessionView(APIView):
|
|||||||
Custom accounts are required for embedded onboarding (Standard accounts
|
Custom accounts are required for embedded onboarding (Standard accounts
|
||||||
require the redirect flow).
|
require the redirect flow).
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Create account session for embedded components."""
|
"""Create account session for embedded components."""
|
||||||
@@ -967,7 +968,7 @@ class ConnectRefreshStatusView(APIView):
|
|||||||
|
|
||||||
POST /payments/connect/refresh-status/
|
POST /payments/connect/refresh-status/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Sync local status with Stripe."""
|
"""Sync local status with Stripe."""
|
||||||
@@ -1036,7 +1037,7 @@ class TransactionListView(APIView):
|
|||||||
|
|
||||||
GET /payments/transactions/
|
GET /payments/transactions/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get paginated list of transactions."""
|
"""Get paginated list of transactions."""
|
||||||
@@ -1104,7 +1105,7 @@ class TransactionSummaryView(APIView):
|
|||||||
|
|
||||||
GET /payments/transactions/summary/
|
GET /payments/transactions/summary/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get transaction summary."""
|
"""Get transaction summary."""
|
||||||
@@ -1159,7 +1160,7 @@ class StripeChargesView(APIView):
|
|||||||
|
|
||||||
GET /payments/transactions/charges/
|
GET /payments/transactions/charges/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get recent charges from Stripe API."""
|
"""Get recent charges from Stripe API."""
|
||||||
@@ -1225,7 +1226,7 @@ class StripePayoutsView(APIView):
|
|||||||
|
|
||||||
GET /payments/transactions/payouts/
|
GET /payments/transactions/payouts/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get payouts from Stripe API."""
|
"""Get payouts from Stripe API."""
|
||||||
@@ -1289,7 +1290,7 @@ class StripeBalanceView(APIView):
|
|||||||
|
|
||||||
GET /payments/transactions/balance/
|
GET /payments/transactions/balance/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get balance from Stripe API."""
|
"""Get balance from Stripe API."""
|
||||||
@@ -1362,7 +1363,7 @@ class TransactionExportView(APIView):
|
|||||||
|
|
||||||
POST /payments/transactions/export/
|
POST /payments/transactions/export/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Export transactions to various formats."""
|
"""Export transactions to various formats."""
|
||||||
@@ -1385,7 +1386,7 @@ class CreatePaymentIntentView(APIView):
|
|||||||
|
|
||||||
POST /payments/payment-intents/
|
POST /payments/payment-intents/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Create payment intent for an event"""
|
"""Create payment intent for an event"""
|
||||||
@@ -1460,7 +1461,7 @@ class TerminalConnectionTokenView(APIView):
|
|||||||
|
|
||||||
POST /payments/terminal/connection-token/
|
POST /payments/terminal/connection-token/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Get terminal connection token"""
|
"""Get terminal connection token"""
|
||||||
@@ -1488,7 +1489,7 @@ class RefundPaymentView(APIView):
|
|||||||
|
|
||||||
POST /payments/refunds/
|
POST /payments/refunds/
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Create refund"""
|
"""Create refund"""
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ class PlatformAdminConfig(AppConfig):
|
|||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'platform_admin'
|
name = 'platform_admin'
|
||||||
verbose_name = 'Platform Management'
|
verbose_name = 'Platform Management'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# Import signals to register them
|
||||||
|
from . import signals # noqa: F401
|
||||||
|
|||||||
@@ -202,6 +202,27 @@ class TenantSerializer(serializers.ModelSerializer):
|
|||||||
'max_resources', 'contact_email', 'phone',
|
'max_resources', 'contact_email', 'phone',
|
||||||
# Platform permissions
|
# Platform permissions
|
||||||
'can_manage_oauth_credentials',
|
'can_manage_oauth_credentials',
|
||||||
|
'can_accept_payments',
|
||||||
|
'can_use_custom_domain',
|
||||||
|
'can_white_label',
|
||||||
|
'can_api_access',
|
||||||
|
# Feature permissions
|
||||||
|
'can_add_video_conferencing',
|
||||||
|
'can_connect_to_api',
|
||||||
|
'can_book_repeated_events',
|
||||||
|
'can_require_2fa',
|
||||||
|
'can_download_logs',
|
||||||
|
'can_delete_data',
|
||||||
|
'can_use_sms_reminders',
|
||||||
|
'can_use_masked_phone_numbers',
|
||||||
|
'can_use_pos',
|
||||||
|
'can_use_mobile_app',
|
||||||
|
'can_export_data',
|
||||||
|
'can_use_plugins',
|
||||||
|
'can_use_tasks',
|
||||||
|
'can_create_plugins',
|
||||||
|
'can_use_webhooks',
|
||||||
|
'can_use_calendar_sync',
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@@ -256,6 +277,23 @@ class TenantUpdateSerializer(serializers.ModelSerializer):
|
|||||||
'can_use_custom_domain',
|
'can_use_custom_domain',
|
||||||
'can_white_label',
|
'can_white_label',
|
||||||
'can_api_access',
|
'can_api_access',
|
||||||
|
# Feature permissions
|
||||||
|
'can_add_video_conferencing',
|
||||||
|
'can_connect_to_api',
|
||||||
|
'can_book_repeated_events',
|
||||||
|
'can_require_2fa',
|
||||||
|
'can_download_logs',
|
||||||
|
'can_delete_data',
|
||||||
|
'can_use_sms_reminders',
|
||||||
|
'can_use_masked_phone_numbers',
|
||||||
|
'can_use_pos',
|
||||||
|
'can_use_mobile_app',
|
||||||
|
'can_export_data',
|
||||||
|
'can_use_plugins',
|
||||||
|
'can_use_tasks',
|
||||||
|
'can_create_plugins',
|
||||||
|
'can_use_webhooks',
|
||||||
|
'can_use_calendar_sync',
|
||||||
]
|
]
|
||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|
||||||
|
|||||||
28
smoothschedule/platform_admin/signals.py
Normal file
28
smoothschedule/platform_admin/signals.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Django signals for platform admin operations.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender='platform_admin.SubscriptionPlan')
|
||||||
|
def subscription_plan_updated(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
When a SubscriptionPlan is updated (not created), trigger a Celery task
|
||||||
|
to sync the plan's permissions and limits to all tenants using that plan.
|
||||||
|
"""
|
||||||
|
if created:
|
||||||
|
# New plan, no tenants to update yet
|
||||||
|
return
|
||||||
|
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from .tasks import sync_subscription_plan_to_tenants
|
||||||
|
|
||||||
|
logger.info(f"SubscriptionPlan '{instance.name}' (ID: {instance.id}) was updated, "
|
||||||
|
f"queuing sync task for all tenants on this plan")
|
||||||
|
|
||||||
|
# Queue the task to run asynchronously
|
||||||
|
sync_subscription_plan_to_tenants.delay(instance.id)
|
||||||
@@ -207,3 +207,142 @@ def send_bulk_appointment_reminders(hours_before: int = 24):
|
|||||||
|
|
||||||
logger.info(f"Queued {reminders_queued} appointment reminder emails for {hours_before}h window")
|
logger.info(f"Queued {reminders_queued} appointment reminder emails for {hours_before}h window")
|
||||||
return {'reminders_queued': reminders_queued, 'hours_before': hours_before}
|
return {'reminders_queued': reminders_queued, 'hours_before': hours_before}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(bind=True, max_retries=3)
|
||||||
|
def sync_subscription_plan_to_tenants(self, plan_id: int):
|
||||||
|
"""
|
||||||
|
Sync a subscription plan's permissions and limits to all tenants using that plan.
|
||||||
|
|
||||||
|
This task is triggered when a SubscriptionPlan is updated, ensuring all tenants
|
||||||
|
on that plan have their permissions and limits updated accordingly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plan_id: ID of the SubscriptionPlan that was updated
|
||||||
|
"""
|
||||||
|
from .models import SubscriptionPlan
|
||||||
|
from core.models import Tenant
|
||||||
|
|
||||||
|
try:
|
||||||
|
plan = SubscriptionPlan.objects.get(id=plan_id)
|
||||||
|
except SubscriptionPlan.DoesNotExist:
|
||||||
|
logger.error(f"SubscriptionPlan {plan_id} not found")
|
||||||
|
return {'success': False, 'error': 'Plan not found'}
|
||||||
|
|
||||||
|
# Get all tenants using this plan
|
||||||
|
tenants = Tenant.objects.filter(subscription_plan=plan)
|
||||||
|
tenant_count = tenants.count()
|
||||||
|
|
||||||
|
if tenant_count == 0:
|
||||||
|
logger.info(f"No tenants found for plan {plan.name} (ID: {plan_id})")
|
||||||
|
return {'success': True, 'tenants_updated': 0, 'plan_name': plan.name}
|
||||||
|
|
||||||
|
logger.info(f"Syncing plan '{plan.name}' to {tenant_count} tenant(s)")
|
||||||
|
|
||||||
|
# Mapping from plan.permissions JSON keys to Tenant boolean field names
|
||||||
|
# Some plan keys differ from tenant field names
|
||||||
|
permission_mapping = {
|
||||||
|
# Plan JSON key -> Tenant field name
|
||||||
|
'can_manage_oauth_credentials': 'can_manage_oauth_credentials',
|
||||||
|
'can_accept_payments': 'can_accept_payments',
|
||||||
|
'can_use_custom_domain': 'can_use_custom_domain',
|
||||||
|
'can_white_label': 'can_white_label',
|
||||||
|
'can_api_access': 'can_api_access',
|
||||||
|
'video_conferencing': 'can_add_video_conferencing',
|
||||||
|
'can_add_video_conferencing': 'can_add_video_conferencing',
|
||||||
|
'can_connect_to_api': 'can_connect_to_api',
|
||||||
|
'can_book_repeated_events': 'can_book_repeated_events',
|
||||||
|
'can_require_2fa': 'can_require_2fa',
|
||||||
|
'can_download_logs': 'can_download_logs',
|
||||||
|
'can_delete_data': 'can_delete_data',
|
||||||
|
# Communication - plan may use short names
|
||||||
|
'sms_reminders': 'can_use_sms_reminders',
|
||||||
|
'can_use_sms_reminders': 'can_use_sms_reminders',
|
||||||
|
'masked_calling': 'can_use_masked_phone_numbers',
|
||||||
|
'can_use_masked_phone_numbers': 'can_use_masked_phone_numbers',
|
||||||
|
'can_use_pos': 'can_use_pos',
|
||||||
|
'can_use_mobile_app': 'can_use_mobile_app',
|
||||||
|
# Advanced features - plan may use short names
|
||||||
|
'export_data': 'can_export_data',
|
||||||
|
'can_export_data': 'can_export_data',
|
||||||
|
'plugins': 'can_use_plugins',
|
||||||
|
'can_use_plugins': 'can_use_plugins',
|
||||||
|
'tasks': 'can_use_tasks',
|
||||||
|
'can_use_tasks': 'can_use_tasks',
|
||||||
|
'can_create_plugins': 'can_create_plugins',
|
||||||
|
'webhooks': 'can_use_webhooks',
|
||||||
|
'can_use_webhooks': 'can_use_webhooks',
|
||||||
|
'calendar_sync': 'can_use_calendar_sync',
|
||||||
|
'can_use_calendar_sync': 'can_use_calendar_sync',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Limit field mappings from plan.limits JSON to Tenant fields
|
||||||
|
limit_fields = [
|
||||||
|
'max_users',
|
||||||
|
'max_resources',
|
||||||
|
]
|
||||||
|
|
||||||
|
plan_permissions = plan.permissions or {}
|
||||||
|
plan_limits = plan.limits or {}
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for tenant in tenants:
|
||||||
|
try:
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
# Update permission fields using the mapping
|
||||||
|
for plan_key, tenant_field in permission_mapping.items():
|
||||||
|
if plan_key in plan_permissions:
|
||||||
|
new_value = bool(plan_permissions[plan_key])
|
||||||
|
if getattr(tenant, tenant_field, None) != new_value:
|
||||||
|
setattr(tenant, tenant_field, new_value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Update limit fields
|
||||||
|
for field in limit_fields:
|
||||||
|
if field in plan_limits:
|
||||||
|
new_value = int(plan_limits[field])
|
||||||
|
if getattr(tenant, field, None) != new_value:
|
||||||
|
setattr(tenant, field, new_value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Update subscription tier if plan has a business_tier
|
||||||
|
if plan.business_tier:
|
||||||
|
tier_mapping = {
|
||||||
|
'Free': 'FREE',
|
||||||
|
'Starter': 'STARTER',
|
||||||
|
'Professional': 'PROFESSIONAL',
|
||||||
|
'Business': 'PROFESSIONAL', # Map Business to Professional
|
||||||
|
'Enterprise': 'ENTERPRISE',
|
||||||
|
}
|
||||||
|
new_tier = tier_mapping.get(plan.business_tier)
|
||||||
|
if new_tier and tenant.subscription_tier != new_tier:
|
||||||
|
tenant.subscription_tier = new_tier
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
tenant.save()
|
||||||
|
updated_count += 1
|
||||||
|
logger.debug(f"Updated tenant '{tenant.name}' (ID: {tenant.id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to update tenant {tenant.id}: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
errors.append(error_msg)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'success': True,
|
||||||
|
'plan_id': plan_id,
|
||||||
|
'plan_name': plan.name,
|
||||||
|
'tenants_found': tenant_count,
|
||||||
|
'tenants_updated': updated_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
result['errors'] = errors
|
||||||
|
result['success'] = len(errors) < tenant_count # Partial success
|
||||||
|
|
||||||
|
logger.info(f"Completed sync for plan '{plan.name}': {updated_count}/{tenant_count} tenants updated")
|
||||||
|
return result
|
||||||
|
|||||||
@@ -687,6 +687,32 @@ class SubscriptionPlanViewSet(viewsets.ModelViewSet):
|
|||||||
return SubscriptionPlanCreateSerializer
|
return SubscriptionPlanCreateSerializer
|
||||||
return SubscriptionPlanSerializer
|
return SubscriptionPlanSerializer
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def sync_tenants(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Sync this plan's permissions to all tenants on this plan.
|
||||||
|
This is called explicitly by the admin after confirming they want to sync.
|
||||||
|
"""
|
||||||
|
plan = self.get_object()
|
||||||
|
|
||||||
|
from .tasks import sync_subscription_plan_to_tenants
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.info(f"SubscriptionPlan '{plan.name}' (ID: {plan.id}) - "
|
||||||
|
f"sync to tenants requested by platform admin")
|
||||||
|
|
||||||
|
# Run the sync task
|
||||||
|
sync_subscription_plan_to_tenants.delay(plan.id)
|
||||||
|
|
||||||
|
# Count tenants on this plan
|
||||||
|
from core.models import Tenant
|
||||||
|
tenant_count = Tenant.objects.filter(subscription_plan=plan).count()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'message': f'Syncing permissions to {tenant_count} tenant(s) on the "{plan.name}" plan',
|
||||||
|
'tenant_count': tenant_count
|
||||||
|
})
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'])
|
||||||
def sync_with_stripe(self, request):
|
def sync_with_stripe(self, request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ def current_business_view(request):
|
|||||||
'custom_domain': tenant.can_use_custom_domain or plan_permissions.get('custom_domain', False),
|
'custom_domain': tenant.can_use_custom_domain or plan_permissions.get('custom_domain', False),
|
||||||
'white_label': tenant.can_white_label or plan_permissions.get('white_label', False),
|
'white_label': tenant.can_white_label or plan_permissions.get('white_label', False),
|
||||||
'custom_oauth': tenant.can_manage_oauth_credentials or plan_permissions.get('custom_oauth', False),
|
'custom_oauth': tenant.can_manage_oauth_credentials or plan_permissions.get('custom_oauth', False),
|
||||||
'plugins': tenant.can_create_plugins or plan_permissions.get('plugins', False),
|
'plugins': tenant.can_use_plugins or plan_permissions.get('plugins', False),
|
||||||
|
'tasks': tenant.can_use_tasks or plan_permissions.get('tasks', False),
|
||||||
'export_data': tenant.can_export_data or plan_permissions.get('export_data', False),
|
'export_data': tenant.can_export_data or plan_permissions.get('export_data', False),
|
||||||
'video_conferencing': tenant.can_add_video_conferencing or plan_permissions.get('video_conferencing', False),
|
'video_conferencing': tenant.can_add_video_conferencing or plan_permissions.get('video_conferencing', False),
|
||||||
'two_factor_auth': tenant.can_require_2fa or plan_permissions.get('two_factor_auth', False),
|
'two_factor_auth': tenant.can_require_2fa or plan_permissions.get('two_factor_auth', False),
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ from django.utils import timezone
|
|||||||
from schedule.models import PluginTemplate
|
from schedule.models import PluginTemplate
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
def get_platform_plugins():
|
||||||
help = 'Seed platform-owned plugins into the database'
|
"""
|
||||||
|
Returns the list of platform plugin definitions.
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
This function is shared between the management command and the signal
|
||||||
plugins_data = [
|
that auto-seeds plugins on tenant creation.
|
||||||
{
|
"""
|
||||||
'name': 'Daily Appointment Summary Email',
|
return [
|
||||||
'slug': 'daily-appointment-summary',
|
{
|
||||||
'category': PluginTemplate.Category.EMAIL,
|
'name': 'Daily Appointment Summary Email',
|
||||||
'short_description': 'Send daily email summary of appointments',
|
'slug': 'daily-appointment-summary',
|
||||||
'description': '''Stay on top of your schedule with automated daily appointment summaries.
|
'category': PluginTemplate.Category.EMAIL,
|
||||||
|
'short_description': 'Send daily email summary of appointments',
|
||||||
|
'description': '''Stay on top of your schedule with automated daily appointment summaries.
|
||||||
|
|
||||||
This plugin sends a comprehensive email digest every morning with:
|
This plugin sends a comprehensive email digest every morning with:
|
||||||
- List of all appointments for the day
|
- List of all appointments for the day
|
||||||
@@ -23,7 +26,7 @@ This plugin sends a comprehensive email digest every morning with:
|
|||||||
- Any special notes or requirements
|
- Any special notes or requirements
|
||||||
|
|
||||||
Perfect for managers and staff who want to start their day informed and prepared.''',
|
Perfect for managers and staff who want to start their day informed and prepared.''',
|
||||||
'plugin_code': '''from datetime import datetime, timedelta
|
'plugin_code': '''from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Get today's appointments
|
# Get today's appointments
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
@@ -51,14 +54,14 @@ api.send_email(
|
|||||||
body=summary
|
body=summary
|
||||||
)
|
)
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/daily-appointment-summary.png',
|
'logo_url': '/plugin-logos/daily-appointment-summary.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'No-Show Customer Tracker',
|
'name': 'No-Show Customer Tracker',
|
||||||
'slug': 'no-show-tracker',
|
'slug': 'no-show-tracker',
|
||||||
'category': PluginTemplate.Category.REPORTS,
|
'category': PluginTemplate.Category.REPORTS,
|
||||||
'short_description': 'Track customers who miss appointments',
|
'short_description': 'Track customers who miss appointments',
|
||||||
'description': '''Identify patterns of missed appointments and reduce no-shows.
|
'description': '''Identify patterns of missed appointments and reduce no-shows.
|
||||||
|
|
||||||
This plugin automatically tracks and reports on:
|
This plugin automatically tracks and reports on:
|
||||||
- Customers who didn\'t show up for scheduled appointments
|
- Customers who didn\'t show up for scheduled appointments
|
||||||
@@ -67,7 +70,7 @@ This plugin automatically tracks and reports on:
|
|||||||
- Trends over time
|
- Trends over time
|
||||||
|
|
||||||
Helps you identify customers who may need reminder calls or deposits, improving your booking efficiency and revenue.''',
|
Helps you identify customers who may need reminder calls or deposits, improving your booking efficiency and revenue.''',
|
||||||
'plugin_code': '''from datetime import datetime, timedelta
|
'plugin_code': '''from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Get configuration
|
# Get configuration
|
||||||
days_back = int('{{PROMPT:days_back|Days to Look Back|7}}')
|
days_back = int('{{PROMPT:days_back|Days to Look Back|7}}')
|
||||||
@@ -108,14 +111,14 @@ api.send_email(
|
|||||||
body=report
|
body=report
|
||||||
)
|
)
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/no-show-tracker.png',
|
'logo_url': '/plugin-logos/no-show-tracker.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Birthday Greeting Campaign',
|
'name': 'Birthday Greeting Campaign',
|
||||||
'slug': 'birthday-greetings',
|
'slug': 'birthday-greetings',
|
||||||
'category': PluginTemplate.Category.CUSTOMER,
|
'category': PluginTemplate.Category.CUSTOMER,
|
||||||
'short_description': 'Send birthday emails with offers',
|
'short_description': 'Send birthday emails with offers',
|
||||||
'description': '''Delight your customers with personalized birthday greetings and special offers.
|
'description': '''Delight your customers with personalized birthday greetings and special offers.
|
||||||
|
|
||||||
This plugin automatically:
|
This plugin automatically:
|
||||||
- Identifies customers with birthdays today
|
- Identifies customers with birthdays today
|
||||||
@@ -124,7 +127,7 @@ This plugin automatically:
|
|||||||
- Helps drive repeat bookings and customer loyalty
|
- Helps drive repeat bookings and customer loyalty
|
||||||
|
|
||||||
A simple way to show customers you care while encouraging them to book their next appointment.''',
|
A simple way to show customers you care while encouraging them to book their next appointment.''',
|
||||||
'plugin_code': '''# Get all customers with email addresses
|
'plugin_code': '''# Get all customers with email addresses
|
||||||
customers = api.get_customers(has_email=True, limit=1000)
|
customers = api.get_customers(has_email=True, limit=1000)
|
||||||
|
|
||||||
# Get customizable email template
|
# Get customizable email template
|
||||||
@@ -146,14 +149,14 @@ for customer in customers:
|
|||||||
|
|
||||||
api.log(f"Sent {len(customers)} birthday greetings")
|
api.log(f"Sent {len(customers)} birthday greetings")
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/birthday-greetings.png',
|
'logo_url': '/plugin-logos/birthday-greetings.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Monthly Revenue Report',
|
'name': 'Monthly Revenue Report',
|
||||||
'slug': 'monthly-revenue-report',
|
'slug': 'monthly-revenue-report',
|
||||||
'category': PluginTemplate.Category.REPORTS,
|
'category': PluginTemplate.Category.REPORTS,
|
||||||
'short_description': 'Monthly business statistics',
|
'short_description': 'Monthly business statistics',
|
||||||
'description': '''Get comprehensive monthly insights into your business performance.
|
'description': '''Get comprehensive monthly insights into your business performance.
|
||||||
|
|
||||||
This plugin generates detailed reports including:
|
This plugin generates detailed reports including:
|
||||||
- Total revenue and number of appointments
|
- Total revenue and number of appointments
|
||||||
@@ -164,7 +167,7 @@ This plugin generates detailed reports including:
|
|||||||
- Year-over-year comparisons
|
- Year-over-year comparisons
|
||||||
|
|
||||||
Perfect for owners and managers who want to track business growth and identify opportunities.''',
|
Perfect for owners and managers who want to track business growth and identify opportunities.''',
|
||||||
'plugin_code': '''from datetime import datetime, timedelta
|
'plugin_code': '''from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Get last month's date range
|
# Get last month's date range
|
||||||
today = datetime.now()
|
today = datetime.now()
|
||||||
@@ -212,14 +215,14 @@ api.send_email(
|
|||||||
body=report
|
body=report
|
||||||
)
|
)
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/monthly-revenue-report.png',
|
'logo_url': '/plugin-logos/monthly-revenue-report.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Appointment Reminder (24hr)',
|
'name': 'Appointment Reminder (24hr)',
|
||||||
'slug': 'appointment-reminder-24hr',
|
'slug': 'appointment-reminder-24hr',
|
||||||
'category': PluginTemplate.Category.BOOKING,
|
'category': PluginTemplate.Category.BOOKING,
|
||||||
'short_description': 'Remind customers 24hrs before appointments',
|
'short_description': 'Remind customers 24hrs before appointments',
|
||||||
'description': '''Reduce no-shows with automated appointment reminders.
|
'description': '''Reduce no-shows with automated appointment reminders.
|
||||||
|
|
||||||
This plugin sends friendly reminder emails to customers 24 hours before their scheduled appointments, including:
|
This plugin sends friendly reminder emails to customers 24 hours before their scheduled appointments, including:
|
||||||
- Appointment date and time
|
- Appointment date and time
|
||||||
@@ -229,7 +232,7 @@ This plugin sends friendly reminder emails to customers 24 hours before their sc
|
|||||||
- Cancellation policy reminder
|
- Cancellation policy reminder
|
||||||
|
|
||||||
Studies show that appointment reminders can reduce no-shows by up to 90%.''',
|
Studies show that appointment reminders can reduce no-shows by up to 90%.''',
|
||||||
'plugin_code': '''from datetime import datetime, timedelta
|
'plugin_code': '''from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Get appointments 24 hours from now
|
# Get appointments 24 hours from now
|
||||||
tomorrow = (datetime.now() + timedelta(days=1)).date()
|
tomorrow = (datetime.now() + timedelta(days=1)).date()
|
||||||
@@ -255,14 +258,14 @@ for apt in appointments:
|
|||||||
|
|
||||||
api.log(f"Sent {len(appointments)} appointment reminders")
|
api.log(f"Sent {len(appointments)} appointment reminders")
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/appointment-reminder-24hr.png',
|
'logo_url': '/plugin-logos/appointment-reminder-24hr.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Inactive Customer Re-engagement',
|
'name': 'Inactive Customer Re-engagement',
|
||||||
'slug': 'inactive-customer-reengagement',
|
'slug': 'inactive-customer-reengagement',
|
||||||
'category': PluginTemplate.Category.CUSTOMER,
|
'category': PluginTemplate.Category.CUSTOMER,
|
||||||
'short_description': 'Email inactive customers with offers',
|
'short_description': 'Email inactive customers with offers',
|
||||||
'description': '''Win back customers who haven\'t booked in a while.
|
'description': '''Win back customers who haven\'t booked in a while.
|
||||||
|
|
||||||
This plugin automatically identifies customers who haven\'t made an appointment recently and sends them:
|
This plugin automatically identifies customers who haven\'t made an appointment recently and sends them:
|
||||||
- Personalized "we miss you" messages
|
- Personalized "we miss you" messages
|
||||||
@@ -271,7 +274,7 @@ This plugin automatically identifies customers who haven\'t made an appointment
|
|||||||
- Easy booking links
|
- Easy booking links
|
||||||
|
|
||||||
Configurable inactivity period (default: 60 days). A proven strategy for increasing customer lifetime value and reducing churn.''',
|
Configurable inactivity period (default: 60 days). A proven strategy for increasing customer lifetime value and reducing churn.''',
|
||||||
'plugin_code': '''from datetime import datetime, timedelta
|
'plugin_code': '''from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Get configuration
|
# Get configuration
|
||||||
inactive_days = int('{{PROMPT:inactive_days|Days Inactive|60}}')
|
inactive_days = int('{{PROMPT:inactive_days|Days Inactive|60}}')
|
||||||
@@ -308,20 +311,74 @@ for customer in all_customers:
|
|||||||
|
|
||||||
api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
|
api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
|
||||||
''',
|
''',
|
||||||
'logo_url': '/plugin-logos/inactive-customer-reengagement.png',
|
'logo_url': '/plugin-logos/inactive-customer-reengagement.png',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Seed or update platform-owned plugins in the database'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--update',
|
||||||
|
action='store_true',
|
||||||
|
default=True,
|
||||||
|
help='Update existing plugins if they have changed (default: True)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-update',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip existing plugins instead of updating them',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
plugins_data = get_platform_plugins()
|
||||||
|
update_existing = not options.get('no_update', False)
|
||||||
|
|
||||||
created_count = 0
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
|
|
||||||
for plugin_data in plugins_data:
|
for plugin_data in plugins_data:
|
||||||
# Check if plugin already exists by slug
|
existing = PluginTemplate.objects.filter(slug=plugin_data['slug']).first()
|
||||||
if PluginTemplate.objects.filter(slug=plugin_data['slug']).exists():
|
|
||||||
self.stdout.write(
|
if existing:
|
||||||
self.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists")
|
if update_existing:
|
||||||
)
|
# Check if plugin needs updating by comparing key fields
|
||||||
skipped_count += 1
|
needs_update = (
|
||||||
|
existing.name != plugin_data['name'] or
|
||||||
|
existing.short_description != plugin_data['short_description'] or
|
||||||
|
existing.description != plugin_data['description'] or
|
||||||
|
existing.plugin_code != plugin_data['plugin_code'] or
|
||||||
|
existing.category != plugin_data['category'] or
|
||||||
|
existing.logo_url != plugin_data.get('logo_url', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
if needs_update:
|
||||||
|
existing.name = plugin_data['name']
|
||||||
|
existing.short_description = plugin_data['short_description']
|
||||||
|
existing.description = plugin_data['description']
|
||||||
|
existing.plugin_code = plugin_data['plugin_code']
|
||||||
|
existing.category = plugin_data['category']
|
||||||
|
existing.logo_url = plugin_data.get('logo_url', '')
|
||||||
|
existing.updated_at = timezone.now()
|
||||||
|
existing.save()
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"Updated plugin: '{plugin_data['name']}'")
|
||||||
|
)
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(f"Skipping '{plugin_data['name']}' - no changes")
|
||||||
|
)
|
||||||
|
skipped_count += 1
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists")
|
||||||
|
)
|
||||||
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Create the plugin
|
# Create the plugin
|
||||||
@@ -348,6 +405,6 @@ api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
|
|||||||
# Summary
|
# Summary
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
self.style.SUCCESS(
|
||||||
f'\nSuccessfully created {created_count} plugin(s), {skipped_count} already existed.'
|
f'\nSuccessfully created {created_count}, updated {updated_count}, skipped {skipped_count} plugin(s).'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -433,6 +433,7 @@ class ScheduledTaskViewSet(viewsets.ModelViewSet):
|
|||||||
- Must be authenticated
|
- Must be authenticated
|
||||||
- Only owners/managers can create/update/delete
|
- Only owners/managers can create/update/delete
|
||||||
- Subject to MAX_AUTOMATED_TASKS quota (hard block on creation)
|
- Subject to MAX_AUTOMATED_TASKS quota (hard block on creation)
|
||||||
|
- Requires can_use_plugins AND can_use_tasks features
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- List all scheduled tasks
|
- List all scheduled tasks
|
||||||
@@ -448,8 +449,52 @@ class ScheduledTaskViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [AllowAny, HasQuota('MAX_AUTOMATED_TASKS')] # TODO: Change to IsAuthenticated for production
|
permission_classes = [AllowAny, HasQuota('MAX_AUTOMATED_TASKS')] # TODO: Change to IsAuthenticated for production
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def _check_tasks_permission(self):
|
||||||
|
"""Check if tenant has permission to access scheduled tasks."""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant:
|
||||||
|
if not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use scheduled tasks."
|
||||||
|
)
|
||||||
|
if not tenant.has_feature('can_use_tasks'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Scheduled Tasks. "
|
||||||
|
"Please upgrade your subscription to view or create scheduled tasks."
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""List scheduled tasks with permission check."""
|
||||||
|
self._check_tasks_permission()
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
"""Retrieve a scheduled task with permission check."""
|
||||||
|
self._check_tasks_permission()
|
||||||
|
return super().retrieve(request, *args, **kwargs)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Set created_by to current user"""
|
"""Set created_by to current user and check permissions"""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant:
|
||||||
|
# Check permission to use plugins (tasks require plugin access)
|
||||||
|
if not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use scheduled tasks."
|
||||||
|
)
|
||||||
|
# Check permission to use scheduled tasks
|
||||||
|
if not tenant.has_feature('can_use_tasks'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Scheduled Tasks. "
|
||||||
|
"Please upgrade your subscription to create scheduled tasks."
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Uncomment when auth is enabled
|
# TODO: Uncomment when auth is enabled
|
||||||
# serializer.save(created_by=self.request.user)
|
# serializer.save(created_by=self.request.user)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
@@ -628,6 +673,12 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
- Install a template as a ScheduledTask
|
- Install a template as a ScheduledTask
|
||||||
- Request approval (for marketplace publishing)
|
- Request approval (for marketplace publishing)
|
||||||
- Approve/reject templates (platform admins only)
|
- Approve/reject templates (platform admins only)
|
||||||
|
|
||||||
|
Permissions:
|
||||||
|
- Marketplace view: Always accessible (for discovery)
|
||||||
|
- My Plugins view: Requires can_use_plugins feature
|
||||||
|
- Install action: Requires can_use_plugins feature
|
||||||
|
- Create: Requires can_use_plugins AND can_create_plugins features
|
||||||
"""
|
"""
|
||||||
queryset = PluginTemplate.objects.all()
|
queryset = PluginTemplate.objects.all()
|
||||||
serializer_class = PluginTemplateSerializer
|
serializer_class = PluginTemplateSerializer
|
||||||
@@ -636,12 +687,19 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
filterset_fields = ['visibility', 'category', 'is_approved']
|
filterset_fields = ['visibility', 'category', 'is_approved']
|
||||||
search_fields = ['name', 'short_description', 'description', 'tags']
|
search_fields = ['name', 'short_description', 'description', 'tags']
|
||||||
|
|
||||||
|
def _has_plugins_permission(self):
|
||||||
|
"""Check if tenant has permission to use plugins."""
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant:
|
||||||
|
return tenant.has_feature('can_use_plugins')
|
||||||
|
return True # Allow if no tenant context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Filter templates based on user permissions.
|
Filter templates based on user permissions.
|
||||||
|
|
||||||
- Marketplace view: Only approved PUBLIC templates
|
- Marketplace view: Only approved PUBLIC templates (always accessible)
|
||||||
- My Plugins: User's own templates (all visibilities)
|
- My Plugins: User's own templates (requires can_use_plugins)
|
||||||
- Platform admins: All templates
|
- Platform admins: All templates
|
||||||
"""
|
"""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
@@ -649,19 +707,22 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
if view_mode == 'marketplace':
|
if view_mode == 'marketplace':
|
||||||
# Public marketplace - platform official + approved public templates
|
# Public marketplace - platform official + approved public templates
|
||||||
|
# Always accessible for discovery/marketing purposes
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(visibility=PluginTemplate.Visibility.PLATFORM) |
|
Q(visibility=PluginTemplate.Visibility.PLATFORM) |
|
||||||
Q(visibility=PluginTemplate.Visibility.PUBLIC, is_approved=True)
|
Q(visibility=PluginTemplate.Visibility.PUBLIC, is_approved=True)
|
||||||
)
|
)
|
||||||
elif view_mode == 'my_plugins':
|
elif view_mode == 'my_plugins':
|
||||||
# User's own templates
|
# User's own templates - requires plugin permission
|
||||||
if self.request.user.is_authenticated:
|
if not self._has_plugins_permission():
|
||||||
|
queryset = queryset.none()
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
queryset = queryset.filter(author=self.request.user)
|
queryset = queryset.filter(author=self.request.user)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.none()
|
queryset = queryset.none()
|
||||||
elif view_mode == 'platform':
|
elif view_mode == 'platform':
|
||||||
# Platform official plugins
|
# Platform official plugins - always accessible for discovery
|
||||||
queryset = queryset.filter(visibility=PluginTemplate.Visibility.PLATFORM)
|
queryset = queryset.filter(visibility=PluginTemplate.Visibility.PLATFORM)
|
||||||
# else: all templates (for platform admins)
|
# else: all templates (for platform admins)
|
||||||
|
|
||||||
@@ -694,8 +755,15 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
from .template_parser import TemplateVariableParser
|
from .template_parser import TemplateVariableParser
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
# Check permission to create plugins
|
# Check permission to use plugins first
|
||||||
tenant = getattr(self.request, 'tenant', None)
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use plugins."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check permission to create plugins (requires can_use_plugins)
|
||||||
if tenant and not tenant.has_feature('can_create_plugins'):
|
if tenant and not tenant.has_feature('can_create_plugins'):
|
||||||
raise PermissionDenied(
|
raise PermissionDenied(
|
||||||
"Your current plan does not include Plugin Creation. "
|
"Your current plan does not include Plugin Creation. "
|
||||||
@@ -773,6 +841,14 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
"cron_expression": "0 0 * * *"
|
"cron_expression": "0 0 * * *"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
# Check permission to use plugins
|
||||||
|
tenant = getattr(request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
return Response(
|
||||||
|
{'error': 'Your current plan does not include Plugin access. Please upgrade your subscription to install plugins.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
template = self.get_object()
|
template = self.get_object()
|
||||||
|
|
||||||
# Check if template is accessible
|
# Check if template is accessible
|
||||||
@@ -957,12 +1033,36 @@ class PluginInstallationViewSet(viewsets.ModelViewSet):
|
|||||||
- Update installation (update to latest version)
|
- Update installation (update to latest version)
|
||||||
- Uninstall plugin
|
- Uninstall plugin
|
||||||
- Rate and review plugin
|
- Rate and review plugin
|
||||||
|
|
||||||
|
Permissions:
|
||||||
|
- Requires can_use_plugins feature for all operations
|
||||||
"""
|
"""
|
||||||
queryset = PluginInstallation.objects.select_related('template', 'scheduled_task').all()
|
queryset = PluginInstallation.objects.select_related('template', 'scheduled_task').all()
|
||||||
serializer_class = PluginInstallationSerializer
|
serializer_class = PluginInstallationSerializer
|
||||||
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
|
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
|
||||||
ordering = ['-installed_at']
|
ordering = ['-installed_at']
|
||||||
|
|
||||||
|
def _check_plugins_permission(self):
|
||||||
|
"""Check if tenant has permission to access plugin installations."""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use plugins."
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""List plugin installations with permission check."""
|
||||||
|
self._check_plugins_permission()
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
"""Retrieve a plugin installation with permission check."""
|
||||||
|
self._check_plugins_permission()
|
||||||
|
return super().retrieve(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return installations for current user/tenant"""
|
"""Return installations for current user/tenant"""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
@@ -973,6 +1073,20 @@ class PluginInstallationViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Check permission to use plugins before installing"""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
# Check permission to use plugins
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use plugins."
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def update_to_latest(self, request, pk=None):
|
def update_to_latest(self, request, pk=None):
|
||||||
"""Update installed plugin to latest template version"""
|
"""Update installed plugin to latest template version"""
|
||||||
@@ -1080,6 +1194,19 @@ class EventPluginViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return queryset.order_by('execution_order', 'created_at')
|
return queryset.order_by('execution_order', 'created_at')
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Check permission to use plugins before attaching to event"""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use plugins."
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
"""
|
"""
|
||||||
List event plugins.
|
List event plugins.
|
||||||
@@ -1195,7 +1322,16 @@ class GlobalEventPluginViewSet(viewsets.ModelViewSet):
|
|||||||
return queryset.order_by('execution_order', 'created_at')
|
return queryset.order_by('execution_order', 'created_at')
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Set created_by on creation"""
|
"""Check permission to use plugins and set created_by on creation"""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
tenant = getattr(self.request, 'tenant', None)
|
||||||
|
if tenant and not tenant.has_feature('can_use_plugins'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include Plugin access. "
|
||||||
|
"Please upgrade your subscription to use plugins."
|
||||||
|
)
|
||||||
|
|
||||||
user = self.request.user if self.request.user.is_authenticated else None
|
user = self.request.user if self.request.user.is_authenticated else None
|
||||||
serializer.save(created_by=user)
|
serializer.save(created_by=user)
|
||||||
|
|
||||||
|
|||||||
@@ -812,6 +812,13 @@ def list_phone_numbers_view(request):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if tenant has masked calling feature
|
||||||
|
if not tenant.has_feature('can_use_masked_phone_numbers'):
|
||||||
|
return Response(
|
||||||
|
{'error': 'Masked calling feature not available on your plan'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
numbers = ProxyPhoneNumber.objects.filter(
|
numbers = ProxyPhoneNumber.objects.filter(
|
||||||
assigned_tenant=tenant,
|
assigned_tenant=tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
|
|||||||
@@ -135,11 +135,23 @@ class APITokenViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
This endpoint requires regular user authentication (not API token auth)
|
This endpoint requires regular user authentication (not API token auth)
|
||||||
and is intended for business owners to manage their API tokens.
|
and is intended for business owners to manage their API tokens.
|
||||||
|
|
||||||
|
Requires can_api_access permission.
|
||||||
"""
|
"""
|
||||||
# Use session/token auth for token management, not API token auth
|
# Use session/token auth for token management, not API token auth
|
||||||
authentication_classes = [SessionAuthentication, TokenAuthentication]
|
authentication_classes = [SessionAuthentication, TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def _check_api_access_permission(self, tenant):
|
||||||
|
"""Check if tenant has permission to use API access."""
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
if tenant and not tenant.has_feature('can_api_access'):
|
||||||
|
raise PermissionDenied(
|
||||||
|
"Your current plan does not include API Access. "
|
||||||
|
"Please upgrade your subscription to manage API tokens."
|
||||||
|
)
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
"""List all API tokens for the current business."""
|
"""List all API tokens for the current business."""
|
||||||
user = request.user
|
user = request.user
|
||||||
@@ -154,6 +166,9 @@ class APITokenViewSet(viewsets.ViewSet):
|
|||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check API access permission
|
||||||
|
self._check_api_access_permission(tenant)
|
||||||
|
|
||||||
# Only owners can manage API tokens (roles are uppercase in DB)
|
# Only owners can manage API tokens (roles are uppercase in DB)
|
||||||
allowed_roles = ['OWNER', 'TENANT_OWNER', 'SUPERUSER', 'PLATFORM_MANAGER', 'TENANT_MANAGER']
|
allowed_roles = ['OWNER', 'TENANT_OWNER', 'SUPERUSER', 'PLATFORM_MANAGER', 'TENANT_MANAGER']
|
||||||
if user.role.upper() not in allowed_roles:
|
if user.role.upper() not in allowed_roles:
|
||||||
@@ -180,6 +195,9 @@ class APITokenViewSet(viewsets.ViewSet):
|
|||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check API access permission
|
||||||
|
self._check_api_access_permission(tenant)
|
||||||
|
|
||||||
allowed_roles = ['OWNER', 'TENANT_OWNER', 'SUPERUSER', 'PLATFORM_MANAGER', 'TENANT_MANAGER']
|
allowed_roles = ['OWNER', 'TENANT_OWNER', 'SUPERUSER', 'PLATFORM_MANAGER', 'TENANT_MANAGER']
|
||||||
if user.role.upper() not in allowed_roles:
|
if user.role.upper() not in allowed_roles:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -1116,18 +1134,10 @@ class WebhookViewSet(PublicAPIViewMixin, viewsets.ViewSet):
|
|||||||
"""
|
"""
|
||||||
permission_classes = [HasAPIToken, CanManageWebhooks]
|
permission_classes = [HasAPIToken, CanManageWebhooks]
|
||||||
|
|
||||||
def list(self, request):
|
def _check_webhooks_permission(self, request):
|
||||||
"""List webhook subscriptions for the current API token."""
|
"""Check if tenant has permission to use webhooks."""
|
||||||
token = request.api_token
|
|
||||||
subscriptions = WebhookSubscription.objects.filter(api_token=token)
|
|
||||||
serializer = WebhookSubscriptionSerializer(subscriptions, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
def create(self, request):
|
|
||||||
"""Create a new webhook subscription."""
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
# Check permission to use webhooks
|
|
||||||
token = request.api_token
|
token = request.api_token
|
||||||
tenant = token.tenant
|
tenant = token.tenant
|
||||||
if tenant and not tenant.has_feature('can_use_webhooks'):
|
if tenant and not tenant.has_feature('can_use_webhooks'):
|
||||||
@@ -1136,6 +1146,18 @@ class WebhookViewSet(PublicAPIViewMixin, viewsets.ViewSet):
|
|||||||
"Please upgrade your subscription to use webhooks."
|
"Please upgrade your subscription to use webhooks."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
"""List webhook subscriptions for the current API token."""
|
||||||
|
self._check_webhooks_permission(request)
|
||||||
|
token = request.api_token
|
||||||
|
subscriptions = WebhookSubscription.objects.filter(api_token=token)
|
||||||
|
serializer = WebhookSubscriptionSerializer(subscriptions, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
"""Create a new webhook subscription."""
|
||||||
|
self._check_webhooks_permission(request)
|
||||||
|
|
||||||
serializer = WebhookSubscriptionCreateSerializer(data=request.data)
|
serializer = WebhookSubscriptionCreateSerializer(data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
@@ -1160,6 +1182,7 @@ class WebhookViewSet(PublicAPIViewMixin, viewsets.ViewSet):
|
|||||||
|
|
||||||
def retrieve(self, request, pk=None):
|
def retrieve(self, request, pk=None):
|
||||||
"""Get webhook subscription details."""
|
"""Get webhook subscription details."""
|
||||||
|
self._check_webhooks_permission(request)
|
||||||
token = request.api_token
|
token = request.api_token
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user