feat: Add comprehensive plugin documentation and advanced template system

Added complete plugin documentation with visual mockups and expanded template
variable system with CONTEXT, DATE helpers, and default values.

Backend Changes:
- Extended template_parser.py to support all new template types
- Added PROMPT with default values: {{PROMPT:var|desc|default}}
- Added CONTEXT variables: {{CONTEXT:business_name}}, {{CONTEXT:owner_email}}
- Added DATE helpers: {{DATE:today}}, {{DATE:+7d}}, {{DATE:monday}}
- Implemented date expression evaluation for relative dates
- Updated compile_template to handle all template types
- Added context parameter for business data auto-fill

Frontend Changes:
- Created comprehensive HelpPluginDocs.tsx with Stripe-style API docs
- Added visual mockup of plugin configuration form
- Documented all template types with examples and benefits
- Added Command Reference section with allowed/blocked Python commands
- Documented all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Added URL whitelisting requirements and approval process
- Created Platform Staff management page with edit modal
- Added can_approve_plugins and can_whitelist_urls permissions

Platform Staff Features:
- List all platform_manager and platform_support users
- Edit user details with role-based permissions
- Superusers can edit anyone
- Platform managers can only edit platform_support users
- Permission cascade: users can only grant permissions they have
- Real-time updates via React Query cache invalidation

Documentation Highlights:
- 4 template types: PROMPT, CONTEXT, DATE, and automatic validation
- Visual form mockup showing exactly what users see
- All allowed control flow (if/elif/else, for, while, try/except, etc.)
- All allowed built-in functions (len, range, min, max, etc.)
- All blocked operations (import, exec, eval, class/function defs)
- Complete HTTP API reference with examples
- URL whitelisting process: contact pluginaccess@smoothschedule.com

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-28 20:54:07 -05:00
parent a9719a5fd2
commit 3fef0d5749
46 changed files with 8883 additions and 555 deletions

View File

