import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ApiError, can, createRole, deleteRole, fetchRoles, updateRole, type ManagedRole, type Permission, type SessionUser, } from "./api.js"; import { Modal } from "./ui/Modal.js"; // Role management (admin). Compose a role from the permission grid (a checkbox // matrix of resource × action) and name it; users are then assigned a role. The // built-in `admin` role is shown read-only/locked (it always has every permission // and can't be edited or deleted). The server enforces the same. See // @parking/shared PERMISSIONS. /** Group "resource:action" permissions by resource for the grid rows. */ function groupByResource(perms: Permission[]): Record { const out: Record = {}; for (const p of perms) { const resource = p.split(":")[0]!; (out[resource] ??= []).push(p); } return out; } export function RolesManager({ user }: { user: SessionUser | null }) { const { t } = useTranslation(); const qc = useQueryClient(); const rolesQ = useQuery({ queryKey: ["roles"], queryFn: fetchRoles }); const canCreate = can(user, "role:create"); const canUpdate = can(user, "role:update"); const canDelete = can(user, "role:delete"); const catalog = rolesQ.data?.catalog ?? []; const roles = rolesQ.data?.roles ?? []; const grouped = useMemo(() => groupByResource(catalog), [catalog]); const [error, setError] = useState(null); const [editing, setEditing] = useState(null); const invalidate = () => { void qc.invalidateQueries({ queryKey: ["roles"] }); void qc.invalidateQueries({ queryKey: ["users"] }); }; const onError = (e: unknown) => setError(e instanceof ApiError ? e.message : (e as Error).message); return (

{t("roles.title")}

{canCreate && ( )}
{error &&
{error}
} setEditing(null)} title={editing && editing !== "new" ? t("roles.editTitle") : t("roles.new")} width="max-w-2xl" > {editing && ( setEditing(null)} onSubmit={async (v) => { try { if (editing === "new") await createRole(v); else await updateRole(editing.id, v); setEditing(null); invalidate(); } catch (e) { onError(e); } }} /> )}
{roles.map((r) => (
{r.name} {r.builtin && ( {t("roles.builtin")} )} {t("roles.permCount", { count: r.permissions.length })} · {t("roles.userCount", { count: r.userCount })}
{canUpdate && !r.builtin && ( )} {canDelete && !r.builtin && ( )}
))}
); } async function deleteRoleSafe(id: string, ok: () => void, onError: (e: unknown) => void) { try { await deleteRole(id); ok(); } catch (e) { onError(e); } } function RoleEditor({ role, grouped, onCancel, onSubmit, }: { role: ManagedRole | null; grouped: Record; onCancel: () => void; onSubmit: (v: { name: string; permissions: Permission[] }) => void; }) { const { t } = useTranslation(); const [name, setName] = useState(role?.name ?? ""); const [perms, setPerms] = useState>(new Set(role?.permissions ?? [])); const toggle = (p: Permission) => setPerms((prev) => { const next = new Set(prev); next.has(p) ? next.delete(p) : next.add(p); return next; }); const valid = name.trim().length > 0; return (
{t("roles.name")} setName(e.target.value)} />
{t("roles.permissions")}
{Object.entries(grouped).map(([resource, list]) => (
{resource} {list.map((p) => { const action = p.split(":")[1]!; return ( ); })}
))}
); }