/** * Staff Roles Settings Page * * Create and manage staff roles with granular permissions. * Roles control what menu items and features are accessible to staff members. */ import React, { useState, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutletContext } from 'react-router-dom'; import { Shield, Plus, X, Pencil, Trash2, Users, Lock, Check, GripVertical } from 'lucide-react'; import { Business, User, StaffRole } from '../../types'; import { useStaffRoles, useAvailablePermissions, useCreateStaffRole, useUpdateStaffRole, useDeleteStaffRole, useReorderStaffRoles, } from '../../hooks/useStaffRoles'; import { RolePermissionsEditor } from '../../components/staff/RolePermissions'; const StaffRolesSettings: React.FC = () => { const { t } = useTranslation(); const { user } = useOutletContext<{ business: Business; user: User; }>(); const { data: staffRoles = [], isLoading } = useStaffRoles(); const { data: availablePermissions } = useAvailablePermissions(); const createStaffRole = useCreateStaffRole(); const updateStaffRole = useUpdateStaffRole(); const deleteStaffRole = useDeleteStaffRole(); const reorderStaffRoles = useReorderStaffRoles(); const [isModalOpen, setIsModalOpen] = useState(false); const [draggedRoleId, setDraggedRoleId] = useState(null); const [dragOverRoleId, setDragOverRoleId] = useState(null); const draggedRef = useRef(null); const [editingRole, setEditingRole] = useState(null); const [formData, setFormData] = useState({ name: '', description: '', permissions: {} as Record, }); const [error, setError] = useState(null); const isOwner = user.role === 'owner'; // Only owners can manage roles (staff with permissions can view but not edit) const canManageRoles = isOwner; // Merge menu, settings, and dangerous permissions for display const allPermissions = useMemo(() => { if (!availablePermissions) return { menu: {}, settings: {}, dangerous: {} }; return { menu: availablePermissions.menu_permissions || {}, settings: availablePermissions.settings_permissions || {}, dangerous: availablePermissions.dangerous_permissions || {}, }; }, [availablePermissions]); const openCreateModal = () => { setEditingRole(null); setFormData({ name: '', description: '', permissions: {}, }); setError(null); setIsModalOpen(true); }; const openEditModal = (role: StaffRole) => { setEditingRole(role); setFormData({ name: role.name, description: role.description || '', permissions: { ...role.permissions }, }); setError(null); setIsModalOpen(true); }; const closeModal = () => { setIsModalOpen(false); setEditingRole(null); setError(null); }; const handlePermissionsChange = (newPermissions: Record) => { setFormData((prev) => ({ ...prev, permissions: newPermissions, })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); try { if (editingRole) { await updateStaffRole.mutateAsync({ id: editingRole.id, name: formData.name, description: formData.description, permissions: formData.permissions, }); } else { await createStaffRole.mutateAsync({ name: formData.name, description: formData.description, permissions: formData.permissions, }); } closeModal(); } catch (err: any) { const message = err.response?.data?.error || err.response?.data?.name?.[0] || 'Failed to save role'; setError(message); } }; const handleDelete = async (role: StaffRole) => { if (!role.can_delete) { alert( role.is_default ? t('settings.staffRoles.cannotDeleteDefault', 'Default roles cannot be deleted.') : t('settings.staffRoles.cannotDeleteInUse', 'Remove all staff from this role before deleting.') ); return; } if (window.confirm(t('settings.staffRoles.confirmDelete', `Are you sure you want to delete the "${role.name}" role?`))) { try { await deleteStaffRole.mutateAsync(role.id); } catch (err: any) { alert(err.response?.data?.error || 'Failed to delete role'); } } }; const countEnabledPermissions = (permissions: Record) => { return Object.values(permissions).filter(Boolean).length; }; // Drag and drop handlers const handleDragStart = (e: React.DragEvent, roleId: number) => { e.dataTransfer.setData('text/plain', String(roleId)); e.dataTransfer.effectAllowed = 'move'; draggedRef.current = roleId; setDraggedRoleId(roleId); }; const handleDragEnd = () => { setDraggedRoleId(null); setDragOverRoleId(null); draggedRef.current = null; }; const handleDragOver = (e: React.DragEvent, roleId: number) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; if (draggedRef.current && draggedRef.current !== roleId) { setDragOverRoleId(roleId); } }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOverRoleId(null); }; const handleDrop = async (e: React.DragEvent, targetRoleId: number) => { e.preventDefault(); e.stopPropagation(); const draggedId = draggedRef.current; setDragOverRoleId(null); if (!draggedId || draggedId === targetRoleId) { return; } // Reorder the roles const currentOrder = staffRoles.map(r => r.id); const draggedIndex = currentOrder.indexOf(draggedId); const targetIndex = currentOrder.indexOf(targetRoleId); if (draggedIndex === -1 || targetIndex === -1) { return; } // Remove dragged item and insert at target position const newOrder = [...currentOrder]; newOrder.splice(draggedIndex, 1); newOrder.splice(targetIndex, 0, draggedId); try { await reorderStaffRoles.mutateAsync(newOrder); } catch (err) { // Silently fail - the UI will remain unchanged } }; if (!canManageRoles) { return (

{t('settings.staffRoles.noAccess', 'Only the business owner can manage staff roles.')}

); } return (
{/* Header */}

{t('settings.staffRoles.title', 'Staff Roles')}

{t('settings.staffRoles.subtitle', 'Create roles to control what staff members can access in your business.')}

{/* Roles List */}

{t('settings.staffRoles.yourRoles', 'Your Staff Roles')}

{t('settings.staffRoles.rolesDescription', 'Assign staff members to roles to control their permissions.')}

{isLoading ? (
) : staffRoles.length === 0 ? (

{t('settings.staffRoles.noRoles', 'No staff roles configured.')}

{t('settings.staffRoles.createFirst', 'Create your first role to manage staff permissions.')}

) : (
e.preventDefault()} > {staffRoles.map((role) => (
handleDragStart(e, role.id)} onDragEnd={handleDragEnd} onDragOver={(e) => handleDragOver(e, role.id)} onDragLeave={(e) => handleDragLeave(e)} onDrop={(e) => handleDrop(e, role.id)} className={`p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg border transition-all select-none ${ dragOverRoleId === role.id ? 'border-brand-500 border-2 bg-brand-50 dark:bg-brand-900/20' : draggedRoleId === role.id ? 'border-gray-300 dark:border-gray-600 opacity-50' : 'border-gray-200 dark:border-gray-700' }`} >
{/* Drag Handle */}

{role.name} {role.is_default && ( {t('common.default', 'Default')} )}

{t('settings.staffRoles.staffAssigned', '{{count}} staff', { count: role.staff_count })} {t('settings.staffRoles.permissionsEnabled', '{{count}} permissions', { count: countEnabledPermissions(role.permissions), })}

{role.description && (

{role.description}

)}
e.preventDefault()}>
))}
)}
{/* Create/Edit Modal */} {isModalOpen && (

{editingRole ? t('settings.staffRoles.editRole', 'Edit Role') : t('settings.staffRoles.createRole', 'Create Role')}

{error && (
{error}
)} {/* Basic Info */}
setFormData({ ...formData, name: e.target.value })} required 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 focus:border-brand-500" placeholder={t('settings.staffRoles.roleNamePlaceholder', 'e.g., Front Desk, Senior Stylist')} />