@@ -0,0 +1,84 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e7]:
- generic [ref=e9]:
- img [ref=e10]
- generic [ref=e16]: Smooth Schedule
- generic [ref=e17]:
- heading "Orchestrate your business with precision." [level=1] [ref=e18]
- paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e24]: © 2025 Smooth Schedule Inc.
- generic [ref=e26]:
- generic [ref=e27]:
- heading "Welcome back" [level=2] [ref=e28]
- paragraph [ref=e29]: Please enter your details to sign in.
- generic [ref=e30]:
- generic [ref=e31]:
- generic [ref=e32]:
- generic [ref=e33]: Username
- generic [ref=e34]:
- generic:
- img
- textbox "Username" [active] [ref=e35]:
- /placeholder: Enter your username
- text: superuser
- generic [ref=e36]:
- generic [ref=e37]: Password
- generic [ref=e38]:
- generic:
- img
- textbox "Password" [ref=e39]:
- /placeholder: ••••••••
- button "Sign in" [ref=e40]:
- generic [ref=e41]:
- text: Sign in
- img [ref=e42]
- generic [ref=e50]: Or continue with
- button "🇺🇸 English" [ref=e53]:
- img [ref=e54]
- generic [ref=e58]: 🇺🇸
- generic [ref=e59]: English
- img [ref=e60]
- generic [ref=e62]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e64]:
- generic [ref=e65]: 🔓
- generic [ref=e66]: Quick Login (Dev Only)
- generic [ref=e67]:
- button "Platform Superuser SUPERUSER" [ref=e68]:
- generic [ref=e69]:
- generic [ref=e70]: Platform Superuser
- generic [ref=e71]: SUPERUSER
- button "Platform Manager PLATFORM_MANAGER" [ref=e72]:
- generic [ref=e73]:
- generic [ref=e74]: Platform Manager
- generic [ref=e75]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [ref=e76]:
- generic [ref=e77]:
- generic [ref=e78]: Platform Sales
- generic [ref=e79]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Platform Support
- generic [ref=e83]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [ref=e84]:
- generic [ref=e85]:
- generic [ref=e86]: Business Owner
- generic [ref=e87]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [ref=e88]:
- generic [ref=e89]:
- generic [ref=e90]: Business Manager
- generic [ref=e91]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [ref=e92]:
- generic [ref=e93]:
- generic [ref=e94]: Staff Member
- generic [ref=e95]: TENANT_STAFF
- button "Customer CUSTOMER" [ref=e96]:
- generic [ref=e97]:
- generic [ref=e98]: Customer
- generic [ref=e99]: CUSTOMER
- generic [ref=e100]:
- text: "Password for all:"
- code [ref=e101]: test123
```

View File

@@ -1,268 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e5]:
- button "Collapse sidebar" [ref=e6]:
- generic [ref=e7]: DE
- generic [ref=e8]:
- heading "Demo Company" [level=1] [ref=e9]
- paragraph [ref=e10]: demo.smoothschedule.com
- navigation [ref=e11]:
- link "Dashboard" [ref=e12] [cursor=pointer]:
- /url: "#/"
- img [ref=e13]
- generic [ref=e18]: Dashboard
- link "Scheduler" [ref=e19] [cursor=pointer]:
- /url: "#/scheduler"
- img [ref=e20]
- generic [ref=e22]: Scheduler
- link "Customers" [ref=e23] [cursor=pointer]:
- /url: "#/customers"
- img [ref=e24]
- generic [ref=e29]: Customers
- link "Services" [ref=e30] [cursor=pointer]:
- /url: "#/services"
- img [ref=e31]
- generic [ref=e34]: Services
- link "Resources" [ref=e35] [cursor=pointer]:
- /url: "#/resources"
- img [ref=e36]
- generic [ref=e39]: Resources
- generic "Payments are disabled. Enable them in Business Settings to accept payments from customers." [ref=e40]:
- img [ref=e41]
- generic [ref=e43]: Payments
- link "Messages" [ref=e44] [cursor=pointer]:
- /url: "#/messages"
- img [ref=e45]
- generic [ref=e47]: Messages
- link "Staff" [ref=e48] [cursor=pointer]:
- /url: "#/staff"
- img [ref=e49]
- generic [ref=e54]: Staff
- link "Business Settings" [ref=e56] [cursor=pointer]:
- /url: "#/settings"
- img [ref=e57]
- generic [ref=e60]: Business Settings
- generic [ref=e61]:
- generic [ref=e62]:
- img [ref=e63]
- generic [ref=e69]:
- generic [ref=e70]: Powered by
- text: Smooth Schedule
- button "Sign Out" [ref=e71]:
- img [ref=e72]
- generic [ref=e75]: Sign Out
- generic [ref=e76]:
- banner [ref=e77]:
- generic [ref=e79]:
- img [ref=e81]
- textbox "Search" [ref=e84]
- generic [ref=e85]:
- button "🇺🇸 English" [ref=e87]:
- img [ref=e88]
- generic [ref=e91]: 🇺🇸
- generic [ref=e92]: English
- img [ref=e93]
- button [ref=e95]:
- img [ref=e96]
- button [ref=e98]:
- img [ref=e99]
- button "Business Owner Owner BO" [ref=e104]:
- generic [ref=e105]:
- paragraph [ref=e106]: Business Owner
- paragraph [ref=e107]: Owner
- generic [ref=e108]: BO
- img [ref=e109]
- main [active] [ref=e111]:
- generic [ref=e112]:
- generic [ref=e113]:
- heading "Dashboard" [level=2] [ref=e114]
- paragraph [ref=e115]: Today's Overview
- generic [ref=e116]:
- generic [ref=e117]:
- paragraph [ref=e118]: Total Appointments
- generic [ref=e119]:
- generic [ref=e120]: "50"
- generic [ref=e121]:
- img [ref=e122]
- text: +12%
- generic [ref=e125]:
- paragraph [ref=e126]: Customers
- generic [ref=e127]:
- generic [ref=e128]: "1"
- generic [ref=e129]:
- img [ref=e130]
- text: +8%
- generic [ref=e133]:
- paragraph [ref=e134]: Services
- generic [ref=e135]:
- generic [ref=e136]: "5"
- generic [ref=e137]:
- img [ref=e138]
- text: 0%
- generic [ref=e139]:
- paragraph [ref=e140]: Resources
- generic [ref=e141]:
- generic [ref=e142]: "4"
- generic [ref=e143]:
- img [ref=e144]
- text: +3%
- generic [ref=e147]:
- generic [ref=e149]:
- generic [ref=e150]:
- img [ref=e152]
- heading "Quick Add Appointment" [level=3] [ref=e154]
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- img [ref=e158]
- text: Customer
- combobox [ref=e161]:
- option "Walk-in / No customer" [selected]
- option "Customer User (customer@demo.com)"
- generic [ref=e162]:
- generic [ref=e163]:
- img [ref=e164]
- text: Service *
- combobox [ref=e167]:
- option "Select service..." [selected]
- option "Beard Trim (15 min - $15)"
- option "Consultation (30 min - $0)"
- option "Full Styling (60 min - $75)"
- option "Hair Coloring (90 min - $120)"
- option "Haircut (30 min - $35)"
- generic [ref=e168]:
- generic [ref=e169]:
- img [ref=e170]
- text: Resource
- combobox [ref=e173]:
- option "Unassigned" [selected]
- option "Conference Room A"
- option "Dental Chair 1"
- option "Meeting Room B"
- option "Meeting Room B"
- generic [ref=e174]:
- generic [ref=e175]:
- generic [ref=e176]: Date *
- textbox [ref=e177]: 2025-11-27
- generic [ref=e178]:
- generic [ref=e179]:
- img [ref=e180]
- text: Time *
- combobox [ref=e183]:
- option "06:00"
- option "06:15"
- option "06:30"
- option "06:45"
- option "07:00"
- option "07:15"
- option "07:30"
- option "07:45"
- option "08:00"
- option "08:15"
- option "08:30"
- option "08:45"
- option "09:00" [selected]
- option "09:15"
- option "09:30"
- option "09:45"
- option "10:00"
- option "10:15"
- option "10:30"
- option "10:45"
- option "11:00"
- option "11:15"
- option "11:30"
- option "11:45"
- option "12:00"
- option "12:15"
- option "12:30"
- option "12:45"
- option "13:00"
- option "13:15"
- option "13:30"
- option "13:45"
- option "14:00"
- option "14:15"
- option "14:30"
- option "14:45"
- option "15:00"
- option "15:15"
- option "15:30"
- option "15:45"
- option "16:00"
- option "16:15"
- option "16:30"
- option "16:45"
- option "17:00"
- option "17:15"
- option "17:30"
- option "17:45"
- option "18:00"
- option "18:15"
- option "18:30"
- option "18:45"
- option "19:00"
- option "19:15"
- option "19:30"
- option "19:45"
- option "20:00"
- option "20:15"
- option "20:30"
- option "20:45"
- option "21:00"
- option "21:15"
- option "21:30"
- option "21:45"
- option "22:00"
- option "22:15"
- option "22:30"
- option "22:45"
- generic [ref=e184]:
- generic [ref=e185]:
- img [ref=e186]
- text: Notes
- textbox "Optional notes..." [ref=e189]
- button "Add Appointment" [disabled] [ref=e190]:
- img [ref=e191]
- text: Add Appointment
- generic [ref=e193]:
- heading "Total Revenue" [level=3] [ref=e194]
- application [ref=e198]:
- generic [ref=e202]:
- generic [ref=e203]:
- generic [ref=e205]: Mon
- generic [ref=e207]: Tue
- generic [ref=e209]: Wed
- generic [ref=e211]: Thu
- generic [ref=e213]: Fri
- generic [ref=e215]: Sat
- generic [ref=e217]: Sun
- generic [ref=e218]:
- generic [ref=e220]: $0
- generic [ref=e222]: $1
- generic [ref=e224]: $2
- generic [ref=e226]: $3
- generic [ref=e228]: $4
- generic [ref=e229]:
- heading "Upcoming Appointments" [level=3] [ref=e230]
- application [ref=e234]:
- generic [ref=e250]:
- generic [ref=e251]:
- generic [ref=e253]: Mon
- generic [ref=e255]: Tue
- generic [ref=e257]: Wed
- generic [ref=e259]: Thu
- generic [ref=e261]: Fri
- generic [ref=e263]: Sat
- generic [ref=e265]: Sun
- generic [ref=e266]:
- generic [ref=e268]: "0"
- generic [ref=e270]: "3"
- generic [ref=e272]: "6"
- generic [ref=e274]: "9"
- generic [ref=e276]: "12"
- generic [ref=e277]: "0"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -0,0 +1,84 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e7]:
- generic [ref=e9]:
- img [ref=e10]
- generic [ref=e16]: Smooth Schedule
- generic [ref=e17]:
- heading "Orchestrate your business with precision." [level=1] [ref=e18]
- paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e24]: © 2025 Smooth Schedule Inc.
- generic [ref=e26]:
- generic [ref=e27]:
- heading "Welcome back" [level=2] [ref=e28]
- paragraph [ref=e29]: Please enter your details to sign in.
- generic [ref=e30]:
- generic [ref=e31]:
- generic [ref=e32]:
- generic [ref=e33]: Username
- generic [ref=e34]:
- generic:
- img
- textbox "Username" [active] [ref=e35]:
- /placeholder: Enter your username
- text: superuser
- generic [ref=e36]:
- generic [ref=e37]: Password
- generic [ref=e38]:
- generic:
- img
- textbox "Password" [ref=e39]:
- /placeholder: ••••••••
- button "Sign in" [ref=e40]:
- generic [ref=e41]:
- text: Sign in
- img [ref=e42]
- generic [ref=e49]: Or continue with
- button "🇺🇸 English" [ref=e52]:
- img [ref=e53]
- generic [ref=e56]: 🇺🇸
- generic [ref=e57]: English
- img [ref=e58]
- generic [ref=e60]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e62]:
- generic [ref=e63]: 🔓
- generic [ref=e64]: Quick Login (Dev Only)
- generic [ref=e65]:
- button "Platform Superuser SUPERUSER" [ref=e66]:
- generic [ref=e67]:
- generic [ref=e68]: Platform Superuser
- generic [ref=e69]: SUPERUSER
- button "Platform Manager PLATFORM_MANAGER" [ref=e70]:
- generic [ref=e71]:
- generic [ref=e72]: Platform Manager
- generic [ref=e73]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [ref=e74]:
- generic [ref=e75]:
- generic [ref=e76]: Platform Sales
- generic [ref=e77]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [ref=e78]:
- generic [ref=e79]:
- generic [ref=e80]: Platform Support
- generic [ref=e81]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]: Business Owner
- generic [ref=e85]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]: Business Manager
- generic [ref=e89]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [ref=e90]:
- generic [ref=e91]:
- generic [ref=e92]: Staff Member
- generic [ref=e93]: TENANT_STAFF
- button "Customer CUSTOMER" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e96]: Customer
- generic [ref=e97]: CUSTOMER
- generic [ref=e98]:
- text: "Password for all:"
- code [ref=e99]: test123
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

View File

@@ -50,6 +50,7 @@ import PlatformDashboard from './pages/platform/PlatformDashboard';
import PlatformBusinesses from './pages/platform/PlatformBusinesses';
import PlatformSupportPage from './pages/platform/PlatformSupport';
import PlatformUsers from './pages/platform/PlatformUsers';
import PlatformStaff from './pages/platform/PlatformStaff';
import PlatformSettings from './pages/platform/PlatformSettings';
import ProfileSettings from './pages/ProfileSettings';
import VerifyEmail from './pages/VerifyEmail';
@@ -60,6 +61,7 @@ import Tickets from './pages/Tickets'; // Import Tickets page
import HelpGuide from './pages/HelpGuide'; // Import Platform Guide page
import HelpTicketing from './pages/HelpTicketing'; // Import Help page for ticketing
import HelpApiDocs from './pages/HelpApiDocs'; // Import API documentation page
import HelpPluginDocs from './pages/HelpPluginDocs'; // Import Plugin documentation page
import PlatformSupport from './pages/PlatformSupport'; // Import Platform Support page (for businesses to contact SmoothSchedule)
import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications
@@ -324,12 +326,14 @@ const AppContent: React.FC = () => {
<Route path="/platform/dashboard" element={<PlatformDashboard />} />
<Route path="/platform/businesses" element={<PlatformBusinesses onMasquerade={handleMasquerade} />} />
<Route path="/platform/users" element={<PlatformUsers onMasquerade={handleMasquerade} />} />
<Route path="/platform/staff" element={<PlatformStaff />} />
</>
)}
<Route path="/platform/support" element={<PlatformSupportPage />} />
<Route path="/help/guide" element={<HelpGuide />} />
<Route path="/help/ticketing" element={<HelpTicketing />} />
<Route path="/help/api" element={<HelpApiDocs />} />
<Route path="/help/plugins" element={<HelpPluginDocs />} />
{user.role === 'superuser' && (
<Route path="/platform/settings" element={<PlatformSettings />} />
)}
@@ -509,6 +513,7 @@ const AppContent: React.FC = () => {
<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="/support" element={<PlatformSupport />} />
<Route
path="/customers"

View File

@@ -17,7 +17,8 @@ import {
ChevronDown,
BookOpen,
FileQuestion,
LifeBuoy
LifeBuoy,
Zap
} from 'lucide-react';
import { Business, User } from '../types';
import { useLogout } from '../hooks/useAuth';
@@ -215,14 +216,24 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
<span>{t('nav.ticketingHelp', 'Ticketing System')}</span>
</Link>
{role === 'owner' && (
<Link
to="/help/api"
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/api' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
title={t('nav.apiDocs', 'API Documentation')}
>
<Code size={16} className="shrink-0" />
<span>{t('nav.apiDocs', 'API Docs')}</span>
</Link>
<>
<Link
to="/help/api"
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/api' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
title={t('nav.apiDocs', 'API Documentation')}
>
<Code size={16} className="shrink-0" />
<span>{t('nav.apiDocs', 'API Docs')}</span>
</Link>
<Link
to="/help/plugins"
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${location.pathname === '/help/plugins' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
title={t('nav.pluginDocs', 'Automation Plugins')}
>
<Zap size={16} className="shrink-0" />
<span>{t('nav.pluginDocs', 'Plugin Docs')}</span>
</Link>
</>
)}
<div className="pt-2 mt-2 border-t border-white/10">
<Link

View File

@@ -93,6 +93,7 @@
"platformGuide": "Platform Guide",
"ticketingHelp": "Ticketing System",
"apiDocs": "API Docs",
"pluginDocs": "Plugin Docs",
"contactSupport": "Contact Support"
},
"help": {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,348 @@
/**
* Platform Staff Management Page
* Allows superusers to manage platform staff (platform_manager, platform_support)
* with full editing capabilities including permissions
*/
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Users,
Search,
Plus,
Pencil,
Shield,
Mail,
Calendar,
CheckCircle,
XCircle,
Loader2,
} from 'lucide-react';
import { usePlatformUsers } from '../../hooks/usePlatform';
import { useCurrentUser } from '../../hooks/useAuth';
import EditPlatformUserModal from './components/EditPlatformUserModal';
interface PlatformUser {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
full_name?: string;
role: string;
is_active: boolean;
is_superuser: boolean;
permissions: {
can_approve_plugins?: boolean;
can_whitelist_urls?: boolean;
[key: string]: any;
};
created_at: string;
last_login?: string;
}
const PlatformStaff: React.FC = () => {
const { t } = useTranslation();
const { data: currentUser } = useCurrentUser();
const [searchTerm, setSearchTerm] = useState('');
const [selectedUser, setSelectedUser] = useState<PlatformUser | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const { data: allUsers, isLoading, error } = usePlatformUsers();
// Filter to only show platform staff (not superusers, not business users)
const platformStaff = (allUsers || []).filter(
(u: any) => u.role === 'platform_manager' || u.role === 'platform_support'
);
const filteredStaff = platformStaff.filter((u: any) => {
const searchLower = searchTerm.toLowerCase();
return (
(u.full_name || u.username || '').toLowerCase().includes(searchLower) ||
u.email.toLowerCase().includes(searchLower) ||
u.username.toLowerCase().includes(searchLower)
);
});
const canEditUser = (user: PlatformUser) => {
if (!currentUser) {
return false;
}
const currentRole = currentUser.role.toLowerCase();
const targetRole = user.role.toLowerCase();
// Superusers can edit anyone
if (currentRole === 'superuser') {
return true;
}
// Platform managers can only edit platform_support users, not other managers or superusers
if (currentRole === 'platform_manager') {
return targetRole === 'platform_support';
}
// All others cannot edit
return false;
};
const handleEdit = (user: any) => {
if (!canEditUser(user)) {
return; // Silently ignore if user cannot be edited
}
setSelectedUser(user);
setIsEditModalOpen(true);
};
const getRoleBadge = (role: string) => {
if (role === 'platform_manager') {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300">
<Shield className="w-3 h-3 mr-1" />
Platform Manager
</span>
);
}
if (role === 'platform_support') {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
<Mail className="w-3 h-3 mr-1" />
Platform Support
</span>
);
}
return null;
};
const formatDate = (dateString?: string) => {
if (!dateString) return 'Never';
return new Date(dateString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-indigo-600 dark:text-indigo-400" />
</div>
);
}
if (error) {
return (
<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 text-red-700 dark:text-red-400">
<XCircle className="w-5 h-5" />
<span>Failed to load platform staff</span>
</div>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<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">
<Users className="w-7 h-7" />
Platform Staff
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">
Manage platform managers and support staff
</p>
</div>
<button
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
onClick={() => {
// TODO: Implement create new staff member
alert('Create new staff member - coming soon');
}}
>
<Plus className="w-4 h-4" />
Add Staff Member
</button>
</div>
</div>
{/* Search */}
<div className="mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search staff by name, email, or username..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white"
/>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-sm text-gray-500 dark:text-gray-400">Total Staff</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{platformStaff.length}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-sm text-gray-500 dark:text-gray-400">Platform Managers</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{platformStaff.filter((u: any) => u.role === 'platform_manager').length}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-sm text-gray-500 dark:text-gray-400">Support Staff</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{platformStaff.filter((u: any) => u.role === 'platform_support').length}
</div>
</div>
</div>
{/* Staff List */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Staff Member
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Permissions
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last Login
</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
{filteredStaff.map((user: any) => (
<tr
key={user.id}
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
{/* Staff Member */}
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-semibold">
{(user.full_name || user.username).charAt(0).toUpperCase()}
</div>
<div>
<div className="font-medium text-gray-900 dark:text-white">
{user.full_name || user.username}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono">
{user.email}
</div>
</div>
</div>
</td>
{/* Role */}
<td className="px-6 py-4">{getRoleBadge(user.role)}</td>
{/* Permissions */}
<td className="px-6 py-4">
<div className="flex flex-wrap gap-1">
{user.permissions?.can_approve_plugins && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
Plugin Approver
</span>
)}
{user.permissions?.can_whitelist_urls && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
URL Whitelister
</span>
)}
{!user.permissions?.can_approve_plugins && !user.permissions?.can_whitelist_urls && (
<span className="text-xs text-gray-400 dark:text-gray-500">
No special permissions
</span>
)}
</div>
</td>
{/* Status */}
<td className="px-6 py-4">
{user.is_active ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
<CheckCircle className="w-3 h-3 mr-1" />
Active
</span>
) : (
<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">
<XCircle className="w-3 h-3 mr-1" />
Inactive
</span>
)}
</td>
{/* Last Login */}
<td className="px-6 py-4">
<div className="flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400">
<Calendar className="w-3 h-3" />
{formatDate(user.last_login)}
</div>
</td>
{/* Actions */}
<td className="px-6 py-4 text-right">
<button
onClick={() => handleEdit(user)}
disabled={!canEditUser(user)}
className="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 border border-indigo-200 dark:border-indigo-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent"
>
<Pencil className="w-3.5 h-3.5" />
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Empty State */}
{filteredStaff.length === 0 && (
<div className="p-12 text-center">
<Users className="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
No staff members found
</h3>
<p className="text-gray-500 dark:text-gray-400">
{searchTerm
? 'Try adjusting your search criteria'
: 'Add your first platform staff member to get started'}
</p>
</div>
)}
</div>
{/* Edit Modal */}
{selectedUser && (
<EditPlatformUserModal
isOpen={isEditModalOpen}
onClose={() => {
setIsEditModalOpen(false);
setSelectedUser(null);
}}
user={selectedUser}
/>
)}
</div>
);
};
export default PlatformStaff;

View File

@@ -0,0 +1,472 @@
/**
* Edit Platform User Modal
* Allows superusers to edit all aspects of platform staff including:
* - Basic info (name, email, username)
* - Password reset
* - Role assignment
* - Permissions (can_approve_plugins, etc.)
* - Account status (active/inactive)
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
X,
User,
Mail,
Lock,
Shield,
CheckCircle,
XCircle,
Save,
Eye,
EyeOff,
AlertCircle,
} from 'lucide-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../../api/client';
import { useCurrentUser } from '../../../hooks/useAuth';
interface EditPlatformUserModalProps {
isOpen: boolean;
onClose: () => void;
user: {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
role: string;
is_active: boolean;
permissions: {
can_approve_plugins?: boolean;
[key: string]: any;
};
};
}
const EditPlatformUserModal: React.FC<EditPlatformUserModalProps> = ({
isOpen,
onClose,
user,
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: currentUser } = useCurrentUser();
// Check if current user can edit this user
const currentRole = currentUser?.role?.toLowerCase();
const targetRole = user.role?.toLowerCase();
const canEditRole = currentRole === 'superuser' ||
(currentRole === 'platform_manager' && targetRole === 'platform_support');
// Get available permissions for current user
// Superusers always have all permissions, others check the permissions field
const availablePermissions = {
can_approve_plugins: currentRole === 'superuser' || !!currentUser?.permissions?.can_approve_plugins,
can_whitelist_urls: currentRole === 'superuser' || !!currentUser?.permissions?.can_whitelist_urls,
};
// Form state
const [formData, setFormData] = useState({
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
is_active: user.is_active,
});
const [permissions, setPermissions] = useState({
can_approve_plugins: user.permissions?.can_approve_plugins || false,
can_whitelist_urls: user.permissions?.can_whitelist_urls || false,
});
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [passwordError, setPasswordError] = useState('');
// Update mutation
const updateMutation = useMutation({
mutationFn: async (data: any) => {
const response = await apiClient.patch(`/api/platform/users/${user.id}/`, data);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['platform', 'users'] });
onClose();
},
});
// Reset form when user changes
useEffect(() => {
setFormData({
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
is_active: user.is_active,
});
setPermissions({
can_approve_plugins: user.permissions?.can_approve_plugins || false,
can_whitelist_urls: user.permissions?.can_whitelist_urls || false,
});
setPassword('');
setConfirmPassword('');
setPasswordError('');
}, [user]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate password if provided
if (password) {
if (password !== confirmPassword) {
setPasswordError('Passwords do not match');
return;
}
if (password.length < 8) {
setPasswordError('Password must be at least 8 characters');
return;
}
}
// Prepare update data
const updateData: any = {
...formData,
permissions: permissions,
};
// Only include password if changed
if (password) {
updateData.password = password;
}
updateMutation.mutate(updateData);
};
const handlePermissionToggle = (permission: string) => {
setPermissions((prev) => ({
...prev,
[permission]: !prev[permission as keyof typeof prev],
}));
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
{/* Background overlay */}
<div
className="fixed inset-0 bg-gray-900/75 transition-opacity"
onClick={onClose}
/>
{/* Modal panel */}
<div className="relative inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full z-50">
{/* Header */}
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
<Shield className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-white">Edit Platform User</h3>
<p className="text-sm text-indigo-100">
{user.username} ({user.email})
</p>
</div>
</div>
<button
onClick={onClose}
className="text-white hover:text-indigo-100 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6">
<div className="space-y-6">
{/* Basic Info */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<User className="w-4 h-4" />
Basic Information
</h4>
<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">
First Name
</label>
<input
type="text"
value={formData.first_name}
onChange={(e) =>
setFormData({ ...formData, first_name: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
type="text"
value={formData.last_name}
onChange={(e) =>
setFormData({ ...formData, last_name: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
</div>
{/* Account Details */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Mail className="w-4 h-4" />
Account Details
</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Username
</label>
<input
type="text"
value={formData.username}
onChange={(e) =>
setFormData({ ...formData, username: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white font-mono"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email Address
</label>
<input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
</div>
{/* Role */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Shield className="w-4 h-4" />
Role & Access
</h4>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Platform Role
</label>
<select
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
disabled={!canEditRole}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="platform_manager">Platform Manager</option>
<option value="platform_support">Platform Support</option>
</select>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{canEditRole
? 'Platform Managers have full administrative access. Support staff have limited access.'
: 'You do not have permission to change this user\'s role.'}
</p>
</div>
</div>
{/* Permissions */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
Special Permissions
</h4>
<div className="space-y-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
{availablePermissions.can_approve_plugins && (
<label className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
checked={permissions.can_approve_plugins}
onChange={() => handlePermissionToggle('can_approve_plugins')}
className="mt-0.5 w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
Can Approve Plugins
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
Allow this user to review and approve community plugins for the marketplace
</div>
</div>
</label>
)}
{availablePermissions.can_whitelist_urls && (
<label className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
checked={permissions.can_whitelist_urls}
onChange={() => handlePermissionToggle('can_whitelist_urls')}
className="mt-0.5 w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
Can Whitelist URLs
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
Allow this user to whitelist external URLs for plugin API calls (per-user and platform-wide)
</div>
</div>
</label>
)}
{!availablePermissions.can_approve_plugins && !availablePermissions.can_whitelist_urls && (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
You don't have any special permissions to grant.
</p>
)}
</div>
</div>
{/* Password Reset */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Lock className="w-4 h-4" />
Reset Password (Optional)
</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => {
setPassword(e.target.value);
setPasswordError('');
}}
placeholder="Leave blank to keep current password"
className="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{password && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password
</label>
<input
type={showPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
setPasswordError('');
}}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
/>
</div>
)}
{passwordError && (
<div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
<AlertCircle className="w-4 h-4" />
{passwordError}
</div>
)}
</div>
</div>
{/* Account Status */}
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
Account Status
</h4>
<label className="flex items-center gap-3 cursor-pointer group bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) =>
setFormData({ ...formData, is_active: e.target.checked })
}
className="w-4 h-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
{formData.is_active ? (
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<XCircle className="w-4 h-4 text-gray-400" />
)}
<span className="text-sm font-medium text-gray-900 dark:text-white">
{formData.is_active ? 'Account Active' : 'Account Inactive'}
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{formData.is_active
? 'User can log in and access the platform'
: 'User cannot log in or access the platform'}
</p>
</div>
</label>
</div>
</div>
{/* Error Display */}
{updateMutation.isError && (
<div className="mt-4 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 text-red-700 dark:text-red-400">
<AlertCircle className="w-5 h-5" />
<span>Failed to update user. Please try again.</span>
</div>
</div>
)}
{/* Actions */}
<div className="mt-6 flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={updateMutation.isPending}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white text-sm font-medium rounded-lg transition-colors"
>
<Save className="w-4 h-4" />
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default EditPlatformUserModal;

View File

@@ -1,6 +1,8 @@
{
"status": "failed",
"failedTests": [
"4ccd3b6df344f024c4e8-470435a1aee1bc432b30"
"590ad8d7fc7ae2069797-2afcd486fa868ee7fcc3",
"590ad8d7fc7ae2069797-90df2b140e1ff4bac88e",
"590ad8d7fc7ae2069797-def5944da7e0860b9fef"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -1,268 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e5]:
- button "Collapse sidebar" [ref=e6]:
- generic [ref=e7]: DE
- generic [ref=e8]:
- heading "Demo Company" [level=1] [ref=e9]
- paragraph [ref=e10]: demo.smoothschedule.com
- navigation [ref=e11]:
- link "Dashboard" [ref=e12] [cursor=pointer]:
- /url: "#/"
- img [ref=e13]
- generic [ref=e18]: Dashboard
- link "Scheduler" [ref=e19] [cursor=pointer]:
- /url: "#/scheduler"
- img [ref=e20]
- generic [ref=e22]: Scheduler
- link "Customers" [ref=e23] [cursor=pointer]:
- /url: "#/customers"
- img [ref=e24]
- generic [ref=e29]: Customers
- link "Services" [ref=e30] [cursor=pointer]:
- /url: "#/services"
- img [ref=e31]
- generic [ref=e34]: Services
- link "Resources" [ref=e35] [cursor=pointer]:
- /url: "#/resources"
- img [ref=e36]
- generic [ref=e39]: Resources
- generic "Payments are disabled. Enable them in Business Settings to accept payments from customers." [ref=e40]:
- img [ref=e41]
- generic [ref=e43]: Payments
- link "Messages" [ref=e44] [cursor=pointer]:
- /url: "#/messages"
- img [ref=e45]
- generic [ref=e47]: Messages
- link "Staff" [ref=e48] [cursor=pointer]:
- /url: "#/staff"
- img [ref=e49]
- generic [ref=e54]: Staff
- link "Business Settings" [ref=e56] [cursor=pointer]:
- /url: "#/settings"
- img [ref=e57]
- generic [ref=e60]: Business Settings
- generic [ref=e61]:
- generic [ref=e62]:
- img [ref=e63]
- generic [ref=e69]:
- generic [ref=e70]: Powered by
- text: Smooth Schedule
- button "Sign Out" [ref=e71]:
- img [ref=e72]
- generic [ref=e75]: Sign Out
- generic [ref=e76]:
- banner [ref=e77]:
- generic [ref=e79]:
- img [ref=e81]
- textbox "Search" [ref=e84]
- generic [ref=e85]:
- button "🇺🇸 English" [ref=e87]:
- img [ref=e88]
- generic [ref=e91]: 🇺🇸
- generic [ref=e92]: English
- img [ref=e93]
- button [ref=e95]:
- img [ref=e96]
- button [ref=e98]:
- img [ref=e99]
- button "Business Owner Owner BO" [ref=e104]:
- generic [ref=e105]:
- paragraph [ref=e106]: Business Owner
- paragraph [ref=e107]: Owner
- generic [ref=e108]: BO
- img [ref=e109]
- main [active] [ref=e111]:
- generic [ref=e112]:
- generic [ref=e113]:
- heading "Dashboard" [level=2] [ref=e114]
- paragraph [ref=e115]: Today's Overview
- generic [ref=e116]:
- generic [ref=e117]:
- paragraph [ref=e118]: Total Appointments
- generic [ref=e119]:
- generic [ref=e120]: "50"
- generic [ref=e121]:
- img [ref=e122]
- text: +12%
- generic [ref=e125]:
- paragraph [ref=e126]: Customers
- generic [ref=e127]:
- generic [ref=e128]: "1"
- generic [ref=e129]:
- img [ref=e130]
- text: +8%
- generic [ref=e133]:
- paragraph [ref=e134]: Services
- generic [ref=e135]:
- generic [ref=e136]: "5"
- generic [ref=e137]:
- img [ref=e138]
- text: 0%
- generic [ref=e139]:
- paragraph [ref=e140]: Resources
- generic [ref=e141]:
- generic [ref=e142]: "4"
- generic [ref=e143]:
- img [ref=e144]
- text: +3%
- generic [ref=e147]:
- generic [ref=e149]:
- generic [ref=e150]:
- img [ref=e152]
- heading "Quick Add Appointment" [level=3] [ref=e154]
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- img [ref=e158]
- text: Customer
- combobox [ref=e161]:
- option "Walk-in / No customer" [selected]
- option "Customer User (customer@demo.com)"
- generic [ref=e162]:
- generic [ref=e163]:
- img [ref=e164]
- text: Service *
- combobox [ref=e167]:
- option "Select service..." [selected]
- option "Beard Trim (15 min - $15)"
- option "Consultation (30 min - $0)"
- option "Full Styling (60 min - $75)"
- option "Hair Coloring (90 min - $120)"
- option "Haircut (30 min - $35)"
- generic [ref=e168]:
- generic [ref=e169]:
- img [ref=e170]
- text: Resource
- combobox [ref=e173]:
- option "Unassigned" [selected]
- option "Conference Room A"
- option "Dental Chair 1"
- option "Meeting Room B"
- option "Meeting Room B"
- generic [ref=e174]:
- generic [ref=e175]:
- generic [ref=e176]: Date *
- textbox [ref=e177]: 2025-11-27
- generic [ref=e178]:
- generic [ref=e179]:
- img [ref=e180]
- text: Time *
- combobox [ref=e183]:
- option "06:00"
- option "06:15"
- option "06:30"
- option "06:45"
- option "07:00"
- option "07:15"
- option "07:30"
- option "07:45"
- option "08:00"
- option "08:15"
- option "08:30"
- option "08:45"
- option "09:00" [selected]
- option "09:15"
- option "09:30"
- option "09:45"
- option "10:00"
- option "10:15"
- option "10:30"
- option "10:45"
- option "11:00"
- option "11:15"
- option "11:30"
- option "11:45"
- option "12:00"
- option "12:15"
- option "12:30"
- option "12:45"
- option "13:00"
- option "13:15"
- option "13:30"
- option "13:45"
- option "14:00"
- option "14:15"
- option "14:30"
- option "14:45"
- option "15:00"
- option "15:15"
- option "15:30"
- option "15:45"
- option "16:00"
- option "16:15"
- option "16:30"
- option "16:45"
- option "17:00"
- option "17:15"
- option "17:30"
- option "17:45"
- option "18:00"
- option "18:15"
- option "18:30"
- option "18:45"
- option "19:00"
- option "19:15"
- option "19:30"
- option "19:45"
- option "20:00"
- option "20:15"
- option "20:30"
- option "20:45"
- option "21:00"
- option "21:15"
- option "21:30"
- option "21:45"
- option "22:00"
- option "22:15"
- option "22:30"
- option "22:45"
- generic [ref=e184]:
- generic [ref=e185]:
- img [ref=e186]
- text: Notes
- textbox "Optional notes..." [ref=e189]
- button "Add Appointment" [disabled] [ref=e190]:
- img [ref=e191]
- text: Add Appointment
- generic [ref=e193]:
- heading "Total Revenue" [level=3] [ref=e194]
- application [ref=e198]:
- generic [ref=e202]:
- generic [ref=e203]:
- generic [ref=e205]: Mon
- generic [ref=e207]: Tue
- generic [ref=e209]: Wed
- generic [ref=e211]: Thu
- generic [ref=e213]: Fri
- generic [ref=e215]: Sat
- generic [ref=e217]: Sun
- generic [ref=e218]:
- generic [ref=e220]: $0
- generic [ref=e222]: $1
- generic [ref=e224]: $2
- generic [ref=e226]: $3
- generic [ref=e228]: $4
- generic [ref=e229]:
- heading "Upcoming Appointments" [level=3] [ref=e230]
- application [ref=e234]:
- generic [ref=e250]:
- generic [ref=e251]:
- generic [ref=e253]: Mon
- generic [ref=e255]: Tue
- generic [ref=e257]: Wed
- generic [ref=e259]: Thu
- generic [ref=e261]: Fri
- generic [ref=e263]: Sat
- generic [ref=e265]: Sun
- generic [ref=e266]:
- generic [ref=e268]: "0"
- generic [ref=e270]: "3"
- generic [ref=e272]: "6"
- generic [ref=e274]: "9"
- generic [ref=e276]: "12"
- generic [ref=e277]: "0"
```

