"use client"; import { useState } from "react"; import { PermissionKey } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; const ALL_PERMISSION_KEYS = Object.values(PermissionKey); const PERMISSION_LABELS: Record = { viewCosts: "View Costs", useAssistantAdvancedTools: "Assistant Advanced Tools", exportData: "Export Data", importData: "Import Data", approveVacations: "Approve Vacations", manageBlueprints: "Manage Blueprints", viewAllResources: "View All Resources", manageResources: "Manage Resources", manageProjects: "Manage Projects", manageAllocations: "Manage Allocations", manageRoles: "Manage Roles", manageUsers: "Manage Users", viewScores: "View Scores", }; const PERMISSION_DESCRIPTIONS: Record = { viewCosts: "Access to cost data, budget views, and financial reports", useAssistantAdvancedTools: "Unlocks advanced AI assistant workflows for complex cross-entity analyses", exportData: "Export data to Excel, CSV, or PDF formats", importData: "Import data from external sources (Dispo, Excel)", approveVacations: "Approve or reject vacation requests", manageBlueprints: "Create and edit blueprint field definitions", viewAllResources: "View all resources (not just own team)", manageResources: "Create, edit, and deactivate resource records", manageProjects: "Create, edit, and manage project records", manageAllocations: "Create, edit, and delete allocations", manageRoles: "Create and edit project roles", manageUsers: "Manage user accounts and permissions", viewScores: "View value scores and skill analytics", }; const COLOR_OPTIONS = [ { value: "purple", label: "Purple", class: "bg-purple-500" }, { value: "blue", label: "Blue", class: "bg-blue-500" }, { value: "amber", label: "Amber", class: "bg-amber-500" }, { value: "green", label: "Green", class: "bg-green-500" }, { value: "red", label: "Red", class: "bg-red-500" }, { value: "gray", label: "Gray", class: "bg-gray-500" }, { value: "indigo", label: "Indigo", class: "bg-indigo-500" }, { value: "teal", label: "Teal", class: "bg-teal-500" }, ]; const ROLE_COLOR_MAP: Record = { purple: "border-purple-300 bg-purple-50 dark:border-purple-700 dark:bg-purple-900/20", blue: "border-blue-300 bg-blue-50 dark:border-blue-700 dark:bg-blue-900/20", amber: "border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20", green: "border-green-300 bg-green-50 dark:border-green-700 dark:bg-green-900/20", red: "border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/20", gray: "border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50", indigo: "border-indigo-300 bg-indigo-50 dark:border-indigo-700 dark:bg-indigo-900/20", teal: "border-teal-300 bg-teal-50 dark:border-teal-700 dark:bg-teal-900/20", }; const ROLE_BADGE_MAP: Record = { purple: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400", blue: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400", amber: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400", green: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400", red: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400", gray: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400", indigo: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-400", teal: "bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-400", }; type RoleConfig = { role: string; label: string; description: string | null; defaultPermissions: unknown; color: string | null; sortOrder: number; }; type EditingRole = { role: string; label: string; description: string; color: string; permissions: Set; }; export function SystemRolesClient() { const [editingRole, setEditingRole] = useState(null); const [actionError, setActionError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const utils = trpc.useUtils(); const { data: roleConfigs, isLoading } = trpc.systemRoleConfig.list.useQuery(undefined, { staleTime: 10_000, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore TS2589: tRPC infers union type too deeply for the role config update payload const updateMutation = trpc.systemRoleConfig.update.useMutation({ onSuccess: async () => { await utils.systemRoleConfig.list.invalidate(); setEditingRole(null); setActionError(null); setSuccessMessage("Role permissions updated successfully"); setTimeout(() => setSuccessMessage(null), 3000); }, onError: (err) => setActionError(err.message), }); function openEdit(config: RoleConfig) { setEditingRole({ role: config.role, label: config.label, description: config.description ?? "", color: config.color ?? "gray", permissions: new Set(config.defaultPermissions as string[]), }); setActionError(null); setSuccessMessage(null); } function togglePermission(key: string) { if (!editingRole) return; const next = new Set(editingRole.permissions); if (next.has(key)) { next.delete(key); } else { next.add(key); } setEditingRole({ ...editingRole, permissions: next }); } function selectAll() { if (!editingRole) return; setEditingRole({ ...editingRole, permissions: new Set(ALL_PERMISSION_KEYS) }); } function selectNone() { if (!editingRole) return; setEditingRole({ ...editingRole, permissions: new Set() }); } async function handleSave() { if (!editingRole) return; setActionError(null); await updateMutation.mutateAsync({ role: editingRole.role, label: editingRole.label, description: editingRole.description || null, color: editingRole.color, defaultPermissions: Array.from(editingRole.permissions), }); } const configs = (roleConfigs ?? []) as unknown as RoleConfig[]; return (

System Role Management

Configure default permissions for each system role. Changes apply to all users with that role.

{successMessage && (
{successMessage}
)} {isLoading && (
{[...Array(5)].map((_, i) => (
))}
)} {/* Role Cards */}
{configs.map((config) => { const perms = config.defaultPermissions as string[]; const color = config.color ?? "gray"; return (
{config.label} {config.role}
{config.description && (

{config.description}

)}
{perms.length === 0 ? ( No default permissions ) : ( perms.map((p) => ( {PERMISSION_LABELS[p] ?? p} )) )}
); })}
{/* Permission Matrix Overview */} {configs.length > 0 && (

Permission Matrix

{configs.map((c) => ( ))} {ALL_PERMISSION_KEYS.map((key) => ( {configs.map((c) => { const perms = c.defaultPermissions as string[]; const has = perms.includes(key); return ( ); })} ))}
Permission {c.label}
{PERMISSION_LABELS[key] ?? key} {has ? ( ) : ( )}
)} {/* Edit Modal */} {editingRole && (

Configure Role

{editingRole.role}

{actionError && (
{actionError}
)} {/* Label */}
setEditingRole({ ...editingRole, label: e.target.value })} className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
{/* Description */}
setEditingRole({ ...editingRole, description: e.target.value })} placeholder="Brief description of this role..." className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" />
{/* Color */}
{COLOR_OPTIONS.map((opt) => (
{/* Permissions */}
{ALL_PERMISSION_KEYS.map((key) => { const isActive = editingRole.permissions.has(key); return ( ); })}
)}
); }