6 Commits

Author SHA1 Message Date
poduck
dcb14503a2 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>
2025-12-03 13:02:44 -05:00
poduck
9444e26924 docs(help): Comprehensive rewrites for Resources, Services, Customers, Staff guides
HelpResources.tsx:
- Added resource types section with Staff/Room/Equipment
- Documented table columns and their meanings
- Added step-by-step resource creation guide
- Added staff autocomplete with keyboard navigation
- Detailed multilane mode for concurrent bookings
- Documented View Calendar and Edit features

HelpServices.tsx:
- Documented two-column layout with customer preview
- Added drag-and-drop reordering instructions
- Detailed service properties (name, duration, price, description)
- Added photo gallery section with upload, reorder, delete
- Documented customer preview mockup feature

HelpCustomers.tsx:
- Documented customer table columns
- Added search and sorting capabilities
- Step-by-step customer creation guide
- Documented customer statuses (Active, Inactive, Blocked)
- Added tags section for customer organization
- Documented masquerading feature for customer support

HelpStaff.tsx:
- Detailed staff roles (Owner, Manager, Staff) with badges
- Staff table columns documentation
- Step-by-step staff invitation workflow
- Pending invitations management (resend, cancel)
- Edit staff modal with permissions
- Make Bookable feature for linking to resources
- Inactive staff section with reactivation
- Masquerading as staff for training/troubleshooting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 02:44:02 -05:00
poduck
445b2bb3fc fix(help): Correct pending appointments sidebar position to left
Fixed documentation that incorrectly stated the pending appointments
sidebar appears on the right side of the scheduler when it actually
appears on the left side.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 02:35:06 -05:00
poduck
baffe7e577 docs(help): Comprehensive Scheduler documentation with all features
Rewrote HelpScheduler.tsx to document actual scheduler features including:
- Drag-and-drop to reschedule, change resource, or delete appointments
- Resize appointments by dragging edges (start or end)
- Pending appointments sidebar with archive zone
- Undo/Redo with Ctrl+Z/Ctrl+Y (up to 50 actions)
- Zoom controls for timeline detail
- Status colors (blue/yellow/red/green/gray)
- Filtering by status, resource, and service
- Overlapping appointment lanes
- Real-time WebSocket updates
- Month view click-to-day and drag overlay features

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 02:27:39 -05:00
poduck
5aa49399d0 feat(help): Add floating help button to all pages
Replaced inline HelpButton components with a global FloatingHelpButton
that appears fixed in the top-right corner of all pages. The button:
- Automatically detects the current route and links to the appropriate help page
- Uses a consistent position across all pages (fixed, top-right)
- Is hidden on help pages themselves
- Works on both business and platform layouts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 02:23:28 -05:00
poduck
11bb83a85d feat: Add comprehensive help documentation system and plugin creation page
- Add CreatePlugin.tsx page for custom plugin creation with code editor
- Add HelpButton component for contextual help links
- Create 21 new help pages covering all dashboard features:
  - Core: Dashboard, Scheduler, Tasks
  - Manage: Customers, Services, Resources, Staff
  - Communicate: Messages (Ticketing already existed)
  - Money: Payments
  - Extend: Plugins overview and creation guide
  - Settings: General, Resource Types, Booking, Appearance, Email, Domains, API, Auth, Billing, Quota
- Update HelpGuide.tsx as main documentation hub with quick start guide
- Add routes for all help pages in App.tsx
- Add HelpButton to Dashboard, Customers, Services, and Tasks pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 02:18:05 -05:00
75 changed files with 12685 additions and 537 deletions

