"use client" import * as React from "react" import { toast } from "sonner" import { PlusIcon, ShieldIcon, Trash2Icon, UsersRoundIcon } from "lucide-react" import { getStaff } from "@/lib/api/staff" import { assignStaffRole, createGymRole, deleteGymRole, getGymRoles, setRolePermissions, } from "@/lib/api/gym-roles" import type { GymRole } from "@/lib/models/gym-role" import type { StaffMember } from "@/lib/models/staff" import { NAV_ITEMS } from "@/lib/nav" import { useFetch } from "@/lib/use-fetch" import { runMutation } from "@/lib/use-mutation" import { useNavItems } from "@/hooks/use-nav-items" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { PageSkeleton } from "@/components/hexa/page-skeleton" // "dashboard" is always visible to every staff member and "settings" is owner-only — // neither is a meaningful permission to grant/deny, so they're excluded from the checklist. const NOT_ASSIGNABLE = new Set(["dashboard", "settings"]) const DEMO_ROLES: GymRole[] = [ { gymRoleId: "demo-front-desk", name: "Front Desk", isActive: true, navKeys: ["attendance", "members", "memberships", "customer-payments"], }, { gymRoleId: "demo-trainer", name: "Trainer", isActive: true, navKeys: ["attendance", "members", "workout-plans", "meal-plans", "feed"], }, ] const DEMO_ROLE_STAFF: StaffMember[] = [ { staffId: "demo-staff-1", userId: "demo-user-1", userName: "Amaya Rodrigo", email: "amaya@pulsefitness.lk", phoneNumber: "077 201 4455", clientId: "demo", clientName: "Pulse Fitness Colombo", gymRoleId: "demo-front-desk", gymRoleName: "Front Desk", staffPosition: "Receptionist", department: "Operations", hireDate: "2025-11-04", isActive: true, createdAt: "2025-11-04", }, { staffId: "demo-staff-2", userId: "demo-user-2", userName: "Ravindu Senanayake", email: "ravindu@pulsefitness.lk", phoneNumber: "071 488 2210", clientId: "demo", clientName: "Pulse Fitness Colombo", gymRoleId: "demo-trainer", gymRoleName: "Trainer", staffPosition: "Senior Trainer", department: "Coaching", hireDate: "2025-08-12", isActive: true, createdAt: "2025-08-12", }, ] interface RolesPermissionsViewProps { /** Sandbox mode keeps roles, permissions, and assignments in local state. */ demo?: boolean } function RolesPermissionsView({ demo = false }: RolesPermissionsViewProps) { const rolesFetch = useFetch(demo ? async () => DEMO_ROLES : getGymRoles) const staffFetch = useFetch(demo ? async () => DEMO_ROLE_STAFF : getStaff) const { items: liveNavItems } = useNavItems(!demo) const navItems = demo ? NAV_ITEMS : liveNavItems const [newRoleName, setNewRoleName] = React.useState("") const [creating, setCreating] = React.useState(false) // Per-row busy tracking: gymRoleId (delete), `${gymRoleId}:${navKey}` (toggle), staffId (assign). const [busyKeys, setBusyKeys] = React.useState>(new Set()) function withBusy(key: string) { setBusyKeys((prev) => new Set(prev).add(key)) } function clearBusy(key: string) { setBusyKeys((prev) => { const next = new Set(prev) next.delete(key) return next }) } const assignablePermissions = navItems.filter((i) => !NOT_ASSIGNABLE.has(i.key)) const roles = rolesFetch.data ?? [] const staff = staffFetch.data ?? [] async function handleCreateRole() { if (!newRoleName.trim()) { toast.error("Enter a role name first.") return } setCreating(true) try { if (demo) { const role: GymRole = { gymRoleId: `demo-role-${roles.length + 1}`, name: newRoleName.trim(), isActive: true, navKeys: [], } rolesFetch.mutate((current) => [...(current ?? []), role]) setNewRoleName("") toast.success("Demo role created") return } await runMutation(() => createGymRole({ name: newRoleName.trim() })) setNewRoleName("") toast.success("Role created") rolesFetch.reload() } catch { // runMutation() already toasted the real error. } finally { setCreating(false) } } async function handleDeleteRole(role: GymRole) { if (demo) { rolesFetch.mutate((current) => current?.filter((item) => item.gymRoleId !== role.gymRoleId) ?? null ) staffFetch.mutate((current) => current?.map((member) => member.gymRoleId === role.gymRoleId ? { ...member, gymRoleId: null, gymRoleName: null } : member ) ?? null ) toast("Demo role deleted") return } withBusy(role.gymRoleId) try { await runMutation(() => deleteGymRole(role.gymRoleId)) toast("Role deleted") rolesFetch.reload() } catch { // runMutation() already toasted the real error. } finally { clearBusy(role.gymRoleId) } } async function handleTogglePermission(role: GymRole, navKey: string, checked: boolean) { const nextKeys = checked ? [...role.navKeys, navKey] : role.navKeys.filter((k) => k !== navKey) if (demo) { rolesFetch.mutate((current) => current?.map((item) => item.gymRoleId === role.gymRoleId ? { ...item, navKeys: nextKeys } : item ) ?? null ) return } const key = `${role.gymRoleId}:${navKey}` withBusy(key) try { await runMutation(() => setRolePermissions(role.gymRoleId, { navKeys: nextKeys })) rolesFetch.reload() } catch { // runMutation() already toasted the real error. } finally { clearBusy(key) } } async function handleAssignRole(staffId: string, gymRoleId: string | null) { if (demo) { const roleName = roles.find((role) => role.gymRoleId === gymRoleId)?.name ?? null staffFetch.mutate((current) => current?.map((member) => member.staffId === staffId ? { ...member, gymRoleId, gymRoleName: roleName } : member ) ?? null ) toast.success("Demo staff role updated") return } withBusy(staffId) try { await runMutation(() => assignStaffRole(staffId, gymRoleId)) toast.success("Staff role updated") staffFetch.reload() } catch { // runMutation() already toasted the real error. } finally { clearBusy(staffId) } } if (rolesFetch.loading || staffFetch.loading) { return } return (
Roles Create custom roles and choose which sections of the dashboard each one can access. New roles start with zero permissions.
setNewRoleName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreateRole()} />
{roles.length === 0 && (

No roles yet. Add one above to get started.

)} {roles.map((role) => (

{role.name}

{assignablePermissions.map((item) => { const checked = role.navKeys.includes(item.key) return ( ) })}
))}
Assign staff to a role Staff with no role assigned only see the Dashboard until you assign one. {staff.length === 0 && (

No staff members yet.

)} {staff.map((member) => (

{member.userName ?? "Unknown"}

{member.staffPosition ?? "—"}

))}
) } export { RolesPermissionsView }