View File

@@ -0,0 +1,84 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e7]:
- generic [ref=e9]:
- img [ref=e10]
- generic [ref=e16]: Smooth Schedule
- generic [ref=e17]:
- heading "Orchestrate your business with precision." [level=1] [ref=e18]
- paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e24]: © 2025 Smooth Schedule Inc.
- generic [ref=e26]:
- generic [ref=e27]:
- heading "Welcome back" [level=2] [ref=e28]
- paragraph [ref=e29]: Please enter your details to sign in.
- generic [ref=e30]:
- generic [ref=e31]:
- generic [ref=e32]:
- generic [ref=e33]: Username
- generic [ref=e34]:
- generic:
- img
- textbox "Username" [active] [ref=e35]:
- /placeholder: Enter your username
- text: superuser
- generic [ref=e36]:
- generic [ref=e37]: Password
- generic [ref=e38]:
- generic:
- img
- textbox "Password" [ref=e39]:
- /placeholder: ••••••••
- button "Sign in" [ref=e40]:
- generic [ref=e41]:
- text: Sign in
- img [ref=e42]
- generic [ref=e49]: Or continue with
- button "🇺🇸 English" [ref=e52]:
- img [ref=e53]
- generic [ref=e56]: 🇺🇸
- generic [ref=e57]: English
- img [ref=e58]
- generic [ref=e60]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e62]:
- generic [ref=e63]: 🔓
- generic [ref=e64]: Quick Login (Dev Only)
- generic [ref=e65]:
- button "Platform Superuser SUPERUSER" [ref=e66]:
- generic [ref=e67]:
- generic [ref=e68]: Platform Superuser
- generic [ref=e69]: SUPERUSER
- button "Platform Manager PLATFORM_MANAGER" [ref=e70]:
- generic [ref=e71]:
- generic [ref=e72]: Platform Manager
- generic [ref=e73]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [ref=e74]:
- generic [ref=e75]:
- generic [ref=e76]: Platform Sales
- generic [ref=e77]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [ref=e78]:
- generic [ref=e79]:
- generic [ref=e80]: Platform Support
- generic [ref=e81]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]: Business Owner
- generic [ref=e85]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]: Business Manager
- generic [ref=e89]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [ref=e90]:
- generic [ref=e91]:
- generic [ref=e92]: Staff Member
- generic [ref=e93]: TENANT_STAFF
- button "Customer CUSTOMER" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e96]: Customer
- generic [ref=e97]: CUSTOMER
- generic [ref=e98]:
- text: "Password for all:"
- code [ref=e99]: test123
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