179
PLAN_HELP_DOCS.md Normal file
View 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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -65,11 +65,35 @@ 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
const HelpDashboard = React.lazy(() => import('./pages/help/HelpDashboard'));
const HelpScheduler = React.lazy(() => import('./pages/help/HelpScheduler'));
const HelpTasks = React.lazy(() => import('./pages/help/HelpTasks'));
const HelpCustomers = React.lazy(() => import('./pages/help/HelpCustomers'));
const HelpServices = React.lazy(() => import('./pages/help/HelpServices'));
const HelpResources = React.lazy(() => import('./pages/help/HelpResources'));
const HelpStaff = React.lazy(() => import('./pages/help/HelpStaff'));
const HelpMessages = React.lazy(() => import('./pages/help/HelpMessages'));
const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments'));
const HelpPlugins = React.lazy(() => import('./pages/help/HelpPlugins'));
const HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral'));
const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes'));
const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking'));
const HelpSettingsAppearance = React.lazy(() => import('./pages/help/HelpSettingsAppearance'));
const HelpSettingsEmail = React.lazy(() => import('./pages/help/HelpSettingsEmail'));
const HelpSettingsDomains = React.lazy(() => import('./pages/help/HelpSettingsDomains'));
const HelpSettingsApi = React.lazy(() => import('./pages/help/HelpSettingsApi'));
const HelpSettingsAuth = React.lazy(() => import('./pages/help/HelpSettingsAuth'));
const HelpSettingsBilling = React.lazy(() => import('./pages/help/HelpSettingsBilling'));
const HelpSettingsQuota = React.lazy(() => import('./pages/help/HelpSettingsQuota'));
const HelpComprehensive = React.lazy(() => import('./pages/help/HelpComprehensive'));
const PlatformSupport = React.lazy(() => import('./pages/PlatformSupport')); // Import Platform Support page (for businesses to contact SmoothSchedule) const 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
const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin')); // Import Create Plugin page
const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled plugin executions const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled plugin executions
const EmailTemplates = React.lazy(() => import('./pages/EmailTemplates')); // Import Email Templates page const EmailTemplates = React.lazy(() => import('./pages/EmailTemplates')); // Import Email Templates page
@@ -589,11 +613,33 @@ 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 />} />
<Route path="/help/plugins" element={<HelpPluginDocs />} /> <Route path="/help/plugins/docs" element={<HelpPluginDocs />} />
<Route path="/help/email" element={<HelpEmailSettings />} /> <Route path="/help/email" element={<HelpEmailSettings />} />
{/* New help pages */}
<Route path="/help/dashboard" element={<HelpDashboard />} />
<Route path="/help/scheduler" element={<HelpScheduler />} />
<Route path="/help/tasks" element={<HelpTasks />} />
<Route path="/help/customers" element={<HelpCustomers />} />
<Route path="/help/services" element={<HelpServices />} />
<Route path="/help/resources" element={<HelpResources />} />
<Route path="/help/staff" element={<HelpStaff />} />
<Route path="/help/messages" element={<HelpMessages />} />
<Route path="/help/payments" element={<HelpPayments />} />
<Route path="/help/plugins" element={<HelpPlugins />} />
<Route path="/help/settings/general" element={<HelpSettingsGeneral />} />
<Route path="/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
<Route path="/help/settings/booking" element={<HelpSettingsBooking />} />
<Route path="/help/settings/appearance" element={<HelpSettingsAppearance />} />
<Route path="/help/settings/email" element={<HelpSettingsEmail />} />
<Route path="/help/settings/domains" element={<HelpSettingsDomains />} />
<Route path="/help/settings/api" element={<HelpSettingsApi />} />
<Route path="/help/settings/auth" element={<HelpSettingsAuth />} />
<Route path="/help/settings/billing" element={<HelpSettingsBilling />} />
<Route path="/help/settings/quota" element={<HelpSettingsQuota />} />
<Route <Route
path="/plugins/marketplace" path="/plugins/marketplace"
element={ element={
@@ -614,6 +660,16 @@ const AppContent: React.FC = () => {
) )
} }
/> />
<Route
path="/plugins/create"
element={
hasAccess(['owner', 'manager']) ? (
<CreatePlugin />
) : (
<Navigate to="/" />
)
}
/>
<Route <Route
path="/tasks" path="/tasks"
element={ element={

View File

@@ -0,0 +1,94 @@
/**
* FloatingHelpButton Component
*
* A floating help button fixed in the top-right corner of the screen.
* Automatically determines the help path based on the current route.
*/
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { HelpCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
// Map routes to their help paths
const routeToHelpPath: Record<string, string> = {
'/': '/help/dashboard',
'/dashboard': '/help/dashboard',
'/scheduler': '/help/scheduler',
'/tasks': '/help/tasks',
'/customers': '/help/customers',
'/services': '/help/services',
'/resources': '/help/resources',
'/staff': '/help/staff',
'/messages': '/help/messages',
'/tickets': '/help/ticketing',
'/payments': '/help/payments',
'/plugins': '/help/plugins',
'/plugins/marketplace': '/help/plugins',
'/plugins/my-plugins': '/help/plugins',
'/plugins/create': '/help/plugins/create',
'/settings': '/help/settings/general',
'/settings/general': '/help/settings/general',
'/settings/resource-types': '/help/settings/resource-types',
'/settings/booking': '/help/settings/booking',
'/settings/appearance': '/help/settings/appearance',
'/settings/email': '/help/settings/email',
'/settings/domains': '/help/settings/domains',
'/settings/api': '/help/settings/api',
'/settings/auth': '/help/settings/auth',
'/settings/billing': '/help/settings/billing',
'/settings/quota': '/help/settings/quota',
// Platform routes
'/platform/dashboard': '/help/dashboard',
'/platform/businesses': '/help/dashboard',
'/platform/users': '/help/staff',
'/platform/tickets': '/help/ticketing',
};
const FloatingHelpButton: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
// Get the help path for the current route
const getHelpPath = (): string => {
// Exact match first
if (routeToHelpPath[location.pathname]) {
return routeToHelpPath[location.pathname];
}
// Try matching with a prefix (for dynamic routes like /customers/:id)
const pathSegments = location.pathname.split('/').filter(Boolean);
if (pathSegments.length > 0) {
// Try progressively shorter paths
for (let i = pathSegments.length; i > 0; i--) {
const testPath = '/' + pathSegments.slice(0, i).join('/');
if (routeToHelpPath[testPath]) {
return routeToHelpPath[testPath];
}
}
}
// Default to the main help guide
return '/help';
};
const helpPath = getHelpPath();
// Don't show on help pages themselves
if (location.pathname.startsWith('/help')) {
return null;
}
return (
<Link
to={helpPath}
className="fixed top-20 right-4 z-50 inline-flex items-center justify-center w-10 h-10 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 transition-all duration-200 hover:scale-110"
title={t('common.help', 'Help')}
aria-label={t('common.help', 'Help')}
>
<HelpCircle size={20} />
</Link>
);
};
export default FloatingHelpButton;

View File

@@ -0,0 +1,33 @@
/**
* HelpButton Component
*
* A contextual help button that appears at the top-right of pages
* and links to the relevant help documentation.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { HelpCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface HelpButtonProps {
helpPath: string;
className?: string;
}
const HelpButton: React.FC<HelpButtonProps> = ({ helpPath, className = '' }) => {
const { t } = useTranslation();
return (
<Link
to={helpPath}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm 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-lg transition-colors ${className}`}
title={t('common.help', 'Help')}
>
<HelpCircle size={18} />
<span className="hidden sm:inline">{t('common.help', 'Help')}</span>
</Link>
);
};
export default HelpButton;

View 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);
};

View File

@@ -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}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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';

View 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 },
],
};

View File

@@ -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',

View File

@@ -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 };
},
});
};

View File

@@ -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(() => {
if (containerRef?.current) {
containerRef.current.scrollTo(0, 0);
} else {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, [pathname]); }
}, [pathname, containerRef]);
} }

View File

@@ -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;
}

View File

@@ -9,6 +9,7 @@ import { Business, User } from '../types';
import MasqueradeBanner from '../components/MasqueradeBanner'; import MasqueradeBanner from '../components/MasqueradeBanner';
import OnboardingWizard from '../components/OnboardingWizard'; import OnboardingWizard from '../components/OnboardingWizard';
import TicketModal from '../components/TicketModal'; import TicketModal from '../components/TicketModal';
import FloatingHelpButton from '../components/FloatingHelpButton';
import { useStopMasquerade } from '../hooks/useAuth'; import { useStopMasquerade } from '../hooks/useAuth';
import { useNotificationWebSocket } from '../hooks/useNotificationWebSocket'; import { useNotificationWebSocket } from '../hooks/useNotificationWebSocket';
import { useTicket } from '../hooks/useTickets'; import { useTicket } from '../hooks/useTickets';
@@ -51,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);
@@ -167,6 +168,9 @@ const BusinessLayoutContent: React.FC<BusinessLayoutProps> = ({ business, user,
return ( return (
<div className="flex h-full bg-gray-50 dark:bg-gray-900 transition-colors duration-200"> <div className="flex h-full bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
{/* Floating Help Button */}
<FloatingHelpButton />
<div className={`fixed inset-y-0 left-0 z-40 transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out md:hidden`}> <div className={`fixed inset-y-0 left-0 z-40 transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out md:hidden`}>
<Sidebar business={business} user={user} isCollapsed={false} toggleCollapse={() => { }} /> <Sidebar business={business} user={user} isCollapsed={false} toggleCollapse={() => { }} />
</div> </div>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';
@@ -6,6 +6,7 @@ import PlatformSidebar from '../components/PlatformSidebar';
import UserProfileDropdown from '../components/UserProfileDropdown'; import UserProfileDropdown from '../components/UserProfileDropdown';
import NotificationDropdown from '../components/NotificationDropdown'; import NotificationDropdown from '../components/NotificationDropdown';
import TicketModal from '../components/TicketModal'; import TicketModal from '../components/TicketModal';
import FloatingHelpButton from '../components/FloatingHelpButton';
import { useTicket } from '../hooks/useTickets'; import { useTicket } from '../hooks/useTickets';
import { useScrollToTop } from '../hooks/useScrollToTop'; import { useScrollToTop } from '../hooks/useScrollToTop';
@@ -20,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);
@@ -36,6 +38,9 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
return ( return (
<div className="flex h-screen bg-gray-100 dark:bg-gray-900"> <div className="flex h-screen bg-gray-100 dark:bg-gray-900">
{/* Floating Help Button */}
<FloatingHelpButton />
{/* Mobile menu */} {/* Mobile menu */}
<div className={`fixed inset-y-0 left-0 z-40 transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out md:hidden`}> <div className={`fixed inset-y-0 left-0 z-40 transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out md:hidden`}>
<PlatformSidebar user={user} isCollapsed={false} toggleCollapse={() => { }} /> <PlatformSidebar user={user} isCollapsed={false} toggleCollapse={() => { }} />
@@ -79,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>

View File

@@ -0,0 +1,568 @@
/**
* Create Plugin Page
*
* Allows businesses to create custom plugins with code editor,
* category selection, and visibility options.
*/
import React, { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
Code,
Save,
Eye,
EyeOff,
ArrowLeft,
Info,
CheckCircle,
AlertTriangle,
Mail,
BarChart3,
Users,
Calendar,
Link as LinkIcon,
Bot,
Package,
Image,
HelpCircle,
} from 'lucide-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import api from '../api/client';
import { PluginCategory } from '../types';
import { usePlanFeatures } from '../hooks/usePlanFeatures';
// Category icon mapping
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
EMAIL: <Mail className="h-4 w-4" />,
REPORTS: <BarChart3 className="h-4 w-4" />,
CUSTOMER: <Users className="h-4 w-4" />,
BOOKING: <Calendar className="h-4 w-4" />,
INTEGRATION: <LinkIcon className="h-4 w-4" />,
AUTOMATION: <Bot className="h-4 w-4" />,
OTHER: <Package className="h-4 w-4" />,
};
// Category descriptions
const categoryDescriptions: Record<PluginCategory, string> = {
EMAIL: 'Email notifications and automated messaging',
REPORTS: 'Analytics, reports, and data exports',
CUSTOMER: 'Customer engagement and retention',
BOOKING: 'Scheduling and booking automation',
INTEGRATION: 'Third-party service integrations',
AUTOMATION: 'General business automation',
OTHER: 'Miscellaneous plugins',
};
// Default plugin code template
const DEFAULT_PLUGIN_CODE = `# My Custom Plugin
#
# This plugin runs on a schedule and can interact with your business data.
# Use template variables to make your plugin configurable.
#
# Available template variables:
# {{PROMPT:variable_name:default_value:description}}
# {{CONTEXT:context_type}} - Access business context (CUSTOMERS, EVENTS, etc.)
# {{DATE:format}} - Current date in specified format
# Example: Get all customers who haven't booked in 30 days
inactive_days = int("{{PROMPT:inactive_days:30:Days of inactivity}}")
# Access customer data
customers = {{CONTEXT:CUSTOMERS}}
# Filter inactive customers
from datetime import datetime, timedelta
cutoff_date = datetime.now() - timedelta(days=inactive_days)
inactive_customers = [
c for c in customers
if not c.get('last_booking') or
datetime.fromisoformat(c['last_booking']) < cutoff_date
]
# Return results (will be logged)
result = {
'inactive_count': len(inactive_customers),
'customers': inactive_customers[:10], # First 10 for preview
'message': f"Found {len(inactive_customers)} inactive customers"
}
`;
interface FormData {
name: string;
shortDescription: string;
description: string;
category: PluginCategory;
pluginCode: string;
version: string;
logoUrl: string;
visibility: 'PRIVATE' | 'PUBLIC';
}
const CreatePlugin: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { canUse } = usePlanFeatures();
const [formData, setFormData] = useState<FormData>({
name: '',
shortDescription: '',
description: '',
category: 'AUTOMATION',
pluginCode: DEFAULT_PLUGIN_CODE,
version: '1.0.0',
logoUrl: '',
visibility: 'PRIVATE',
});
const [showPreview, setShowPreview] = useState(false);
const [extractedVariables, setExtractedVariables] = useState<any[]>([]);
// Extract template variables from code
const extractVariables = useCallback((code: string) => {
const promptPattern = /\{\{PROMPT:([^:}]+):([^:}]*):([^}]*)\}\}/g;
const contextPattern = /\{\{CONTEXT:([^}]+)\}\}/g;
const datePattern = /\{\{DATE:([^}]+)\}\}/g;
const variables: any[] = [];
let match;
while ((match = promptPattern.exec(code)) !== null) {
variables.push({
type: 'PROMPT',
name: match[1],
default: match[2],
description: match[3],
});
}
while ((match = contextPattern.exec(code)) !== null) {
variables.push({
type: 'CONTEXT',
name: match[1],
});
}
while ((match = datePattern.exec(code)) !== null) {
variables.push({
type: 'DATE',
format: match[1],
});
}
return variables;
}, []);
// Update extracted variables when code changes
const handleCodeChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newCode = e.target.value;
setFormData(prev => ({ ...prev, pluginCode: newCode }));
setExtractedVariables(extractVariables(newCode));
};
// Create plugin mutation
const createMutation = useMutation({
mutationFn: async (data: FormData) => {
const payload = {
name: data.name,
short_description: data.shortDescription,
description: data.description,
category: data.category,
plugin_code: data.pluginCode,
version: data.version,
logo_url: data.logoUrl || undefined,
visibility: data.visibility,
};
const response = await api.post('/plugin-templates/', payload);
return response.data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['plugin-templates'] });
navigate('/plugins/my-plugins');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate(formData);
};
// Check if user can create plugins
const canCreatePlugins = canUse('can_create_plugins');
if (!canCreatePlugins) {
return (
<div className="p-8 max-w-4xl mx-auto">
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-8 text-center">
<AlertTriangle className="h-16 w-16 mx-auto text-amber-500 mb-4" />
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
{t('plugins.upgradeRequired', 'Upgrade Required')}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{t('plugins.upgradeToCreate', 'Plugin creation is available on higher-tier plans. Upgrade your subscription to create custom plugins.')}
</p>
<button
onClick={() => navigate('/settings/billing')}
className="px-6 py-3 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors font-medium"
>
{t('plugins.viewPlans', 'View Plans')}
</button>
</div>
</div>
);
}
return (
<div className="p-8 max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<button
onClick={() => navigate('/plugins/my-plugins')}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-4 transition-colors"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
<Code className="text-brand-500" />
{t('plugins.createPlugin', 'Create Custom Plugin')}
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
{t('plugins.createPluginDescription', 'Build a custom automation for your business')}
</p>
</div>
<a
href="/help/plugins"
target="_blank"
className="flex items-center gap-2 text-brand-600 dark:text-brand-400 hover:underline"
>
<HelpCircle size={18} />
{t('plugins.viewDocs', 'View Documentation')}
</a>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Basic Info */}
<div className="lg:col-span-1 space-y-6">
{/* Plugin Name */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('plugins.basicInfo', 'Basic Information')}
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('plugins.pluginName', 'Plugin Name')} *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
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-brand-500"
placeholder="e.g., Win Back Inactive Customers"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('plugins.shortDescription', 'Short Description')} *
</label>
<input
type="text"
value={formData.shortDescription}
onChange={(e) => setFormData(prev => ({ ...prev, shortDescription: e.target.value }))}
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-brand-500"
placeholder="Brief summary for marketplace listing"
maxLength={200}
required
/>
<p className="mt-1 text-xs text-gray-500">{formData.shortDescription.length}/200</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('plugins.description', 'Full Description')}
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={4}
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-brand-500 resize-none"
placeholder="Detailed description of what this plugin does..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('plugins.category', 'Category')} *
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as PluginCategory }))}
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-brand-500"
>
{Object.entries(categoryDescriptions).map(([key, desc]) => (
<option key={key} value={key}>
{key} - {desc}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('plugins.version', 'Version')}
</label>
<input
type="text"
value={formData.version}
onChange={(e) => setFormData(prev => ({ ...prev, version: e.target.value }))}
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-brand-500"
placeholder="1.0.0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<Image size={16} className="inline mr-1" />
{t('plugins.logoUrl', 'Logo URL')} ({t('common.optional', 'optional')})
</label>
<input
type="url"
value={formData.logoUrl}
onChange={(e) => setFormData(prev => ({ ...prev, logoUrl: e.target.value }))}
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-brand-500"
placeholder="https://example.com/logo.png"
/>
</div>
</div>
</div>
{/* Visibility */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('plugins.visibility', 'Visibility')}
</h3>
<div className="space-y-3">
<label className="flex items-start gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<input
type="radio"
name="visibility"
value="PRIVATE"
checked={formData.visibility === 'PRIVATE'}
onChange={(e) => setFormData(prev => ({ ...prev, visibility: 'PRIVATE' }))}
className="mt-1"
/>
<div>
<div className="flex items-center gap-2">
<EyeOff size={16} className="text-gray-500" />
<span className="font-medium text-gray-900 dark:text-white">
{t('plugins.private', 'Private')}
</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('plugins.privateDescription', 'Only you can see and use this plugin')}
</p>
</div>
</label>
<label className="flex items-start gap-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<input
type="radio"
name="visibility"
value="PUBLIC"
checked={formData.visibility === 'PUBLIC'}
onChange={(e) => setFormData(prev => ({ ...prev, visibility: 'PUBLIC' }))}
className="mt-1"
/>
<div>
<div className="flex items-center gap-2">
<Eye size={16} className="text-green-500" />
<span className="font-medium text-gray-900 dark:text-white">
{t('plugins.public', 'Public (Marketplace)')}
</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('plugins.publicDescription', 'Submit for review to be listed in the marketplace')}
</p>
</div>
</label>
</div>
{formData.visibility === 'PUBLIC' && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex items-start gap-2">
<Info size={16} className="text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-800 dark:text-blue-200">
{t('plugins.publicNote', 'Public plugins require approval before appearing in the marketplace. Our team will review your code for security and quality.')}
</p>
</div>
</div>
)}
</div>
{/* Extracted Variables */}
{extractedVariables.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('plugins.templateVariables', 'Detected Variables')}
</h3>
<div className="space-y-2">
{extractedVariables.map((v, idx) => (
<div key={idx} className="p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-sm">
<div className="flex items-center gap-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
v.type === 'PROMPT' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' :
v.type === 'CONTEXT' ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' :
'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
}`}>
{v.type}
</span>
<span className="font-mono text-gray-900 dark:text-white">
{v.name || v.format}
</span>
</div>
{v.description && (
<p className="text-gray-500 dark:text-gray-400 mt-1 ml-4">
{v.description}
</p>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* Right Column - Code Editor */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Code size={20} className="text-brand-500" />
{t('plugins.pluginCode', 'Plugin Code')}
</h3>
<button
type="button"
onClick={() => setShowPreview(!showPreview)}
className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 hover:underline"
>
{showPreview ? <EyeOff size={16} /> : <Eye size={16} />}
{showPreview ? t('plugins.hidePreview', 'Hide Preview') : t('plugins.showPreview', 'Show Preview')}
</button>
</div>
{showPreview ? (
<div className="max-h-[600px] overflow-auto">
<SyntaxHighlighter
language="python"
style={vscDarkPlus}
customStyle={{
margin: 0,
borderRadius: 0,
fontSize: '0.875rem',
}}
showLineNumbers
>
{formData.pluginCode}
</SyntaxHighlighter>
</div>
) : (
<textarea
value={formData.pluginCode}
onChange={handleCodeChange}
rows={25}
className="w-full px-4 py-4 bg-[#1e1e1e] text-gray-100 font-mono text-sm focus:outline-none resize-none"
placeholder="# Write your plugin code here..."
spellCheck={false}
/>
)}
</div>
{/* Quick Reference */}
<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">
<h4 className="font-semibold text-gray-900 dark:text-white mb-3">
{t('plugins.quickReference', 'Quick Reference')}
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<p className="font-medium text-gray-900 dark:text-white mb-1">Prompt Variable</p>
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
{`{{PROMPT:name:default:desc}}`}
</code>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white mb-1">Context Data</p>
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
{`{{CONTEXT:CUSTOMERS}}`}
</code>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white mb-1">Date Format</p>
<code className="text-xs bg-white dark:bg-gray-800 px-2 py-1 rounded block">
{`{{DATE:%Y-%m-%d}}`}
</code>
</div>
</div>
</div>
</div>
</div>
{/* Error Message */}
{createMutation.isError && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="text-red-500" size={20} />
<p className="text-red-800 dark:text-red-200">
{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to create plugin'}
</p>
</div>
</div>
)}
{/* Submit Buttons */}
<div className="flex items-center justify-end gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={() => navigate('/plugins/my-plugins')}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
{t('common.cancel', 'Cancel')}
</button>
<button
type="submit"
disabled={createMutation.isPending || !formData.name || !formData.shortDescription}
className="flex items-center gap-2 px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
{createMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
{t('plugins.creating', 'Creating...')}
</>
) : (
<>
<Save size={18} />
{formData.visibility === 'PUBLIC'
? t('plugins.createAndSubmit', 'Create & Submit for Review')
: t('plugins.createPlugin', 'Create Plugin')}
</>
)}
</button>
</div>
</form>
</div>
);
};
export default CreatePlugin;

View File

@@ -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
.filter(appt => isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }))
.forEach(appt => {
const date = new Date(appt.startTime); const date = new Date(appt.startTime);
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const dayName = dayNames[date.getDay()]; 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,83 +357,84 @@ 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">
{/* Header */}
<div className="flex items-center justify-between">
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2> <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> <p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
</div> </div>
<div className="flex items-center gap-2">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"> <button
{metrics.map((metric, index) => ( onClick={() => setIsEditing(!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"> className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{metric.label}</p> isEditing
<div className="flex items-baseline gap-2 mt-2"> ? 'bg-brand-600 text-white'
<span className="text-2xl font-bold text-gray-900 dark:text-white">{metric.value}</span> : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
<span className={`flex items-center text-xs font-medium px-2 py-0.5 rounded-full ${ }`}
metric.trend === 'up' ? 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400' : >
metric.trend === 'down' ? 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400' : 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300' {isEditing ? <Check size={16} /> : <Edit2 size={16} />}
}`}> <span className="text-sm">{isEditing ? 'Done' : 'Edit Layout'}</span>
{metric.trend === 'up' && <TrendingUp size={12} className="mr-1" />} </button>
{metric.trend === 'down' && <TrendingDown size={12} className="mr-1" />} <button
{metric.trend === 'neutral' && <Minus size={12} className="mr-1" />} onClick={() => setShowConfig(true)}
{metric.change} 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"
</span> >
<Settings size={16} />
<span className="text-sm">Widgets</span>
</button>
</div> </div>
</div> </div>
{/* Edit mode hint */}
{isEditing && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 text-sm text-blue-700 dark:text-blue-300">
Drag widgets to reposition them. Drag the corner to resize. Hover over a widget and click the X to remove it.
</div>
)}
{/* Grid Layout */}
<div className="max-w-[1200px] mx-auto">
<GridLayout
className="layout"
layout={layoutWithConstraints}
cols={12}
rowHeight={60}
width={1200}
isDraggable={isEditing}
isResizable={isEditing}
onLayoutChange={onLayoutChange}
draggableHandle=".drag-handle"
compactType="vertical"
preventCollision={false}
>
{dashboardLayout.widgets.map(widgetId => (
<div key={widgetId} className="widget-container">
{renderWidget(widgetId)}
</div>
))} ))}
</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>
); );
}; };

View File

