Compare commits
6 Commits
5cef01ad0d
...
dcb14503a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb14503a2 | ||
|
|
9444e26924 | ||
|
|
445b2bb3fc | ||
|
|
baffe7e577 | ||
|
|
5aa49399d0 | ||
|
|
11bb83a85d |
179
PLAN_HELP_DOCS.md
Normal file
179
PLAN_HELP_DOCS.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Help Documentation Implementation Plan
|
||||
|
||||
## Overview
|
||||
This plan covers creating comprehensive help documentation for the SmoothSchedule business dashboard, adding contextual help buttons to each page, and creating a monolithic help document.
|
||||
|
||||
## Phase 1: Create Plugin Page First (User Request)
|
||||
|
||||
### Task 1.1: Create CreatePlugin.tsx Page
|
||||
- Create `/frontend/src/pages/CreatePlugin.tsx`
|
||||
- Features:
|
||||
- Name, description, short description fields
|
||||
- Category dropdown (EMAIL, REPORTS, CUSTOMER, BOOKING, INTEGRATION, AUTOMATION, OTHER)
|
||||
- Plugin code editor with syntax highlighting (using same Prism setup as HelpPluginDocs)
|
||||
- Template variables preview (auto-extracted from code)
|
||||
- Version field (default 1.0.0)
|
||||
- Logo URL field (optional)
|
||||
- Save as Private / Submit to Marketplace options
|
||||
- Visibility selector (PRIVATE, PUBLIC)
|
||||
- Uses API endpoint: `POST /api/plugin-templates/`
|
||||
- Plan feature gate: `can_create_plugins`
|
||||
|
||||
### Task 1.2: Add Route for CreatePlugin
|
||||
- Add lazy import: `const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin'));`
|
||||
- Add route: `/plugins/create` pointing to CreatePlugin component
|
||||
|
||||
## Phase 2: Create Reusable HelpButton Component
|
||||
|
||||
### Task 2.1: Create HelpButton Component
|
||||
- Create `/frontend/src/components/HelpButton.tsx`
|
||||
- Props: `helpPath: string` (route to help page)
|
||||
- Renders: HelpCircle icon button at fixed position (top-right of page)
|
||||
- Styling: Circular button with question mark icon, tooltip on hover
|
||||
- Uses Link from react-router-dom to navigate to help page
|
||||
|
||||
## Phase 3: Create Individual Help Pages
|
||||
|
||||
### 3.1 Core Pages Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Dashboard | HelpDashboard.tsx | /help/dashboard |
|
||||
| Scheduler | HelpScheduler.tsx | /help/scheduler |
|
||||
| Tasks | HelpTasks.tsx | /help/tasks |
|
||||
|
||||
### 3.2 Manage Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Customers | HelpCustomers.tsx | /help/customers |
|
||||
| Services | HelpServices.tsx | /help/services |
|
||||
| Resources | HelpResources.tsx | /help/resources |
|
||||
| Staff | HelpStaff.tsx | /help/staff |
|
||||
|
||||
### 3.3 Communicate Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Messages | HelpMessages.tsx | /help/messages |
|
||||
| Tickets | HelpTicketing.tsx (exists) | /help/ticketing |
|
||||
|
||||
### 3.4 Money Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Payments | HelpPayments.tsx | /help/payments |
|
||||
|
||||
### 3.5 Extend Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| Plugins | HelpPluginsOverview.tsx | /help/plugins-overview |
|
||||
| Plugin Marketplace | (link to existing HelpPluginDocs) | /help/plugins |
|
||||
| My Plugins | HelpMyPlugins.tsx | /help/my-plugins |
|
||||
| Create Plugin | HelpCreatePlugin.tsx | /help/create-plugin |
|
||||
|
||||
### 3.6 Settings Section Help
|
||||
| Page | Help File | Route |
|
||||
|------|-----------|-------|
|
||||
| General | HelpSettingsGeneral.tsx | /help/settings/general |
|
||||
| Resource Types | HelpSettingsResourceTypes.tsx | /help/settings/resource-types |
|
||||
| Booking | HelpSettingsBooking.tsx | /help/settings/booking |
|
||||
| Appearance | HelpSettingsAppearance.tsx | /help/settings/appearance |
|
||||
| Email Templates | HelpSettingsEmailTemplates.tsx | /help/settings/email-templates |
|
||||
| Custom Domains | HelpSettingsCustomDomains.tsx | /help/settings/custom-domains |
|
||||
| API & Webhooks | HelpSettingsApi.tsx | /help/settings/api |
|
||||
| Authentication | HelpSettingsAuth.tsx | /help/settings/authentication |
|
||||
| Email Setup | HelpEmailSettings.tsx (exists) | /help/email-settings |
|
||||
| SMS & Calling | HelpSettingsSmsCalling.tsx | /help/settings/sms-calling |
|
||||
| Plan & Billing | HelpSettingsBilling.tsx | /help/settings/billing |
|
||||
| Quota Management | HelpSettingsQuota.tsx | /help/settings/quota |
|
||||
|
||||
## Phase 4: Add HelpButton to Each Page
|
||||
|
||||
Add the HelpButton component to the top-right of each dashboard page, linking to its corresponding help page.
|
||||
|
||||
## Phase 5: Update HelpPluginDocs
|
||||
|
||||
### Task 5.1: Review and Update Plugin Documentation
|
||||
- Verify plugin documentation matches current codebase
|
||||
- Add section for "Creating Custom Plugins"
|
||||
- Add links to API documentation
|
||||
- Ensure examples work with current API
|
||||
|
||||
## Phase 6: Create Monolithic Help Document
|
||||
|
||||
### Task 6.1: Create HelpGuideComplete.tsx
|
||||
- Compile all help content into single comprehensive page
|
||||
- Table of contents with anchor links
|
||||
- Searchable content
|
||||
- Organized by sections (Core, Manage, Communicate, Money, Extend, Settings)
|
||||
|
||||
### Task 6.2: Update HelpGuide.tsx
|
||||
- Replace "Coming Soon" with actual compiled documentation
|
||||
- Or redirect to HelpGuideComplete
|
||||
|
||||
## Phase 7: Register All Routes
|
||||
|
||||
Add all new help page routes to App.tsx in the business dashboard section.
|
||||
|
||||
## Help Page Template Structure
|
||||
|
||||
Each help page should follow this structure:
|
||||
```tsx
|
||||
- Header with icon and title
|
||||
- Overview/Introduction
|
||||
- Key Features section
|
||||
- How to Use section (step-by-step)
|
||||
- Benefits section
|
||||
- Tips & Best Practices
|
||||
- Related Features (links to other help pages)
|
||||
- Need More Help? (link to support/tickets)
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Create CreatePlugin.tsx page and route
|
||||
2. Create HelpButton component
|
||||
3. Create help pages for core pages (Dashboard, Scheduler, Tasks)
|
||||
4. Create help pages for Manage section
|
||||
5. Create help pages for Communicate section
|
||||
6. Create help pages for Money section
|
||||
7. Create help pages for Extend section (including plugin docs update)
|
||||
8. Create help pages for Settings section
|
||||
9. Add HelpButton to all pages
|
||||
10. Create monolithic help document
|
||||
11. Test all help pages and navigation
|
||||
|
||||
## Files to Create
|
||||
|
||||
### New Components
|
||||
- `/frontend/src/components/HelpButton.tsx`
|
||||
|
||||
### New Pages
|
||||
- `/frontend/src/pages/CreatePlugin.tsx`
|
||||
- `/frontend/src/pages/help/HelpDashboard.tsx`
|
||||
- `/frontend/src/pages/help/HelpScheduler.tsx`
|
||||
- `/frontend/src/pages/help/HelpTasks.tsx`
|
||||
- `/frontend/src/pages/help/HelpCustomers.tsx`
|
||||
- `/frontend/src/pages/help/HelpServices.tsx`
|
||||
- `/frontend/src/pages/help/HelpResources.tsx`
|
||||
- `/frontend/src/pages/help/HelpStaff.tsx`
|
||||
- `/frontend/src/pages/help/HelpMessages.tsx`
|
||||
- `/frontend/src/pages/help/HelpPayments.tsx`
|
||||
- `/frontend/src/pages/help/HelpPluginsOverview.tsx`
|
||||
- `/frontend/src/pages/help/HelpMyPlugins.tsx`
|
||||
- `/frontend/src/pages/help/HelpCreatePlugin.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsGeneral.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsResourceTypes.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsBooking.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsAppearance.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsEmailTemplates.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsCustomDomains.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsApi.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsAuth.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsSmsCalling.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsBilling.tsx`
|
||||
- `/frontend/src/pages/help/HelpSettingsQuota.tsx`
|
||||
- `/frontend/src/pages/help/HelpGuideComplete.tsx`
|
||||
|
||||
### Files to Modify
|
||||
- `/frontend/src/App.tsx` - Add routes
|
||||
- `/frontend/src/pages/HelpPluginDocs.tsx` - Update with current codebase info
|
||||
- `/frontend/src/pages/HelpGuide.tsx` - Replace Coming Soon
|
||||
- All dashboard pages - Add HelpButton component
|
||||
11
deploy.sh
11
deploy.sh
@@ -98,6 +98,17 @@ docker compose -f docker-compose.production.yml exec -T django sh -c 'export DAT
|
||||
echo ">>> Collecting static files..."
|
||||
docker compose -f docker-compose.production.yml exec -T django sh -c 'export DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} && python manage.py collectstatic --noinput'
|
||||
|
||||
echo ">>> Seeding/updating platform plugins for all tenants..."
|
||||
docker compose -f docker-compose.production.yml exec -T django python -c "
|
||||
from django_tenants.utils import get_tenant_model
|
||||
from django.core.management import call_command
|
||||
Tenant = get_tenant_model()
|
||||
for tenant in Tenant.objects.exclude(schema_name='public'):
|
||||
print(f' Seeding plugins for {tenant.schema_name}...')
|
||||
call_command('tenant_command', 'seed_platform_plugins', schema=tenant.schema_name, verbosity=0)
|
||||
print(' Done!')
|
||||
"
|
||||
|
||||
echo ">>> Checking container status..."
|
||||
docker compose -f docker-compose.production.yml ps
|
||||
|
||||
|
||||
68
frontend/package-lock.json
generated
68
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@stripe/react-stripe-js": "^5.4.1",
|
||||
"@stripe/stripe-js": "^8.5.3",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -25,6 +26,7 @@
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
@@ -1960,6 +1962,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-grid-layout": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.6.tgz",
|
||||
"integrity": "sha512-Cw7+sb3yyjtmxwwJiXtEXcu5h4cgs+sCGkHwHXsFmPyV30bf14LeD/fa2LwQovuD2HWxCcjIdNhDlcYGj95qGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
@@ -2922,6 +2933,12 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
|
||||
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -4376,6 +4393,38 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
||||
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-grid-layout": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.2.tgz",
|
||||
"integrity": "sha512-vT7xmQqszTT+sQw/LfisrEO4le1EPNnSEMVHy6sBZyzS3yGkMywdOd+5iEFFwQwt0NSaGkxuRmYwa1JsP6OJdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"fast-equals": "^4.0.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-resizable": "^3.0.5",
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
@@ -4477,6 +4526,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
|
||||
"integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "15.x",
|
||||
"react-draggable": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||
@@ -4603,6 +4665,12 @@
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@stripe/react-stripe-js": "^5.4.1",
|
||||
"@stripe/stripe-js": "^8.5.3",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -21,6 +22,7 @@
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-grid-layout": "^1.5.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-phone-number-input": "^3.4.14",
|
||||
|
||||
@@ -65,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 HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing
|
||||
const HelpApiDocs = React.lazy(() => import('./pages/HelpApiDocs')); // Import API documentation page
|
||||
const HelpPluginDocs = React.lazy(() => import('./pages/HelpPluginDocs')); // Import Plugin documentation page
|
||||
const HelpPluginDocs = React.lazy(() => import('./pages/help/HelpPluginDocs')); // Import Plugin documentation page
|
||||
const HelpEmailSettings = React.lazy(() => import('./pages/HelpEmailSettings')); // Import Email settings help page
|
||||
|
||||
// Import new help pages
|
||||
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 PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); // Import Plugin Marketplace 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 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="/tickets" element={<Tickets />} />
|
||||
<Route path="/help" element={<HelpComprehensive />} />
|
||||
<Route path="/help/guide" element={<HelpGuide />} />
|
||||
<Route path="/help/ticketing" element={<HelpTicketing />} />
|
||||
<Route path="/help/api" element={<HelpApiDocs />} />
|
||||
<Route path="/help/plugins" element={<HelpPluginDocs />} />
|
||||
<Route path="/help/plugins/docs" element={<HelpPluginDocs />} />
|
||||
<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
|
||||
path="/plugins/marketplace"
|
||||
element={
|
||||
@@ -614,6 +660,16 @@ const AppContent: React.FC = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/plugins/create"
|
||||
element={
|
||||
hasAccess(['owner', 'manager']) ? (
|
||||
<CreatePlugin />
|
||||
) : (
|
||||
<Navigate to="/" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks"
|
||||
element={
|
||||
|
||||
94
frontend/src/components/FloatingHelpButton.tsx
Normal file
94
frontend/src/components/FloatingHelpButton.tsx
Normal 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;
|
||||
33
frontend/src/components/HelpButton.tsx
Normal file
33
frontend/src/components/HelpButton.tsx
Normal 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;
|
||||
268
frontend/src/components/QuotaOverageModal.tsx
Normal file
268
frontend/src/components/QuotaOverageModal.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* QuotaOverageModal Component
|
||||
*
|
||||
* Modal that appears on login/masquerade when the tenant has exceeded quotas.
|
||||
* Shows warning about grace period and what will happen when it expires.
|
||||
* Uses sessionStorage to only show once per session.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
AlertTriangle,
|
||||
X,
|
||||
Clock,
|
||||
Archive,
|
||||
ChevronRight,
|
||||
Users,
|
||||
Layers,
|
||||
Briefcase,
|
||||
Mail,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { QuotaOverage } from '../api/auth';
|
||||
|
||||
interface QuotaOverageModalProps {
|
||||
overages: QuotaOverage[];
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const QUOTA_ICONS: Record<string, React.ReactNode> = {
|
||||
'MAX_ADDITIONAL_USERS': <Users className="w-5 h-5" />,
|
||||
'MAX_RESOURCES': <Layers className="w-5 h-5" />,
|
||||
'MAX_SERVICES': <Briefcase className="w-5 h-5" />,
|
||||
'MAX_EMAIL_TEMPLATES': <Mail className="w-5 h-5" />,
|
||||
'MAX_AUTOMATED_TASKS': <Zap className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const SESSION_STORAGE_KEY = 'quota_overage_modal_dismissed';
|
||||
|
||||
const QuotaOverageModal: React.FC<QuotaOverageModalProps> = ({ overages, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if already dismissed this session
|
||||
const dismissed = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
||||
if (!dismissed && overages && overages.length > 0) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, [overages]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, 'true');
|
||||
setIsVisible(false);
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
if (!isVisible || !overages || overages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the most urgent overage (least days remaining)
|
||||
const mostUrgent = overages.reduce((prev, curr) =>
|
||||
curr.days_remaining < prev.days_remaining ? curr : prev
|
||||
);
|
||||
|
||||
const isCritical = mostUrgent.days_remaining <= 1;
|
||||
const isUrgent = mostUrgent.days_remaining <= 7;
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-lg w-full overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className={`px-6 py-4 ${
|
||||
isCritical
|
||||
? 'bg-red-600'
|
||||
: isUrgent
|
||||
? 'bg-amber-500'
|
||||
: 'bg-amber-100 dark:bg-amber-900/30'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-full ${
|
||||
isCritical || isUrgent
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-200 dark:bg-amber-800'
|
||||
}`}>
|
||||
<AlertTriangle className={`w-6 h-6 ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white'
|
||||
: 'text-amber-700 dark:text-amber-300'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`text-lg font-bold ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white'
|
||||
: 'text-amber-900 dark:text-amber-100'
|
||||
}`}>
|
||||
{isCritical
|
||||
? t('quota.modal.titleCritical', 'Action Required Immediately!')
|
||||
: isUrgent
|
||||
? t('quota.modal.titleUrgent', 'Action Required Soon')
|
||||
: t('quota.modal.title', 'Quota Exceeded')
|
||||
}
|
||||
</h2>
|
||||
<p className={`text-sm ${
|
||||
isCritical || isUrgent
|
||||
? 'text-white/90'
|
||||
: 'text-amber-700 dark:text-amber-200'
|
||||
}`}>
|
||||
{mostUrgent.days_remaining <= 0
|
||||
? t('quota.modal.subtitleExpired', 'Grace period has expired')
|
||||
: mostUrgent.days_remaining === 1
|
||||
? t('quota.modal.subtitleOneDay', '1 day remaining')
|
||||
: t('quota.modal.subtitle', '{{days}} days remaining', { days: mostUrgent.days_remaining })
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isCritical || isUrgent
|
||||
? 'hover:bg-white/20 text-white'
|
||||
: 'hover:bg-amber-200 dark:hover:bg-amber-800 text-amber-700 dark:text-amber-300'
|
||||
}`}
|
||||
aria-label={t('common.close', 'Close')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-5 space-y-5">
|
||||
{/* Main message */}
|
||||
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-gray-500 dark:text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="font-medium mb-1">
|
||||
{t('quota.modal.gracePeriodEnds', 'Grace period ends on {{date}}', {
|
||||
date: formatDate(mostUrgent.grace_period_ends_at)
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('quota.modal.explanation',
|
||||
'Your account has exceeded its plan limits. Please remove or archive excess items before the grace period ends, or they will be automatically archived.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overage list */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{t('quota.modal.overagesTitle', 'Items Over Quota')}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{overages.map((overage) => (
|
||||
<div
|
||||
key={overage.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${
|
||||
overage.days_remaining <= 1
|
||||
? 'border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20'
|
||||
: overage.days_remaining <= 7
|
||||
? 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
overage.days_remaining <= 1
|
||||
? 'bg-red-100 dark:bg-red-800/50 text-red-600 dark:text-red-400'
|
||||
: overage.days_remaining <= 7
|
||||
? 'bg-amber-100 dark:bg-amber-800/50 text-amber-600 dark:text-amber-400'
|
||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300'
|
||||
}`}>
|
||||
{QUOTA_ICONS[overage.quota_type] || <Layers className="w-5 h-5" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{overage.display_name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('quota.modal.usageInfo', '{{current}} used / {{limit}} allowed', {
|
||||
current: overage.current_usage,
|
||||
limit: overage.allowed_limit
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`font-bold ${
|
||||
overage.days_remaining <= 1
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: overage.days_remaining <= 7
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-gray-600 dark:text-gray-300'
|
||||
}`}>
|
||||
+{overage.overage_amount}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('quota.modal.overLimit', 'over limit')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* What happens section */}
|
||||
<div className="flex items-start gap-3 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<Archive className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">
|
||||
{t('quota.modal.whatHappens', 'What happens if I don\'t take action?')}
|
||||
</p>
|
||||
<p>
|
||||
{t('quota.modal.autoArchiveExplanation',
|
||||
'After the grace period ends, the oldest items over your limit will be automatically archived. Archived items remain in your account but cannot be used until you upgrade or remove other items.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between gap-3">
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
{t('quota.modal.dismissButton', 'Remind Me Later')}
|
||||
</button>
|
||||
<Link
|
||||
to="/settings/quota"
|
||||
onClick={handleDismiss}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{t('quota.modal.manageButton', 'Manage Quota')}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuotaOverageModal;
|
||||
|
||||
/**
|
||||
* Clear the session storage dismissal flag
|
||||
* Call this when user logs out or masquerade changes
|
||||
*/
|
||||
export const resetQuotaOverageModalDismissal = () => {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||
};
|
||||
@@ -119,6 +119,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
icon={Clock}
|
||||
label={t('nav.tasks', 'Tasks')}
|
||||
isCollapsed={isCollapsed}
|
||||
locked={!canUse('plugins') || !canUse('tasks')}
|
||||
/>
|
||||
</SidebarSection>
|
||||
|
||||
@@ -193,7 +194,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
{canViewAdminPages && (
|
||||
<SidebarSection title={t('nav.sections.extend', 'Extend')} isCollapsed={isCollapsed}>
|
||||
<SidebarItem
|
||||
to="/plugins/marketplace"
|
||||
to="/plugins/my-plugins"
|
||||
icon={Plug}
|
||||
label={t('nav.plugins', 'Plugins')}
|
||||
isCollapsed={isCollapsed}
|
||||
|
||||
140
frontend/src/components/dashboard/CapacityWidget.tsx
Normal file
140
frontend/src/components/dashboard/CapacityWidget.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Users, User } from 'lucide-react';
|
||||
import { Appointment, Resource } from '../../types';
|
||||
import { startOfWeek, endOfWeek, isWithinInterval } from 'date-fns';
|
||||
|
||||
interface CapacityWidgetProps {
|
||||
appointments: Appointment[];
|
||||
resources: Resource[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const CapacityWidget: React.FC<CapacityWidgetProps> = ({
|
||||
appointments,
|
||||
resources,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const capacityData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||
const weekEnd = endOfWeek(now, { weekStartsOn: 1 });
|
||||
|
||||
// Calculate for each resource
|
||||
const resourceStats = resources.map((resource) => {
|
||||
// Filter appointments for this resource this week
|
||||
const resourceAppointments = appointments.filter(
|
||||
(appt) =>
|
||||
appt.resourceId === resource.id &&
|
||||
isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }) &&
|
||||
appt.status !== 'CANCELLED'
|
||||
);
|
||||
|
||||
// Calculate total booked minutes
|
||||
const bookedMinutes = resourceAppointments.reduce(
|
||||
(sum, appt) => sum + appt.durationMinutes,
|
||||
0
|
||||
);
|
||||
|
||||
// Assume 8 hours/day, 5 days/week = 2400 minutes capacity
|
||||
const totalCapacityMinutes = 8 * 60 * 5;
|
||||
const utilization = Math.min((bookedMinutes / totalCapacityMinutes) * 100, 100);
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
utilization: Math.round(utilization),
|
||||
bookedHours: Math.round(bookedMinutes / 60),
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate overall utilization
|
||||
const totalBooked = resourceStats.reduce((sum, r) => sum + r.bookedHours, 0);
|
||||
const totalCapacity = resources.length * 40; // 40 hours/week per resource
|
||||
const overallUtilization = totalCapacity > 0 ? Math.round((totalBooked / totalCapacity) * 100) : 0;
|
||||
|
||||
return {
|
||||
overall: overallUtilization,
|
||||
resources: resourceStats.sort((a, b) => b.utilization - a.utilization),
|
||||
};
|
||||
}, [appointments, resources]);
|
||||
|
||||
const getUtilizationColor = (utilization: number) => {
|
||||
if (utilization >= 80) return 'bg-green-500';
|
||||
if (utilization >= 50) return 'bg-yellow-500';
|
||||
return 'bg-gray-300 dark:bg-gray-600';
|
||||
};
|
||||
|
||||
const getUtilizationTextColor = (utilization: number) => {
|
||||
if (utilization >= 80) return 'text-green-600 dark:text-green-400';
|
||||
if (utilization >= 50) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-gray-500 dark:text-gray-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center justify-between mb-3 ${isEditing ? 'pl-5' : ''}`}>
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
Capacity This Week
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users size={14} className="text-gray-400" />
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{capacityData.overall}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{capacityData.resources.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<Users size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No resources configured</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 grid grid-cols-2 gap-2 auto-rows-min">
|
||||
{capacityData.resources.map((resource) => (
|
||||
<div
|
||||
key={resource.id}
|
||||
className="p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<User size={12} className="text-gray-400 flex-shrink-0" />
|
||||
<span className="text-xs text-gray-600 dark:text-gray-300 truncate">
|
||||
{resource.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getUtilizationColor(resource.utilization)} transition-all duration-300`}
|
||||
style={{ width: `${resource.utilization}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold ${getUtilizationTextColor(resource.utilization)}`}>
|
||||
{resource.utilization}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapacityWidget;
|
||||
105
frontend/src/components/dashboard/ChartWidget.tsx
Normal file
105
frontend/src/components/dashboard/ChartWidget.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
} from 'recharts';
|
||||
import { GripVertical, X } from 'lucide-react';
|
||||
|
||||
interface ChartData {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ChartWidgetProps {
|
||||
title: string;
|
||||
data: ChartData[];
|
||||
type: 'bar' | 'line';
|
||||
color?: string;
|
||||
valuePrefix?: string;
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const ChartWidget: React.FC<ChartWidgetProps> = ({
|
||||
title,
|
||||
data,
|
||||
type,
|
||||
color = '#3b82f6',
|
||||
valuePrefix = '',
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const formatValue = (value: number) => `${valuePrefix}${value}`;
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{type === 'bar' ? (
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tickFormatter={formatValue} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(107, 114, 128, 0.1)' }}
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
formatter={(value: number) => [formatValue(value), title]}
|
||||
/>
|
||||
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
) : (
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
formatter={(value: number) => [value, title]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="value" stroke={color} strokeWidth={3} dot={{ r: 4, fill: color }} />
|
||||
</LineChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartWidget;
|
||||
134
frontend/src/components/dashboard/CustomerBreakdownWidget.tsx
Normal file
134
frontend/src/components/dashboard/CustomerBreakdownWidget.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Users, UserPlus, UserCheck } from 'lucide-react';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { Customer } from '../../types';
|
||||
|
||||
interface CustomerBreakdownWidgetProps {
|
||||
customers: Customer[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
|
||||
customers,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const breakdownData = useMemo(() => {
|
||||
// Customers with lastVisit are returning, without are new
|
||||
const returning = customers.filter((c) => c.lastVisit !== null).length;
|
||||
const newCustomers = customers.filter((c) => c.lastVisit === null).length;
|
||||
const total = customers.length;
|
||||
|
||||
return {
|
||||
new: newCustomers,
|
||||
returning,
|
||||
total,
|
||||
newPercentage: total > 0 ? Math.round((newCustomers / total) * 100) : 0,
|
||||
returningPercentage: total > 0 ? Math.round((returning / total) * 100) : 0,
|
||||
chartData: [
|
||||
{ name: 'New', value: newCustomers, color: '#8b5cf6' },
|
||||
{ name: 'Returning', value: returning, color: '#10b981' },
|
||||
],
|
||||
};
|
||||
}, [customers]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-base font-semibold text-gray-900 dark:text-white mb-2 ${isEditing ? 'pl-5' : ''}`}>
|
||||
Customers This Month
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 flex items-center gap-3 min-h-0">
|
||||
{/* Pie Chart */}
|
||||
<div className="w-20 h-20 flex-shrink-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={breakdownData.chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={20}
|
||||
outerRadius={35}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{breakdownData.chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 rounded-lg bg-purple-100 dark:bg-purple-900/30">
|
||||
<UserPlus size={12} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">New</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{breakdownData.new}{' '}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
({breakdownData.newPercentage}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 rounded-lg bg-green-100 dark:bg-green-900/30">
|
||||
<UserCheck size={12} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Returning</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{breakdownData.returning}{' '}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
({breakdownData.returningPercentage}%)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-gray-100 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
||||
<Users size={12} />
|
||||
<span>Total Customers</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{breakdownData.total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerBreakdownWidget;
|
||||
90
frontend/src/components/dashboard/MetricWidget.tsx
Normal file
90
frontend/src/components/dashboard/MetricWidget.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp, TrendingDown, Minus, GripVertical, X } from 'lucide-react';
|
||||
|
||||
interface GrowthData {
|
||||
weekly: { value: number; change: number };
|
||||
monthly: { value: number; change: number };
|
||||
}
|
||||
|
||||
interface MetricWidgetProps {
|
||||
title: string;
|
||||
value: number | string;
|
||||
growth: GrowthData;
|
||||
icon?: React.ReactNode;
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const MetricWidget: React.FC<MetricWidgetProps> = ({
|
||||
title,
|
||||
value,
|
||||
growth,
|
||||
icon,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const formatChange = (change: number) => {
|
||||
if (change === 0) return '0%';
|
||||
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const getTrendIcon = (change: number) => {
|
||||
if (change > 0) return <TrendingUp size={12} className="mr-1" />;
|
||||
if (change < 0) return <TrendingDown size={12} className="mr-1" />;
|
||||
return <Minus size={12} className="mr-1" />;
|
||||
};
|
||||
|
||||
const getTrendClass = (change: number) => {
|
||||
if (change > 0) return 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400';
|
||||
if (change < 0) return 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400';
|
||||
return 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={isEditing ? 'pl-5' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{icon && <span className="text-brand-500">{icon}</span>}
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
|
||||
{value}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.weekly.change)}`}>
|
||||
{getTrendIcon(growth.weekly.change)}
|
||||
{formatChange(growth.weekly.change)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(growth.monthly.change)}`}>
|
||||
{getTrendIcon(growth.monthly.change)}
|
||||
{formatChange(growth.monthly.change)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricWidget;
|
||||
144
frontend/src/components/dashboard/NoShowRateWidget.tsx
Normal file
144
frontend/src/components/dashboard/NoShowRateWidget.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, UserX, TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
import { Appointment } from '../../types';
|
||||
import { subDays, subMonths, isAfter } from 'date-fns';
|
||||
|
||||
interface NoShowRateWidgetProps {
|
||||
appointments: Appointment[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const NoShowRateWidget: React.FC<NoShowRateWidgetProps> = ({
|
||||
appointments,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const noShowData = useMemo(() => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = subDays(now, 7);
|
||||
const twoWeeksAgo = subDays(now, 14);
|
||||
const oneMonthAgo = subMonths(now, 1);
|
||||
const twoMonthsAgo = subMonths(now, 2);
|
||||
|
||||
// Calculate rates for different periods
|
||||
const calculateRate = (appts: Appointment[]) => {
|
||||
const completed = appts.filter(
|
||||
(a) => a.status === 'COMPLETED' || a.status === 'NO_SHOW' || a.status === 'CANCELLED'
|
||||
);
|
||||
const noShows = completed.filter((a) => a.status === 'NO_SHOW');
|
||||
return completed.length > 0 ? (noShows.length / completed.length) * 100 : 0;
|
||||
};
|
||||
|
||||
// Current week
|
||||
const thisWeekAppts = appointments.filter((a) => isAfter(new Date(a.startTime), oneWeekAgo));
|
||||
const currentWeekRate = calculateRate(thisWeekAppts);
|
||||
|
||||
// Last week
|
||||
const lastWeekAppts = appointments.filter(
|
||||
(a) => isAfter(new Date(a.startTime), twoWeeksAgo) && !isAfter(new Date(a.startTime), oneWeekAgo)
|
||||
);
|
||||
const lastWeekRate = calculateRate(lastWeekAppts);
|
||||
|
||||
// Current month
|
||||
const thisMonthAppts = appointments.filter((a) => isAfter(new Date(a.startTime), oneMonthAgo));
|
||||
const currentMonthRate = calculateRate(thisMonthAppts);
|
||||
|
||||
// Last month
|
||||
const lastMonthAppts = appointments.filter(
|
||||
(a) => isAfter(new Date(a.startTime), twoMonthsAgo) && !isAfter(new Date(a.startTime), oneMonthAgo)
|
||||
);
|
||||
const lastMonthRate = calculateRate(lastMonthAppts);
|
||||
|
||||
// Calculate changes (negative is good for no-show rate)
|
||||
const weeklyChange = lastWeekRate !== 0 ? ((currentWeekRate - lastWeekRate) / lastWeekRate) * 100 : 0;
|
||||
const monthlyChange = lastMonthRate !== 0 ? ((currentMonthRate - lastMonthRate) / lastMonthRate) * 100 : 0;
|
||||
|
||||
// Count total no-shows this month
|
||||
const noShowsThisMonth = thisMonthAppts.filter((a) => a.status === 'NO_SHOW').length;
|
||||
|
||||
return {
|
||||
currentRate: currentMonthRate,
|
||||
noShowCount: noShowsThisMonth,
|
||||
weeklyChange,
|
||||
monthlyChange,
|
||||
};
|
||||
}, [appointments]);
|
||||
|
||||
const formatChange = (change: number) => {
|
||||
if (change === 0) return '0%';
|
||||
return change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
// For no-show rate, down is good (green), up is bad (red)
|
||||
const getTrendIcon = (change: number) => {
|
||||
if (change < 0) return <TrendingDown size={12} className="mr-1" />;
|
||||
if (change > 0) return <TrendingUp size={12} className="mr-1" />;
|
||||
return <Minus size={12} className="mr-1" />;
|
||||
};
|
||||
|
||||
const getTrendClass = (change: number) => {
|
||||
if (change < 0) return 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400';
|
||||
if (change > 0) return 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400';
|
||||
return 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300';
|
||||
};
|
||||
|
||||
const getRateColor = (rate: number) => {
|
||||
if (rate <= 5) return 'text-green-600 dark:text-green-400';
|
||||
if (rate <= 10) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={isEditing ? 'pl-5' : ''}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<UserX size={18} className="text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">No-Show Rate</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className={`text-2xl font-bold ${getRateColor(noShowData.currentRate)}`}>
|
||||
{noShowData.currentRate.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({noShowData.noShowCount} this month)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs mt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Week:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.weeklyChange)}`}>
|
||||
{getTrendIcon(noShowData.weeklyChange)}
|
||||
{formatChange(noShowData.weeklyChange)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-500 dark:text-gray-400">Month:</span>
|
||||
<span className={`flex items-center px-1.5 py-0.5 rounded-full ${getTrendClass(noShowData.monthlyChange)}`}>
|
||||
{getTrendIcon(noShowData.monthlyChange)}
|
||||
{formatChange(noShowData.monthlyChange)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoShowRateWidget;
|
||||
121
frontend/src/components/dashboard/OpenTicketsWidget.tsx
Normal file
121
frontend/src/components/dashboard/OpenTicketsWidget.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GripVertical, X, AlertCircle, Clock, ChevronRight } from 'lucide-react';
|
||||
import { Ticket } from '../../types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
interface OpenTicketsWidgetProps {
|
||||
tickets: Ticket[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const OpenTicketsWidget: React.FC<OpenTicketsWidgetProps> = ({
|
||||
tickets,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const openTickets = tickets.filter(t => t.status === 'open' || t.status === 'in_progress');
|
||||
const urgentCount = openTickets.filter(t => t.priority === 'urgent' || t.isOverdue).length;
|
||||
|
||||
const getPriorityColor = (priority: string, isOverdue?: boolean) => {
|
||||
if (isOverdue) return 'text-red-600 dark:text-red-400';
|
||||
switch (priority) {
|
||||
case 'urgent': return 'text-red-600 dark:text-red-400';
|
||||
case 'high': return 'text-orange-600 dark:text-orange-400';
|
||||
case 'medium': return 'text-yellow-600 dark:text-yellow-400';
|
||||
default: return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBg = (priority: string, isOverdue?: boolean) => {
|
||||
if (isOverdue) return 'bg-red-50 dark:bg-red-900/20';
|
||||
switch (priority) {
|
||||
case 'urgent': return 'bg-red-50 dark:bg-red-900/20';
|
||||
case 'high': return 'bg-orange-50 dark:bg-orange-900/20';
|
||||
case 'medium': return 'bg-yellow-50 dark:bg-yellow-900/20';
|
||||
default: return 'bg-gray-50 dark:bg-gray-700/50';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`flex items-center justify-between mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Open Tickets
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{urgentCount > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400">
|
||||
<AlertCircle size={12} />
|
||||
{urgentCount} urgent
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{openTickets.length} open
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{openTickets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<AlertCircle size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No open tickets</p>
|
||||
</div>
|
||||
) : (
|
||||
openTickets.slice(0, 5).map((ticket) => (
|
||||
<Link
|
||||
key={ticket.id}
|
||||
to="/tickets"
|
||||
className={`block p-3 rounded-lg ${getPriorityBg(ticket.priority, ticket.isOverdue)} hover:opacity-80 transition-opacity`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{ticket.subject}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className={getPriorityColor(ticket.priority, ticket.isOverdue)}>
|
||||
{ticket.isOverdue ? 'Overdue' : ticket.priority}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={10} />
|
||||
{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-400 flex-shrink-0" />
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openTickets.length > 5 && (
|
||||
<Link
|
||||
to="/tickets"
|
||||
className="mt-3 text-sm text-brand-600 dark:text-brand-400 hover:underline text-center"
|
||||
>
|
||||
View all {openTickets.length} tickets
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenTicketsWidget;
|
||||
144
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
144
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GripVertical, X, Calendar, UserPlus, XCircle, CheckCircle, DollarSign } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Appointment, Customer } from '../../types';
|
||||
|
||||
interface ActivityItem {
|
||||
id: string;
|
||||
type: 'booking' | 'cancellation' | 'completion' | 'new_customer' | 'payment';
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
icon: React.ReactNode;
|
||||
iconBg: string;
|
||||
}
|
||||
|
||||
interface RecentActivityWidgetProps {
|
||||
appointments: Appointment[];
|
||||
customers: Customer[];
|
||||
isEditing?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const RecentActivityWidget: React.FC<RecentActivityWidgetProps> = ({
|
||||
appointments,
|
||||
customers,
|
||||
isEditing,
|
||||
onRemove,
|
||||
}) => {
|
||||
const activities = useMemo(() => {
|
||||
const items: ActivityItem[] = [];
|
||||
|
||||
// Add appointments as activity
|
||||
appointments.forEach((appt) => {
|
||||
const timestamp = new Date(appt.startTime);
|
||||
|
||||
if (appt.status === 'CONFIRMED' || appt.status === 'PENDING') {
|
||||
items.push({
|
||||
id: `booking-${appt.id}`,
|
||||
type: 'booking',
|
||||
title: 'New Booking',
|
||||
description: `${appt.customerName} booked an appointment`,
|
||||
timestamp,
|
||||
icon: <Calendar size={14} />,
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
|
||||
});
|
||||
} else if (appt.status === 'CANCELLED') {
|
||||
items.push({
|
||||
id: `cancel-${appt.id}`,
|
||||
type: 'cancellation',
|
||||
title: 'Cancellation',
|
||||
description: `${appt.customerName} cancelled their appointment`,
|
||||
timestamp,
|
||||
icon: <XCircle size={14} />,
|
||||
iconBg: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
||||
});
|
||||
} else if (appt.status === 'COMPLETED') {
|
||||
items.push({
|
||||
id: `complete-${appt.id}`,
|
||||
type: 'completion',
|
||||
title: 'Completed',
|
||||
description: `${appt.customerName}'s appointment completed`,
|
||||
timestamp,
|
||||
icon: <CheckCircle size={14} />,
|
||||
iconBg: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add recent customers (those with no lastVisit are new)
|
||||
customers
|
||||
.filter(c => !c.lastVisit)
|
||||
.slice(0, 5)
|
||||
.forEach((customer) => {
|
||||
items.push({
|
||||
id: `customer-${customer.id}`,
|
||||
type: 'new_customer',
|
||||
title: 'New Customer',
|
||||
description: `${customer.name} signed up`,
|
||||
timestamp: new Date(), // Approximate - would need createdAt field
|
||||
icon: <UserPlus size={14} />,
|
||||
iconBg: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by timestamp descending
|
||||
items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
|
||||
return items.slice(0, 10);
|
||||
}, [appointments, customers]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
|
||||
{isEditing && (
|
||||
<>
|
||||
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white mb-4 ${isEditing ? 'pl-5' : ''}`}>
|
||||
Recent Activity
|
||||
</h3>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activities.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
||||
<Calendar size={32} className="mb-2 opacity-50" />
|
||||
<p className="text-sm">No recent activity</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{activities.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3">
|
||||
<div className={`p-1.5 rounded-lg ${activity.iconBg} flex-shrink-0`}>
|
||||
{activity.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{activity.title}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{activity.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
|
||||
{formatDistanceToNow(activity.timestamp, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentActivityWidget;
|
||||
131
frontend/src/components/dashboard/WidgetConfigModal.tsx
Normal file
131
frontend/src/components/dashboard/WidgetConfigModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { X, Plus, Check, LayoutDashboard, BarChart2, Ticket, Activity, Users, UserX, PieChart } from 'lucide-react';
|
||||
import { WIDGET_DEFINITIONS, WidgetType } from './types';
|
||||
|
||||
interface WidgetConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
activeWidgets: string[];
|
||||
onToggleWidget: (widgetId: string) => void;
|
||||
onResetLayout: () => void;
|
||||
}
|
||||
|
||||
const WIDGET_ICONS: Record<WidgetType, React.ReactNode> = {
|
||||
'appointments-metric': <LayoutDashboard size={18} />,
|
||||
'customers-metric': <Users size={18} />,
|
||||
'services-metric': <LayoutDashboard size={18} />,
|
||||
'resources-metric': <LayoutDashboard size={18} />,
|
||||
'revenue-chart': <BarChart2 size={18} />,
|
||||
'appointments-chart': <BarChart2 size={18} />,
|
||||
'open-tickets': <Ticket size={18} />,
|
||||
'recent-activity': <Activity size={18} />,
|
||||
'capacity-utilization': <Users size={18} />,
|
||||
'no-show-rate': <UserX size={18} />,
|
||||
'customer-breakdown': <PieChart size={18} />,
|
||||
};
|
||||
|
||||
const WidgetConfigModal: React.FC<WidgetConfigModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
activeWidgets,
|
||||
onToggleWidget,
|
||||
onResetLayout,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const widgets = Object.values(WIDGET_DEFINITIONS);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Configure Dashboard Widgets
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
Select which widgets to show on your dashboard. You can drag widgets to reposition them.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{widgets.map((widget) => {
|
||||
const isActive = activeWidgets.includes(widget.id);
|
||||
return (
|
||||
<button
|
||||
key={widget.id}
|
||||
onClick={() => onToggleWidget(widget.id)}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors text-left ${
|
||||
isActive
|
||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`p-2 rounded-lg ${
|
||||
isActive
|
||||
? 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{WIDGET_ICONS[widget.type]}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
isActive
|
||||
? 'text-brand-700 dark:text-brand-300'
|
||||
: 'text-gray-900 dark:text-white'
|
||||
}`}
|
||||
>
|
||||
{widget.title}
|
||||
</p>
|
||||
{isActive && (
|
||||
<Check size={14} className="text-brand-600 dark:text-brand-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{widget.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={onResetLayout}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetConfigModal;
|
||||
9
frontend/src/components/dashboard/index.ts
Normal file
9
frontend/src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './types';
|
||||
export { default as MetricWidget } from './MetricWidget';
|
||||
export { default as ChartWidget } from './ChartWidget';
|
||||
export { default as OpenTicketsWidget } from './OpenTicketsWidget';
|
||||
export { default as RecentActivityWidget } from './RecentActivityWidget';
|
||||
export { default as CapacityWidget } from './CapacityWidget';
|
||||
export { default as NoShowRateWidget } from './NoShowRateWidget';
|
||||
export { default as CustomerBreakdownWidget } from './CustomerBreakdownWidget';
|
||||
export { default as WidgetConfigModal } from './WidgetConfigModal';
|
||||
146
frontend/src/components/dashboard/types.ts
Normal file
146
frontend/src/components/dashboard/types.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
export type WidgetType =
|
||||
| 'appointments-metric'
|
||||
| 'customers-metric'
|
||||
| 'services-metric'
|
||||
| 'resources-metric'
|
||||
| 'revenue-chart'
|
||||
| 'appointments-chart'
|
||||
| 'open-tickets'
|
||||
| 'recent-activity'
|
||||
| 'capacity-utilization'
|
||||
| 'no-show-rate'
|
||||
| 'customer-breakdown';
|
||||
|
||||
export interface WidgetConfig {
|
||||
id: string;
|
||||
type: WidgetType;
|
||||
title: string;
|
||||
description: string;
|
||||
defaultSize: { w: number; h: number };
|
||||
minSize?: { w: number; h: number };
|
||||
}
|
||||
|
||||
export interface DashboardLayout {
|
||||
widgets: string[]; // Widget IDs that are visible
|
||||
layout: Layout[];
|
||||
}
|
||||
|
||||
// Widget definitions with metadata
|
||||
export const WIDGET_DEFINITIONS: Record<WidgetType, WidgetConfig> = {
|
||||
'appointments-metric': {
|
||||
id: 'appointments-metric',
|
||||
type: 'appointments-metric',
|
||||
title: 'Total Appointments',
|
||||
description: 'Shows appointment count with weekly and monthly growth',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'customers-metric': {
|
||||
id: 'customers-metric',
|
||||
type: 'customers-metric',
|
||||
title: 'Active Customers',
|
||||
description: 'Shows customer count with weekly and monthly growth',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'services-metric': {
|
||||
id: 'services-metric',
|
||||
type: 'services-metric',
|
||||
title: 'Services',
|
||||
description: 'Shows number of services offered',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'resources-metric': {
|
||||
id: 'resources-metric',
|
||||
type: 'resources-metric',
|
||||
title: 'Resources',
|
||||
description: 'Shows number of resources available',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'revenue-chart': {
|
||||
id: 'revenue-chart',
|
||||
type: 'revenue-chart',
|
||||
title: 'Revenue',
|
||||
description: 'Weekly revenue bar chart',
|
||||
defaultSize: { w: 6, h: 4 },
|
||||
minSize: { w: 4, h: 3 },
|
||||
},
|
||||
'appointments-chart': {
|
||||
id: 'appointments-chart',
|
||||
type: 'appointments-chart',
|
||||
title: 'Appointments Trend',
|
||||
description: 'Weekly appointments line chart',
|
||||
defaultSize: { w: 6, h: 4 },
|
||||
minSize: { w: 4, h: 3 },
|
||||
},
|
||||
'open-tickets': {
|
||||
id: 'open-tickets',
|
||||
type: 'open-tickets',
|
||||
title: 'Open Tickets',
|
||||
description: 'Shows open support tickets requiring attention',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'recent-activity': {
|
||||
id: 'recent-activity',
|
||||
type: 'recent-activity',
|
||||
title: 'Recent Activity',
|
||||
description: 'Timeline of recent business events',
|
||||
defaultSize: { w: 4, h: 5 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'capacity-utilization': {
|
||||
id: 'capacity-utilization',
|
||||
type: 'capacity-utilization',
|
||||
title: 'Capacity Utilization',
|
||||
description: 'Shows how booked your resources are this week',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
'no-show-rate': {
|
||||
id: 'no-show-rate',
|
||||
type: 'no-show-rate',
|
||||
title: 'No-Show Rate',
|
||||
description: 'Percentage of appointments marked as no-show',
|
||||
defaultSize: { w: 3, h: 2 },
|
||||
minSize: { w: 2, h: 2 },
|
||||
},
|
||||
'customer-breakdown': {
|
||||
id: 'customer-breakdown',
|
||||
type: 'customer-breakdown',
|
||||
title: 'New vs Returning',
|
||||
description: 'Customer breakdown this month',
|
||||
defaultSize: { w: 4, h: 4 },
|
||||
minSize: { w: 3, h: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
// Default layout for new users
|
||||
export const DEFAULT_LAYOUT: DashboardLayout = {
|
||||
widgets: [
|
||||
'appointments-metric',
|
||||
'customers-metric',
|
||||
'no-show-rate',
|
||||
'revenue-chart',
|
||||
'appointments-chart',
|
||||
'open-tickets',
|
||||
'recent-activity',
|
||||
'capacity-utilization',
|
||||
'customer-breakdown',
|
||||
],
|
||||
layout: [
|
||||
{ i: 'appointments-metric', x: 0, y: 0, w: 4, h: 2 },
|
||||
{ i: 'customers-metric', x: 4, y: 0, w: 4, h: 2 },
|
||||
{ i: 'no-show-rate', x: 8, y: 0, w: 4, h: 2 },
|
||||
{ i: 'revenue-chart', x: 0, y: 2, w: 6, h: 4 },
|
||||
{ i: 'appointments-chart', x: 6, y: 2, w: 6, h: 4 },
|
||||
{ i: 'open-tickets', x: 0, y: 6, w: 4, h: 4 },
|
||||
{ i: 'recent-activity', x: 4, y: 6, w: 4, h: 4 },
|
||||
{ i: 'capacity-utilization', x: 8, y: 6, w: 4, h: 4 },
|
||||
{ i: 'customer-breakdown', x: 0, y: 10, w: 4, h: 4 },
|
||||
],
|
||||
};
|
||||
@@ -84,6 +84,7 @@ export const FEATURE_NAMES: Record<FeatureKey, string> = {
|
||||
white_label: 'White Label',
|
||||
custom_oauth: 'Custom OAuth',
|
||||
plugins: 'Custom Plugins',
|
||||
tasks: 'Scheduled Tasks',
|
||||
export_data: 'Data Export',
|
||||
video_conferencing: 'Video Conferencing',
|
||||
two_factor_auth: 'Two-Factor Authentication',
|
||||
@@ -103,6 +104,7 @@ export const FEATURE_DESCRIPTIONS: Record<FeatureKey, string> = {
|
||||
white_label: 'Remove SmoothSchedule branding and use your own',
|
||||
custom_oauth: 'Configure your own OAuth credentials for social login',
|
||||
plugins: 'Create custom plugins to extend functionality',
|
||||
tasks: 'Create scheduled tasks to automate plugin execution',
|
||||
export_data: 'Export your data to CSV or other formats',
|
||||
video_conferencing: 'Add video conferencing links to appointments',
|
||||
two_factor_auth: 'Require two-factor authentication for enhanced security',
|
||||
|
||||
@@ -243,3 +243,15 @@ export const useSyncPlansWithStripe = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to sync a plan's permissions to all tenants on that plan
|
||||
*/
|
||||
export const useSyncPlanToTenants = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (planId: number) => {
|
||||
const { data } = await apiClient.post(`/platform/subscription-plans/${planId}/sync_tenants/`);
|
||||
return data as { message: string; tenant_count: number };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Hook to scroll to top on route changes
|
||||
* Should be used in layout components to ensure scroll restoration
|
||||
* works consistently across all routes
|
||||
*
|
||||
* @param containerRef - Optional ref to a scrollable container element.
|
||||
* If provided, scrolls that element instead of window.
|
||||
*/
|
||||
export function useScrollToTop() {
|
||||
export function useScrollToTop(containerRef?: RefObject<HTMLElement | null>) {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
if (containerRef?.current) {
|
||||
containerRef.current.scrollTo(0, 0);
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [pathname, containerRef]);
|
||||
}
|
||||
|
||||
@@ -33,3 +33,96 @@ body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* React Grid Layout Dashboard Styling */
|
||||
.layout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.widget-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.widget-container > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Drag handle styling */
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* React Grid Layout overrides */
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .react-grid-item > .react-resizable-handle::after {
|
||||
border-right-color: rgba(255, 255, 255, 0.3);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Business, User } from '../types';
|
||||
import MasqueradeBanner from '../components/MasqueradeBanner';
|
||||
import OnboardingWizard from '../components/OnboardingWizard';
|
||||
import TicketModal from '../components/TicketModal';
|
||||
import FloatingHelpButton from '../components/FloatingHelpButton';
|
||||
import { useStopMasquerade } from '../hooks/useAuth';
|
||||
import { useNotificationWebSocket } from '../hooks/useNotificationWebSocket';
|
||||
import { useTicket } from '../hooks/useTickets';
|
||||
@@ -51,7 +52,7 @@ const BusinessLayoutContent: React.FC<BusinessLayoutProps> = ({ business, user,
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Fetch ticket data when modal is opened from notification
|
||||
const { data: ticketFromNotification } = useTicket(ticketModalId || undefined);
|
||||
@@ -167,6 +168,9 @@ const BusinessLayoutContent: React.FC<BusinessLayoutProps> = ({ business, user,
|
||||
|
||||
return (
|
||||
<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`}>
|
||||
<Sidebar business={business} user={user} isCollapsed={false} toggleCollapse={() => { }} />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Outlet, Link, useNavigate } from 'react-router-dom';
|
||||
import { User, Business } from '../types';
|
||||
import { LayoutDashboard, CalendarPlus, CreditCard, HelpCircle, Sun, Moon } from 'lucide-react';
|
||||
@@ -18,7 +18,8 @@ interface CustomerLayoutProps {
|
||||
|
||||
const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMode, toggleTheme }) => {
|
||||
const navigate = useNavigate();
|
||||
useScrollToTop();
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Masquerade logic
|
||||
const [masqueradeStack, setMasqueradeStack] = useState<MasqueradeStackEntry[]>([]);
|
||||
@@ -116,7 +117,7 @@ const CustomerLayout: React.FC<CustomerLayoutProps> = ({ business, user, darkMod
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-y-auto">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Outlet context={{ business, user }} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Moon, Sun, Bell, Globe, Menu } from 'lucide-react';
|
||||
import { User } from '../types';
|
||||
@@ -16,8 +16,9 @@ interface ManagerLayoutProps {
|
||||
const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleTheme, onSignOut }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
return (
|
||||
<div className="flex h-full bg-gray-100 dark:bg-gray-900">
|
||||
@@ -63,7 +64,7 @@ const ManagerLayout: React.FC<ManagerLayoutProps> = ({ user, darkMode, toggleThe
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Moon, Sun, Globe, Menu } from 'lucide-react';
|
||||
import { User } from '../types';
|
||||
@@ -6,6 +6,7 @@ import PlatformSidebar from '../components/PlatformSidebar';
|
||||
import UserProfileDropdown from '../components/UserProfileDropdown';
|
||||
import NotificationDropdown from '../components/NotificationDropdown';
|
||||
import TicketModal from '../components/TicketModal';
|
||||
import FloatingHelpButton from '../components/FloatingHelpButton';
|
||||
import { useTicket } from '../hooks/useTickets';
|
||||
import { useScrollToTop } from '../hooks/useScrollToTop';
|
||||
|
||||
@@ -20,8 +21,9 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [ticketModalId, setTicketModalId] = useState<string | null>(null);
|
||||
const mainContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useScrollToTop();
|
||||
useScrollToTop(mainContentRef);
|
||||
|
||||
// Fetch ticket data when modal is opened from notification
|
||||
const { data: ticketFromNotification } = useTicket(ticketModalId && ticketModalId !== 'undefined' ? ticketModalId : undefined);
|
||||
@@ -36,6 +38,9 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100 dark:bg-gray-900">
|
||||
{/* Floating Help Button */}
|
||||
<FloatingHelpButton />
|
||||
|
||||
{/* 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`}>
|
||||
<PlatformSidebar user={user} isCollapsed={false} toggleCollapse={() => { }} />
|
||||
@@ -79,7 +84,7 @@ const PlatformLayout: React.FC<PlatformLayoutProps> = ({ user, darkMode, toggleT
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<main ref={mainContentRef} className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
568
frontend/src/pages/CreatePlugin.tsx
Normal file
568
frontend/src/pages/CreatePlugin.tsx
Normal 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;
|
||||
@@ -1,28 +1,31 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line
|
||||
} from 'recharts';
|
||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
import GridLayout, { Layout } from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { Settings, Calendar, Users, Briefcase, ClipboardList, Edit2, Check } from 'lucide-react';
|
||||
import { useServices } from '../hooks/useServices';
|
||||
import { useResources } from '../hooks/useResources';
|
||||
import { useAppointments } from '../hooks/useAppointments';
|
||||
import { useCustomers } from '../hooks/useCustomers';
|
||||
import { useTickets } from '../hooks/useTickets';
|
||||
import { subDays, subMonths, isAfter, startOfWeek, endOfWeek, isWithinInterval } from 'date-fns';
|
||||
import {
|
||||
MetricWidget,
|
||||
ChartWidget,
|
||||
OpenTicketsWidget,
|
||||
RecentActivityWidget,
|
||||
CapacityWidget,
|
||||
NoShowRateWidget,
|
||||
CustomerBreakdownWidget,
|
||||
WidgetConfigModal,
|
||||
WIDGET_DEFINITIONS,
|
||||
DEFAULT_LAYOUT,
|
||||
DashboardLayout,
|
||||
WidgetType,
|
||||
} from '../components/dashboard';
|
||||
|
||||
interface Metric {
|
||||
label: string;
|
||||
value: string;
|
||||
trend: 'up' | 'down' | 'neutral';
|
||||
change: string;
|
||||
}
|
||||
const STORAGE_KEY = 'dashboard_layout';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -30,76 +33,311 @@ const Dashboard: React.FC = () => {
|
||||
const { data: resources, isLoading: resourcesLoading } = useResources();
|
||||
const { data: appointments, isLoading: appointmentsLoading } = useAppointments();
|
||||
const { data: customers, isLoading: customersLoading } = useCustomers();
|
||||
const { data: tickets, isLoading: ticketsLoading } = useTickets();
|
||||
|
||||
const isLoading = servicesLoading || resourcesLoading || appointmentsLoading || customersLoading;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [dashboardLayout, setDashboardLayout] = useState<DashboardLayout>(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
return JSON.parse(saved);
|
||||
} catch {
|
||||
return DEFAULT_LAYOUT;
|
||||
}
|
||||
}
|
||||
return DEFAULT_LAYOUT;
|
||||
});
|
||||
|
||||
// Calculate metrics from real data
|
||||
const metrics: Metric[] = useMemo(() => {
|
||||
// Save layout to localStorage when it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(dashboardLayout));
|
||||
}, [dashboardLayout]);
|
||||
|
||||
const isLoading = servicesLoading || resourcesLoading || appointmentsLoading || customersLoading || ticketsLoading;
|
||||
|
||||
// Calculate growth percentages
|
||||
const calculateGrowth = useCallback((
|
||||
items: any[],
|
||||
dateField: string,
|
||||
filterFn?: (item: any) => boolean
|
||||
) => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = subDays(now, 7);
|
||||
const twoWeeksAgo = subDays(now, 14);
|
||||
const oneMonthAgo = subMonths(now, 1);
|
||||
const twoMonthsAgo = subMonths(now, 2);
|
||||
|
||||
const filteredItems = filterFn ? items.filter(filterFn) : items;
|
||||
|
||||
const thisWeek = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, oneWeekAgo);
|
||||
}).length;
|
||||
|
||||
const lastWeek = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, twoWeeksAgo) && !isAfter(date, oneWeekAgo);
|
||||
}).length;
|
||||
|
||||
const thisMonth = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, oneMonthAgo);
|
||||
}).length;
|
||||
|
||||
const lastMonth = filteredItems.filter(item => {
|
||||
const date = new Date(item[dateField]);
|
||||
return isAfter(date, twoMonthsAgo) && !isAfter(date, oneMonthAgo);
|
||||
}).length;
|
||||
|
||||
const weeklyChange = lastWeek !== 0 ? ((thisWeek - lastWeek) / lastWeek) * 100 : (thisWeek > 0 ? 100 : 0);
|
||||
const monthlyChange = lastMonth !== 0 ? ((thisMonth - lastMonth) / lastMonth) * 100 : (thisMonth > 0 ? 100 : 0);
|
||||
|
||||
return {
|
||||
weekly: { value: thisWeek, change: weeklyChange },
|
||||
monthly: { value: thisMonth, change: monthlyChange },
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Calculate metrics with real growth data
|
||||
const metrics = useMemo(() => {
|
||||
if (!appointments || !customers || !services || !resources) {
|
||||
return [
|
||||
{ label: t('dashboard.totalAppointments'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('customers.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('services.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
{ label: t('resources.title'), value: '0', trend: 'neutral', change: '0%' },
|
||||
];
|
||||
return {
|
||||
appointments: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||
customers: { count: 0, growth: { weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } } },
|
||||
services: { count: 0 },
|
||||
resources: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const activeCustomers = customers.filter(c => c.status === 'Active').length;
|
||||
const activeCustomers = customers.filter(c => c.status === 'Active');
|
||||
|
||||
return [
|
||||
{ label: t('dashboard.totalAppointments'), value: appointments.length.toString(), trend: 'up', change: '+12%' },
|
||||
{ label: t('customers.title'), value: activeCustomers.toString(), trend: 'up', change: '+8%' },
|
||||
{ label: t('services.title'), value: services.length.toString(), trend: 'neutral', change: '0%' },
|
||||
{ label: t('resources.title'), value: resources.length.toString(), trend: 'up', change: '+3%' },
|
||||
];
|
||||
}, [appointments, customers, services, resources, t]);
|
||||
return {
|
||||
appointments: {
|
||||
count: appointments.length,
|
||||
growth: calculateGrowth(appointments, 'startTime'),
|
||||
},
|
||||
customers: {
|
||||
count: activeCustomers.length,
|
||||
growth: calculateGrowth(customers, 'lastVisit', c => c.status === 'Active' && c.lastVisit),
|
||||
},
|
||||
services: { count: services.length },
|
||||
resources: { count: resources.length },
|
||||
};
|
||||
}, [appointments, customers, services, resources, calculateGrowth]);
|
||||
|
||||
// Calculate weekly data from appointments
|
||||
// Calculate weekly chart data
|
||||
const weeklyData = useMemo(() => {
|
||||
if (!appointments) {
|
||||
return [
|
||||
{ name: 'Mon', revenue: 0, appointments: 0 },
|
||||
{ name: 'Tue', revenue: 0, appointments: 0 },
|
||||
{ name: 'Wed', revenue: 0, appointments: 0 },
|
||||
{ name: 'Thu', revenue: 0, appointments: 0 },
|
||||
{ name: 'Fri', revenue: 0, appointments: 0 },
|
||||
{ name: 'Sat', revenue: 0, appointments: 0 },
|
||||
{ name: 'Sun', revenue: 0, appointments: 0 },
|
||||
];
|
||||
return { revenue: [], appointments: [] };
|
||||
}
|
||||
|
||||
// Group appointments by day of week
|
||||
const dayMap: { [key: string]: { revenue: number; count: number } } = {
|
||||
'Mon': { revenue: 0, count: 0 },
|
||||
'Tue': { revenue: 0, count: 0 },
|
||||
'Wed': { revenue: 0, count: 0 },
|
||||
'Thu': { revenue: 0, count: 0 },
|
||||
'Fri': { revenue: 0, count: 0 },
|
||||
'Sat': { revenue: 0, count: 0 },
|
||||
'Sun': { revenue: 0, count: 0 },
|
||||
const now = new Date();
|
||||
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
|
||||
const weekEnd = endOfWeek(now, { weekStartsOn: 1 });
|
||||
|
||||
const dayMap: Record<string, { revenue: number; count: number }> = {
|
||||
Mon: { revenue: 0, count: 0 },
|
||||
Tue: { revenue: 0, count: 0 },
|
||||
Wed: { revenue: 0, count: 0 },
|
||||
Thu: { revenue: 0, count: 0 },
|
||||
Fri: { revenue: 0, count: 0 },
|
||||
Sat: { revenue: 0, count: 0 },
|
||||
Sun: { revenue: 0, count: 0 },
|
||||
};
|
||||
|
||||
appointments.forEach(appt => {
|
||||
const date = new Date(appt.startTime);
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
appointments
|
||||
.filter(appt => isWithinInterval(new Date(appt.startTime), { start: weekStart, end: weekEnd }))
|
||||
.forEach(appt => {
|
||||
const date = new Date(appt.startTime);
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const dayName = dayNames[date.getDay()];
|
||||
|
||||
dayMap[dayName].count++;
|
||||
// Use price from appointment or default to 0
|
||||
dayMap[dayName].revenue += appt.price || 0;
|
||||
});
|
||||
dayMap[dayName].count++;
|
||||
dayMap[dayName].revenue += (appt as any).price || 0;
|
||||
});
|
||||
|
||||
return [
|
||||
{ name: 'Mon', revenue: dayMap['Mon'].revenue, appointments: dayMap['Mon'].count },
|
||||
{ name: 'Tue', revenue: dayMap['Tue'].revenue, appointments: dayMap['Tue'].count },
|
||||
{ name: 'Wed', revenue: dayMap['Wed'].revenue, appointments: dayMap['Wed'].count },
|
||||
{ name: 'Thu', revenue: dayMap['Thu'].revenue, appointments: dayMap['Thu'].count },
|
||||
{ name: 'Fri', revenue: dayMap['Fri'].revenue, appointments: dayMap['Fri'].count },
|
||||
{ name: 'Sat', revenue: dayMap['Sat'].revenue, appointments: dayMap['Sat'].count },
|
||||
{ name: 'Sun', revenue: dayMap['Sun'].revenue, appointments: dayMap['Sun'].count },
|
||||
];
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return {
|
||||
revenue: days.map(day => ({ name: day, value: dayMap[day].revenue })),
|
||||
appointments: days.map(day => ({ name: day, value: dayMap[day].count })),
|
||||
};
|
||||
}, [appointments]);
|
||||
|
||||
// Handle layout change
|
||||
const onLayoutChange = useCallback((newLayout: Layout[]) => {
|
||||
setDashboardLayout(prev => ({
|
||||
...prev,
|
||||
layout: newLayout,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Toggle widget visibility
|
||||
const toggleWidget = useCallback((widgetId: string) => {
|
||||
setDashboardLayout(prev => {
|
||||
const isActive = prev.widgets.includes(widgetId);
|
||||
if (isActive) {
|
||||
return {
|
||||
widgets: prev.widgets.filter(id => id !== widgetId),
|
||||
layout: prev.layout.filter(l => l.i !== widgetId),
|
||||
};
|
||||
} else {
|
||||
const widgetDef = WIDGET_DEFINITIONS[widgetId as WidgetType];
|
||||
const maxY = Math.max(0, ...prev.layout.map(l => l.y + l.h));
|
||||
return {
|
||||
widgets: [...prev.widgets, widgetId],
|
||||
layout: [
|
||||
...prev.layout,
|
||||
{
|
||||
i: widgetId,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: widgetDef.defaultSize.w,
|
||||
h: widgetDef.defaultSize.h,
|
||||
minW: widgetDef.minSize?.w,
|
||||
minH: widgetDef.minSize?.h,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Remove widget
|
||||
const removeWidget = useCallback((widgetId: string) => {
|
||||
setDashboardLayout(prev => ({
|
||||
widgets: prev.widgets.filter(id => id !== widgetId),
|
||||
layout: prev.layout.filter(l => l.i !== widgetId),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reset to default layout
|
||||
const resetLayout = useCallback(() => {
|
||||
setDashboardLayout(DEFAULT_LAYOUT);
|
||||
}, []);
|
||||
|
||||
// Render individual widget
|
||||
const renderWidget = useCallback((widgetId: string) => {
|
||||
const widgetProps = {
|
||||
isEditing,
|
||||
onRemove: () => removeWidget(widgetId),
|
||||
};
|
||||
|
||||
switch (widgetId) {
|
||||
case 'appointments-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.totalAppointments')}
|
||||
value={metrics.appointments.count}
|
||||
growth={metrics.appointments.growth}
|
||||
icon={<Calendar size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'customers-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('customers.title')}
|
||||
value={metrics.customers.count}
|
||||
growth={metrics.customers.growth}
|
||||
icon={<Users size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'services-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('services.title')}
|
||||
value={metrics.services.count}
|
||||
growth={{ weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } }}
|
||||
icon={<Briefcase size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'resources-metric':
|
||||
return (
|
||||
<MetricWidget
|
||||
key={widgetId}
|
||||
title={t('resources.title')}
|
||||
value={metrics.resources.count}
|
||||
growth={{ weekly: { value: 0, change: 0 }, monthly: { value: 0, change: 0 } }}
|
||||
icon={<ClipboardList size={18} />}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'revenue-chart':
|
||||
return (
|
||||
<ChartWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.totalRevenue')}
|
||||
data={weeklyData.revenue}
|
||||
type="bar"
|
||||
color="#3b82f6"
|
||||
valuePrefix="$"
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'appointments-chart':
|
||||
return (
|
||||
<ChartWidget
|
||||
key={widgetId}
|
||||
title={t('dashboard.upcomingAppointments')}
|
||||
data={weeklyData.appointments}
|
||||
type="line"
|
||||
color="#10b981"
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'open-tickets':
|
||||
return (
|
||||
<OpenTicketsWidget
|
||||
key={widgetId}
|
||||
tickets={tickets || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'recent-activity':
|
||||
return (
|
||||
<RecentActivityWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
customers={customers || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'capacity-utilization':
|
||||
return (
|
||||
<CapacityWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
resources={resources || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'no-show-rate':
|
||||
return (
|
||||
<NoShowRateWidget
|
||||
key={widgetId}
|
||||
appointments={appointments || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
case 'customer-breakdown':
|
||||
return (
|
||||
<CustomerBreakdownWidget
|
||||
key={widgetId}
|
||||
customers={customers || []}
|
||||
{...widgetProps}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [t, metrics, weeklyData, tickets, appointments, customers, resources, isEditing, removeWidget]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
@@ -119,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 (
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
|
||||
<div className="p-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('dashboard.title')}</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">{t('dashboard.todayOverview')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${
|
||||
isEditing
|
||||
? 'bg-brand-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isEditing ? <Check size={16} /> : <Edit2 size={16} />}
|
||||
<span className="text-sm">{isEditing ? 'Done' : 'Edit Layout'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfig(true)}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<Settings size={16} />
|
||||
<span className="text-sm">Widgets</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index} className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{metric.label}</p>
|
||||
<div className="flex items-baseline gap-2 mt-2">
|
||||
<span className="text-2xl font-bold text-gray-900 dark:text-white">{metric.value}</span>
|
||||
<span className={`flex items-center text-xs font-medium px-2 py-0.5 rounded-full ${
|
||||
metric.trend === 'up' ? 'text-green-700 bg-green-50 dark:bg-green-900/30 dark:text-green-400' :
|
||||
metric.trend === 'down' ? 'text-red-700 bg-red-50 dark:bg-red-900/30 dark:text-red-400' : 'text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}>
|
||||
{metric.trend === 'up' && <TrendingUp size={12} className="mr-1" />}
|
||||
{metric.trend === 'down' && <TrendingDown size={12} className="mr-1" />}
|
||||
{metric.trend === 'neutral' && <Minus size={12} className="mr-1" />}
|
||||
{metric.change}
|
||||
</span>
|
||||
{/* Edit mode hint */}
|
||||
{isEditing && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
Drag widgets to reposition them. Drag the corner to resize. Hover over a widget and click the X to remove it.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid Layout */}
|
||||
<div className="max-w-[1200px] mx-auto">
|
||||
<GridLayout
|
||||
className="layout"
|
||||
layout={layoutWithConstraints}
|
||||
cols={12}
|
||||
rowHeight={60}
|
||||
width={1200}
|
||||
isDraggable={isEditing}
|
||||
isResizable={isEditing}
|
||||
onLayoutChange={onLayoutChange}
|
||||
draggableHandle=".drag-handle"
|
||||
compactType="vertical"
|
||||
preventCollision={false}
|
||||
>
|
||||
{dashboardLayout.widgets.map(widgetId => (
|
||||
<div key={widgetId} className="widget-container">
|
||||
{renderWidget(widgetId)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</GridLayout>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* Revenue Chart */}
|
||||
<div className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<h3 className="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{t('dashboard.totalRevenue')}</h3>
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={weeklyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<YAxis axisLine={false} tickLine={false} tickFormatter={(value) => `$${value}`} tick={{ fill: '#9CA3AF' }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(107, 114, 128, 0.1)' }}
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="revenue" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Appointments Chart - Full Width */}
|
||||
<div className="p-6 bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<h3 className="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{t('dashboard.upcomingAppointments')}</h3>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={weeklyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF' }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
/>
|
||||
<Line type="monotone" dataKey="appointments" stroke="#10b981" strokeWidth={3} dot={{ r: 4, fill: '#10b981' }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/* Widget Config Modal */}
|
||||
<WidgetConfigModal
|
||||
isOpen={showConfig}
|
||||
onClose={() => setShowConfig(false)}
|
||||
activeWidgets={dashboardLayout.widgets}
|
||||
onToggleWidget={toggleWidget}
|
||||
onResetLayout={resetLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,194 @@
|
||||
/**
|
||||
* Help Guide - Main Documentation Hub
|
||||
*
|
||||
* Comprehensive guide linking to all help pages and providing quick overviews.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
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 { 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 (
|
||||
<div className="p-8 max-w-4xl mx-auto">
|
||||
<div className="p-8 max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<BookOpen className="text-brand-600" />
|
||||
{t('help.guide.title', 'Platform Guide')}
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-2">
|
||||
{t('help.guide.subtitle', 'Learn how to use SmoothSchedule effectively')}
|
||||
</p>
|
||||
<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">
|
||||
<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')}
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{t('help.guide.subtitle', 'Learn how to use SmoothSchedule effectively')}
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import api from '../api/client';
|
||||
import { PluginInstallation, PluginCategory } from '../types';
|
||||
import EmailTemplateSelector from '../components/EmailTemplateSelector';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Category icon mapping
|
||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||
@@ -60,6 +62,11 @@ const MyPlugins: React.FC = () => {
|
||||
const [review, setReview] = useState('');
|
||||
const [configValues, setConfigValues] = useState<Record<string, any>>({});
|
||||
|
||||
// Check plan permissions
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const isLocked = !hasPluginsFeature;
|
||||
|
||||
// Fetch installed plugins
|
||||
const { data: plugins = [], isLoading, error } = useQuery<PluginInstallation[]>({
|
||||
queryKey: ['plugin-installations'],
|
||||
@@ -227,7 +234,7 @@ const MyPlugins: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -250,6 +257,7 @@ const MyPlugins: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -763,6 +771,7 @@ const MyPlugins: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
const [editDateTime, setEditDateTime] = useState('');
|
||||
const [editResource, setEditResource] = useState('');
|
||||
const [editDuration, setEditDuration] = useState(0);
|
||||
const [editStatus, setEditStatus] = useState<AppointmentStatus>('CONFIRMED');
|
||||
|
||||
// Filter state
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
@@ -113,9 +114,17 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
// Update edit state when selected appointment changes
|
||||
useEffect(() => {
|
||||
if (selectedAppointment) {
|
||||
setEditDateTime(new Date(selectedAppointment.startTime).toISOString().slice(0, 16));
|
||||
// Format date in local time for datetime-local input (toISOString uses UTC)
|
||||
const date = new Date(selectedAppointment.startTime);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
setEditDateTime(`${year}-${month}-${day}T${hours}:${minutes}`);
|
||||
setEditResource(selectedAppointment.resourceId || '');
|
||||
setEditDuration(selectedAppointment.durationMinutes);
|
||||
setEditStatus(selectedAppointment.status);
|
||||
}
|
||||
}, [selectedAppointment]);
|
||||
|
||||
@@ -551,7 +560,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
|
||||
const getStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
||||
if (status === 'COMPLETED') return 'bg-green-100 border-green-500 text-green-900 dark:bg-green-900/50 dark:border-green-500 dark:text-green-200';
|
||||
if (status === 'NO_SHOW') return 'bg-gray-100 border-gray-400 text-gray-600 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400';
|
||||
if (status === 'NO_SHOW') return 'bg-orange-100 border-orange-500 text-orange-900 dark:bg-orange-900/50 dark:border-orange-500 dark:text-orange-200';
|
||||
if (status === 'CANCELLED') return 'bg-gray-100 border-gray-400 text-gray-500 opacity-75 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400';
|
||||
const now = new Date();
|
||||
if (now > endTime) return 'bg-red-100 border-red-500 text-red-900 dark:bg-red-900/50 dark:border-red-500 dark:text-red-200';
|
||||
@@ -562,7 +571,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
// Simplified status colors for month view (no border classes)
|
||||
const getMonthStatusColor = (status: Appointment['status'], startTime: Date, endTime: Date) => {
|
||||
if (status === 'COMPLETED') return 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800/50';
|
||||
if (status === 'NO_SHOW') return 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600';
|
||||
if (status === 'NO_SHOW') return 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-200 hover:bg-orange-200 dark:hover:bg-orange-800/50';
|
||||
if (status === 'CANCELLED') return 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 opacity-75 hover:bg-gray-200 dark:hover:bg-gray-600';
|
||||
const now = new Date();
|
||||
if (now > endTime) return 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800/50';
|
||||
@@ -823,6 +832,7 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
const updates: any = {
|
||||
startTime: new Date(editDateTime),
|
||||
durationMinutes: validDuration,
|
||||
status: editStatus,
|
||||
};
|
||||
|
||||
if (editResource) {
|
||||
@@ -1112,9 +1122,9 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
<div className={`w-2 h-2 rounded-full ml-auto ${
|
||||
status === 'COMPLETED' ? 'bg-green-500' :
|
||||
status === 'CANCELLED' ? 'bg-gray-400' :
|
||||
status === 'NO_SHOW' ? 'bg-gray-400' :
|
||||
status === 'NO_SHOW' ? 'bg-orange-500' :
|
||||
status === 'CONFIRMED' ? 'bg-blue-500' :
|
||||
'bg-orange-400'
|
||||
'bg-yellow-400'
|
||||
}`}></div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1719,8 +1729,18 @@ const OwnerScheduler: React.FC<OwnerSchedulerProps> = ({ user, business }) => {
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-white">{services.find(s => s.id === selectedAppointment.serviceId)?.name}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Status</p>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-white capitalize">{selectedAppointment.status.toLowerCase().replace('_', ' ')}</p>
|
||||
<label className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1 block">Status</label>
|
||||
<select
|
||||
value={editStatus}
|
||||
onChange={(e) => setEditStatus(e.target.value as AppointmentStatus)}
|
||||
className="w-full px-2 py-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-sm font-semibold text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
<option value="PENDING">Pending</option>
|
||||
<option value="CONFIRMED">Confirmed</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="CANCELLED">Cancelled</option>
|
||||
<option value="NO_SHOW">No Show</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import api from '../api/client';
|
||||
import { PluginTemplate, PluginCategory } from '../types';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Category icon mapping
|
||||
const categoryIcons: Record<PluginCategory, React.ReactNode> = {
|
||||
@@ -91,6 +93,11 @@ const PluginMarketplace: React.FC = () => {
|
||||
const [showWhatsNextModal, setShowWhatsNextModal] = useState(false);
|
||||
const [installedPluginId, setInstalledPluginId] = useState<string | null>(null);
|
||||
|
||||
// Check plan permissions
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const isLocked = !hasPluginsFeature;
|
||||
|
||||
// Fetch marketplace plugins
|
||||
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
|
||||
queryKey: ['plugin-templates', 'marketplace'],
|
||||
@@ -206,7 +213,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -229,6 +236,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-8 space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -702,6 +710,7 @@ const PluginMarketplace: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import toast from 'react-hot-toast';
|
||||
import CreateTaskModal from '../components/CreateTaskModal';
|
||||
import EditTaskModal from '../components/EditTaskModal';
|
||||
import { usePlanFeatures } from '../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../components/UpgradePrompt';
|
||||
|
||||
// Types
|
||||
interface ScheduledTask {
|
||||
@@ -95,6 +97,12 @@ const Tasks: React.FC = () => {
|
||||
const [editingTask, setEditingTask] = useState<ScheduledTask | null>(null);
|
||||
const [editingEventAutomation, setEditingEventAutomation] = useState<GlobalEventPlugin | null>(null);
|
||||
|
||||
// Check plan permissions - tasks requires both plugins AND tasks features
|
||||
const { canUse, isLoading: permissionsLoading } = usePlanFeatures();
|
||||
const hasPluginsFeature = canUse('plugins');
|
||||
const hasTasksFeature = canUse('tasks');
|
||||
const isLocked = !hasPluginsFeature || !hasTasksFeature;
|
||||
|
||||
// Fetch scheduled tasks
|
||||
const { data: scheduledTasks = [], isLoading: tasksLoading } = useQuery<ScheduledTask[]>({
|
||||
queryKey: ['scheduled-tasks'],
|
||||
@@ -246,7 +254,7 @@ const Tasks: React.FC = () => {
|
||||
return AlertCircle;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || permissionsLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
@@ -255,6 +263,7 @@ const Tasks: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LockedSection feature="tasks" isLocked={isLocked} variant="overlay">
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -588,6 +597,7 @@ const Tasks: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LockedSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
829
frontend/src/pages/help/HelpComprehensive.tsx
Normal file
829
frontend/src/pages/help/HelpComprehensive.tsx
Normal file
@@ -0,0 +1,829 @@
|
||||
/**
|
||||
* Comprehensive Help Guide - Monolithic Documentation
|
||||
*
|
||||
* Complete documentation for SmoothSchedule in a single scrollable page.
|
||||
* Includes all features: Dashboard, Scheduler, Services, Resources, Customers, Staff, and Settings.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft, BookOpen, LayoutDashboard, Calendar, Briefcase, Users, UserCog,
|
||||
ClipboardList, Settings, ChevronRight, HelpCircle, CheckCircle, AlertCircle,
|
||||
Clock, Eye, Palette, Link2, Mail, Globe, CreditCard, Zap, Search, Filter,
|
||||
Plus, Edit, Trash2, ArrowUpDown, GripVertical, Image, Save, ExternalLink,
|
||||
MessageSquare, Tag, UserPlus, Shield, Copy, Layers, Play, Pause, Puzzle,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TocSubItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface TocItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
subItems?: TocSubItem[];
|
||||
}
|
||||
|
||||
const HelpComprehensive: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [expandedItems, setExpandedItems] = React.useState<string[]>(['getting-started', 'settings']);
|
||||
|
||||
// Table of contents items with sub-items
|
||||
const tocItems: TocItem[] = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
label: 'Getting Started',
|
||||
icon: <Zap size={16} />,
|
||||
subItems: [
|
||||
{ label: 'Services Setup', href: '/help/services' },
|
||||
{ label: 'Resources Setup', href: '/help/resources' },
|
||||
{ label: 'Branding', href: '/help/settings/appearance' },
|
||||
{ label: 'Booking URL', href: '/help/settings/booking' },
|
||||
{ label: 'Scheduler', href: '/help/scheduler' },
|
||||
],
|
||||
},
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} /> },
|
||||
{ id: 'scheduler', label: 'Scheduler', icon: <Calendar size={16} /> },
|
||||
{ id: 'services', label: 'Services', icon: <Briefcase size={16} /> },
|
||||
{ id: 'resources', label: 'Resources', icon: <ClipboardList size={16} /> },
|
||||
{ id: 'customers', label: 'Customers', icon: <Users size={16} /> },
|
||||
{ id: 'staff', label: 'Staff', icon: <UserCog size={16} /> },
|
||||
{ id: 'plugins', label: 'Plugins', icon: <Puzzle size={16} /> },
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
icon: <Settings size={16} />,
|
||||
subItems: [
|
||||
{ label: 'Resource Types', href: '/help/settings/resource-types' },
|
||||
{ label: 'Email Settings', href: '/help/settings/email' },
|
||||
{ label: 'Custom Domains', href: '/help/settings/domains' },
|
||||
{ label: 'Billing', href: '/help/settings/billing' },
|
||||
{ label: 'API Settings', href: '/help/settings/api' },
|
||||
{ label: 'Authentication', href: '/help/settings/auth' },
|
||||
{ label: 'Usage & Quota', href: '/help/settings/quota' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedItems((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
{/* Fixed Header */}
|
||||
<div className="sticky top-0 z-20 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400">
|
||||
<ArrowLeft size={20} /> Back
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen size={24} className="text-brand-600 dark:text-brand-400" />
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">SmoothSchedule Complete Guide</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/tickets" className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400">
|
||||
Contact Support
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 py-8 flex gap-8">
|
||||
{/* Sidebar Table of Contents */}
|
||||
<nav className="hidden lg:block w-64 shrink-0">
|
||||
<div className="sticky top-24">
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">Contents</h2>
|
||||
<ul className="space-y-1">
|
||||
{tocItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-left"
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
{item.subItems && (
|
||||
<button
|
||||
onClick={() => toggleExpanded(item.id)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className={`transition-transform ${expandedItems.includes(item.id) ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{item.subItems && expandedItems.includes(item.id) && (
|
||||
<ul className="ml-6 mt-1 space-y-1 border-l border-gray-200 dark:border-gray-700 pl-2">
|
||||
{item.subItems.map((subItem) => (
|
||||
<li key={subItem.href}>
|
||||
<Link
|
||||
to={subItem.href}
|
||||
onClick={scrollToTop}
|
||||
className="block px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
||||
>
|
||||
{subItem.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 min-w-0">
|
||||
{/* Introduction */}
|
||||
<section className="mb-12">
|
||||
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Welcome to SmoothSchedule</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
SmoothSchedule is a complete scheduling platform designed to help businesses manage appointments,
|
||||
customers, staff, and services. This comprehensive guide covers everything you need to know to
|
||||
get the most out of the platform.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Use the table of contents on the left to jump to specific sections, or scroll through the entire guide.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* GETTING STARTED */}
|
||||
{/* ============================================== */}
|
||||
<section id="getting-started" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
|
||||
<Zap size={20} className="text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Getting Started</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Quick Setup Checklist</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Follow these steps to get your scheduling system up and running:
|
||||
</p>
|
||||
<ol className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">1</span>
|
||||
<div>
|
||||
<Link to="/help/services" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Set up your Services
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Define what you offer - consultations, appointments, classes, etc. Include names, durations, and prices.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">2</span>
|
||||
<div>
|
||||
<Link to="/help/resources" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Add your Resources
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Create staff members, rooms, or equipment that can be booked. Set their availability schedules.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">3</span>
|
||||
<div>
|
||||
<Link to="/help/settings/appearance" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Configure your Branding
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Upload your logo and set your brand colors so customers recognize your business.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">4</span>
|
||||
<div>
|
||||
<Link to="/help/settings/booking" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Share your Booking URL
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Copy your booking URL from Settings → Booking and share it with customers.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center font-medium">5</span>
|
||||
<div>
|
||||
<Link to="/help/scheduler" onClick={scrollToTop} className="font-medium text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 flex items-center gap-1">
|
||||
Start Managing Appointments
|
||||
<ChevronRight size={14} className="text-brand-500" />
|
||||
</Link>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Use the Scheduler to view, create, and manage bookings as they come in.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* DASHBOARD */}
|
||||
{/* ============================================== */}
|
||||
<section id="dashboard" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<LayoutDashboard size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Dashboard provides an at-a-glance overview of your business performance. It displays key metrics
|
||||
and charts to help you understand how your scheduling business is doing.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Metrics</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Total Appointments</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Number of bookings in the system</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Active Customers</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customers with Active status</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Services</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Total number of services offered</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Resources</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Staff, rooms, and equipment available</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Charts</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Revenue Chart:</strong> Bar chart showing daily revenue by day of week</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Appointments Chart:</strong> Line chart showing appointment volume by day</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SCHEDULER */}
|
||||
{/* ============================================== */}
|
||||
<section id="scheduler" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
||||
<Calendar size={20} className="text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Scheduler</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Scheduler is the heart of SmoothSchedule. It provides a visual calendar interface for managing
|
||||
all your appointments with full drag-and-drop support.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Interface Layout</h3>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Clock size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Left Sidebar - Pending Appointments</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Unscheduled appointments waiting to be placed on the calendar. Drag them onto available time slots.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Calendar size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Center - Calendar View</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Main calendar showing appointments organized by resource in columns. Switch between day, 3-day, week, and month views.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Edit size={18} className="text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Right Sidebar - Appointment Details</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Click any appointment to view/edit details, add notes, change status, or send reminders.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Drag & Drop:</strong> Move appointments between time slots and resources</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Resize:</strong> Drag appointment edges to change duration</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Quick Create:</strong> Double-click any empty slot to create a new appointment</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Resource Filtering:</strong> Toggle which resources are visible in the calendar</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500 flex-shrink-0" />
|
||||
<span><strong>Status Colors:</strong> Appointments are color-coded by status (confirmed, pending, cancelled)</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Appointment Statuses</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Pending</span>
|
||||
</div>
|
||||
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-300">Confirmed</span>
|
||||
</div>
|
||||
<div className="p-2 bg-red-50 dark:bg-red-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">Cancelled</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Completed</span>
|
||||
</div>
|
||||
<div className="p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg text-center">
|
||||
<span className="text-sm font-medium text-yellow-700 dark:text-yellow-300">No-Show</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SERVICES */}
|
||||
{/* ============================================== */}
|
||||
<section id="services" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<Briefcase size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Services</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Services define what customers can book with you. Each service has a name, duration, price, and
|
||||
description. The Services page uses a two-column layout: an editable list on the left and a
|
||||
customer preview on the right.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Service Properties</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Name</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">The service title shown to customers</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Duration</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">How long the appointment takes (in minutes)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Price</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Cost of the service (displayed to customers)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Description</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Details about what the service includes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<GripVertical size={16} className="text-brand-500" />
|
||||
<span><strong>Drag to Reorder:</strong> Change the display order by dragging services up/down</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Image size={16} className="text-brand-500" />
|
||||
<span><strong>Photo Gallery:</strong> Add, reorder, and remove images for each service</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Eye size={16} className="text-brand-500" />
|
||||
<span><strong>Live Preview:</strong> See how customers will view your service in real-time</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Plus size={16} className="text-brand-500" />
|
||||
<span><strong>Quick Add:</strong> Create new services with the Add Service button</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* RESOURCES */}
|
||||
{/* ============================================== */}
|
||||
<section id="resources" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
||||
<ClipboardList size={20} className="text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Resources</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Resources are the things that get booked - staff members, rooms, equipment, or any other bookable
|
||||
entity. Each resource appears as a column in the scheduler calendar.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Resource Types</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<UserCog size={24} className="text-blue-600 dark:text-blue-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Staff</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">People who provide services (employees, contractors, etc.)</p>
|
||||
</div>
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<Layers size={24} className="text-green-600 dark:text-green-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Room</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Physical spaces (meeting rooms, studios, treatment rooms)</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<ClipboardList size={24} className="text-purple-600 dark:text-purple-400 mb-2" />
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Equipment</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Physical items (cameras, projectors, vehicles)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Staff Autocomplete:</strong> When creating staff resources, link to existing staff members</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Multilane Mode:</strong> Enable for resources that can handle multiple concurrent bookings</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>View Calendar:</strong> Click the calendar icon to see a resource's schedule</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Table Actions:</strong> Edit or delete resources from the actions column</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* CUSTOMERS */}
|
||||
{/* ============================================== */}
|
||||
<section id="customers" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-pink-100 dark:bg-pink-900/30 flex items-center justify-center">
|
||||
<Users size={20} className="text-pink-600 dark:text-pink-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Customers</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Customers page lets you manage all the people who book appointments with your business.
|
||||
Track their information, booking history, and status.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Customer Statuses</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<h4 className="font-medium text-green-700 dark:text-green-300 text-sm">Active</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer can book appointments normally</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg">
|
||||
<h4 className="font-medium text-gray-700 dark:text-gray-300 text-sm">Inactive</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer record is dormant</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<h4 className="font-medium text-red-700 dark:text-red-300 text-sm">Blocked</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customer cannot make new bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<Search size={16} className="text-brand-500" />
|
||||
<span><strong>Search:</strong> Find customers by name, email, or phone</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Filter size={16} className="text-brand-500" />
|
||||
<span><strong>Filter:</strong> Filter by status (Active, Inactive, Blocked)</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={16} className="text-brand-500" />
|
||||
<span><strong>Tags:</strong> Organize customers with custom tags (VIP, New, etc.)</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<ArrowUpDown size={16} className="text-brand-500" />
|
||||
<span><strong>Sorting:</strong> Click column headers to sort the table</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Eye size={18} className="text-purple-500" /> Masquerading
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Use the Masquerade feature to see exactly what a customer sees when they log in. This is helpful
|
||||
for walking customers through tasks or troubleshooting issues. Click the eye icon in a customer's
|
||||
row to start masquerading.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* STAFF */}
|
||||
{/* ============================================== */}
|
||||
<section id="staff" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-teal-100 dark:bg-teal-900/30 flex items-center justify-center">
|
||||
<UserCog size={20} className="text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Staff</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
The Staff page lets you manage team members who help run your business. Invite new staff,
|
||||
assign roles, and control what each person can access.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Staff Roles</h3>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<Shield size={18} className="text-yellow-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Owner</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Full access to everything including billing and settings. Cannot be removed.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<Shield size={18} className="text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Manager</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Can manage staff, customers, services, and appointments. No billing access.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<Shield size={18} className="text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Staff</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Basic access. Can view scheduler and manage own appointments if bookable.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Inviting Staff</h3>
|
||||
<ol className="space-y-2 text-sm text-gray-600 dark:text-gray-300 list-decimal list-inside mb-6">
|
||||
<li>Click the <strong>Invite Staff</strong> button</li>
|
||||
<li>Enter their email address</li>
|
||||
<li>Select a role (Manager or Staff)</li>
|
||||
<li>Click <strong>Send Invitation</strong></li>
|
||||
<li>They'll receive an email with a link to join</li>
|
||||
</ol>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Make Bookable</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
The "Make Bookable" option creates a bookable resource for a staff member. When enabled, they
|
||||
appear as a column in the scheduler and customers can book appointments with them directly.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* PLUGINS */}
|
||||
{/* ============================================== */}
|
||||
<section id="plugins" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
|
||||
<Puzzle size={20} className="text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Plugins</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Plugins extend SmoothSchedule with custom automation and integrations. Browse the marketplace
|
||||
for pre-built plugins or create your own using our scripting language.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">What Plugins Can Do</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Send Emails:</strong> Automated reminders, confirmations, and follow-ups</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Webhooks:</strong> Integrate with external services when events occur</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Reports:</strong> Generate and email business reports on a schedule</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span><strong>Cleanup:</strong> Automatically archive old data or manage records</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Plugin Types</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Marketplace Plugins</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Pre-built plugins available to install immediately. Browse, install, and configure with a few clicks.</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Custom Plugins</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Create your own plugins using our scripting language. Full control over logic and triggers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Triggers</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Plugins can be triggered in various ways:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">Before Event</span>
|
||||
</div>
|
||||
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-green-700 dark:text-green-300">At Start</span>
|
||||
</div>
|
||||
<div className="p-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">After End</span>
|
||||
</div>
|
||||
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-center">
|
||||
<span className="text-xs font-medium text-purple-700 dark:text-purple-300">On Status Change</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Learn More</h3>
|
||||
<Link to="/help/plugins/docs" onClick={scrollToTop} className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<Puzzle size={24} className="text-indigo-500" />
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Plugin Documentation</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Complete guide to creating and using plugins, including API reference and examples</p>
|
||||
</div>
|
||||
<ChevronRight size={20} className="text-gray-400 ml-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ============================================== */}
|
||||
{/* SETTINGS */}
|
||||
{/* ============================================== */}
|
||||
<section id="settings" className="mb-16 scroll-mt-24">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||||
<Settings size={20} className="text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Settings is where business owners configure their scheduling platform. Most settings are
|
||||
owner-only and affect how your business operates.
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={20} className="text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Owner Access Required:</strong> Only business owners can access most settings pages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* General Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">General Settings</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Configure your business name, timezone, and contact information.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Business Name:</strong> Your company name displayed throughout the app</li>
|
||||
<li>• <strong>Subdomain:</strong> Your booking URL (read-only after creation)</li>
|
||||
<li>• <strong>Timezone:</strong> Business operating timezone</li>
|
||||
<li>• <strong>Time Display Mode:</strong> Show times in business timezone or viewer's timezone</li>
|
||||
<li>• <strong>Contact Email/Phone:</strong> How customers can reach you</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Booking Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Booking Settings</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Your booking URL and post-booking redirect configuration.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Booking URL:</strong> The link customers use to book (copy/share it)</li>
|
||||
<li>• <strong>Return URL:</strong> Where to redirect customers after booking (optional)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Branding Settings */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Branding (Appearance)</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
Customize your business appearance with logos and colors.
|
||||
</p>
|
||||
<ul className="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li>• <strong>Website Logo:</strong> Appears in sidebar and booking pages (500×500px recommended)</li>
|
||||
<li>• <strong>Email Logo:</strong> Appears in email notifications (600×200px recommended)</li>
|
||||
<li>• <strong>Display Mode:</strong> Text Only, Logo Only, or Logo and Text</li>
|
||||
<li>• <strong>Color Palettes:</strong> 10 preset palettes to choose from</li>
|
||||
<li>• <strong>Custom Colors:</strong> Set your own primary and secondary colors</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Other Settings */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Other Settings</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Link to="/help/settings/resource-types" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Resource Types</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Configure staff, room, equipment types</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/email" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Email Templates</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Customize email notifications</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/domains" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Custom Domains</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Use your own domain for booking</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/billing" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Billing</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Manage subscription and payments</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/api" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">API Settings</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">API keys and webhooks</p>
|
||||
</Link>
|
||||
<Link to="/help/settings/quota" onClick={scrollToTop} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">Usage & Quota</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Track usage and limits</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Help Footer */}
|
||||
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
|
||||
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need More Help?</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Can't find what you're looking for? Our support team is ready to help.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpComprehensive;
|
||||
480
frontend/src/pages/help/HelpCustomers.tsx
Normal file
480
frontend/src/pages/help/HelpCustomers.tsx
Normal 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;
|
||||
354
frontend/src/pages/help/HelpDashboard.tsx
Normal file
354
frontend/src/pages/help/HelpDashboard.tsx
Normal 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;
|
||||
162
frontend/src/pages/help/HelpMessages.tsx
Normal file
162
frontend/src/pages/help/HelpMessages.tsx
Normal 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;
|
||||
167
frontend/src/pages/help/HelpPayments.tsx
Normal file
167
frontend/src/pages/help/HelpPayments.tsx
Normal 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;
|
||||
272
frontend/src/pages/help/HelpPlugins.tsx
Normal file
272
frontend/src/pages/help/HelpPlugins.tsx
Normal 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 → 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;
|
||||
545
frontend/src/pages/help/HelpResources.tsx
Normal file
545
frontend/src/pages/help/HelpResources.tsx
Normal 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 > 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;
|
||||
644
frontend/src/pages/help/HelpScheduler.tsx
Normal file
644
frontend/src/pages/help/HelpScheduler.tsx
Normal 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;
|
||||
528
frontend/src/pages/help/HelpServices.tsx
Normal file
528
frontend/src/pages/help/HelpServices.tsx
Normal 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;
|
||||
395
frontend/src/pages/help/HelpSettingsApi.tsx
Normal file
395
frontend/src/pages/help/HelpSettingsApi.tsx
Normal 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 > 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;
|
||||
388
frontend/src/pages/help/HelpSettingsAppearance.tsx
Normal file
388
frontend/src/pages/help/HelpSettingsAppearance.tsx
Normal 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;
|
||||
378
frontend/src/pages/help/HelpSettingsAuth.tsx
Normal file
378
frontend/src/pages/help/HelpSettingsAuth.tsx
Normal 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">🔍</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Google</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🍎</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Apple</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">📘</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Facebook</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">💼</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">LinkedIn</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🪧</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Microsoft</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🐦</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">X (Twitter)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
|
||||
<span className="text-2xl">🎮</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mt-1">Twitch</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<Check size={18} className="text-brand-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Enabled providers show a checkmark badge. Click <strong>Save</strong> after making changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* OAuth Settings */}
|
||||
<section className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Key size={20} className="text-brand-500" />
|
||||
OAuth Settings
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Configure additional options for OAuth authentication:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 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 > 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;
|
||||
375
frontend/src/pages/help/HelpSettingsBilling.tsx
Normal file
375
frontend/src/pages/help/HelpSettingsBilling.tsx
Normal 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;
|
||||
317
frontend/src/pages/help/HelpSettingsBooking.tsx
Normal file
317
frontend/src/pages/help/HelpSettingsBooking.tsx
Normal 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;
|
||||
408
frontend/src/pages/help/HelpSettingsDomains.tsx
Normal file
408
frontend/src/pages/help/HelpSettingsDomains.tsx
Normal 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 > 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;
|
||||
343
frontend/src/pages/help/HelpSettingsEmail.tsx
Normal file
343
frontend/src/pages/help/HelpSettingsEmail.tsx
Normal 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;
|
||||
328
frontend/src/pages/help/HelpSettingsGeneral.tsx
Normal file
328
frontend/src/pages/help/HelpSettingsGeneral.tsx
Normal 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;
|
||||
417
frontend/src/pages/help/HelpSettingsQuota.tsx
Normal file
417
frontend/src/pages/help/HelpSettingsQuota.tsx
Normal 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 > 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;
|
||||
333
frontend/src/pages/help/HelpSettingsResourceTypes.tsx
Normal file
333
frontend/src/pages/help/HelpSettingsResourceTypes.tsx
Normal 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;
|
||||
540
frontend/src/pages/help/HelpStaff.tsx
Normal file
540
frontend/src/pages/help/HelpStaff.tsx
Normal 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;
|
||||
352
frontend/src/pages/help/HelpTasks.tsx
Normal file
352
frontend/src/pages/help/HelpTasks.tsx
Normal 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;
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
useUpdateSubscriptionPlan,
|
||||
useDeleteSubscriptionPlan,
|
||||
useSyncPlansWithStripe,
|
||||
useSyncPlanToTenants,
|
||||
SubscriptionPlan,
|
||||
SubscriptionPlanCreate,
|
||||
} from '../../hooks/usePlatformSettings';
|
||||
@@ -527,9 +528,12 @@ const TiersSettingsTab: React.FC = () => {
|
||||
const updatePlanMutation = useUpdateSubscriptionPlan();
|
||||
const deletePlanMutation = useDeleteSubscriptionPlan();
|
||||
const syncMutation = useSyncPlansWithStripe();
|
||||
const syncTenantsMutation = useSyncPlanToTenants();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null);
|
||||
const [showSyncConfirmModal, setShowSyncConfirmModal] = useState(false);
|
||||
const [savedPlanForSync, setSavedPlanForSync] = useState<SubscriptionPlan | null>(null);
|
||||
|
||||
const handleCreatePlan = () => {
|
||||
setEditingPlan(null);
|
||||
@@ -550,6 +554,9 @@ const TiersSettingsTab: React.FC = () => {
|
||||
const handleSavePlan = async (data: SubscriptionPlanCreate) => {
|
||||
if (editingPlan) {
|
||||
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 {
|
||||
await createPlanMutation.mutateAsync(data);
|
||||
}
|
||||
@@ -557,6 +564,19 @@ const TiersSettingsTab: React.FC = () => {
|
||||
setEditingPlan(null);
|
||||
};
|
||||
|
||||
const handleSyncConfirm = async () => {
|
||||
if (savedPlanForSync) {
|
||||
await syncTenantsMutation.mutateAsync(savedPlanForSync.id);
|
||||
}
|
||||
setShowSyncConfirmModal(false);
|
||||
setSavedPlanForSync(null);
|
||||
};
|
||||
|
||||
const handleSyncCancel = () => {
|
||||
setShowSyncConfirmModal(false);
|
||||
setSavedPlanForSync(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
@@ -672,6 +692,48 @@ const TiersSettingsTab: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -780,6 +842,8 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
|
||||
advanced_reporting: false,
|
||||
priority_support: false,
|
||||
can_use_custom_domain: false,
|
||||
can_use_plugins: false,
|
||||
can_use_tasks: false,
|
||||
can_create_plugins: false,
|
||||
can_white_label: 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>
|
||||
</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={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
|
||||
type="checkbox"
|
||||
checked={formData.permissions?.can_create_plugins || false}
|
||||
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"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Create Plugins</span>
|
||||
|
||||
@@ -69,38 +69,29 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
// Limits
|
||||
max_users: 5,
|
||||
max_resources: 10,
|
||||
max_services: 0,
|
||||
max_appointments: 0,
|
||||
max_email_templates: 0,
|
||||
max_automated_tasks: 0,
|
||||
// Platform Permissions
|
||||
// Platform Permissions (flat, matching backend model)
|
||||
can_manage_oauth_credentials: false,
|
||||
can_accept_payments: false,
|
||||
can_use_custom_domain: false,
|
||||
can_white_label: false,
|
||||
can_api_access: false,
|
||||
// Extended Permissions
|
||||
permissions: {
|
||||
// Payments & Revenue
|
||||
can_process_refunds: false,
|
||||
can_create_packages: false,
|
||||
// Communication
|
||||
sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_email_templates: false,
|
||||
// Customization
|
||||
can_customize_booking_page: false,
|
||||
// Advanced Features
|
||||
advanced_reporting: false,
|
||||
can_create_plugins: false,
|
||||
can_export_data: false,
|
||||
can_use_webhooks: false,
|
||||
calendar_sync: false,
|
||||
// Support & Enterprise
|
||||
priority_support: false,
|
||||
dedicated_support: false,
|
||||
sso_enabled: false,
|
||||
},
|
||||
// Feature permissions (flat, matching backend model)
|
||||
can_add_video_conferencing: false,
|
||||
can_connect_to_api: false,
|
||||
can_book_repeated_events: true,
|
||||
can_require_2fa: false,
|
||||
can_download_logs: false,
|
||||
can_delete_data: false,
|
||||
can_use_sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_pos: false,
|
||||
can_use_mobile_app: false,
|
||||
can_export_data: false,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: false,
|
||||
can_use_calendar_sync: false,
|
||||
});
|
||||
|
||||
// Get tier defaults from subscription plans or fallback to static defaults
|
||||
@@ -122,33 +113,29 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
// Limits
|
||||
max_users: plan.limits?.max_users ?? staticDefaults.max_users,
|
||||
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
|
||||
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_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_api_access: plan.permissions?.can_api_access ?? staticDefaults.can_api_access,
|
||||
// Extended Permissions
|
||||
permissions: {
|
||||
can_process_refunds: plan.permissions?.can_process_refunds ?? false,
|
||||
can_create_packages: plan.permissions?.can_create_packages ?? false,
|
||||
sms_reminders: plan.permissions?.sms_reminders ?? false,
|
||||
can_use_masked_phone_numbers: plan.permissions?.can_use_masked_phone_numbers ?? false,
|
||||
can_use_email_templates: plan.permissions?.can_use_email_templates ?? false,
|
||||
can_customize_booking_page: plan.permissions?.can_customize_booking_page ?? false,
|
||||
advanced_reporting: plan.permissions?.advanced_reporting ?? false,
|
||||
can_create_plugins: plan.permissions?.can_create_plugins ?? false,
|
||||
can_export_data: plan.permissions?.can_export_data ?? false,
|
||||
can_use_webhooks: plan.permissions?.can_use_webhooks ?? 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,
|
||||
},
|
||||
// Feature permissions (flat, matching backend model)
|
||||
can_add_video_conferencing: plan.permissions?.video_conferencing ?? false,
|
||||
can_connect_to_api: plan.permissions?.can_api_access ?? false,
|
||||
can_book_repeated_events: true,
|
||||
can_require_2fa: false,
|
||||
can_download_logs: false,
|
||||
can_delete_data: false,
|
||||
can_use_sms_reminders: plan.permissions?.sms_reminders ?? 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_use_webhooks: plan.permissions?.webhooks ?? false,
|
||||
can_use_calendar_sync: plan.permissions?.calendar_sync ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -156,26 +143,22 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
const staticDefaults = TIER_DEFAULTS[tier] || TIER_DEFAULTS.FREE;
|
||||
return {
|
||||
...staticDefaults,
|
||||
max_services: 0,
|
||||
max_appointments: 0,
|
||||
max_email_templates: 0,
|
||||
max_automated_tasks: 0,
|
||||
permissions: {
|
||||
can_process_refunds: false,
|
||||
can_create_packages: false,
|
||||
sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_email_templates: false,
|
||||
can_customize_booking_page: false,
|
||||
advanced_reporting: false,
|
||||
can_create_plugins: false,
|
||||
can_export_data: false,
|
||||
can_use_webhooks: false,
|
||||
calendar_sync: false,
|
||||
priority_support: false,
|
||||
dedicated_support: false,
|
||||
sso_enabled: false,
|
||||
},
|
||||
can_add_video_conferencing: false,
|
||||
can_connect_to_api: staticDefaults.can_api_access,
|
||||
can_book_repeated_events: true,
|
||||
can_require_2fa: false,
|
||||
can_download_logs: false,
|
||||
can_delete_data: false,
|
||||
can_use_sms_reminders: false,
|
||||
can_use_masked_phone_numbers: false,
|
||||
can_use_pos: false,
|
||||
can_use_mobile_app: false,
|
||||
can_export_data: false,
|
||||
can_use_plugins: true,
|
||||
can_use_tasks: true,
|
||||
can_create_plugins: false,
|
||||
can_use_webhooks: false,
|
||||
can_use_calendar_sync: false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -201,6 +184,7 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
// Update form when business changes
|
||||
useEffect(() => {
|
||||
if (business) {
|
||||
const b = business as any;
|
||||
setEditForm({
|
||||
name: business.name,
|
||||
is_active: business.is_active,
|
||||
@@ -208,47 +192,33 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
// Limits
|
||||
max_users: business.max_users || 5,
|
||||
max_resources: business.max_resources || 10,
|
||||
max_services: (business as any).max_services || 0,
|
||||
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
|
||||
// Platform Permissions (flat, matching backend)
|
||||
can_manage_oauth_credentials: business.can_manage_oauth_credentials || false,
|
||||
can_accept_payments: business.can_accept_payments || false,
|
||||
can_use_custom_domain: business.can_use_custom_domain || false,
|
||||
can_white_label: business.can_white_label || false,
|
||||
can_api_access: business.can_api_access || false,
|
||||
// Extended Permissions
|
||||
permissions: {
|
||||
can_process_refunds: (business as any).permissions?.can_process_refunds || false,
|
||||
can_create_packages: (business as any).permissions?.can_create_packages || false,
|
||||
sms_reminders: (business as any).permissions?.sms_reminders || false,
|
||||
can_use_masked_phone_numbers: (business as any).permissions?.can_use_masked_phone_numbers || false,
|
||||
can_use_email_templates: (business as any).permissions?.can_use_email_templates || false,
|
||||
can_customize_booking_page: (business as any).permissions?.can_customize_booking_page || false,
|
||||
advanced_reporting: (business as any).permissions?.advanced_reporting || false,
|
||||
can_create_plugins: (business as any).permissions?.can_create_plugins || false,
|
||||
can_export_data: (business as any).permissions?.can_export_data || false,
|
||||
can_use_webhooks: (business as any).permissions?.can_use_webhooks || false,
|
||||
calendar_sync: (business as any).permissions?.calendar_sync || false,
|
||||
priority_support: (business as any).permissions?.priority_support || false,
|
||||
dedicated_support: (business as any).permissions?.dedicated_support || false,
|
||||
sso_enabled: (business as any).permissions?.sso_enabled || false,
|
||||
},
|
||||
can_accept_payments: b.can_accept_payments || false,
|
||||
can_use_custom_domain: b.can_use_custom_domain || false,
|
||||
can_white_label: b.can_white_label || false,
|
||||
can_api_access: b.can_api_access || false,
|
||||
// Feature permissions (flat, matching backend)
|
||||
can_add_video_conferencing: b.can_add_video_conferencing || false,
|
||||
can_connect_to_api: b.can_connect_to_api || false,
|
||||
can_book_repeated_events: b.can_book_repeated_events ?? true,
|
||||
can_require_2fa: b.can_require_2fa || false,
|
||||
can_download_logs: b.can_download_logs || false,
|
||||
can_delete_data: b.can_delete_data || false,
|
||||
can_use_sms_reminders: b.can_use_sms_reminders || false,
|
||||
can_use_masked_phone_numbers: b.can_use_masked_phone_numbers || false,
|
||||
can_use_pos: b.can_use_pos || false,
|
||||
can_use_mobile_app: b.can_use_mobile_app || false,
|
||||
can_export_data: b.can_export_data || false,
|
||||
can_use_plugins: b.can_use_plugins ?? true,
|
||||
can_use_tasks: b.can_use_tasks ?? true,
|
||||
can_create_plugins: b.can_create_plugins || false,
|
||||
can_use_webhooks: b.can_use_webhooks || false,
|
||||
can_use_calendar_sync: b.can_use_calendar_sync || false,
|
||||
});
|
||||
}
|
||||
}, [business]);
|
||||
|
||||
// Helper for permission changes
|
||||
const handlePermissionChange = (key: string, value: boolean) => {
|
||||
setEditForm(prev => ({
|
||||
...prev,
|
||||
permissions: {
|
||||
...prev.permissions,
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleEditSave = () => {
|
||||
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">
|
||||
Use -1 for unlimited. These limits control what this business can create.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
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"
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
</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>
|
||||
|
||||
@@ -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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.permissions.sms_reminders}
|
||||
onChange={(e) => handlePermissionChange('sms_reminders', e.target.checked)}
|
||||
checked={editForm.can_use_sms_reminders}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_sms_reminders: 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">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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.permissions.can_use_masked_phone_numbers}
|
||||
onChange={(e) => handlePermissionChange('can_use_masked_phone_numbers', e.target.checked)}
|
||||
checked={editForm.can_use_masked_phone_numbers}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_masked_phone_numbers: 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">Masked Calling</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_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>
|
||||
|
||||
@@ -515,15 +410,6 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
<div>
|
||||
<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">
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -545,19 +431,59 @@ const BusinessEditModal: React.FC<BusinessEditModalProps> = ({ business, isOpen,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Features */}
|
||||
{/* Plugins & Automation */}
|
||||
<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">
|
||||
<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.advanced_reporting}
|
||||
onChange={(e) => handlePermissionChange('advanced_reporting', e.target.checked)}
|
||||
checked={editForm.can_use_plugins}
|
||||
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"
|
||||
/>
|
||||
<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 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">
|
||||
<input
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.permissions.can_create_plugins}
|
||||
onChange={(e) => handlePermissionChange('can_create_plugins', 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)}
|
||||
checked={editForm.can_use_webhooks}
|
||||
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">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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.permissions.calendar_sync}
|
||||
onChange={(e) => handlePermissionChange('calendar_sync', e.target.checked)}
|
||||
checked={editForm.can_use_calendar_sync}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_use_calendar_sync: 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">Calendar Sync</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_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>
|
||||
|
||||
{/* Support & Enterprise */}
|
||||
{/* Enterprise */}
|
||||
<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">
|
||||
<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
|
||||
@@ -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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.permissions.priority_support}
|
||||
onChange={(e) => handlePermissionChange('priority_support', e.target.checked)}
|
||||
checked={editForm.can_require_2fa}
|
||||
onChange={(e) => setEditForm({ ...editForm, can_require_2fa: 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">Priority 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.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>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Require 2FA</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface PlanPermissions {
|
||||
white_label: boolean;
|
||||
custom_oauth: boolean;
|
||||
plugins: boolean;
|
||||
tasks: boolean;
|
||||
export_data: boolean;
|
||||
video_conferencing: boolean;
|
||||
two_factor_auth: boolean;
|
||||
|
||||
@@ -13,6 +13,4 @@ class CoreConfig(AppConfig):
|
||||
"""
|
||||
Import signals and perform app initialization.
|
||||
"""
|
||||
# Import signals here when needed
|
||||
# from . import signals
|
||||
pass
|
||||
from . import signals # noqa: F401
|
||||
|
||||
23
smoothschedule/core/migrations/0021_add_can_use_plugins.py
Normal file
23
smoothschedule/core/migrations/0021_add_can_use_plugins.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-03 15:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_booking_return_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='can_use_plugins',
|
||||
field=models.BooleanField(default=True, help_text='Whether this business can use plugins from the marketplace'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenant',
|
||||
name='can_create_plugins',
|
||||
field=models.BooleanField(default=False, help_text='Whether this business can create custom plugins for automation (requires can_use_plugins)'),
|
||||
),
|
||||
]
|
||||
18
smoothschedule/core/migrations/0022_add_can_use_tasks.py
Normal file
18
smoothschedule/core/migrations/0022_add_can_use_tasks.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-03 15:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_add_can_use_plugins'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tenant',
|
||||
name='can_use_tasks',
|
||||
field=models.BooleanField(default=True, help_text='Whether this business can create scheduled tasks (requires can_use_plugins)'),
|
||||
),
|
||||
]
|
||||
@@ -207,9 +207,17 @@ class Tenant(TenantMixin):
|
||||
default=False,
|
||||
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(
|
||||
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(
|
||||
default=False,
|
||||
|
||||
67
smoothschedule/core/signals.py
Normal file
67
smoothschedule/core/signals.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Core App Signals
|
||||
|
||||
Handles automatic setup tasks when tenants are created.
|
||||
"""
|
||||
import logging
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender='core.Tenant')
|
||||
def seed_platform_plugins_on_tenant_create(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Seed platform plugins when a new tenant is created.
|
||||
|
||||
This ensures new tenants have access to all marketplace plugins immediately.
|
||||
"""
|
||||
if not created:
|
||||
return
|
||||
|
||||
# Skip public schema
|
||||
if instance.schema_name == 'public':
|
||||
return
|
||||
|
||||
# Defer the import to avoid circular imports
|
||||
from django.db import connection
|
||||
from django_tenants.utils import schema_context
|
||||
from schedule.models import PluginTemplate
|
||||
from django.utils import timezone
|
||||
|
||||
logger.info(f"Seeding platform plugins for new tenant: {instance.schema_name}")
|
||||
|
||||
try:
|
||||
with schema_context(instance.schema_name):
|
||||
# Import the plugin definitions from the seed command
|
||||
from schedule.management.commands.seed_platform_plugins import get_platform_plugins
|
||||
|
||||
plugins_data = get_platform_plugins()
|
||||
created_count = 0
|
||||
|
||||
for plugin_data in plugins_data:
|
||||
# Check if plugin already exists by slug
|
||||
if PluginTemplate.objects.filter(slug=plugin_data['slug']).exists():
|
||||
continue
|
||||
|
||||
# Create the plugin
|
||||
PluginTemplate.objects.create(
|
||||
name=plugin_data['name'],
|
||||
slug=plugin_data['slug'],
|
||||
category=plugin_data['category'],
|
||||
short_description=plugin_data['short_description'],
|
||||
description=plugin_data['description'],
|
||||
plugin_code=plugin_data['plugin_code'],
|
||||
logo_url=plugin_data.get('logo_url', ''),
|
||||
visibility=PluginTemplate.Visibility.PLATFORM,
|
||||
is_approved=True,
|
||||
approved_at=timezone.now(),
|
||||
author_name='Smooth Schedule',
|
||||
license_type='PLATFORM',
|
||||
)
|
||||
created_count += 1
|
||||
|
||||
logger.info(f"Created {created_count} platform plugins for tenant: {instance.schema_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to seed plugins for tenant {instance.schema_name}: {e}")
|
||||
@@ -10,6 +10,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework import status
|
||||
from core.permissions import HasFeaturePermission
|
||||
from decimal import Decimal
|
||||
from .services import get_stripe_service_for_tenant
|
||||
from .models import TransactionLink
|
||||
@@ -517,7 +518,7 @@ class ApiKeysView(APIView):
|
||||
GET /payments/api-keys/
|
||||
POST /payments/api-keys/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get current API key configuration."""
|
||||
@@ -627,7 +628,7 @@ class ApiKeysValidateView(APIView):
|
||||
|
||||
POST /payments/api-keys/validate/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Validate keys without saving."""
|
||||
@@ -672,7 +673,7 @@ class ApiKeysRevalidateView(APIView):
|
||||
|
||||
POST /payments/api-keys/revalidate/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Re-validate stored keys."""
|
||||
@@ -721,7 +722,7 @@ class ApiKeysDeleteView(APIView):
|
||||
|
||||
DELETE /payments/api-keys/delete/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def delete(self, request):
|
||||
"""Delete stored keys."""
|
||||
@@ -792,7 +793,7 @@ class ConnectOnboardView(APIView):
|
||||
|
||||
POST /payments/connect/onboard/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Start Connect onboarding flow."""
|
||||
@@ -855,7 +856,7 @@ class ConnectRefreshLinkView(APIView):
|
||||
|
||||
POST /payments/connect/refresh-link/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Get a new onboarding link."""
|
||||
@@ -905,7 +906,7 @@ class ConnectAccountSessionView(APIView):
|
||||
Custom accounts are required for embedded onboarding (Standard accounts
|
||||
require the redirect flow).
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Create account session for embedded components."""
|
||||
@@ -967,7 +968,7 @@ class ConnectRefreshStatusView(APIView):
|
||||
|
||||
POST /payments/connect/refresh-status/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Sync local status with Stripe."""
|
||||
@@ -1036,7 +1037,7 @@ class TransactionListView(APIView):
|
||||
|
||||
GET /payments/transactions/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get paginated list of transactions."""
|
||||
@@ -1104,7 +1105,7 @@ class TransactionSummaryView(APIView):
|
||||
|
||||
GET /payments/transactions/summary/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get transaction summary."""
|
||||
@@ -1159,7 +1160,7 @@ class StripeChargesView(APIView):
|
||||
|
||||
GET /payments/transactions/charges/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get recent charges from Stripe API."""
|
||||
@@ -1225,7 +1226,7 @@ class StripePayoutsView(APIView):
|
||||
|
||||
GET /payments/transactions/payouts/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get payouts from Stripe API."""
|
||||
@@ -1289,7 +1290,7 @@ class StripeBalanceView(APIView):
|
||||
|
||||
GET /payments/transactions/balance/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def get(self, request):
|
||||
"""Get balance from Stripe API."""
|
||||
@@ -1362,7 +1363,7 @@ class TransactionExportView(APIView):
|
||||
|
||||
POST /payments/transactions/export/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Export transactions to various formats."""
|
||||
@@ -1385,7 +1386,7 @@ class CreatePaymentIntentView(APIView):
|
||||
|
||||
POST /payments/payment-intents/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Create payment intent for an event"""
|
||||
@@ -1460,7 +1461,7 @@ class TerminalConnectionTokenView(APIView):
|
||||
|
||||
POST /payments/terminal/connection-token/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Get terminal connection token"""
|
||||
@@ -1488,7 +1489,7 @@ class RefundPaymentView(APIView):
|
||||
|
||||
POST /payments/refunds/
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated, HasFeaturePermission('can_accept_payments')]
|
||||
|
||||
def post(self, request):
|
||||
"""Create refund"""
|
||||
|
||||
@@ -5,3 +5,7 @@ class PlatformAdminConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'platform_admin'
|
||||
verbose_name = 'Platform Management'
|
||||
|
||||
def ready(self):
|
||||
# Import signals to register them
|
||||
from . import signals # noqa: F401
|
||||
|
||||
@@ -202,6 +202,27 @@ class TenantSerializer(serializers.ModelSerializer):
|
||||
'max_resources', 'contact_email', 'phone',
|
||||
# Platform permissions
|
||||
'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
|
||||
|
||||
@@ -256,6 +277,23 @@ class TenantUpdateSerializer(serializers.ModelSerializer):
|
||||
'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 = ['id']
|
||||
|
||||
|
||||
28
smoothschedule/platform_admin/signals.py
Normal file
28
smoothschedule/platform_admin/signals.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Django signals for platform admin operations.
|
||||
"""
|
||||
import logging
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender='platform_admin.SubscriptionPlan')
|
||||
def subscription_plan_updated(sender, instance, created, **kwargs):
|
||||
"""
|
||||
When a SubscriptionPlan is updated (not created), trigger a Celery task
|
||||
to sync the plan's permissions and limits to all tenants using that plan.
|
||||
"""
|
||||
if created:
|
||||
# New plan, no tenants to update yet
|
||||
return
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from .tasks import sync_subscription_plan_to_tenants
|
||||
|
||||
logger.info(f"SubscriptionPlan '{instance.name}' (ID: {instance.id}) was updated, "
|
||||
f"queuing sync task for all tenants on this plan")
|
||||
|
||||
# Queue the task to run asynchronously
|
||||
sync_subscription_plan_to_tenants.delay(instance.id)
|
||||
@@ -207,3 +207,142 @@ def send_bulk_appointment_reminders(hours_before: int = 24):
|
||||
|
||||
logger.info(f"Queued {reminders_queued} appointment reminder emails for {hours_before}h window")
|
||||
return {'reminders_queued': reminders_queued, 'hours_before': hours_before}
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def sync_subscription_plan_to_tenants(self, plan_id: int):
|
||||
"""
|
||||
Sync a subscription plan's permissions and limits to all tenants using that plan.
|
||||
|
||||
This task is triggered when a SubscriptionPlan is updated, ensuring all tenants
|
||||
on that plan have their permissions and limits updated accordingly.
|
||||
|
||||
Args:
|
||||
plan_id: ID of the SubscriptionPlan that was updated
|
||||
"""
|
||||
from .models import SubscriptionPlan
|
||||
from core.models import Tenant
|
||||
|
||||
try:
|
||||
plan = SubscriptionPlan.objects.get(id=plan_id)
|
||||
except SubscriptionPlan.DoesNotExist:
|
||||
logger.error(f"SubscriptionPlan {plan_id} not found")
|
||||
return {'success': False, 'error': 'Plan not found'}
|
||||
|
||||
# Get all tenants using this plan
|
||||
tenants = Tenant.objects.filter(subscription_plan=plan)
|
||||
tenant_count = tenants.count()
|
||||
|
||||
if tenant_count == 0:
|
||||
logger.info(f"No tenants found for plan {plan.name} (ID: {plan_id})")
|
||||
return {'success': True, 'tenants_updated': 0, 'plan_name': plan.name}
|
||||
|
||||
logger.info(f"Syncing plan '{plan.name}' to {tenant_count} tenant(s)")
|
||||
|
||||
# Mapping from plan.permissions JSON keys to Tenant boolean field names
|
||||
# Some plan keys differ from tenant field names
|
||||
permission_mapping = {
|
||||
# Plan JSON key -> Tenant field name
|
||||
'can_manage_oauth_credentials': 'can_manage_oauth_credentials',
|
||||
'can_accept_payments': 'can_accept_payments',
|
||||
'can_use_custom_domain': 'can_use_custom_domain',
|
||||
'can_white_label': 'can_white_label',
|
||||
'can_api_access': 'can_api_access',
|
||||
'video_conferencing': 'can_add_video_conferencing',
|
||||
'can_add_video_conferencing': 'can_add_video_conferencing',
|
||||
'can_connect_to_api': 'can_connect_to_api',
|
||||
'can_book_repeated_events': 'can_book_repeated_events',
|
||||
'can_require_2fa': 'can_require_2fa',
|
||||
'can_download_logs': 'can_download_logs',
|
||||
'can_delete_data': 'can_delete_data',
|
||||
# Communication - plan may use short names
|
||||
'sms_reminders': 'can_use_sms_reminders',
|
||||
'can_use_sms_reminders': 'can_use_sms_reminders',
|
||||
'masked_calling': 'can_use_masked_phone_numbers',
|
||||
'can_use_masked_phone_numbers': 'can_use_masked_phone_numbers',
|
||||
'can_use_pos': 'can_use_pos',
|
||||
'can_use_mobile_app': 'can_use_mobile_app',
|
||||
# Advanced features - plan may use short names
|
||||
'export_data': 'can_export_data',
|
||||
'can_export_data': 'can_export_data',
|
||||
'plugins': 'can_use_plugins',
|
||||
'can_use_plugins': 'can_use_plugins',
|
||||
'tasks': 'can_use_tasks',
|
||||
'can_use_tasks': 'can_use_tasks',
|
||||
'can_create_plugins': 'can_create_plugins',
|
||||
'webhooks': 'can_use_webhooks',
|
||||
'can_use_webhooks': 'can_use_webhooks',
|
||||
'calendar_sync': 'can_use_calendar_sync',
|
||||
'can_use_calendar_sync': 'can_use_calendar_sync',
|
||||
}
|
||||
|
||||
# Limit field mappings from plan.limits JSON to Tenant fields
|
||||
limit_fields = [
|
||||
'max_users',
|
||||
'max_resources',
|
||||
]
|
||||
|
||||
plan_permissions = plan.permissions or {}
|
||||
plan_limits = plan.limits or {}
|
||||
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
for tenant in tenants:
|
||||
try:
|
||||
changed = False
|
||||
|
||||
# Update permission fields using the mapping
|
||||
for plan_key, tenant_field in permission_mapping.items():
|
||||
if plan_key in plan_permissions:
|
||||
new_value = bool(plan_permissions[plan_key])
|
||||
if getattr(tenant, tenant_field, None) != new_value:
|
||||
setattr(tenant, tenant_field, new_value)
|
||||
changed = True
|
||||
|
||||
# Update limit fields
|
||||
for field in limit_fields:
|
||||
if field in plan_limits:
|
||||
new_value = int(plan_limits[field])
|
||||
if getattr(tenant, field, None) != new_value:
|
||||
setattr(tenant, field, new_value)
|
||||
changed = True
|
||||
|
||||
# Update subscription tier if plan has a business_tier
|
||||
if plan.business_tier:
|
||||
tier_mapping = {
|
||||
'Free': 'FREE',
|
||||
'Starter': 'STARTER',
|
||||
'Professional': 'PROFESSIONAL',
|
||||
'Business': 'PROFESSIONAL', # Map Business to Professional
|
||||
'Enterprise': 'ENTERPRISE',
|
||||
}
|
||||
new_tier = tier_mapping.get(plan.business_tier)
|
||||
if new_tier and tenant.subscription_tier != new_tier:
|
||||
tenant.subscription_tier = new_tier
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
tenant.save()
|
||||
updated_count += 1
|
||||
logger.debug(f"Updated tenant '{tenant.name}' (ID: {tenant.id})")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to update tenant {tenant.id}: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
errors.append(error_msg)
|
||||
|
||||
result = {
|
||||
'success': True,
|
||||
'plan_id': plan_id,
|
||||
'plan_name': plan.name,
|
||||
'tenants_found': tenant_count,
|
||||
'tenants_updated': updated_count,
|
||||
}
|
||||
|
||||
if errors:
|
||||
result['errors'] = errors
|
||||
result['success'] = len(errors) < tenant_count # Partial success
|
||||
|
||||
logger.info(f"Completed sync for plan '{plan.name}': {updated_count}/{tenant_count} tenants updated")
|
||||
return result
|
||||
|
||||
@@ -687,6 +687,32 @@ class SubscriptionPlanViewSet(viewsets.ModelViewSet):
|
||||
return SubscriptionPlanCreateSerializer
|
||||
return 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'])
|
||||
def sync_with_stripe(self, request):
|
||||
"""
|
||||
|
||||
@@ -176,7 +176,8 @@ def current_business_view(request):
|
||||
'custom_domain': tenant.can_use_custom_domain or plan_permissions.get('custom_domain', False),
|
||||
'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),
|
||||
'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),
|
||||
'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),
|
||||
|
||||
@@ -3,17 +3,20 @@ from django.utils import timezone
|
||||
from schedule.models import PluginTemplate
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seed platform-owned plugins into the database'
|
||||
def get_platform_plugins():
|
||||
"""
|
||||
Returns the list of platform plugin definitions.
|
||||
|
||||
def handle(self, *args, **options):
|
||||
plugins_data = [
|
||||
{
|
||||
'name': 'Daily Appointment Summary Email',
|
||||
'slug': 'daily-appointment-summary',
|
||||
'category': PluginTemplate.Category.EMAIL,
|
||||
'short_description': 'Send daily email summary of appointments',
|
||||
'description': '''Stay on top of your schedule with automated daily appointment summaries.
|
||||
This function is shared between the management command and the signal
|
||||
that auto-seeds plugins on tenant creation.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'name': 'Daily Appointment Summary Email',
|
||||
'slug': 'daily-appointment-summary',
|
||||
'category': PluginTemplate.Category.EMAIL,
|
||||
'short_description': 'Send daily email summary of appointments',
|
||||
'description': '''Stay on top of your schedule with automated daily appointment summaries.
|
||||
|
||||
This plugin sends a comprehensive email digest every morning with:
|
||||
- List of all appointments for the day
|
||||
@@ -23,7 +26,7 @@ This plugin sends a comprehensive email digest every morning with:
|
||||
- Any special notes or requirements
|
||||
|
||||
Perfect for managers and staff who want to start their day informed and prepared.''',
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
|
||||
# Get today's appointments
|
||||
today = datetime.now().date()
|
||||
@@ -51,14 +54,14 @@ api.send_email(
|
||||
body=summary
|
||||
)
|
||||
''',
|
||||
'logo_url': '/plugin-logos/daily-appointment-summary.png',
|
||||
},
|
||||
{
|
||||
'name': 'No-Show Customer Tracker',
|
||||
'slug': 'no-show-tracker',
|
||||
'category': PluginTemplate.Category.REPORTS,
|
||||
'short_description': 'Track customers who miss appointments',
|
||||
'description': '''Identify patterns of missed appointments and reduce no-shows.
|
||||
'logo_url': '/plugin-logos/daily-appointment-summary.png',
|
||||
},
|
||||
{
|
||||
'name': 'No-Show Customer Tracker',
|
||||
'slug': 'no-show-tracker',
|
||||
'category': PluginTemplate.Category.REPORTS,
|
||||
'short_description': 'Track customers who miss appointments',
|
||||
'description': '''Identify patterns of missed appointments and reduce no-shows.
|
||||
|
||||
This plugin automatically tracks and reports on:
|
||||
- Customers who didn\'t show up for scheduled appointments
|
||||
@@ -67,7 +70,7 @@ This plugin automatically tracks and reports on:
|
||||
- Trends over time
|
||||
|
||||
Helps you identify customers who may need reminder calls or deposits, improving your booking efficiency and revenue.''',
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
|
||||
# Get configuration
|
||||
days_back = int('{{PROMPT:days_back|Days to Look Back|7}}')
|
||||
@@ -108,14 +111,14 @@ api.send_email(
|
||||
body=report
|
||||
)
|
||||
''',
|
||||
'logo_url': '/plugin-logos/no-show-tracker.png',
|
||||
},
|
||||
{
|
||||
'name': 'Birthday Greeting Campaign',
|
||||
'slug': 'birthday-greetings',
|
||||
'category': PluginTemplate.Category.CUSTOMER,
|
||||
'short_description': 'Send birthday emails with offers',
|
||||
'description': '''Delight your customers with personalized birthday greetings and special offers.
|
||||
'logo_url': '/plugin-logos/no-show-tracker.png',
|
||||
},
|
||||
{
|
||||
'name': 'Birthday Greeting Campaign',
|
||||
'slug': 'birthday-greetings',
|
||||
'category': PluginTemplate.Category.CUSTOMER,
|
||||
'short_description': 'Send birthday emails with offers',
|
||||
'description': '''Delight your customers with personalized birthday greetings and special offers.
|
||||
|
||||
This plugin automatically:
|
||||
- Identifies customers with birthdays today
|
||||
@@ -124,7 +127,7 @@ This plugin automatically:
|
||||
- Helps drive repeat bookings and customer loyalty
|
||||
|
||||
A simple way to show customers you care while encouraging them to book their next appointment.''',
|
||||
'plugin_code': '''# Get all customers with email addresses
|
||||
'plugin_code': '''# Get all customers with email addresses
|
||||
customers = api.get_customers(has_email=True, limit=1000)
|
||||
|
||||
# Get customizable email template
|
||||
@@ -146,14 +149,14 @@ for customer in customers:
|
||||
|
||||
api.log(f"Sent {len(customers)} birthday greetings")
|
||||
''',
|
||||
'logo_url': '/plugin-logos/birthday-greetings.png',
|
||||
},
|
||||
{
|
||||
'name': 'Monthly Revenue Report',
|
||||
'slug': 'monthly-revenue-report',
|
||||
'category': PluginTemplate.Category.REPORTS,
|
||||
'short_description': 'Monthly business statistics',
|
||||
'description': '''Get comprehensive monthly insights into your business performance.
|
||||
'logo_url': '/plugin-logos/birthday-greetings.png',
|
||||
},
|
||||
{
|
||||
'name': 'Monthly Revenue Report',
|
||||
'slug': 'monthly-revenue-report',
|
||||
'category': PluginTemplate.Category.REPORTS,
|
||||
'short_description': 'Monthly business statistics',
|
||||
'description': '''Get comprehensive monthly insights into your business performance.
|
||||
|
||||
This plugin generates detailed reports including:
|
||||
- Total revenue and number of appointments
|
||||
@@ -164,7 +167,7 @@ This plugin generates detailed reports including:
|
||||
- Year-over-year comparisons
|
||||
|
||||
Perfect for owners and managers who want to track business growth and identify opportunities.''',
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
|
||||
# Get last month's date range
|
||||
today = datetime.now()
|
||||
@@ -212,14 +215,14 @@ api.send_email(
|
||||
body=report
|
||||
)
|
||||
''',
|
||||
'logo_url': '/plugin-logos/monthly-revenue-report.png',
|
||||
},
|
||||
{
|
||||
'name': 'Appointment Reminder (24hr)',
|
||||
'slug': 'appointment-reminder-24hr',
|
||||
'category': PluginTemplate.Category.BOOKING,
|
||||
'short_description': 'Remind customers 24hrs before appointments',
|
||||
'description': '''Reduce no-shows with automated appointment reminders.
|
||||
'logo_url': '/plugin-logos/monthly-revenue-report.png',
|
||||
},
|
||||
{
|
||||
'name': 'Appointment Reminder (24hr)',
|
||||
'slug': 'appointment-reminder-24hr',
|
||||
'category': PluginTemplate.Category.BOOKING,
|
||||
'short_description': 'Remind customers 24hrs before appointments',
|
||||
'description': '''Reduce no-shows with automated appointment reminders.
|
||||
|
||||
This plugin sends friendly reminder emails to customers 24 hours before their scheduled appointments, including:
|
||||
- Appointment date and time
|
||||
@@ -229,7 +232,7 @@ This plugin sends friendly reminder emails to customers 24 hours before their sc
|
||||
- Cancellation policy reminder
|
||||
|
||||
Studies show that appointment reminders can reduce no-shows by up to 90%.''',
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
|
||||
# Get appointments 24 hours from now
|
||||
tomorrow = (datetime.now() + timedelta(days=1)).date()
|
||||
@@ -255,14 +258,14 @@ for apt in appointments:
|
||||
|
||||
api.log(f"Sent {len(appointments)} appointment reminders")
|
||||
''',
|
||||
'logo_url': '/plugin-logos/appointment-reminder-24hr.png',
|
||||
},
|
||||
{
|
||||
'name': 'Inactive Customer Re-engagement',
|
||||
'slug': 'inactive-customer-reengagement',
|
||||
'category': PluginTemplate.Category.CUSTOMER,
|
||||
'short_description': 'Email inactive customers with offers',
|
||||
'description': '''Win back customers who haven\'t booked in a while.
|
||||
'logo_url': '/plugin-logos/appointment-reminder-24hr.png',
|
||||
},
|
||||
{
|
||||
'name': 'Inactive Customer Re-engagement',
|
||||
'slug': 'inactive-customer-reengagement',
|
||||
'category': PluginTemplate.Category.CUSTOMER,
|
||||
'short_description': 'Email inactive customers with offers',
|
||||
'description': '''Win back customers who haven\'t booked in a while.
|
||||
|
||||
This plugin automatically identifies customers who haven\'t made an appointment recently and sends them:
|
||||
- Personalized "we miss you" messages
|
||||
@@ -271,7 +274,7 @@ This plugin automatically identifies customers who haven\'t made an appointment
|
||||
- Easy booking links
|
||||
|
||||
Configurable inactivity period (default: 60 days). A proven strategy for increasing customer lifetime value and reducing churn.''',
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
'plugin_code': '''from datetime import datetime, timedelta
|
||||
|
||||
# Get configuration
|
||||
inactive_days = int('{{PROMPT:inactive_days|Days Inactive|60}}')
|
||||
@@ -308,20 +311,74 @@ for customer in all_customers:
|
||||
|
||||
api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
|
||||
''',
|
||||
'logo_url': '/plugin-logos/inactive-customer-reengagement.png',
|
||||
},
|
||||
]
|
||||
'logo_url': '/plugin-logos/inactive-customer-reengagement.png',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seed or update platform-owned plugins in the database'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--update',
|
||||
action='store_true',
|
||||
default=True,
|
||||
help='Update existing plugins if they have changed (default: True)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-update',
|
||||
action='store_true',
|
||||
help='Skip existing plugins instead of updating them',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
plugins_data = get_platform_plugins()
|
||||
update_existing = not options.get('no_update', False)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for plugin_data in plugins_data:
|
||||
# Check if plugin already exists by slug
|
||||
if PluginTemplate.objects.filter(slug=plugin_data['slug']).exists():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists")
|
||||
)
|
||||
skipped_count += 1
|
||||
existing = PluginTemplate.objects.filter(slug=plugin_data['slug']).first()
|
||||
|
||||
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.style.WARNING(f"Skipping '{plugin_data['name']}' - already exists")
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Create the plugin
|
||||
@@ -348,6 +405,6 @@ api.log(f"Sent re-engagement emails to {inactive_count} inactive customers")
|
||||
# Summary
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'\nSuccessfully created {created_count} plugin(s), {skipped_count} already existed.'
|
||||
f'\nSuccessfully created {created_count}, updated {updated_count}, skipped {skipped_count} plugin(s).'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -433,6 +433,7 @@ class ScheduledTaskViewSet(viewsets.ModelViewSet):
|
||||
- Must be authenticated
|
||||
- Only owners/managers can create/update/delete
|
||||
- Subject to MAX_AUTOMATED_TASKS quota (hard block on creation)
|
||||
- Requires can_use_plugins AND can_use_tasks features
|
||||
|
||||
Features:
|
||||
- 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
|
||||
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):
|
||||
"""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
|
||||
# serializer.save(created_by=self.request.user)
|
||||
serializer.save()
|
||||
@@ -628,6 +673,12 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
||||
- Install a template as a ScheduledTask
|
||||
- Request approval (for marketplace publishing)
|
||||
- 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()
|
||||
serializer_class = PluginTemplateSerializer
|
||||
@@ -636,12 +687,19 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
||||
filterset_fields = ['visibility', 'category', 'is_approved']
|
||||
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):
|
||||
"""
|
||||
Filter templates based on user permissions.
|
||||
|
||||
- Marketplace view: Only approved PUBLIC templates
|
||||
- My Plugins: User's own templates (all visibilities)
|
||||
- Marketplace view: Only approved PUBLIC templates (always accessible)
|
||||
- My Plugins: User's own templates (requires can_use_plugins)
|
||||
- Platform admins: All templates
|
||||
"""
|
||||
queryset = super().get_queryset()
|
||||
@@ -649,19 +707,22 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
||||
|
||||
if view_mode == 'marketplace':
|
||||
# Public marketplace - platform official + approved public templates
|
||||
# Always accessible for discovery/marketing purposes
|
||||
from django.db.models import Q
|
||||
queryset = queryset.filter(
|
||||
Q(visibility=PluginTemplate.Visibility.PLATFORM) |
|
||||
Q(visibility=PluginTemplate.Visibility.PUBLIC, is_approved=True)
|
||||
)
|
||||
elif view_mode == 'my_plugins':
|
||||
# User's own templates
|
||||
if self.request.user.is_authenticated:
|
||||
# User's own templates - requires plugin permission
|
||||
if not self._has_plugins_permission():
|
||||
queryset = queryset.none()
|
||||
elif self.request.user.is_authenticated:
|
||||
queryset = queryset.filter(author=self.request.user)
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
elif view_mode == 'platform':
|
||||
# Platform official plugins
|
||||
# Platform official plugins - always accessible for discovery
|
||||
queryset = queryset.filter(visibility=PluginTemplate.Visibility.PLATFORM)
|
||||
# else: all templates (for platform admins)
|
||||
|
||||
@@ -694,8 +755,15 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
||||
from .template_parser import TemplateVariableParser
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
# Check permission to create plugins
|
||||
# Check permission to use plugins first
|
||||
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'):
|
||||
raise PermissionDenied(
|
||||
"Your current plan does not include Plugin Creation. "
|
||||
@@ -773,6 +841,14 @@ class PluginTemplateViewSet(viewsets.ModelViewSet):
|
||||
"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()
|
||||
|
||||
# Check if template is accessible
|
||||
@@ -957,12 +1033,36 @@ class PluginInstallationViewSet(viewsets.ModelViewSet):
|
||||
- Update installation (update to latest version)
|
||||
- Uninstall plugin
|
||||
- Rate and review plugin
|
||||
|
||||
Permissions:
|
||||
- Requires can_use_plugins feature for all operations
|
||||
"""
|
||||
queryset = PluginInstallation.objects.select_related('template', 'scheduled_task').all()
|
||||
serializer_class = PluginInstallationSerializer
|
||||
permission_classes = [AllowAny] # TODO: Change to IsAuthenticated for production
|
||||
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):
|
||||
"""Return installations for current user/tenant"""
|
||||
queryset = super().get_queryset()
|
||||
@@ -973,6 +1073,20 @@ class PluginInstallationViewSet(viewsets.ModelViewSet):
|
||||
|
||||
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'])
|
||||
def update_to_latest(self, request, pk=None):
|
||||
"""Update installed plugin to latest template version"""
|
||||
@@ -1080,6 +1194,19 @@ class EventPluginViewSet(viewsets.ModelViewSet):
|
||||
|
||||
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):
|
||||
"""
|
||||
List event plugins.
|
||||
@@ -1195,7 +1322,16 @@ class GlobalEventPluginViewSet(viewsets.ModelViewSet):
|
||||
return queryset.order_by('execution_order', 'created_at')
|
||||
|
||||
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
|
||||
serializer.save(created_by=user)
|
||||
|
||||
|
||||
@@ -812,6 +812,13 @@ def list_phone_numbers_view(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(
|
||||
assigned_tenant=tenant,
|
||||
is_active=True,
|
||||
|
||||
@@ -135,11 +135,23 @@ class APITokenViewSet(viewsets.ViewSet):
|
||||
|
||||
This endpoint requires regular user authentication (not API token auth)
|
||||
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
|
||||
authentication_classes = [SessionAuthentication, TokenAuthentication]
|
||||
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):
|
||||
"""List all API tokens for the current business."""
|
||||
user = request.user
|
||||
@@ -154,6 +166,9 @@ class APITokenViewSet(viewsets.ViewSet):
|
||||
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)
|
||||
allowed_roles = ['OWNER', 'TENANT_OWNER', 'SUPERUSER', 'PLATFORM_MANAGER', 'TENANT_MANAGER']
|
||||
if user.role.upper() not in allowed_roles:
|
||||
@@ -180,6 +195,9 @@ class APITokenViewSet(viewsets.ViewSet):
|
||||
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']
|
||||
if user.role.upper() not in allowed_roles:
|
||||
return Response(
|
||||
@@ -1116,18 +1134,10 @@ class WebhookViewSet(PublicAPIViewMixin, viewsets.ViewSet):
|
||||
"""
|
||||
permission_classes = [HasAPIToken, CanManageWebhooks]
|
||||
|
||||
def list(self, request):
|
||||
"""List webhook subscriptions for the current API token."""
|
||||
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."""
|
||||
def _check_webhooks_permission(self, request):
|
||||
"""Check if tenant has permission to use webhooks."""
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
# Check permission to use webhooks
|
||||
token = request.api_token
|
||||
tenant = token.tenant
|
||||
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."
|
||||
)
|
||||
|
||||
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)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
@@ -1160,6 +1182,7 @@ class WebhookViewSet(PublicAPIViewMixin, viewsets.ViewSet):
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
"""Get webhook subscription details."""
|
||||
self._check_webhooks_permission(request)
|
||||
token = request.api_token
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user