'use client'; import { formatErrorBanner } from '@/lib/api/toast-error'; import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; import { apiFetch } from '@/lib/api/client'; /** Default permissions structure matching RolePermissions type in * src/lib/db/schema/users.ts. Keep this in sync when actions are added. */ const DEFAULT_PERMISSIONS: Record> = { clients: { view: false, create: false, edit: false, delete: false, merge: false, export: false }, interests: { view: false, create: false, edit: false, delete: false, change_stage: false, override_stage: false, generate_eoi: false, export: false, }, berths: { view: false, edit: false, import: false, manage_waiting_list: false }, documents: { view: false, create: false, edit: false, send_for_signing: false, upload_signed: false, delete: false, }, expenses: { view: false, create: false, edit: false, delete: false, export: false, scan_receipt: false, }, invoices: { view: false, create: false, edit: false, delete: false, send: false, record_payment: false, export: false, }, files: { view: false, upload: false, edit: false, delete: false, manage_folders: false }, email: { view: false, send: false, configure_account: false }, reminders: { view_own: false, view_all: false, create: false, edit_own: false, edit_all: false, assign_others: false, }, calendar: { connect: false, view_events: false }, reports: { view_dashboard: false, view_analytics: false, export: false }, document_templates: { view: false, generate: false, manage: false }, yachts: { view: false, create: false, edit: false, delete: false, transfer: false }, companies: { view: false, create: false, edit: false, delete: false }, memberships: { view: false, manage: false }, reservations: { view: false, create: false, activate: false, cancel: false }, admin: { manage_users: false, view_audit_log: false, manage_settings: false, manage_webhooks: false, manage_reports: false, manage_custom_fields: false, manage_forms: false, manage_tags: false, system_backup: false, permanently_delete_clients: false, }, residential_clients: { view: false, create: false, edit: false, delete: false }, residential_interests: { view: false, create: false, edit: false, delete: false, change_stage: false, }, }; const GROUP_LABELS: Record = { clients: 'Clients', interests: 'Interests / Pipeline', berths: 'Berths', documents: 'Documents', expenses: 'Expenses', invoices: 'Invoices', files: 'Files', email: 'Email', reminders: 'Reminders', calendar: 'Calendar', reports: 'Reports', document_templates: 'Document Templates', yachts: 'Yachts', companies: 'Companies', memberships: 'Company Memberships', reservations: 'Reservations', admin: 'Administration', residential_clients: 'Residential Clients', residential_interests: 'Residential Interests', }; function formatAction(action: string): string { return action.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); } interface RoleFormProps { open: boolean; onOpenChange: (open: boolean) => void; role?: { id: string; name: string; description: string | null; isSystem: boolean; permissions: Record>; } | null; onSuccess: () => void; } export function RoleForm({ open, onOpenChange, role, onSuccess }: RoleFormProps) { const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [permissions, setPermissions] = useState>>( structuredClone(DEFAULT_PERMISSIONS), ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const isEdit = !!role; useEffect(() => { if (open) { if (role) { setName(role.name); setDescription(role.description ?? ''); // Merge role permissions over defaults to fill any missing keys const merged = structuredClone(DEFAULT_PERMISSIONS); for (const [group, actions] of Object.entries(role.permissions)) { if (merged[group]) { for (const [action, value] of Object.entries(actions as Record)) { merged[group]![action] = value; } } } setPermissions(merged); } else { setName(''); setDescription(''); setPermissions(structuredClone(DEFAULT_PERMISSIONS)); } setError(null); } }, [open, role]); function togglePermission(group: string, action: string) { setPermissions((prev) => { const next = structuredClone(prev); next[group]![action] = !next[group]![action]; return next; }); } function toggleGroup(group: string, value: boolean) { setPermissions((prev) => { const next = structuredClone(prev); for (const action of Object.keys(next[group]!)) { next[group]![action] = value; } return next; }); } function isGroupAllChecked(group: string): boolean { return Object.values(permissions[group]!).every(Boolean); } function isGroupPartial(group: string): boolean { const vals = Object.values(permissions[group]!); return vals.some(Boolean) && !vals.every(Boolean); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); setLoading(true); try { if (isEdit) { await apiFetch(`/api/v1/admin/roles/${role.id}`, { method: 'PATCH', body: { name, description: description || null, permissions }, }); } else { await apiFetch('/api/v1/admin/roles', { method: 'POST', body: { name, description: description || undefined, permissions }, }); } onSuccess(); onOpenChange(false); } catch (err: unknown) { const message = formatErrorBanner(err); setError(message); } finally { setLoading(false); } } return ( {isEdit ? 'Edit Role' : 'New Role'}
setName(e.target.value)} placeholder="e.g. Sales Manager" required />