@@ -1,32 +1,195 @@
/**
* Help Guide - Main Documentation Hub
*
* Comprehensive guide linking to all help pages and providing quick overviews.
*/
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BookOpen, Construction } from 'lucide-react'; import { Link } from 'react-router-dom';
import {
BookOpen,
LayoutDashboard,
Calendar,
CheckSquare,
Users,
Briefcase,
ClipboardList,
UserCog,
MessageSquare,
Mail,
CreditCard,
Puzzle,
Settings,
ChevronRight,
HelpCircle,
} from 'lucide-react';
interface HelpSection {
title: string;
description: string;
links: {
label: string;
path: string;
icon: React.ReactNode;
}[];
}
const HelpGuide: React.FC = () => { const HelpGuide: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const sections: HelpSection[] = [
{
title: 'Core Features',
description: 'Essential tools for managing your scheduling business',
links: [
{ label: 'Dashboard', path: '/help/dashboard', icon: <LayoutDashboard size={18} /> },
{ label: 'Scheduler', path: '/help/scheduler', icon: <Calendar size={18} /> },
{ label: 'Tasks', path: '/help/tasks', icon: <CheckSquare size={18} /> },
],
},
{
title: 'Manage',
description: 'Organize your customers, services, and resources',
links: [
{ label: 'Customers', path: '/help/customers', icon: <Users size={18} /> },
{ label: 'Services', path: '/help/services', icon: <Briefcase size={18} /> },
{ label: 'Resources', path: '/help/resources', icon: <ClipboardList size={18} /> },
{ label: 'Staff', path: '/help/staff', icon: <UserCog size={18} /> },
],
},
{
title: 'Communicate',
description: 'Stay connected with your customers',
links: [
{ label: 'Messages', path: '/help/messages', icon: <MessageSquare size={18} /> },
{ label: 'Ticketing', path: '/help/ticketing', icon: <Mail size={18} /> },
],
},
{
title: 'Money',
description: 'Handle payments and track revenue',
links: [
{ label: 'Payments', path: '/help/payments', icon: <CreditCard size={18} /> },
],
},
{
title: 'Extend',
description: 'Add functionality with plugins',
links: [
{ label: 'Plugins', path: '/help/plugins', icon: <Puzzle size={18} /> },
],
},
{
title: 'Settings',
description: 'Configure your business settings',
links: [
{ label: 'General Settings', path: '/help/settings/general', icon: <Settings size={18} /> },
{ label: 'Resource Types', path: '/help/settings/resource-types', icon: <Settings size={18} /> },
{ label: 'Booking Settings', path: '/help/settings/booking', icon: <Settings size={18} /> },
{ label: 'Appearance', path: '/help/settings/appearance', icon: <Settings size={18} /> },
{ label: 'Email Templates', path: '/help/settings/email', icon: <Settings size={18} /> },
{ label: 'Custom Domains', path: '/help/settings/domains', icon: <Settings size={18} /> },
{ label: 'API Settings', path: '/help/settings/api', icon: <Settings size={18} /> },
{ label: 'Authentication', path: '/help/settings/auth', icon: <Settings size={18} /> },
{ label: 'Billing', path: '/help/settings/billing', icon: <Settings size={18} /> },
{ label: 'Usage & Quota', path: '/help/settings/quota', icon: <Settings size={18} /> },
],
},
];
return ( return (
<div className="p-8 max-w-4xl mx-auto"> <div className="p-8 max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3"> <div className="flex items-center gap-3 mb-4">
<BookOpen className="text-brand-600" /> <div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<BookOpen size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
{t('help.guide.title', 'Platform Guide')} {t('help.guide.title', 'Platform Guide')}
</h1> </h1>
<p className="text-gray-500 dark:text-gray-400 mt-2"> <p className="text-gray-500 dark:text-gray-400">
{t('help.guide.subtitle', 'Learn how to use SmoothSchedule effectively')} {t('help.guide.subtitle', 'Learn how to use SmoothSchedule effectively')}
</p> </p>
</div> </div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
<Construction size={64} className="mx-auto text-gray-300 dark:text-gray-600 mb-4" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{t('help.guide.comingSoon', 'Coming Soon')}
</h2>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t('help.guide.comingSoonDesc', 'We are working on comprehensive documentation to help you get the most out of SmoothSchedule. Check back soon!')}
</p>
</div> </div>
</div> </div>
{/* Quick Start */}
<section className="mb-10">
<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-xl font-semibold text-gray-900 dark:text-white mb-4">Quick Start</h2>
<ol className="space-y-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="text-gray-700 dark:text-gray-300">Set up your <Link to="/help/services" className="text-brand-600 hover:underline">services</Link> - what you offer to customers</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">Add your <Link to="/help/resources" className="text-brand-600 hover:underline">resources</Link> - staff, rooms, or equipment</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">Use the <Link to="/help/scheduler" className="text-brand-600 hover:underline">scheduler</Link> to manage appointments</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">Track your business with the <Link to="/help/dashboard" className="text-brand-600 hover:underline">dashboard</Link></span>
</li>
</ol>
</div>
</section>
{/* Help Sections */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sections.map((section, idx) => (
<div
key={idx}
className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6"
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{section.title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
{section.description}
</p>
<ul className="space-y-2">
{section.links.map((link, linkIdx) => (
<li key={linkIdx}>
<Link
to={link.path}
className="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-gray-700 dark:text-gray-300 hover:text-brand-600 dark:hover:text-brand-400"
>
<span className="text-brand-500">{link.icon}</span>
<span className="flex-1">{link.label}</span>
<ChevronRight size={16} className="text-gray-400" />
</Link>
</li>
))}
</ul>
</div>
))}
</div>
{/* Need Help */}
<section className="mt-10 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>
<Link
to="/tickets"
className="inline-flex items-center gap-2 px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</Link>
</section>
</div>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View 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;

View File

@@ -0,0 +1,480 @@
/**
* Help Customers Page
*
* Comprehensive help documentation for the Customers management page.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Users,
UserPlus,
Mail,
Phone,
Search,
Edit,
CheckCircle,
ChevronRight,
HelpCircle,
Tag,
ArrowUpDown,
Filter,
DollarSign,
Calendar,
MapPin,
Eye,
AlertCircle,
MoreHorizontal,
} from 'lucide-react';
const HelpCustomers: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} /> Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Users size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Customers Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Manage your customer database and relationships</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Users size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Customers page is your central hub for managing all client information. Every person who books with your business has a customer record that stores their contact details, booking history, total spend, and custom tags.
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
The page displays customers in a sortable, searchable table with key information at a glance: name with avatar, contact info, status, total spend, and last visit. You can quickly add new customers, filter the list, or take actions on individual customers.
</p>
<p className="text-gray-600 dark:text-gray-300">
A well-maintained customer database enables personalized service, targeted marketing, and tracking of customer lifetime value.
</p>
</div>
</section>
{/* The Customer Table Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Users size={20} className="text-brand-500" /> The Customer Table
</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 main view displays all customers in a table with the following columns:
</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">
<Users size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Customer</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Avatar (or initials), full name, and any tags assigned to the customer. Click the header to sort alphabetically.
</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-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Contact Info</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Email address and phone number for quick reference and communication.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<CheckCircle size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Status</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Customer status badge - <span className="text-green-600 dark:text-green-400">Active</span> (green), <span className="text-gray-600 dark:text-gray-400">Inactive</span> (gray), or blocked (red). Click header to sort.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<DollarSign size={20} className="text-amber-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Total Spend</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Cumulative amount the customer has spent at your business. Click header to sort by highest/lowest spenders.
</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-pink-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Last Visit</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Date of the customer's most recent appointment. Shows "Never" for customers who haven't booked yet. Click header to sort.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<MoreHorizontal size={20} className="text-gray-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Actions</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Action buttons including Masquerade (for owners) and a menu for additional options.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Searching and Sorting Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Search size={20} className="text-brand-500" /> Searching and Sorting
</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">
<Search size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Search</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Type in the search bar to instantly filter customers by name, email, or phone number. Results update as you type.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ArrowUpDown size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Sort Columns</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click any sortable column header (Customer, Status, Total Spend, Last Visit) to sort ascending/descending. Click again to reverse the order.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Filter size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Filters</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Use the Filters button to access advanced filtering options for narrowing down your customer list.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Adding a Customer Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<UserPlus size={20} className="text-brand-500" /> Adding a Customer
</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 <strong>"Add Customer"</strong> button to open the new customer form:
</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">
<Users size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Full Name <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The customer's name as it should appear in your records and communications.
</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-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Email Address <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Primary email for appointment confirmations, reminders, and marketing.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Phone size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Phone Number <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Contact number for SMS reminders and direct communication.
</p>
</div>
</div>
<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 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Location (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
City, State, and ZIP code fields for address information.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Tag size={20} className="text-pink-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Tags (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Comma-separated labels for categorizing customers (e.g., "VIP, Referral, Corporate"). Tags appear as badges in the customer list.
</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">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Note:</strong> New customers are created with "Active" status by default. Total Spend starts at $0.00 and increases automatically as they complete appointments.
</p>
</div>
</div>
</section>
{/* Customer Status 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" /> Customer Statuses
</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">
Customer status controls their ability to book and helps you categorize your customer base:
</p>
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 min-w-[100px]">
Active
</span>
<p className="text-gray-600 dark:text-gray-300">
Customer can book appointments and access the booking system
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 min-w-[100px]">
Inactive
</span>
<p className="text-gray-600 dark:text-gray-300">
Temporarily disabled - customer cannot book but record is preserved
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 min-w-[100px]">
Blocked
</span>
<p className="text-gray-600 dark:text-gray-300">
Customer is blocked from booking (e.g., for policy violations)
</p>
</div>
</div>
</div>
</section>
{/* Tags Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Tag size={20} className="text-brand-500" /> Using Tags
</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">
Tags are custom labels you assign to customers for organization and segmentation:
</p>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>VIP:</strong> Mark your best customers for special treatment</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Referral:</strong> Track customers who came through referrals</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Corporate:</strong> Identify business/corporate accounts</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>New:</strong> Recently acquired customers</span>
</li>
</ul>
<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">
<strong>Adding Tags:</strong> Enter tags as comma-separated values when creating or editing a customer (e.g., "VIP, Monthly, Referral"). Tags appear as small badges next to the customer name in the list.
</p>
</div>
</div>
</section>
{/* Masquerading Section */}
<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" /> Masquerading as Customers
</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">
Masquerading allows business owners to see the application exactly as a specific customer sees it. This powerful feature helps you:
</p>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Provide Support:</strong> Walk customers through tasks step-by-step while seeing exactly what they see</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Troubleshoot Issues:</strong> Diagnose problems by experiencing the customer's perspective</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Test Experience:</strong> Verify that booking flows and features work correctly for customers</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Book on Behalf:</strong> Complete bookings for customers who need assistance</span>
</li>
</ul>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
<Eye size={20} className="text-indigo-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">How to Masquerade</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Click the <strong>"Masquerade"</strong> button in the Actions column for any customer with an associated user account. The interface will switch to show what that customer sees.
</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-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Ending Masquerade</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
A notification banner appears at the top of the screen while masquerading. Click "End Masquerade" to return to your owner view.
</p>
</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>Owner Only:</strong> Masquerading is restricted to business owners for security. The Masquerade button only appears for customers who have created a user account.
</p>
</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>
<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>Personalized Service:</strong> Know your customers' preferences, history, and value at a glance
</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>Quick Booking:</strong> Select existing customers instantly when scheduling appointments
</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>Revenue Tracking:</strong> Monitor customer lifetime value with automatic spend tracking
</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>Segmentation:</strong> Use tags and filters to target specific customer groups
</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>Support Tools:</strong> Masquerade feature enables hands-on customer assistance
</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 Communication:</strong> One-click access to email and phone for direct contact
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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"
>
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/messages"
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">Messages Guide</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"
>
<Tag 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/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" />
<span className="text-gray-900 dark:text-white">Payments Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about managing customers.
</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 HelpCustomers;

View File

@@ -0,0 +1,354 @@
/**
* Help Dashboard Page
*
* Comprehensive help documentation for the Dashboard.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
LayoutDashboard,
BarChart3,
TrendingUp,
Users,
Calendar,
Briefcase,
ClipboardList,
CheckCircle,
ChevronRight,
HelpCircle,
Settings,
Edit2,
GripVertical,
Ticket,
Activity,
UserX,
PieChart,
} from 'lucide-react';
const HelpDashboard: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<LayoutDashboard size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Dashboard Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Your customizable business command center
</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<BarChart3 size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Dashboard is your fully customizable command center for understanding your business performance. It provides real-time insights into appointments, customers, capacity, tickets, and more.
</p>
<p className="text-gray-600 dark:text-gray-300">
You can personalize your dashboard by adding, removing, repositioning, and resizing widgets to focus on the metrics that matter most to you. Your layout is automatically saved.
</p>
</div>
</section>
{/* Customization Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" />
Customizing Your Dashboard
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Edit2 size={16} className="text-blue-500" />
Edit Layout Mode
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
Click the <strong>"Edit Layout"</strong> button to enter edit mode. While in edit mode you can:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-300 ml-4 space-y-1">
<li className="flex items-center gap-2">
<GripVertical size={14} className="text-gray-400" />
<strong>Drag widgets</strong> to reposition them on the dashboard
</li>
<li> <strong>Drag corners</strong> to resize widgets larger or smaller</li>
<li> <strong>Click the X</strong> on any widget to remove it</li>
</ul>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">
Click <strong>"Done"</strong> when finished editing.
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 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">
Click the <strong>"Widgets"</strong> button to open the widget configuration panel. Here you can toggle widgets on/off and click <strong>"Reset to Default"</strong> to restore the original layout.
</p>
</div>
</div>
</div>
</section>
{/* Available Widgets Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<LayoutDashboard size={20} className="text-brand-500" />
Available Widgets
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Choose from a variety of widgets to build your perfect dashboard:
</p>
{/* Metric Widgets */}
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Metric Widgets</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Calendar size={18} className="text-blue-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Total Appointments</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Count with weekly and monthly growth trends
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Users size={18} className="text-green-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Active Customers</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Active customer count with growth trends
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<UserX size={18} className="text-red-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">No-Show Rate</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Percentage of missed appointments with trends
</p>
</div>
</div>
</div>
{/* Chart Widgets */}
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Chart Widgets</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<BarChart3 size={18} className="text-blue-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Weekly Revenue</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Bar chart of revenue by day of week
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<TrendingUp size={18} className="text-green-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Appointments Trend</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Line chart of appointments by day
</p>
</div>
</div>
</div>
{/* Activity Widgets */}
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Activity & Status Widgets</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Ticket size={18} className="text-orange-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Open Tickets</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Support tickets requiring attention
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Activity size={18} className="text-purple-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Recent Activity</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Timeline of recent business events
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Users size={18} className="text-cyan-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Capacity Utilization</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
Shows booking percentage for each resource
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<PieChart size={18} className="text-violet-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Customer Breakdown</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
New vs returning customers this month
</p>
</div>
</div>
</div>
</div>
</section>
{/* Growth Metrics Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<TrendingUp size={20} className="text-brand-500" />
Understanding Growth Metrics
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Metric widgets display both <strong>weekly</strong> and <strong>monthly</strong> growth percentages:
</p>
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 text-xs rounded">
+12%
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-300">
<strong>Green with +</strong> indicates growth compared to the previous period
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="px-2 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 text-xs rounded">
-8%
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-300">
<strong>Red with -</strong> indicates decline compared to the previous period
</p>
</div>
</div>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
Weekly compares this week to last week. Monthly compares this month to last month.
</p>
</div>
</section>
{/* Benefits Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" />
Benefits
</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>Fully Customizable:</strong> Arrange widgets exactly how you want them
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Persistent Layout:</strong> Your dashboard layout is saved automatically
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Real-Time Data:</strong> All metrics update automatically as your data changes
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Trend Tracking:</strong> Weekly and monthly growth percentages help you spot patterns
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Resource Insights:</strong> See capacity utilization for each team member
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/help/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"
>
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/customers"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
If you have questions that aren't covered here, 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>
</div>
);
};
export default HelpDashboard;

View File

@@ -0,0 +1,162 @@
/**
* Help Messages Page
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, MessageSquare, Send, Inbox, Users, Bell,
CheckCircle, ChevronRight, HelpCircle, Search, Archive,
} from 'lucide-react';
const HelpMessages: 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">
<MessageSquare size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Messages Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Communicate with your customers</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">
<MessageSquare size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Messages feature provides a centralized inbox for all customer communications. Send and receive messages, track conversation history, and maintain clear communication records.
</p>
<p className="text-gray-600 dark:text-gray-300">
Keep all customer interactions in one place for easy reference and better customer service.
</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">
<Inbox size={20} className="text-brand-500" /> Message Features
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Inbox size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Unified Inbox</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">All customer messages in one central location</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Send size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Send Messages</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Compose and send messages to customers directly</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Search size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Search History</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Find past conversations quickly with search</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Archive size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Archive & Organize</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Keep your inbox organized with archiving</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">
<Bell size={20} className="text-brand-500" /> Notifications
</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">
Stay informed about new messages with real-time notifications. Configure how and when you want to be notified about incoming customer communications.
</p>
<ul className="space-y-2 text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
In-app notification badges
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Email notifications for new messages
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
Unread message indicators
</li>
</ul>
</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>
<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>Centralized:</strong> All communications in one place</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>History:</strong> Complete record of all customer interactions</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>Responsive:</strong> Quick responses improve customer satisfaction</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>Professional:</strong> Consistent communication experience</span>
</li>
</ul>
</div>
</section>
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link to="/help/customers" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<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">
<MessageSquare size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Ticketing Guide</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 HelpMessages;

View File

@@ -0,0 +1,167 @@
/**
* Help Payments Page
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, CreditCard, DollarSign, Receipt, TrendingUp, RefreshCw,
CheckCircle, ChevronRight, HelpCircle, Calendar, FileText,
} from 'lucide-react';
const HelpPayments: 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">
<CreditCard size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Payments Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Manage transactions and revenue</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">
<CreditCard size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Payments page is your financial hub. Track all transactions, process payments, issue refunds, and view revenue reports. Integrated with Stripe for secure payment processing.
</p>
<p className="text-gray-600 dark:text-gray-300">
Get real-time insights into your business revenue and manage all payment-related activities in one place.
</p>
</div>
</section>
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<DollarSign size={20} className="text-brand-500" /> Payment Features
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<CreditCard size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Process Payments</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Accept credit cards, debit cards, and digital wallets</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Receipt size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Transaction History</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">View all past payments with detailed records</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<RefreshCw size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Refunds</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Process full or partial refunds when needed</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<TrendingUp size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Revenue Reports</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Track earnings by day, week, month, or custom range</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">
<FileText size={20} className="text-brand-500" /> Payment Status
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="w-3 h-3 rounded-full bg-green-500" />
<span className="font-medium text-gray-900 dark:text-white">Completed</span>
<span className="text-sm text-gray-500 dark:text-gray-400">- Payment successfully processed</span>
</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" />
<span className="font-medium text-gray-900 dark:text-white">Pending</span>
<span className="text-sm text-gray-500 dark:text-gray-400">- Payment awaiting processing</span>
</div>
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div className="w-3 h-3 rounded-full bg-red-500" />
<span className="font-medium text-gray-900 dark:text-white">Failed</span>
<span className="text-sm text-gray-500 dark:text-gray-400">- Payment could not be processed</span>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="w-3 h-3 rounded-full bg-gray-500" />
<span className="font-medium text-gray-900 dark:text-white">Refunded</span>
<span className="text-sm text-gray-500 dark:text-gray-400">- Payment has been refunded</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">
<CheckCircle size={20} className="text-brand-500" /> Benefits
</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>Secure:</strong> PCI-compliant payment processing via Stripe</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>Automatic:</strong> Payments linked to appointments automatically</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300"><strong>Reporting:</strong> Detailed financial reports for accounting</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>Flexible:</strong> Support for deposits, full payments, and tips</span>
</li>
</ul>
</div>
</section>
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link to="/help/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">
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<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" />
<span className="text-gray-900 dark:text-white">Billing Settings</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 HelpPayments;

View File

@@ -0,0 +1,272 @@
/**
* Help Plugins Page
*
* User-friendly help documentation for Plugins.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Puzzle,
Store,
Code,
Zap,
Clock,
CheckCircle,
ChevronRight,
HelpCircle,
Play,
Pause,
Settings,
Mail,
BookOpen,
Calendar,
ListTodo,
} from 'lucide-react';
const HelpPlugins: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Puzzle size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Plugins Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Automate your business with powerful plugins
</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Puzzle size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Plugins extend your scheduling platform with automation capabilities. Send reminder emails,
generate reports, track no-shows, and create custom workflows - all running automatically
on schedules you define.
</p>
<p className="text-gray-600 dark:text-gray-300">
Browse the marketplace for ready-made plugins, or create your own custom automations
using our simple scripting language.
</p>
</div>
</section>
{/* Plugin Areas Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" />
Plugin Areas
</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">
<Store size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Marketplace</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse and install pre-built plugins from our library</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Puzzle size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">My Plugins</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Manage your installed and custom plugins</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-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Create Plugin</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Build custom plugins with our scripting tools</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ListTodo size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Tasks</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">View and manage scheduled plugin executions</p>
</div>
</div>
</div>
</div>
</section>
{/* What Plugins Can Do */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Zap size={20} className="text-brand-500" />
What Plugins Can Do
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-4">
<div className="flex items-start gap-3">
<Mail size={18} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Send Emails</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Automated reminders, follow-ups, and notifications to customers</p>
</div>
</div>
<div className="flex items-start gap-3">
<Calendar size={18} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Manage Appointments</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Query, filter, and process appointment data automatically</p>
</div>
</div>
<div className="flex items-start gap-3">
<Clock size={18} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Run on Schedules</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Execute hourly, daily, weekly, or on custom cron schedules</p>
</div>
</div>
<div className="flex items-start gap-3">
<Zap size={18} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">External Integrations</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Connect to approved external APIs for advanced workflows</p>
</div>
</div>
</div>
</div>
</section>
{/* Getting Started */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Play size={20} className="text-brand-500" />
Getting Started
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<ol className="space-y-4">
<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">Browse Marketplace</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Go to Plugins &rarr; Marketplace to explore available plugins.</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">2</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Install a Plugin</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any plugin to add it to your account.</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Configure & Schedule</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Set up plugin options and choose when it should run.</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Monitor Tasks</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">View execution history and logs in the Tasks page.</p>
</div>
</li>
</ol>
</div>
</section>
{/* Task Management */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ListTodo size={20} className="text-brand-500" />
Managing Tasks
</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 install a plugin, it creates a scheduled task that runs automatically. Use the Tasks page to:
</p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2">
<Play size={16} className="text-green-500" />
<span><strong>Run manually:</strong> Execute a task immediately without waiting for the schedule</span>
</li>
<li className="flex items-center gap-2">
<Pause size={16} className="text-yellow-500" />
<span><strong>Pause/Resume:</strong> Temporarily stop a task without deleting it</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-blue-500" />
<span><strong>View logs:</strong> See execution history and any errors</span>
</li>
</ul>
</div>
</section>
{/* Developer Documentation Link */}
<section className="mb-10">
<div className="bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<div className="flex items-start gap-4">
<BookOpen size={24} className="text-brand-600 dark:text-brand-400 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Creating Custom Plugins
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Want to build your own automations? Our comprehensive developer documentation covers
the scripting language, available API methods, and example code.
</p>
<Link
to="/help/plugins/docs"
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
View Developer Docs
<ChevronRight size={16} />
</Link>
</div>
</div>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about plugins.
</p>
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpPlugins;

View File

@@ -0,0 +1,545 @@
/**
* Help Resources Page
*
* Comprehensive help documentation for the Resources management page.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
ClipboardList,
Users,
Building,
Wrench,
Calendar,
Settings,
CheckCircle,
ChevronRight,
HelpCircle,
Clock,
Plus,
Eye,
Pencil,
Search,
Keyboard,
Layers,
ToggleLeft,
AlertCircle,
} from 'lucide-react';
const HelpResources: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} /> Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<ClipboardList size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Resources Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Manage staff, rooms, and equipment for scheduling</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ClipboardList 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">
Resources are the bookable entities in your scheduling system - staff members, rooms, equipment, or any other asset that needs time-slot management. Every appointment in your system is assigned to a resource.
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Resources page displays all your resources in a table format with their type, upcoming appointment counts, capacity settings, and status. From here you can create new resources, edit existing ones, and view individual resource calendars.
</p>
<p className="text-gray-600 dark:text-gray-300">
Properly configured resources are essential for preventing double-booking and ensuring your scheduler displays availability correctly.
</p>
</div>
</section>
{/* Resource Types Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" /> Resource Types
</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">
SmoothSchedule supports three built-in resource types, each with an associated icon and color coding:
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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">
<Users size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Staff</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Team members who provide services - stylists, therapists, consultants, doctors, etc. Can be linked to a staff user account.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<Building size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Room</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Physical spaces - consultation rooms, massage rooms, studios, courts, meeting rooms, etc.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<Wrench size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Equipment</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Tools and machinery - laser machines, MRI scanners, rental equipment, vehicles, etc.
</p>
</div>
</div>
</div>
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-300">
<strong>Custom Resource Types:</strong> You can create additional resource types in Settings &gt; Resource Types to match your specific business needs (e.g., "Vehicle", "Studio", "Instructor").
</p>
</div>
</div>
</section>
{/* The Resources Table Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ClipboardList size={20} className="text-brand-500" /> The Resources Table
</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 main view shows all resources in a table with the following columns:
</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="w-8 h-8 rounded-lg bg-gray-200 dark:bg-gray-600 flex items-center justify-center shrink-0">
<Users size={16} className="text-gray-500" />
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Resource Name</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The name and type icon of the resource. The icon indicates whether it's Staff (blue person), Room (green building), or Equipment (purple wrench).
</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-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center shrink-0">
<span className="text-xs font-medium text-blue-800 dark:text-blue-300">Type</span>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Type</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
A color-coded badge showing the resource type (Staff, Room, or Equipment).
</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-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Upcoming</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The count of future appointments assigned to this resource. Helps you see at a glance how busy each resource is.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Layers size={20} className="text-amber-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Capacity</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Shows "1 at a time" for single-booking resources or "X simultaneous" for multilane resources that can handle multiple concurrent appointments.
</p>
</div>
</div>
<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 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Status</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Indicates whether the resource is active and available for scheduling.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Settings size={20} className="text-gray-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Actions</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
View Calendar button to see the resource's schedule, and an Edit button to modify resource settings.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Creating a Resource Section */}
<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-brand-500" /> Creating a Resource
</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 <strong>"Add Resource"</strong> button in the top right to open the creation form:
</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">Select Resource Type</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Choose Staff, Room, or Equipment from the dropdown. This determines the icon and how the resource behaves.
</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">Link Staff Member (Staff type only)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
For Staff resources, search and select a staff user from your team. This links the resource to their user account. Type to search by name or email, use arrow keys to navigate, and press Enter to select.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Enter Resource Name</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Give the resource a descriptive name that will appear in the scheduler and booking interface (e.g., "Sarah (Stylist)", "Massage Room 1", "Laser Machine").
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Add Description (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add notes about the resource for internal reference.
</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">5</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Configure Multilane Mode (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Toggle on to allow multiple simultaneous bookings. Set the number of lanes (2-10) for resources like group classes or shared equipment.
</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">6</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Save</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Create Resource" to add the resource. It will appear immediately in the table and be available in the scheduler.
</p>
</div>
</li>
</ol>
</div>
</section>
{/* Staff Autocomplete Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Search size={20} className="text-brand-500" /> Staff Member Autocomplete
</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 creating a Staff resource, you'll use an autocomplete field to link the resource to a staff user account:
</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">
<Search size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Search</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Start typing to filter staff members by name or email. Results appear in a dropdown below the input.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Keyboard size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Keyboard Navigation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Use <strong>↑/↓</strong> arrow keys to highlight options, <strong>Enter</strong> to select, and <strong>Escape</strong> to close the dropdown.
</p>
</div>
</div>
<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 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Selection Confirmation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Once selected, a green indicator shows "Selected: [Name]" below the input field confirming your choice.
</p>
</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">
Staff must first be added via the Staff page before they can be linked to a resource. If you don't see someone in the autocomplete, make sure they have a staff account.
</p>
</div>
</div>
</div>
</section>
{/* Multilane Mode Section */}
<section className="mb-10">
<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" /> Multilane 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">
Multilane mode allows a single resource to handle multiple appointments at the same time. This is useful for:
</p>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Group fitness instructors who can teach multiple clients</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Rooms with multiple stations (salon chairs, dental chairs)</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Equipment that can be shared (3D printers, charging stations)</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Classes with limited capacity (yoga classes, workshops)</span>
</li>
</ul>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ToggleLeft size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Enable Multilane</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Toggle the "Multilane Mode" switch in the resource form. When enabled, a number input appears.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Layers size={20} className="text-amber-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Set Lane Count</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Enter the number of simultaneous appointments allowed (2-10). For example, set to 5 for a yoga class with 5 spots.
</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">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>In the Scheduler:</strong> When viewing a multilane resource, overlapping appointments appear in separate horizontal lanes, making it easy to see all concurrent bookings.
</p>
</div>
</div>
</section>
{/* Viewing a Resource Calendar Section */}
<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" /> Viewing a Resource Calendar
</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 <strong>"View Calendar"</strong> button on any resource row to open a dedicated calendar view for that resource:
</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">
<Calendar size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Focused View</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
See only the appointments assigned to this specific resource, without clutter from other resources.
</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-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Date Navigation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Navigate between weeks to see past and future appointments for planning and review.
</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-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Appointment Details</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click on any appointment in the calendar to view its full details.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Editing a Resource Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Pencil size={20} className="text-brand-500" /> Editing a Resource
</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 pencil icon in the Actions column to edit a resource. The edit form pre-fills with the current values.
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
You can change:
</p>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Resource type (Staff, Room, Equipment)</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Linked staff member (for Staff resources)</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Resource name and description</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300">Multilane settings and lane count</span>
</li>
</ul>
<div className="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">
Changing a resource's type or linked staff member may affect how existing appointments display. The appointments themselves remain intact.
</p>
</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>
<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>Prevent Double-Booking:</strong> Resources automatically block conflicting time slots
</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>Accurate Availability:</strong> Customers only see times when resources are actually free
</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>Flexible Capacity:</strong> Multilane mode supports group bookings and shared resources
</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>Staff Integration:</strong> Link resources to staff accounts for proper attribution and filtering
</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>Quick Visibility:</strong> Upcoming appointment counts show resource utilization at a glance
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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"
>
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</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"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Staff Management</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/resource-types"
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">Resource Types 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"
>
<ClipboardList 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>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about managing resources.
</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 HelpResources;

View File

@@ -0,0 +1,644 @@
/**
* Help Scheduler Page
*
* Comprehensive help documentation for the Scheduler.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
CalendarDays,
Clock,
Users,
Filter,
Plus,
Edit,
Trash2,
CheckCircle,
ChevronRight,
HelpCircle,
Eye,
Calendar,
Grid,
Move,
GripVertical,
Undo,
Redo,
ZoomIn,
ZoomOut,
Archive,
AlertCircle,
Inbox,
MousePointer,
Keyboard,
Layers,
} from 'lucide-react';
const HelpScheduler: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<CalendarDays size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Scheduler Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Master your appointment calendar with drag-and-drop scheduling
</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CalendarDays size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Scheduler is a powerful, interactive calendar designed for fast appointment management.
It features full drag-and-drop support, real-time updates via WebSocket, and a horizontal timeline
layout that shows all your resources (staff, rooms, equipment) at once.
</p>
<p className="text-gray-600 dark:text-gray-300">
You can reschedule appointments by dragging them, resize them by pulling their edges,
filter by status/resource/service, and even undo mistakes with keyboard shortcuts.
Pending appointment requests appear in a sidebar, ready to be dragged onto the schedule.
</p>
</div>
</section>
{/* Calendar Views Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Grid size={20} className="text-brand-500" />
Calendar Views
</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">
<Calendar size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Day View</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
See a single day in detail with a horizontal timeline. Each resource gets its own row.
The timeline scrolls horizontally and auto-centers on the current time when you open it.
Perfect for managing a busy day with many appointments across multiple staff or rooms.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Grid size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Week View</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
View all 7 days side-by-side with each day separated on the timeline.
Drag appointments between days or across resources. Great for weekly planning
and seeing patterns in your schedule.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<CalendarDays size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Month View</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
Traditional calendar grid showing the entire month. Each day cell shows a summary of
appointments for that day, color-coded by status.
</p>
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1 ml-4">
<li><strong>Click a day</strong> to jump directly to that day's detailed Day View</li>
<li><strong>Drag an appointment</strong> over a day and hover for 1 second - a mini Day View scheduler overlay opens where you can drop it at the exact time and resource you want</li>
<li>The overlay shows hour slots and resource columns, just like Day View, for precise placement</li>
</ul>
</div>
</div>
</div>
</div>
</section>
{/* Drag and Drop Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Move size={20} className="text-brand-500" />
Drag and Drop
</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">
Drag-and-drop is the fastest way to manage your schedule. Simply click and drag any
appointment to reschedule it.
</p>
<div className="space-y-4">
<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">
<GripVertical size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Reschedule Appointments</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag an appointment left or right to change its time, or up/down to move it to a
different resource. The appointment snaps to 15-minute increments. A ghost preview
shows where it will land.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<Inbox size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Schedule Pending Requests</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Pending appointments (from online booking or manual entry) appear in the sidebar on
the left. Drag them onto the timeline to schedule them - they'll automatically be
confirmed. The pending sidebar shows customer name, service, and requested duration.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<Undo size={20} className="text-yellow-600 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Return to Pending</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag a scheduled appointment back to the pending sidebar to unschedule it.
The appointment returns to pending status and can be rescheduled later.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<Archive size={20} className="text-red-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Archive (Delete)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag an appointment to the archive zone at the bottom of the pending sidebar to
delete it permanently. This is useful for removing cancelled or test appointments.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Resizing Appointments Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<MousePointer size={20} className="text-brand-500" />
Resizing Appointments
</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">
Change an appointment's duration by dragging its edges. This is helpful when a service
takes more or less time than expected.
</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">
<ChevronRight size={20} className="text-brand-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Extend Duration</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Hover over the right edge of an appointment until you see the resize cursor,
then drag right to make it longer. Duration snaps to 15-minute increments.
Minimum duration is 15 minutes.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ChevronRight size={20} className="text-brand-500 mt-0.5 rotate-180" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Change Start Time</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag the left edge to change when the appointment starts. Dragging left makes
it start earlier (and longer), dragging right makes it start later (and shorter).
</p>
</div>
</div>
</div>
</div>
</section>
{/* Status Colors Section */}
<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" />
Status 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">
Appointments are color-coded so you can see their status at a glance:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center gap-3 p-3 bg-blue-100 dark:bg-blue-900/50 border-l-4 border-blue-500 rounded">
<span className="font-medium text-blue-900 dark:text-blue-200">Blue - Upcoming</span>
<span className="text-sm text-blue-700 dark:text-blue-300">Confirmed, hasn't started yet</span>
</div>
<div className="flex items-center gap-3 p-3 bg-yellow-100 dark:bg-yellow-900/50 border-l-4 border-yellow-500 rounded">
<span className="font-medium text-yellow-900 dark:text-yellow-200">Yellow - In Progress</span>
<span className="text-sm text-yellow-700 dark:text-yellow-300">Currently happening</span>
</div>
<div className="flex items-center gap-3 p-3 bg-red-100 dark:bg-red-900/50 border-l-4 border-red-500 rounded">
<span className="font-medium text-red-900 dark:text-red-200">Red - Overdue</span>
<span className="text-sm text-red-700 dark:text-red-300">Past end time, not completed</span>
</div>
<div className="flex items-center gap-3 p-3 bg-green-100 dark:bg-green-900/50 border-l-4 border-green-500 rounded">
<span className="font-medium text-green-900 dark:text-green-200">Green - Completed</span>
<span className="text-sm text-green-700 dark:text-green-300">Successfully finished</span>
</div>
<div className="flex items-center gap-3 p-3 bg-orange-100 dark:bg-orange-900/50 border-l-4 border-orange-500 rounded">
<span className="font-medium text-orange-900 dark:text-orange-200">Orange - No-show</span>
<span className="text-sm text-orange-700 dark:text-orange-300">Customer didn't arrive</span>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-100 dark:bg-gray-700 border-l-4 border-gray-400 rounded opacity-75">
<span className="font-medium text-gray-500 dark:text-gray-400">Gray (faded) - Cancelled</span>
<span className="text-sm text-gray-500 dark:text-gray-500">Appointment was cancelled</span>
</div>
</div>
</div>
</section>
{/* Filtering Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Filter size={20} className="text-brand-500" />
Filtering Appointments
</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 the filter button in the toolbar to narrow down what you see:
</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">
<AlertCircle size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Filter by Status</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Show or hide appointments by status (Pending, Confirmed, Completed, Cancelled, No-show).
Useful for focusing on confirmed appointments only or finding no-shows.
</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-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Filter by Resource</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Show only specific staff members, rooms, or equipment. Great for viewing one
person's schedule or a specific treatment room.
</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-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Filter by Service</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Show only appointments for specific services. Helpful for tracking how often
certain services are booked or finding all massage appointments.
</p>
</div>
</div>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
A badge appears on the filter button when filters are active. Click "Clear All" to reset.
</p>
</div>
</section>
{/* Keyboard Shortcuts Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Keyboard size={20} className="text-brand-500" />
Keyboard Shortcuts
</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-center gap-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex gap-1">
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Ctrl</kbd>
<span>+</span>
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Z</kbd>
</div>
<div>
<span className="font-medium text-gray-900 dark:text-white">Undo</span>
<p className="text-xs text-gray-500 dark:text-gray-400">Undo the last move or resize</p>
</div>
</div>
<div className="flex items-center gap-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex gap-1">
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Ctrl</kbd>
<span>+</span>
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Y</kbd>
</div>
<div>
<span className="font-medium text-gray-900 dark:text-white">Redo</span>
<p className="text-xs text-gray-500 dark:text-gray-400">Redo the last undone action</p>
</div>
</div>
<div className="flex items-center gap-4 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex gap-1">
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Ctrl</kbd>
<span>+</span>
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Shift</kbd>
<span>+</span>
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded text-xs font-mono">Z</kbd>
</div>
<div>
<span className="font-medium text-gray-900 dark:text-white">Redo (Alt)</span>
<p className="text-xs text-gray-500 dark:text-gray-400">Alternative redo shortcut</p>
</div>
</div>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
The scheduler keeps a history of up to 50 actions, so you can undo multiple changes.
</p>
</div>
</section>
{/* Zoom Controls Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ZoomIn size={20} className="text-brand-500" />
Zoom Controls
</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">
Adjust the zoom level to see more or less detail in the timeline (Day and Week views only):
</p>
<div className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2">
<ZoomOut size={20} className="text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">Zoom out to see more hours at once</span>
</div>
<div className="flex items-center gap-2">
<ZoomIn size={20} className="text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">Zoom in for precise time placement</span>
</div>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
Use the zoom slider in the toolbar to adjust. The timeline width scales proportionally.
</p>
</div>
</section>
{/* Overlapping Appointments Section */}
<section className="mb-10">
<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" />
Overlapping Appointments
</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 appointments overlap in time for the same resource, the scheduler automatically
stacks them in multiple "lanes" so you can see all of them:
</p>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300">
Each resource row expands vertically to accommodate overlapping appointments
</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-600 dark:text-gray-300">
Overlapping appointments are shown stacked, with each in its own horizontal lane
</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-600 dark:text-gray-300">
This helps identify double-bookings or intentional concurrent appointments
</span>
</li>
</ul>
</div>
</section>
{/* Pending Sidebar Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Inbox size={20} className="text-brand-500" />
Pending Appointments Sidebar
</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 sidebar on the left shows all pending (unscheduled) appointment requests:
</p>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300">
<strong>Online Bookings:</strong> When customers book online, their requests appear here until you assign them to a resource
</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-600 dark:text-gray-300">
<strong>Drag to Schedule:</strong> Drag any pending appointment onto the timeline to schedule it with a specific resource
</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-600 dark:text-gray-300">
<strong>Auto-Confirm:</strong> Pending appointments automatically change to "Confirmed" when scheduled
</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-600 dark:text-gray-300">
<strong>Archive Zone:</strong> Drag to the archive section at the bottom to delete unwanted requests
</span>
</li>
</ul>
</div>
</section>
{/* Real-time Updates Section */}
<section className="mb-10">
<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" />
Real-time Updates
</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 scheduler uses WebSocket connections to update in real-time:
</p>
<ul className="space-y-2">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300">
When a customer books online, the appointment appears instantly
</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-600 dark:text-gray-300">
When another staff member makes changes, you see them without refreshing
</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-600 dark:text-gray-300">
No need to manually refresh the page - the schedule stays current
</span>
</li>
</ul>
</div>
</section>
{/* Editing Appointments Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Edit size={20} className="text-brand-500" />
Editing Appointment Details
</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 on any appointment to open its detail modal where you can:
</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">
<Clock size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Change Date & Time</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Use the date/time picker for precise scheduling
</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">Reassign Resource</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Move the appointment to a different staff member or room
</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-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Adjust Duration</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Change how long the appointment lasts
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<CheckCircle size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Change Status</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Use the status dropdown to change between Pending, Confirmed, Completed, No-show, or Cancelled
</p>
</div>
</div>
</div>
</div>
</section>
{/* Tips Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<HelpCircle size={20} className="text-brand-500" />
Pro Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Use filters to focus:</strong> When the schedule is busy, filter to show only what you need
</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>Ctrl+Z is your friend:</strong> Made a mistake? Undo it instantly without finding and fixing manually
</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>Month view drag overlay:</strong> While dragging in month view, hover over any day for 1 second to open a mini Day View scheduler - drop the appointment at the exact time and resource
</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>Check red appointments:</strong> Red means overdue - these need to be completed or rescheduled
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<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"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Resources Guide</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"
>
<Clock 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/customers"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
If you have questions that aren't covered here, 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>
</div>
);
};
export default HelpScheduler;

View File

@@ -0,0 +1,528 @@
/**
* Help Services Page
*
* Comprehensive help documentation for the Services management page.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Briefcase,
Clock,
DollarSign,
Users,
Settings,
CheckCircle,
ChevronRight,
HelpCircle,
Tag,
Plus,
Pencil,
Trash2,
GripVertical,
Eye,
Image,
ImagePlus,
Upload,
AlertCircle,
LayoutGrid,
} from 'lucide-react';
const HelpServices: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} /> Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Briefcase size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Services Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Define and manage the services your business offers</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Briefcase 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">
Services are what you offer to your customers - haircuts, massages, consultations, lessons, repairs, or any bookable activity. Each service has a name, duration, price, optional description, and photo gallery.
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Services page features a two-column layout: an editable list on the left where you can reorder services by dragging, and a live Customer Preview on the right showing exactly how your services appear in the booking interface.
</p>
<p className="text-gray-600 dark:text-gray-300">
Well-defined services make booking seamless for customers and ensure consistent pricing and time allocation for every appointment.
</p>
</div>
</section>
{/* Page Layout Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<LayoutGrid size={20} className="text-brand-500" /> Page Layout
</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 Services page is divided into two columns for efficient management:
</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">
<GripVertical size={20} className="text-brand-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Left: Editable Services List</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Your services in a draggable list. Reorder by dragging, edit or delete with action buttons. Shows name, description, duration, price, and photo count.
</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-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Right: Customer Preview</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
A live mockup showing how customers see your services in the booking widget. Updates instantly as you reorder.
</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">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Tip:</strong> The order you see in the editable list is exactly how services appear to customers when booking. Drag your most popular services to the top!
</p>
</div>
</div>
</section>
{/* Service Properties Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" /> Service Properties
</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 service has the following properties:
</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">
<Tag size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Name <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
A clear, descriptive name customers will see (e.g., "Haircut", "60-Minute Massage", "Initial Consultation"). Required.
</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-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Duration <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
How long the service takes in minutes (minimum 5, in 5-minute increments). This determines the time block reserved in the scheduler.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<DollarSign size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Price <span className="text-red-500">*</span></h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The base price for this service displayed to customers. You can override the price on individual appointments if needed.
</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-orange-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Description</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Optional text describing what's included, what to expect, or any special notes. Shown to customers in the booking flow.
</p>
</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-pink-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Photos</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Optional images showcasing the service. Multiple photos supported, reorderable. The first photo is the primary/featured image.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Drag and Drop Reordering Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<GripVertical size={20} className="text-brand-500" /> Drag and Drop Reordering
</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 the display order of your services by dragging and dropping:
</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">Grab the Handle</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click and hold the grip icon (⋮⋮) on the left side of any service card.
</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">Drag to New Position</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Move the service up or down in the list. Visual feedback shows where it will be placed.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Release to Save</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drop the service and the new order is saved automatically. The Customer Preview updates instantly.
</p>
</div>
</li>
</ol>
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<p className="text-sm text-green-800 dark:text-green-200">
<strong>Visual Feedback:</strong> When dragging, the service being moved becomes semi-transparent, and the drop target shows a highlighted border.
</p>
</div>
</div>
</section>
{/* Creating a Service Section */}
<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-brand-500" /> Creating a Service
</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 <strong>"Add Service"</strong> button in the top right to open the creation form:
</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">Enter Service Name</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Give your service a clear name (e.g., "Haircut", "Massage", "Consultation").
</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">Set Duration</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Enter how many minutes the service takes. The scheduler will block this amount of time.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Set Price</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Enter the price customers pay for this service.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Add Description (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Describe what's included or what customers should know.
</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">5</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Upload Photos (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add images to showcase the service. See the Photo Gallery section below for details.
</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">6</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Create</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Create" to add the service. It appears at the end of the list (drag to reorder).
</p>
</div>
</li>
</ol>
</div>
</section>
{/* Photo Gallery Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ImagePlus size={20} className="text-brand-500" /> Photo Gallery
</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 service can have multiple photos to showcase what customers will receive:
</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">
<Upload size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Upload Methods</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
<strong>Drag and drop</strong> images directly onto the drop zone, or click <strong>"browse files"</strong> to select from your computer. Multiple images can be uploaded at once.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<GripVertical size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Reorder Photos</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Drag photos to change their order. The first photo (position 1) is the primary/featured image shown in the service listing.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Trash2 size={20} className="text-red-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Delete Photos</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Hover over a photo and click the red trash button to remove it.
</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 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Photo Count Display</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
In the services list, a small image icon with count shows how many photos each service has.
</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">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Tip:</strong> Use high-quality photos that showcase your work. The first photo is what customers see first when browsing services!
</p>
</div>
</div>
</section>
{/* Editing and Deleting Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Pencil size={20} className="text-brand-500" /> Editing and Deleting
</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">
<Pencil size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Edit a Service</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the pencil icon on any service card to open the edit form. All fields are pre-filled with current values. Update what you need and click "Save".
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Trash2 size={20} className="text-red-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Delete a Service</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the trash icon on a service card. A confirmation dialog appears - click confirm to permanently delete the service.
</p>
</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>Note:</strong> Deleting a service does not affect existing appointments that used that service. Those appointments retain their data.
</p>
</div>
</div>
</div>
</section>
{/* Customer Preview Section */}
<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" /> Customer Preview
</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 right side of the Services page shows a live preview of how your services appear to customers:
</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">
<Eye size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Live Preview</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The preview updates instantly when you reorder services by dragging. See exactly how the booking widget will look.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<LayoutGrid size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Booking Widget Style</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Styled like the actual booking interface with a branded header, service cards showing name, description, duration, and price.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ChevronRight size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Selection Indicator</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Each service shows a chevron () indicating it's clickable. The preview is visual only - clicking doesn't navigate.
</p>
</div>
</div>
</div>
<div className="mt-4 p-4 bg-gray-100 dark:bg-gray-700/50 rounded-lg text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">
Preview only - not clickable
</p>
</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>
<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>Clear Offerings:</strong> Customers see exactly what services you provide with pricing upfront
</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>Consistent Pricing:</strong> Set standard prices that apply automatically to new appointments
</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>Accurate Scheduling:</strong> Duration settings ensure the right amount of time is allocated
</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>Custom Order:</strong> Drag to prioritize popular services at the top of customer-facing lists
</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>Visual Appeal:</strong> Photo galleries showcase your work and attract bookings
</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>Live Preview:</strong> See exactly how customers experience your service catalog
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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"
>
<Clock size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</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"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Resources Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/customers"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/booking"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Settings 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>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about managing services.
</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 HelpServices;

View File

@@ -0,0 +1,395 @@
/**
* Help Settings API Page
*
* Documentation for managing API tokens for third-party integrations.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Key,
Shield,
Plus,
Copy,
Trash2,
Clock,
Eye,
EyeOff,
CheckCircle,
ChevronRight,
HelpCircle,
AlertTriangle,
Lock,
Settings,
} from 'lucide-react';
const HelpSettingsApi: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<Key size={24} className="text-amber-600 dark:text-amber-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
API Settings Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Manage API tokens for integrations
</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Key size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
API Settings allows you to create and manage API tokens for integrating your scheduling
system with external applications. These tokens provide secure, controlled access to your
business data through our REST API.
</p>
<div className="flex items-start gap-2 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<Lock size={18} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-800 dark:text-amber-200">
API access requires a plan with the <strong>api_access</strong> feature. Only the business
owner can manage API tokens.
</p>
</div>
</div>
</section>
{/* Creating API Tokens */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Plus size={20} className="text-brand-500" />
Creating API Tokens
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Click the <strong>New Token</strong> button to create a new API token. You'll need to configure:
</p>
<div className="space-y-4">
{/* Token Name */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Token Name</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Give your token a descriptive name to identify its purpose, like "Website Integration"
or "Mobile App".
</p>
</div>
{/* Permission Presets */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Permission Presets</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
Choose from predefined permission sets:
</p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-start gap-2">
<Eye size={14} className="text-blue-500 mt-1 flex-shrink-0" />
<span><strong>Read Only</strong> - Can only view data, no modifications</span>
</li>
<li className="flex items-start gap-2">
<Shield size={14} className="text-green-500 mt-1 flex-shrink-0" />
<span><strong>Full Access</strong> - Complete read and write access</span>
</li>
<li className="flex items-start gap-2">
<Settings size={14} className="text-purple-500 mt-1 flex-shrink-0" />
<span><strong>Custom</strong> - Select individual permissions</span>
</li>
</ul>
</div>
{/* Individual Permissions */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Individual Permissions</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Expand "Show individual permissions" for granular control over what the token can access:
appointments, customers, services, resources, and more.
</p>
</div>
{/* Expiration */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Clock size={16} className="text-orange-500" />
Expiration
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
Set when the token should expire:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-300 grid grid-cols-2 gap-2">
<li>Never expires</li>
<li>7 days</li>
<li>30 days</li>
<li>90 days</li>
<li>1 year</li>
</ul>
</div>
</div>
</div>
</section>
{/* Token Security */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<AlertTriangle size={20} className="text-brand-500" />
Token Security
</h2>
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 rounded-xl border border-yellow-200 dark:border-yellow-800 p-6">
<div className="flex items-start gap-3 mb-4">
<AlertTriangle size={24} className="text-yellow-600 dark:text-yellow-400 flex-shrink-0" />
<div>
<h4 className="font-medium text-yellow-800 dark:text-yellow-200 mb-1">
Important: Copy Your Token Immediately
</h4>
<p className="text-sm text-yellow-700 dark:text-yellow-300">
When a token is created, you'll see the full token <strong>only once</strong>. Copy it
immediately and store it securely. You will not be able to view it again.
</p>
</div>
</div>
<div className="space-y-2 text-sm text-yellow-800 dark:text-yellow-200">
<p className="flex items-center gap-2">
<Copy size={14} className="flex-shrink-0" />
Use the Copy button to copy the token to your clipboard
</p>
<p className="flex items-center gap-2">
<EyeOff size={14} className="flex-shrink-0" />
Toggle visibility with the eye icon to verify the token
</p>
</div>
</div>
</section>
{/* Managing Tokens */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Shield size={20} className="text-brand-500" />
Managing Existing Tokens
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Your tokens are organized into <strong>Active Tokens</strong> and <strong>Revoked Tokens</strong>.
Click on any token to expand and view its details.
</p>
<div className="space-y-4">
{/* Token Information */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Token Information</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
Each token displays:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 ml-4 list-disc">
<li>Token name and status (Active, Expired, or Revoked)</li>
<li>Key prefix (first few characters of the token)</li>
<li>Created date and who created it</li>
<li>Last used date</li>
<li>Expiration date</li>
<li>Assigned permissions</li>
</ul>
</div>
{/* Revoking Tokens */}
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<h4 className="font-medium text-red-800 dark:text-red-200 mb-2 flex items-center gap-2">
<Trash2 size={16} />
Revoking Tokens
</h4>
<p className="text-sm text-red-700 dark:text-red-300 mb-2">
Click the trash icon to revoke a token. This action:
</p>
<ul className="text-sm text-red-700 dark:text-red-300 space-y-1 ml-4 list-disc">
<li><strong>Cannot be undone</strong></li>
<li>Immediately disables API access for that token</li>
<li>Any applications using the token will lose access instantly</li>
</ul>
</div>
</div>
</div>
</section>
{/* Best Practices */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" />
Best Practices
</h2>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>One token per integration</strong> - Create separate tokens for each application
or service that needs API access
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Minimum permissions</strong> - Grant only the permissions each integration needs
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Set expiration dates</strong> - Use expiring tokens for temporary integrations
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Never share tokens publicly</strong> - Don't commit tokens to version control
or share them in public channels
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Rotate tokens regularly</strong> - Create new tokens and revoke old ones periodically
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Monitor last used dates</strong> - Revoke tokens that haven't been used recently
</span>
</li>
</ul>
</div>
</section>
{/* Step by Step */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
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 &gt; API Settings</strong>
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
2
</span>
<span className="text-gray-700 dark:text-gray-300">
Click the <strong>New Token</strong> button
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
3
</span>
<span className="text-gray-700 dark:text-gray-300">
Enter a descriptive <strong>Token Name</strong> (e.g., "Website Booking Integration")
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
4
</span>
<span className="text-gray-700 dark:text-gray-300">
Select a <strong>Permission Preset</strong> or customize individual permissions
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
5
</span>
<span className="text-gray-700 dark:text-gray-300">
Choose an <strong>Expiration</strong> period if desired
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
6
</span>
<span className="text-gray-700 dark:text-gray-300">
Click <strong>Create Token</strong>
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
7
</span>
<span className="text-gray-700 dark:text-gray-300">
<strong>Immediately copy the token</strong> and store it securely - you won't see it again!
</span>
</li>
</ol>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/help/settings/auth"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Shield size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Authentication Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/plugins"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Settings size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Plugins Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with API integration questions.
</p>
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpSettingsApi;

View File

@@ -0,0 +1,388 @@
/**
* Help Settings Appearance (Branding) Page
*
* Comprehensive documentation for the Branding Settings page.
* Documents: Logo uploads (website/email), display modes, color palettes, custom colors with live preview.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, Palette, Image, Upload, Eye, Save,
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Sparkles,
} from 'lucide-react';
const HelpSettingsAppearance: 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>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<Palette size={24} className="text-purple-600 dark:text-purple-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Branding Settings Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Customize your logos and brand colors</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Palette size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Branding Settings page lets you customize how your business appears throughout SmoothSchedule.
Upload your logos, choose from preset color palettes, or define custom brand colors. Changes
preview instantly so you can see exactly how they'll look before saving.
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Owner Access Required:</strong> Only business owners can modify branding settings.
This feature may be limited based on your subscription plan.
</p>
</div>
</div>
</div>
</section>
{/* Brand Logos */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Image size={20} className="text-brand-500" /> Brand Logos
</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">
You can upload two different logos for different contexts. PNG images with transparent
backgrounds are recommended for the best appearance across light and dark themes.
</p>
<div className="space-y-6">
{/* Website Logo */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Image size={18} className="text-blue-500" /> Website Logo
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
This logo appears in the sidebar navigation and on customer-facing booking pages.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700 dark:text-gray-300">Recommended Size:</span>
<span className="text-gray-500 dark:text-gray-400 ml-2">500×500 pixels</span>
</div>
<div>
<span className="font-medium text-gray-700 dark:text-gray-300">Supported Formats:</span>
<span className="text-gray-500 dark:text-gray-400 ml-2">PNG, JPEG, SVG</span>
</div>
</div>
</div>
{/* Email Logo */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Image size={18} className="text-green-500" /> Email Logo
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
This logo appears at the top of all email notifications sent to customers (booking
confirmations, reminders, etc.). A wider format works best for email headers.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700 dark:text-gray-300">Recommended Size:</span>
<span className="text-gray-500 dark:text-gray-400 ml-2">600×200 pixels (wide)</span>
</div>
<div>
<span className="font-medium text-gray-700 dark:text-gray-300">Supported Formats:</span>
<span className="text-gray-500 dark:text-gray-400 ml-2">PNG, JPEG, SVG</span>
</div>
</div>
</div>
{/* Upload Instructions */}
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Uploading a Logo</h4>
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
<li>Click the <strong>Upload</strong> button next to the logo type</li>
<li>Select an image file from your computer</li>
<li>The logo preview will update immediately</li>
<li>Click <strong>Save Changes</strong> to apply the logo</li>
</ol>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-3">
To remove a logo, click the × button on the preview image, then save.
</p>
</div>
</div>
</div>
</section>
{/* Logo Display Mode */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Eye size={20} className="text-brand-500" /> Logo Display Mode
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Control how your website logo is displayed in the sidebar and on booking pages.
This setting only affects the website logo, not the email logo.
</p>
<div className="space-y-3">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="w-8 h-8 rounded bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold text-gray-500 dark:text-gray-400">
Aa
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Text Only</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Display only your business name in text. Good if you don't have a logo or prefer a clean look.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="w-8 h-8 rounded bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center">
<Image size={16} className="text-brand-500" />
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Logo Only</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Display only your uploaded logo image without text. Best for recognizable brand marks.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="w-8 h-8 rounded bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center">
<div className="flex items-center gap-1">
<Image size={12} className="text-brand-500" />
<span className="text-[8px] font-bold text-brand-600">+</span>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Logo and Text</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Display both your logo and business name together. Ideal for full brand recognition.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Color Palettes */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Palette size={20} className="text-purple-500" /> Color Palettes
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Choose from 10 professionally designed color palettes. Each palette includes a primary
and secondary color that work well together. Click any palette to instantly preview how
it looks throughout the interface.
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
{[
{ name: 'Ocean Blue', colors: ['#2563eb', '#0ea5e9'] },
{ name: 'Sky Blue', colors: ['#0ea5e9', '#38bdf8'] },
{ name: 'Mint Green', colors: ['#10b981', '#34d399'] },
{ name: 'Coral Reef', colors: ['#f97316', '#fb923c'] },
{ name: 'Lavender', colors: ['#a78bfa', '#c4b5fd'] },
{ name: 'Rose Pink', colors: ['#ec4899', '#f472b6'] },
{ name: 'Forest Green', colors: ['#059669', '#10b981'] },
{ name: 'Royal Purple', colors: ['#7c3aed', '#a78bfa'] },
{ name: 'Slate Gray', colors: ['#475569', '#64748b'] },
{ name: 'Crimson Red', colors: ['#dc2626', '#ef4444'] },
].map((palette) => (
<div key={palette.name} className="text-center">
<div
className="h-6 rounded-md mb-1"
style={{ background: `linear-gradient(to right, ${palette.colors[0]}, ${palette.colors[1]})` }}
/>
<span className="text-xs text-gray-600 dark:text-gray-400">{palette.name}</span>
</div>
))}
</div>
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div className="flex items-start gap-3">
<Sparkles size={18} className="text-yellow-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Live Preview</h4>
<p className="text-xs text-gray-600 dark:text-gray-400">
When you click a palette, the interface instantly updates to show those colors.
If you navigate away without saving, colors revert to your saved settings.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Custom Colors */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Palette size={20} className="text-brand-500" /> Custom Colors
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
If the preset palettes don't match your brand, you can define your own colors using
the color picker or by entering hex color codes directly.
</p>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Primary Color</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Used for buttons, links, and interactive elements. This is your main brand color.
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Secondary Color</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Used for accents, hover states, and gradients. Should complement the primary color.
</p>
</div>
</div>
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Setting Custom Colors</h4>
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
<li>Click the color swatch to open the color picker</li>
<li>Select a color visually or enter a hex code (e.g., #3b82f6)</li>
<li>The preview bar shows the gradient of your primary and secondary colors</li>
<li>Click <strong>Save Changes</strong> to apply your custom colors</li>
</ol>
</div>
</div>
</div>
</section>
{/* Saving Changes */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Save size={20} className="text-brand-500" /> Saving Changes
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
All branding changes (logos, display mode, and colors) are saved together when you
click the Save Changes button at the bottom of the page.
</p>
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<CheckCircle size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Changes Save Together</h4>
<p className="text-xs text-gray-600 dark:text-gray-400">
Logos, display mode, and colors are all saved in one action
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<AlertCircle size={18} className="text-yellow-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Unsaved Changes Revert</h4>
<p className="text-xs text-gray-600 dark:text-gray-400">
If you leave the page without saving, colors revert to your last saved settings
</p>
</div>
</div>
</div>
</div>
</section>
{/* Tips */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" /> Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Use PNG images with transparent backgrounds for logos to look best on both light and dark themes
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Test color readability by previewing your booking page in both light and dark modes
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Choose colors that provide good contrast for text readability and accessibility
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Consider using your existing brand colors from your website or marketing materials
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Eye size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">General Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/booking" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Upload size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Booking Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/email" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Image size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Email Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/domains" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Palette size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Custom Domains</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any branding questions.
</p>
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpSettingsAppearance;

View File

@@ -0,0 +1,378 @@
/**
* Help Settings Authentication Page
*
* Documentation for configuring OAuth providers and social login for customers.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Lock,
Users,
Key,
CheckCircle,
ChevronRight,
HelpCircle,
Save,
Eye,
EyeOff,
AlertCircle,
Check,
} from 'lucide-react';
const HelpSettingsAuth: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<Lock size={24} className="text-purple-600 dark:text-purple-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Authentication Settings Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Configure social login for customers
</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Lock size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Authentication Settings allows you to configure how customers can sign in to your booking
system. Enable social login providers like Google, Apple, and Facebook to make it easier
for customers to create accounts and book appointments.
</p>
<div className="flex items-start gap-2 p-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
<Lock size={18} className="text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-purple-800 dark:text-purple-200">
OAuth settings require a plan with the <strong>custom_oauth</strong> feature. Only the
business owner can configure authentication settings.
</p>
</div>
</div>
</section>
{/* Social Login */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Users size={20} className="text-brand-500" />
Social Login Providers
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Enable or disable social login providers that customers can use to sign in. Click on a
provider to toggle it on or off.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
<span className="text-2xl">&#128269;</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">&#127822;</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">&#128216;</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">&#128188;</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">&#129703;</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">&#128038;</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">&#127918;</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">
{/* Allow Registration */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Allow OAuth Registration</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
When enabled, new customers can create accounts using OAuth providers. When disabled,
only existing customers can sign in via OAuth.
</p>
</div>
</div>
</div>
{/* Auto-link by Email */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Auto-link by Email</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
When enabled, if a customer signs in with OAuth and their email matches an existing
account, the accounts are automatically linked. This lets customers sign in with
multiple methods.
</p>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-start gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<Save size={18} className="text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-800 dark:text-blue-200">
Click the <strong>Save</strong> button to apply your changes.
</p>
</div>
</div>
</section>
{/* Custom OAuth Credentials */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<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 &gt; Authentication</strong>
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
2
</span>
<span className="text-gray-700 dark:text-gray-300">
In the <strong>Social Login</strong> section, click on the providers you want to enable
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
3
</span>
<span className="text-gray-700 dark:text-gray-300">
Configure <strong>Allow OAuth Registration</strong> if you want new customers to sign up
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
4
</span>
<span className="text-gray-700 dark:text-gray-300">
Enable <strong>Auto-link by Email</strong> to let customers use multiple sign-in methods
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">
5
</span>
<span className="text-gray-700 dark:text-gray-300">
Click <strong>Save</strong> to apply your changes
</span>
</li>
</ol>
</div>
</section>
{/* Tips */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" />
Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Enable Google and Apple at minimum - these are the most popular sign-in methods
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Keep <strong>Auto-link by Email</strong> enabled to prevent duplicate accounts
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Only enable <strong>Allow OAuth Registration</strong> if you want new customers to create
accounts via OAuth
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Custom OAuth credentials are recommended for businesses that want their own branding
on the sign-in consent screen
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/help/customers"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/api"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Key size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">API Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with authentication configuration.
</p>
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpSettingsAuth;

View File

@@ -0,0 +1,375 @@
/**
* Help Settings Billing Page
*
* Comprehensive documentation for the Plan & Billing Settings page.
* Documents: Current plan, subscriptions, add-ons, payment methods, invoices, upgrade flow.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, CreditCard, Crown, Package, Wallet, FileText,
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Calendar,
Plus, Trash2, Star, RotateCcw, X, Zap, ArrowRight,
} from 'lucide-react';
const HelpSettingsBilling: 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>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<CreditCard size={24} className="text-emerald-600 dark:text-emerald-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Plan & Billing Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Manage your subscription, add-ons, and payment methods</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CreditCard size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Plan & Billing page is where you manage your SmoothSchedule subscription. View your current plan,
upgrade or change plans, add feature add-ons, manage payment methods, and access your billing history.
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Owner Access Required:</strong> Only business owners can access billing settings.
</p>
</div>
</div>
</div>
</section>
{/* Current Plan */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Crown size={20} className="text-amber-500" /> Current Plan
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Current Plan section shows your active subscription tier, its monthly price, and included features.
</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">What You'll See</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li>• <strong>Plan Name:</strong> Your current tier (Free, Starter, Professional, Enterprise)</li>
<li>• <strong>Monthly Price:</strong> What you pay per month (or "Contact Us" for Enterprise)</li>
<li>• <strong>Key Features:</strong> List of what's included in your plan</li>
<li> <strong>Upgrade Button:</strong> Opens the plan selection modal</li>
</ul>
</div>
</div>
</div>
</section>
{/* Active Subscriptions */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Calendar size={20} className="text-blue-500" /> Active Subscriptions
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
This section lists all your active subscriptions, including your main plan and any add-ons you've purchased.
</p>
<div className="space-y-4">
{/* Subscription Details */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">For Each Subscription</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li>• <strong>Plan Name:</strong> The subscription's name</li>
<li> <strong>Type Badge:</strong> "Plan" for main subscription, "Add-on" for extras</li>
<li> <strong>Price & Interval:</strong> Amount charged and billing frequency (monthly/yearly)</li>
<li> <strong>Next Billing Date:</strong> When you'll be charged next</li>
</ul>
</div>
{/* Cancellation States */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<X size={16} className="text-red-500" />
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Cancel Subscription</h4>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Cancels your subscription. You can choose to cancel at period end (keeps access until then) or immediately.
</p>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<RotateCcw size={16} className="text-green-500" />
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Reactivate</h4>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
If you've cancelled but your subscription hasn't ended yet, you can reactivate it.
</p>
</div>
</div>
{/* Cancelling Status */}
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<h4 className="font-medium text-gray-900 dark:text-white text-sm mb-1">Cancelling Status</h4>
<p className="text-xs text-gray-600 dark:text-gray-400">
If you cancelled at period end, the subscription shows a "Cancelling" badge and displays when it will end.
You can still reactivate before that date.
</p>
</div>
</div>
</div>
</section>
{/* Available Add-ons */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Package size={20} className="text-purple-500" /> Available Add-ons
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Add-ons let you enhance your subscription with additional features beyond what's included in your base plan.
</p>
<div className="space-y-4">
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">How Add-ons Work</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li> Each add-on has its own monthly price</li>
<li> Click "Add" to purchase - you'll be taken to Stripe checkout</li>
<li>• Add-ons appear in your Active Subscriptions once purchased</li>
<li>• Cancel anytime just like your main subscription</li>
</ul>
</div>
</div>
</div>
</section>
{/* Wallet */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Wallet size={20} className="text-blue-500" /> Wallet
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Wallet section provides a quick link to manage your communication credits (SMS & Calling).
Credits are used for sending SMS reminders and making calls through the platform.
</p>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-sm text-blue-700 dark:text-blue-300">
To manage credits, top up your balance, or configure auto-reload, go to{' '}
<strong>Settings → SMS & Calling</strong>.
</p>
</div>
</div>
</section>
{/* Payment Methods */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CreditCard size={20} className="text-purple-500" /> Payment Methods
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Manage the credit cards used for your subscriptions and credit purchases.
</p>
<div className="space-y-4">
{/* Card Display */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Each Card Shows</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li>• <strong>Card Brand:</strong> Visa, Mastercard, Amex, etc.</li>
<li>• <strong>Last 4 Digits:</strong> •••• 4242</li>
<li>• <strong>Expiration:</strong> Month/Year the card expires</li>
<li>• <strong>Default Badge:</strong> Shows which card is used for automatic payments</li>
</ul>
</div>
{/* Card Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Plus size={16} className="text-brand-500" />
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Add Card</h4>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Opens Stripe to securely add a new card
</p>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Star size={16} className="text-amber-500" />
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Set Default</h4>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Make a card the default for payments
</p>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Trash2 size={16} className="text-red-500" />
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Remove</h4>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Delete a card from your account
</p>
</div>
</div>
</div>
</div>
</section>
{/* Billing History */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<FileText size={20} className="text-gray-500" /> Billing History
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
View and download invoices for all your past payments. Invoices are generated automatically
after each successful charge.
</p>
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-300">
Each invoice includes the date, amount, what was purchased, and a download link for your records.
Use these for expense tracking, tax purposes, or reimbursement requests.
</p>
</div>
</div>
</section>
{/* Upgrading Your Plan */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<ArrowRight size={20} className="text-brand-500" /> Upgrading Your Plan
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Click the "Upgrade Plan" button to see all available plans and change your subscription.
</p>
<div className="space-y-4">
<div className="p-4 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">How to Upgrade</h4>
<ol className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-decimal list-inside">
<li>Click <strong>Upgrade Plan</strong> in the Current Plan section</li>
<li>Review the available plans and their features</li>
<li>Click <strong>Upgrade</strong> on your desired plan</li>
<li>Complete checkout through Stripe</li>
<li>Your new plan activates immediately</li>
</ol>
</div>
{/* Plan Comparison Note */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-3">
<Zap size={18} className="text-blue-500 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Transaction Fees</h4>
<p className="text-xs text-gray-600 dark:text-gray-400">
Higher tier plans include lower transaction fees on payment processing.
Enterprise plans offer custom pricing - contact support for details.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Tips */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" /> Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-emerald-50 dark:from-brand-900/20 dark:to-emerald-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Keep payment methods up to date to avoid service interruption
</span>
</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">
If you cancel at period end, you can reactivate anytime before it expires
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Download invoices regularly for your accounting records
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Add-ons are billed separately and can be cancelled independently
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link to="/help/settings/quota" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Usage & Quota</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/payments" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<CreditCard size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Payments Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<CheckCircle size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">General Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/appearance" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<Crown size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Branding Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with billing questions.
</p>
<button
onClick={() => navigate('/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpSettingsBilling;

View File

@@ -0,0 +1,317 @@
/**
* Help Settings Booking Page
*
* Comprehensive documentation for the Booking Settings page.
* Documents: Booking URL display/sharing, Return URL configuration.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, Calendar, Link2, ExternalLink, Copy, Share2,
CheckCircle, ChevronRight, HelpCircle, AlertCircle, Globe,
} from 'lucide-react';
const HelpSettingsBooking: 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>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Calendar size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Booking Settings Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Configure your booking URL and customer redirect</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Calendar size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Booking Settings page is where you find your public booking URL to share with customers
and configure where customers go after completing a booking. This is an owner-only section
that controls how customers access your scheduling system.
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-700 dark:text-blue-300">
<strong>Owner Access Required:</strong> Only business owners can view and modify booking settings.
</p>
</div>
</div>
</div>
</section>
{/* Your Booking URL */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Link2 size={20} className="text-brand-500" /> Your Booking URL
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Your booking URL is the public link where customers can view your services and book appointments.
It's based on your business subdomain and is automatically generated when you create your business.
</p>
<div className="space-y-4">
{/* URL Format */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">URL Format</h4>
<code className="text-sm font-mono text-brand-600 dark:text-brand-400">
https://[your-subdomain].smoothschedule.com
</code>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Your subdomain was set when you created your business and cannot be changed.
</p>
</div>
{/* Actions Available */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Copy size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Copy to Clipboard</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the copy icon to copy the full URL to your clipboard for easy sharing
</p>
</div>
</div>
<div 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" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Open Booking Page</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the external link icon to open your booking page in a new tab
</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Sharing Your Booking URL */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Share2 size={20} className="text-brand-500" /> Sharing Your Booking 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">
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="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Website</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Add a "Book Now" button linking to your URL</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">Email Signature</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Include your booking link in every email</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">Social Media</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Add to your bio on Instagram, Facebook, etc.</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">Business Cards</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Print a QR code or short URL on cards</p>
</div>
</div>
</div>
</section>
{/* Return URL */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<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>
<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">
Test your booking URL by opening it in an incognito/private browser window to see what customers see
</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">
Make sure your Return URL is accessible and mobile-friendly before setting it
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
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>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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">
<Calendar 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">
<Share2 size={20} className="text-brand-500" />
<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" />
</Link>
</div>
</section>
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions 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>
</div>
);
};
export default HelpSettingsBooking;

View File

@@ -0,0 +1,408 @@
/**
* Help Settings Custom Domains Page
*
* Documentation for managing custom domains and domain purchases.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Globe,
Copy,
Star,
Trash2,
RefreshCw,
CheckCircle,
AlertCircle,
ShoppingCart,
ChevronRight,
HelpCircle,
Lock,
Settings,
} from 'lucide-react';
const HelpSettingsDomains: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
<Globe size={24} className="text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Custom Domains Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Use your own domain for booking pages
</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Globe 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">
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>)
for your booking pages. You can connect a domain you already own, or purchase a new one directly through the platform.
</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>
</section>
{/* Bring Your Own Domain */}
<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-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>
<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 &gt; Custom Domains</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>Bring Your Own Domain</strong> section, enter your domain
</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 <strong>Add</strong> to add the domain
</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">
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>
</ol>
</div>
</section>
{/* Tips */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" />
Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
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 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">
If verification fails, double-check the TXT record name and value match exactly
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Use an online DNS lookup tool to check if your TXT record is published
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Set 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>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<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"
>
<Settings size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Appearance Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/booking"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<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>
</div>
</section>
{/* Need Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with 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>
</div>
);
};
export default HelpSettingsDomains;

View File

@@ -0,0 +1,343 @@
/**
* 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 { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, Mail, Plus, Edit, Trash2, Star, TestTube,
RefreshCw, CheckCircle, ChevronRight, HelpCircle, AlertCircle, XCircle,
} from 'lucide-react';
const HelpSettingsEmail: 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>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<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-blue-600 dark:text-blue-400" />
</div>
<div>
<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">Configure email addresses for ticket support</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Mail 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">
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>
<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 configure ticket email addresses.
</p>
</div>
</div>
</div>
</section>
{/* Email Address List */}
<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" /> Your Email Addresses
</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 configured email address appears as a card showing:
</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 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>
{/* Status Badges */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<div className="flex items-center gap-2 mb-1">
<Star size={14} className="text-yellow-600" />
<span className="text-sm font-medium text-yellow-800 dark:text-yellow-300">Default</span>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
Primary address for outgoing ticket emails
</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>
</section>
{/* Actions */}
<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" /> 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>
<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">
Use "Test IMAP" after setup to verify your credentials are correct
</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 different email addresses for different departments (support, sales, billing)
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Set 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>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
<Mail size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Ticketing Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<CheckCircle size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">General Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/appearance" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<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" />
</Link>
</div>
</section>
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with 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>
</div>
);
};
export default HelpSettingsEmail;

View File

@@ -0,0 +1,328 @@
/**
* Help Settings General Page
*
* Comprehensive help documentation for General Settings.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Settings,
Building2,
Globe,
Clock,
Mail,
Phone,
CheckCircle,
ChevronRight,
HelpCircle,
AlertCircle,
Save,
Shield,
} from 'lucide-react';
const HelpSettingsGeneral: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} /> Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Building2 size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<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 identity, timezone, and contact information</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings 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">
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>
</div>
</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">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Building2 size={20} className="text-brand-500" /> Business Identity
</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">
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">
<Building2 size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<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 in the sidebar, emails, booking pages, and customer-facing interfaces. This can be changed at any time.
</p>
</div>
</div>
<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 shrink-0" />
<div>
<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: <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 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 shrink-0" />
<div>
<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 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 className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Phone size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Phone Number</h4>
<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>
</section>
{/* Saving Changes Section */}
<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">
<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>
<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>Consistent Branding:</strong> Your business name appears across all customer touchpoints
</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>Accurate Scheduling:</strong> Correct timezone prevents appointment confusion
</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>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>
</ul>
</div>
</section>
{/* Related Settings */}
<section className="mb-10">
<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">
<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" />
<span className="text-gray-900 dark:text-white">Branding Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/booking"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<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" />
<span className="text-gray-900 dark:text-white">Custom Domains</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"
>
<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>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions 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>
</div>
);
};
export default HelpSettingsGeneral;

View File

@@ -0,0 +1,417 @@
/**
* Help Settings Quota Page
*
* Documentation for managing quota overages and usage limits.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
AlertTriangle,
Users,
Briefcase,
Calendar,
Archive,
Clock,
Check,
Download,
ChevronDown,
CheckCircle,
ChevronRight,
HelpCircle,
CreditCard,
} from 'lucide-react';
const HelpSettingsQuota: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
<AlertTriangle size={24} className="text-amber-600 dark:text-amber-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Quota Management Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Manage account limits and overages
</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<AlertTriangle 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">
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>
<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>
</section>
{/* Current Usage */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Check size={20} className="text-brand-500" />
Current Usage
</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 usage overview shows your current consumption for each quota type:
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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>
<p className="text-sm text-gray-600 dark:text-gray-300">
Staff members beyond the plan's included users
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Briefcase size={18} className="text-purple-500" />
<span className="font-medium text-gray-900 dark:text-white">Resources</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
Total resources (staff, rooms, equipment)
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Calendar size={18} className="text-green-500" />
<span className="font-medium text-gray-900 dark:text-white">Services</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
Active services offered to customers
</p>
</div>
</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">
<p className="text-sm text-green-800 dark:text-green-200 flex items-center gap-2">
<Check size={16} className="flex-shrink-0" />
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>
</section>
{/* Resolving Overages */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Archive size={20} className="text-brand-500" />
Resolving 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">
You have two options to resolve an overage:
</p>
<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>
{/* Option 2: Upgrade */}
<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">
<CreditCard size={18} className="text-brand-500" />
Option 2: Upgrade Your Plan
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-2">
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>
</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 &gt; 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">
<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" />
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>
<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>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 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>Archive inactive items first</strong> - Choose resources or services you no longer
actively 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>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>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<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"
>
<CreditCard size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Billing Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</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"
>
<Briefcase size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Resources Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with 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>
</div>
);
};
export default HelpSettingsQuota;

View File

@@ -0,0 +1,333 @@
/**
* 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 { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, Layers, Users, Plus, Pencil, Trash2,
CheckCircle, ChevronRight, HelpCircle, AlertCircle,
} from 'lucide-react';
const HelpSettingsResourceTypes: 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>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<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-indigo-600 dark:text-indigo-400" />
</div>
<div>
<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">Define custom categories for your resources</p>
</div>
</div>
</div>
{/* Overview */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Layers 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">
Resource Types let you define custom categories for your bookable resources. Instead of just
"Staff", "Room", and "Equipment", you can create specific types like "Stylist", "Treatment Room",
"Camera", or any category that fits your business needs.
</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>
</section>
{/* Resource Type Categories */}
<section className="mb-10">
<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" /> Categories
</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 resource type belongs to one of two categories, which determines how it behaves in the system:
</p>
<div className="space-y-4">
{/* 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>
{/* Other Category */}
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<Layers size={20} className="text-gray-600 dark:text-gray-400" />
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Other Category</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
General resources - physical items or spaces that don't require a staff link.
Use this for rooms, equipment, vehicles, or any non-person resource.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Your Resource Types List */}
<section className="mb-10">
<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" /> Your Resource Types
</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 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="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Hair Salon</h4>
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<li> Stylist (Staff)</li>
<li> Colorist (Staff)</li>
<li> Wash Station (Other)</li>
<li> Styling Chair (Other)</li>
</ul>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Medical Clinic</h4>
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<li> Doctor (Staff)</li>
<li> Nurse (Staff)</li>
<li> Exam Room (Other)</li>
<li> X-Ray Machine (Other)</li>
</ul>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Photo Studio</h4>
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<li> Photographer (Staff)</li>
<li> Assistant (Staff)</li>
<li> Studio A (Other)</li>
<li> Lighting Kit (Other)</li>
</ul>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-2">Fitness Center</h4>
<ul className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<li> Personal Trainer (Staff)</li>
<li> Yoga Instructor (Staff)</li>
<li> Training Room (Other)</li>
<li> Spin Bike (Other)</li>
</ul>
</div>
</div>
</div>
</section>
{/* Tips */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" /> Tips
</h2>
<div className="bg-gradient-to-r from-brand-50 to-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">
<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">
Create specific types rather than generic ones - "Stylist" is better than just "Staff"
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
Use the Staff category when the resource represents a person who needs a system account
</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">
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>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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">
<Layers size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Resources Guide</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">
<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">
<Layers size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link to="/help/settings/general" className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors">
<CheckCircle size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">General Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Help Footer */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with 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>
</div>
);
};
export default HelpSettingsResourceTypes;

View File

@@ -0,0 +1,540 @@
/**
* Help Staff Page
*
* Comprehensive help documentation for the Staff management page.
*/
import React from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Users,
UserPlus,
Shield,
Briefcase,
Clock,
Calendar,
Mail,
CheckCircle,
ChevronRight,
HelpCircle,
Settings,
Key,
Send,
RefreshCw,
Trash2,
Pencil,
Power,
Eye,
AlertCircle,
UserX,
ChevronDown,
} from 'lucide-react';
const HelpStaff: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} /> Back
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Users size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Staff Guide</h1>
<p className="text-gray-500 dark:text-gray-400">Manage your team members, roles, and permissions</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Users size={20} className="text-brand-500" /> Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
The Staff page lets you manage team members who can access your business dashboard. Invite employees via email, assign roles with appropriate permissions, and control what each person can see and do within the system.
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Staff members are different from Resources - staff are users who log in to manage the system, while resources are what gets booked. However, a staff member can also be linked to a bookable resource (e.g., a stylist who has both login access AND an appointment calendar).
</p>
<p className="text-gray-600 dark:text-gray-300">
The page displays your team in a table with active staff at the top and an expandable section for inactive (deactivated) staff below.
</p>
</div>
</section>
{/* Staff Roles Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Shield size={20} className="text-brand-500" /> Staff Roles
</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 staff member has a role that determines their base level of access:
</p>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<Key size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-900 dark:text-white">Owner</h4>
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300">
<Shield size={10} /> owner
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Full access to all features including billing, business settings, staff management, and all operational features. Owners cannot be deactivated or have their permissions restricted. Every business has at least one owner.
</p>
</div>
</div>
<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">
<Briefcase size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-900 dark:text-white">Manager</h4>
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
<Briefcase size={10} /> manager
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Can manage scheduling, customers, services, and resources. Has access to view reports. Limited access to settings. Permissions can be customized to expand or restrict access to specific features.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
<Users size={20} className="text-gray-500 mt-0.5 shrink-0" />
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-900 dark:text-white">Staff</h4>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
staff
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Basic access to view schedules and manage their own appointments. Read-only access to most areas. Ideal for employees who only need to see their calendar and handle their assigned bookings.
</p>
</div>
</div>
</div>
</div>
</section>
{/* The Staff Table Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Users size={20} className="text-brand-500" /> The Staff Table
</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 main table displays active staff members with the following columns:
</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">
<Users size={20} className="text-blue-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Name</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Staff member's name and email address. Displays an avatar with their initial.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Shield size={20} className="text-purple-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Role</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Color-coded badge showing their role - purple for Owner, blue for Manager, gray for Staff.
</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-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Bookable Resource</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Shows if the staff member is linked to a bookable resource. Displays "Yes (Resource Name)" with a green badge if linked, or a "Make Bookable" link if not.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Settings size={20} className="text-gray-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Actions</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Masquerade button (owners only), Edit button to modify permissions and settings.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Inviting Staff Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<UserPlus size={20} className="text-brand-500" /> Inviting Staff
</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 <strong>"Invite Staff"</strong> button to send an email invitation:
</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">Enter Email Address</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
The email address where the invitation will be sent. They'll receive a link to create their account.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">2</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Select Role</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Choose Staff Member or Manager. Only owners can invite managers.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Set Permissions (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Customize which features they can access. Different permission options are available based on the selected role.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Make Bookable (Optional)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Check this to automatically create a bookable resource linked to this person. You can set a custom display name or use their name by default.
</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">5</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Send Invitation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Send Invitation" to email them. The invitation appears in Pending Invitations until they accept.
</p>
</div>
</li>
</ol>
</div>
</section>
{/* Pending Invitations 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" /> Pending Invitations
</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 invite someone, their invitation appears in a yellow highlighted section above the staff table:
</p>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
<Mail size={20} className="text-amber-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Invitation Details</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Shows the invitee's email, their assigned role, and when the invitation expires.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<RefreshCw size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Resend Invitation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the refresh icon to send the invitation email again if they haven't received it.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Trash2 size={20} className="text-red-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Cancel Invitation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the trash icon to revoke the invitation. They won't be able to join even if they click the link.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Editing Staff Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Pencil size={20} className="text-brand-500" /> Editing Staff
</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 pencil icon on any staff row to open the edit modal:
</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">
<Users size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Staff Info</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
View the staff member's name, email, and current role badge.
</p>
</div>
</div>
<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 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Edit Permissions</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Toggle individual permissions on/off for Managers and Staff. Owners have full access and cannot be restricted.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<Power size={20} className="text-red-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Danger Zone: Deactivate</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Deactivate a staff member to prevent them from logging in while keeping their data. Only owners can deactivate staff, and they cannot deactivate themselves or other owners.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Make Bookable Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Calendar size={20} className="text-brand-500" /> Make Staff Bookable
</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">
Staff members who provide services to customers need to be "bookable" - this means they have a linked resource in the scheduler:
</p>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<CheckCircle size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Already Bookable</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
Shows "Yes (Resource Name)" if a resource is already linked. Customers can book appointments with this person.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<UserPlus size={20} className="text-brand-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Make Bookable</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click this link in the table to create a bookable resource for the staff member. This links their user account to a schedulable resource automatically.
</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">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Tip:</strong> You can also check "Make Bookable" when sending the initial invitation to create the resource automatically when they join.
</p>
</div>
</div>
</section>
{/* Inactive Staff Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<UserX size={20} className="text-brand-500" /> Inactive Staff
</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">
Deactivated staff members appear in a collapsible section below the main table:
</p>
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 bg-gray-100 dark:bg-gray-700/50 rounded-lg">
<ChevronDown size={20} className="text-gray-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Collapsible Section</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Inactive Staff (X)" to expand and see deactivated team members. They appear grayed out.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Power size={20} className="text-green-500 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Reactivate</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click the "Reactivate" button to restore their access. They can immediately log in again with their existing credentials.
</p>
</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>Note:</strong> Deactivating staff is different from deleting them. Deactivated staff keep their data (appointment history, etc.) but cannot log in. Use this when someone leaves temporarily or you want to revoke access while preserving records.
</p>
</div>
</div>
</div>
</section>
{/* Masquerading Section */}
<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" /> Masquerading as Staff
</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">
Owners can masquerade as staff members to see the system from their perspective:
</p>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Training:</strong> Show new staff how to use features from their view</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Troubleshooting:</strong> Diagnose issues by seeing exactly what they see</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 shrink-0" />
<span className="text-gray-600 dark:text-gray-300"><strong>Permission Testing:</strong> Verify that permissions are configured correctly</span>
</li>
</ul>
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
<div className="flex items-start gap-2">
<Eye size={16} className="text-indigo-500 mt-0.5 shrink-0" />
<p className="text-sm text-indigo-800 dark:text-indigo-200">
Click the <strong>"Masquerade"</strong> button in the Actions column. A banner appears at the top while masquerading - click "End Masquerade" to return to your owner view.
</p>
</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>
<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>Security:</strong> Control who can access sensitive business data with role-based permissions
</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>Delegation:</strong> Let managers and staff handle day-to-day operations independently
</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 Onboarding:</strong> Email invitations make adding new team members seamless
</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>Flexibility:</strong> Adjust permissions as roles evolve without changing job titles
</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 Linking:</strong> Connect staff accounts to bookable calendars with one click
</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>Safe Offboarding:</strong> Deactivate staff while preserving their historical data
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Related Features</h2>
<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"
>
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Resources 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"
>
<Clock size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Scheduler Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/settings/auth"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Shield size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Authentication Settings</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/customers"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Users size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Customers Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about managing staff.
</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 HelpStaff;

View File

@@ -0,0 +1,352 @@
/**
* Help Tasks Page
*
* Comprehensive help documentation for the Tasks (Scheduled Tasks) page.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft,
Clock,
Play,
Pause,
RefreshCw,
Zap,
Calendar,
Settings,
CheckCircle,
ChevronRight,
HelpCircle,
FileText,
AlertTriangle,
} from 'lucide-react';
const HelpTasks: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<Clock size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Tasks Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Automate your business with scheduled tasks
</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Clock 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">
Tasks are scheduled automations that run at specified intervals to help you manage your business. When you install a plugin from the marketplace, you can schedule it to run automatically - hourly, daily, weekly, or on custom schedules.
</p>
<p className="text-gray-600 dark:text-gray-300">
Tasks can send emails, generate reports, engage with customers, sync data with external services, and more. They run in the background so you can focus on running your business.
</p>
</div>
</section>
{/* Schedule Types Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Calendar size={20} className="text-brand-500" />
Schedule Types
</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">
<Clock size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Hourly</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Runs every hour at the specified minute. Great for real-time monitoring and frequent updates.
</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-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Daily</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Runs once per day at your chosen time. Perfect for daily reports, reminders, and cleanup tasks.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<RefreshCw size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Weekly</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Runs once per week on your selected day. Ideal for weekly summaries, marketing campaigns, and reports.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Settings size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Custom (Cron)</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Advanced scheduling using cron expressions for complex schedules like "every Monday and Wednesday at 9am".
</p>
</div>
</div>
</div>
</div>
</section>
{/* Task Management Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Zap size={20} className="text-brand-500" />
Task Management
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Play size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Run Now</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manually trigger a task to run immediately, regardless of its schedule.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Pause size={20} className="text-yellow-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Pause/Resume</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Temporarily disable a task without deleting it. Resume when ready.
</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-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">View Logs</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
See execution history, results, and any errors that occurred during task runs.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Settings size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Edit Configuration</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Modify task settings, change schedule, or update plugin configuration values.
</p>
</div>
</div>
</div>
</div>
</section>
{/* How to Create a Task 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" />
Creating a Scheduled Task
</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>
<div>
<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">
Go to the Plugin Marketplace and install a plugin you want to automate.
</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">Configure the Plugin</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Set up any required configuration values for the plugin in My Plugins.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Navigate to Tasks</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Tasks" in the sidebar to access the scheduled tasks page.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">4</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Create New Task</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Click "Create Task", select your installed plugin, and choose a schedule.
</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">5</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Activate</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Save the task. It will automatically run according to your schedule.
</p>
</div>
</li>
</ol>
</div>
</section>
{/* Task Status Section */}
<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" />
Task Statuses
</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-center gap-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 min-w-[100px]">
<CheckCircle size={14} /> Active
</span>
<p className="text-gray-600 dark:text-gray-300">
Task is scheduled and will run at the specified time
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 min-w-[100px]">
<Pause size={14} /> Paused
</span>
<p className="text-gray-600 dark:text-gray-300">
Task is temporarily disabled and won't run until resumed
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 min-w-[100px]">
<RefreshCw size={14} /> Running
</span>
<p className="text-gray-600 dark:text-gray-300">
Task is currently executing
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 min-w-[100px]">
<AlertTriangle size={14} /> Failed
</span>
<p className="text-gray-600 dark:text-gray-300">
Last execution failed - check logs for details
</p>
</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>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Automation:</strong> Set it and forget it - tasks run automatically on schedule
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Time Savings:</strong> Automate repetitive tasks that would take hours manually
</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>Consistency:</strong> Tasks run exactly when scheduled, every time
</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>Visibility:</strong> Full logging shows exactly what happened and when
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/help/plugins-overview"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Zap size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Plugins Overview</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/help/plugins"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<FileText 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>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
If you have questions that aren't covered here, 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>
</div>
);
};
export default HelpTasks;

View File

@@ -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>

View File

@@ -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_sms_reminders: false,
can_use_masked_phone_numbers: false, can_use_masked_phone_numbers: false,
can_use_email_templates: false, can_use_pos: false,
// Customization can_use_mobile_app: false,
can_customize_booking_page: false,
// Advanced Features
advanced_reporting: false,
can_create_plugins: false,
can_export_data: false, can_export_data: false,
can_use_plugins: true,
can_use_tasks: true,
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_use_pos: false,
can_use_mobile_app: false,
can_export_data: plan.permissions?.export_data ?? false,
can_use_plugins: plan.permissions?.plugins ?? true,
can_use_tasks: plan.permissions?.tasks ?? true,
can_create_plugins: plan.permissions?.can_create_plugins ?? false, can_create_plugins: plan.permissions?.can_create_plugins ?? false,
can_export_data: plan.permissions?.can_export_data ?? false, can_use_webhooks: plan.permissions?.webhooks ?? false,
can_use_webhooks: plan.permissions?.can_use_webhooks ?? false, can_use_calendar_sync: plan.permissions?.calendar_sync ?? false,
calendar_sync: plan.permissions?.calendar_sync ?? false,
priority_support: plan.permissions?.priority_support ?? false,
dedicated_support: plan.permissions?.dedicated_support ?? false,
sso_enabled: plan.permissions?.sso_enabled ?? 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_email_templates: false, can_use_pos: false,
can_customize_booking_page: false, can_use_mobile_app: false,
advanced_reporting: false,
can_create_plugins: false,
can_export_data: false, can_export_data: false,
can_use_plugins: true,
can_use_tasks: true,
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>

View File

@@ -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;

View File

@@ -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

View 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)'),
),
]

View 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)'),
),
]

View File

@@ -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,

View 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}")

View File

@@ -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"""

View File

@@ -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

View File

@@ -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']

View 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)

View File

@@ -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

View File

@@ -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):
""" """

View File

@@ -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),

View File

@@ -3,11 +3,14 @@ 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.
"""
return [
{ {
'name': 'Daily Appointment Summary Email', 'name': 'Daily Appointment Summary Email',
'slug': 'daily-appointment-summary', 'slug': 'daily-appointment-summary',
@@ -312,12 +315,66 @@ api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
}, },
] ]
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():
if existing:
if update_existing:
# Check if plugin needs updating by comparing key fields
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.stdout.write(
self.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists") self.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists")
) )
@@ -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).'
) )
) )

View File

@@ -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)

View File

@@ -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,

View File

@@ -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: