"use client" import { useEffect, useState } from "react" import type { CSSProperties } from "react" import { closestCenter, DndContext, type DragEndEvent, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core" import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { GripVerticalIcon, PlusIcon, RefreshCwIcon, ShieldCheckIcon, ShieldIcon, } from "lucide-react" import { toast } from "sonner" import { assignRolePermissions, createRole, fetchPermissions, fetchRoleDetail, fetchRoles, type AdminPermission, type AdminRole, type CreateAdminRolePayload, type PageResult, updateRoleSort, } from "@/lib/api/admin" import { DashboardPage, DashboardTableShell, DashboardTableStateRow, DashboardTableSummary, DashboardToolbar, } from "@/components/dashboard-page" import { cn } from "@/lib/utils" import { AssignPermissionsDrawer } from "./_components/assign-permissions" import { CreateRoleDrawer } from "./_components/create" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Status } from "@/lib/generated/enums" import { useAppLocale, useI18n } from "@/i18n/provider" import { getRoleDisplayName } from "@/lib/role-i18n" type SortableRoleRowProps = { item: AdminRole disabled: boolean actionLoading: boolean onAssignPermissions: (item: AdminRole) => void } function SortableRoleRow({ item, disabled, actionLoading, onAssignPermissions, }: SortableRoleRowProps) { const t = useI18n() const { locale } = useAppLocale() const displayName = getRoleDisplayName(item.code, item.name, locale) const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled, }) const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, } return (
{displayName}
{item.code} {item.status === Status.Ok ? t("status.ok") : t("status.disabled")} {item.isSystem ? ( {t("role.system")} ) : null} {item.sortNo}
) } export default function DashboardRolesPage() { const t = useI18n() const [loading, setLoading] = useState(true) const [sorting, setSorting] = useState(false) const [creatingOpen, setCreatingOpen] = useState(false) const [savingCreate, setSavingCreate] = useState(false) const [savingPermissions, setSavingPermissions] = useState(false) const [assignPermissionsLoading, setAssignPermissionsLoading] = useState(false) const [assigningRole, setAssigningRole] = useState(null) const [assignPermissionOptions, setAssignPermissionOptions] = useState< AdminPermission[] >([]) const [assignPermissionIds, setAssignPermissionIds] = useState([]) const [actionLoadingId, setActionLoadingId] = useState(null) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 8 }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ) async function loadRoles() { setLoading(true) try { setResult(await fetchRoles({ limit: 200 })) } catch (error) { toast.error(error instanceof Error ? error.message : t("role.loadFailed")) } finally { setLoading(false) } } function handleCreateDrawerOpenChange(open: boolean) { if (savingCreate) { return } setCreatingOpen(open) } async function handleCreateRole(payload: CreateAdminRolePayload) { if (savingCreate) { return } setSavingCreate(true) try { const role = await createRole(payload) toast.success(t("role.created", { name: role.name })) setCreatingOpen(false) await loadRoles() } catch (error) { toast.error(error instanceof Error ? error.message : t("role.createFailed")) } finally { setSavingCreate(false) } } async function openAssignPermissionsDrawer(role: AdminRole) { setActionLoadingId(role.id) setAssigningRole(role) setAssignPermissionsLoading(true) try { const [permissionsResult, roleDetail] = await Promise.all([ fetchPermissions({ limit: 500 }), fetchRoleDetail(role.id), ]) const permissionCodeSet = new Set(roleDetail.permissions || []) setAssignPermissionOptions(permissionsResult.results) setAssignPermissionIds( permissionsResult.results .filter((permission) => permissionCodeSet.has(permission.code)) .map((permission) => permission.id) ) } catch (error) { setAssigningRole(null) toast.error(error instanceof Error ? error.message : t("role.loadAssignFailed")) } finally { setAssignPermissionsLoading(false) setActionLoadingId(null) } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id || sorting) { return } const previousResults = result.results const oldIndex = previousResults.findIndex((item) => item.id === active.id) const newIndex = previousResults.findIndex((item) => item.id === over.id) if (oldIndex < 0 || newIndex < 0) { return } const nextResults = arrayMove(previousResults, oldIndex, newIndex) setResult((current) => ({ ...current, results: nextResults, })) setSorting(true) try { await updateRoleSort(nextResults.map((item) => item.id)) toast.success(t("role.sortUpdated")) await loadRoles() } catch (error) { setResult((current) => ({ ...current, results: previousResults, })) toast.error(error instanceof Error ? error.message : t("role.sortUpdateFailed")) } finally { setSorting(false) } } function handleAssignPermissionsOpenChange(open: boolean) { if (savingPermissions) { return } if (!open) { setAssigningRole(null) setAssignPermissionOptions([]) setAssignPermissionIds([]) } } async function handleAssignPermissions(permissionIds: number[]) { if (!assigningRole || savingPermissions) { return } setSavingPermissions(true) try { await assignRolePermissions(assigningRole.id, permissionIds) toast.success(t("role.permissionsUpdated", { name: assigningRole.name })) setAssigningRole(null) setAssignPermissionOptions([]) setAssignPermissionIds([]) await loadRoles() } catch (error) { toast.error(error instanceof Error ? error.message : t("role.savePermissionsFailed")) } finally { setSavingPermissions(false) } } useEffect(() => { void loadRoles() }, []) return ( } >
{t("role.dragHint")}
{t("role.pageSummary", { page: result.page.page, limit: result.page.limit })} {t("pagination.total", { total: result.page.total })} } > void handleDragEnd(event)} > {t("role.columnRole")} {t("role.columnCode")} {t("role.columnStatus")} {t("role.columnSort")} {t("role.columnActions")} item.id)} strategy={verticalListSortingStrategy} > {result.results.map((item) => ( void openAssignPermissionsDrawer(current) } /> ))} {loading || result.results.length === 0 ? ( ) : null}
) }