View File

@@ -0,0 +1,84 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e7]:
- generic [ref=e9]:
- img [ref=e10]
- generic [ref=e16]: Smooth Schedule
- generic [ref=e17]:
- heading "Orchestrate your business with precision." [level=1] [ref=e18]
- paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e24]: © 2025 Smooth Schedule Inc.
- generic [ref=e26]:
- generic [ref=e27]:
- heading "Welcome back" [level=2] [ref=e28]
- paragraph [ref=e29]: Please enter your details to sign in.
- generic [ref=e30]:
- generic [ref=e31]:
- generic [ref=e32]:
- generic [ref=e33]: Username
- generic [ref=e34]:
- generic:
- img
- textbox "Username" [active] [ref=e35]:
- /placeholder: Enter your username
- text: superuser
- generic [ref=e36]:
- generic [ref=e37]: Password
- generic [ref=e38]:
- generic:
- img
- textbox "Password" [ref=e39]:
- /placeholder: ••••••••
- button "Sign in" [ref=e40]:
- generic [ref=e41]:
- text: Sign in
- img [ref=e42]
- generic [ref=e50]: Or continue with
- button "🇺🇸 English" [ref=e53]:
- img [ref=e54]
- generic [ref=e58]: 🇺🇸
- generic [ref=e59]: English
- img [ref=e60]
- generic [ref=e62]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e64]:
- generic [ref=e65]: 🔓
- generic [ref=e66]: Quick Login (Dev Only)
- generic [ref=e67]:
- button "Platform Superuser SUPERUSER" [ref=e68]:
- generic [ref=e69]:
- generic [ref=e70]: Platform Superuser
- generic [ref=e71]: SUPERUSER
- button "Platform Manager PLATFORM_MANAGER" [ref=e72]:
- generic [ref=e73]:
- generic [ref=e74]: Platform Manager
- generic [ref=e75]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [ref=e76]:
- generic [ref=e77]:
- generic [ref=e78]: Platform Sales
- generic [ref=e79]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [ref=e80]:
- generic [ref=e81]:
- generic [ref=e82]: Platform Support
- generic [ref=e83]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [ref=e84]:
- generic [ref=e85]:
- generic [ref=e86]: Business Owner
- generic [ref=e87]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [ref=e88]:
- generic [ref=e89]:
- generic [ref=e90]: Business Manager
- generic [ref=e91]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [ref=e92]:
- generic [ref=e93]:
- generic [ref=e94]: Staff Member
- generic [ref=e95]: TENANT_STAFF
- button "Customer CUSTOMER" [ref=e96]:
- generic [ref=e97]:
- generic [ref=e98]: Customer
- generic [ref=e99]: CUSTOMER
- generic [ref=e100]:
- text: "Password for all:"
- code [ref=e101]: test123
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -0,0 +1,84 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e7]:
- generic [ref=e9]:
- img [ref=e10]
- generic [ref=e16]: Smooth Schedule
- generic [ref=e17]:
- heading "Orchestrate your business with precision." [level=1] [ref=e18]
- paragraph [ref=e19]: The all-in-one scheduling platform for businesses of all sizes. Manage resources, staff, and bookings effortlessly.
- generic [ref=e24]: © 2025 Smooth Schedule Inc.
- generic [ref=e26]:
- generic [ref=e27]:
- heading "Welcome back" [level=2] [ref=e28]
- paragraph [ref=e29]: Please enter your details to sign in.
- generic [ref=e30]:
- generic [ref=e31]:
- generic [ref=e32]:
- generic [ref=e33]: Username
- generic [ref=e34]:
- generic:
- img
- textbox "Username" [active] [ref=e35]:
- /placeholder: Enter your username
- text: superuser
- generic [ref=e36]:
- generic [ref=e37]: Password
- generic [ref=e38]:
- generic:
- img
- textbox "Password" [ref=e39]:
- /placeholder: ••••••••
- button "Sign in" [ref=e40]:
- generic [ref=e41]:
- text: Sign in
- img [ref=e42]
- generic [ref=e49]: Or continue with
- button "🇺🇸 English" [ref=e52]:
- img [ref=e53]
- generic [ref=e56]: 🇺🇸
- generic [ref=e57]: English
- img [ref=e58]
- generic [ref=e60]:
- heading "🔓 Quick Login (Dev Only)" [level=3] [ref=e62]:
- generic [ref=e63]: 🔓
- generic [ref=e64]: Quick Login (Dev Only)
- generic [ref=e65]:
- button "Platform Superuser SUPERUSER" [ref=e66]:
- generic [ref=e67]:
- generic [ref=e68]: Platform Superuser
- generic [ref=e69]: SUPERUSER
- button "Platform Manager PLATFORM_MANAGER" [ref=e70]:
- generic [ref=e71]:
- generic [ref=e72]: Platform Manager
- generic [ref=e73]: PLATFORM_MANAGER
- button "Platform Sales PLATFORM_SALES" [ref=e74]:
- generic [ref=e75]:
- generic [ref=e76]: Platform Sales
- generic [ref=e77]: PLATFORM_SALES
- button "Platform Support PLATFORM_SUPPORT" [ref=e78]:
- generic [ref=e79]:
- generic [ref=e80]: Platform Support
- generic [ref=e81]: PLATFORM_SUPPORT
- button "Business Owner TENANT_OWNER" [ref=e82]:
- generic [ref=e83]:
- generic [ref=e84]: Business Owner
- generic [ref=e85]: TENANT_OWNER
- button "Business Manager TENANT_MANAGER" [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]: Business Manager
- generic [ref=e89]: TENANT_MANAGER
- button "Staff Member TENANT_STAFF" [ref=e90]:
- generic [ref=e91]:
- generic [ref=e92]: Staff Member
- generic [ref=e93]: TENANT_STAFF
- button "Customer CUSTOMER" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e96]: Customer
- generic [ref=e97]: CUSTOMER
- generic [ref=e98]:
- text: "Password for all:"
- code [ref=e99]: test123
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,82 @@
import { test, expect } from '@playwright/test';
test('Debug Platform Staff Edit Button', async ({ page }) => {
// Navigate to platform login
await page.goto('http://platform.lvh.me:5173/login');
// Login as superuser
await page.getByPlaceholder(/username/i).fill('superuser');
await page.getByPlaceholder(/password/i).fill('starry12');
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for dashboard to load
await page.waitForURL(/platform\/dashboard/, { timeout: 10000 });
// Navigate to Staff page
await page.goto('http://platform.lvh.me:5173/#/platform/staff');
await page.waitForLoadState('networkidle');
// Wait for staff list to load
await page.waitForSelector('table', { timeout: 10000 });
// Log the current user data from localStorage or cookies
const currentUser = await page.evaluate(() => {
return {
cookies: document.cookie,
localStorage: localStorage.getItem('user')
};
});
console.log('Current user data:', currentUser);
// Check if Edit buttons exist
const editButtons = await page.locator('button:has-text("Edit")').all();
console.log('Number of Edit buttons found:', editButtons.length);
// Check if any buttons are disabled
for (let i = 0; i < editButtons.length; i++) {
const isDisabled = await editButtons[i].isDisabled();
const buttonText = await editButtons[i].textContent();
console.log(`Button ${i + 1} (${buttonText}): disabled = ${isDisabled}`);
}
// Try to click the first Edit button
if (editButtons.length > 0) {
const firstButton = editButtons[0];
const isDisabled = await firstButton.isDisabled();
if (isDisabled) {
console.log('First Edit button is DISABLED - this is the bug!');
// Get the user data from the API
const apiResponse = await page.evaluate(async () => {
const response = await fetch('http://lvh.me:8000/api/auth/me/', {
credentials: 'include'
});
return await response.json();
});
console.log('API /auth/me/ response:', apiResponse);
// Get platform users data
const platformUsers = await page.evaluate(async () => {
const response = await fetch('http://lvh.me:8000/api/platform/users/', {
credentials: 'include'
});
return await response.json();
});
console.log('API /platform/users/ response:', platformUsers);
} else {
console.log('First Edit button is ENABLED - attempting to click...');
await firstButton.click();
// Wait for modal to appear
await page.waitForSelector('text=Edit Platform User', { timeout: 5000 });
console.log('Modal opened successfully!');
}
} else {
console.log('No Edit buttons found on the page!');
}
// Take a screenshot
await page.screenshot({ path: 'platform-staff-debug.png', fullPage: true });
});