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

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