"use client"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { applyPreset, bulkImportPermissions, clearAllPermissions, deletePermission, exportPermissions, upsertPermission, } from "@/actions/admin-housekeeping"; const PAGE_SIZE = 25; interface HKPerm { id: bigint; permission: string; minRank: number; description: string | null; groupName: string | null; dependsOn: string | null; } interface SharedProps { permissions: HKPerm[]; groups: string[]; groupCounts: Record; permSet: Set; } export function ManageSection(props: SharedProps) { const t = useTranslations("pages.admin.housekeeping"); const router = useRouter(); const sp = useSearchParams(); const [search, setSearch] = useState(sp.get("search") ?? ""); const [groupFilter, setGroupFilter] = useState(sp.get("group") ?? ""); const [page, setPage] = useState(1); const [pendingDel, setPendingDel] = useState(null); const filtered = useMemo(() => { return props.permissions.filter((p) => { if (search && !p.permission.toLowerCase().includes(search.toLowerCase())) return false; if (groupFilter && (p.groupName ?? "") !== groupFilter) return false; return true; }); }, [props.permissions, search, groupFilter]); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const paginated = filtered.slice( (safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE, ); const syncUrl = useCallback( (s: string, g: string) => { const params = new URLSearchParams(); if (s) params.set("search", s); if (g) params.set("group", g); const qs = params.toString(); router.replace( "/admin/housekeeping?tab=manage" + (qs ? "&" + qs : ""), { scroll: false }, ); }, [router], ); const handleUpsert = useCallback( async (formData: FormData) => { try { await upsertPermission(formData); toast.success(t("saved")); router.refresh(); } catch { toast.error(t("errorOccurred")); } }, [router, t], ); const handleDelete = useCallback( async (id: string) => { if (!confirm(t("confirmDelete"))) return; setPendingDel(id); try { const fd = new FormData(); fd.set("id", id); await deletePermission(fd); toast.success(t("deleted")); router.refresh(); } catch { toast.error(t("errorOccurred")); } setPendingDel(null); }, [router, t], ); return (
{ const v = e.target.value; setSearch(v); setPage(1); syncUrl(v, groupFilter); }} placeholder={t("searchPermission")} className="flex-1 min-w-[200px]" />

{t("addPermission")}

{ e.preventDefault(); handleUpsert(new FormData(e.currentTarget)); }} className="admin-card" >

{t("permissions")}{" "} {filtered.length}

{filtered.length === 0 ? (
{t("noPermissions")}
) : (
{paginated.map((p, i) => ( ))}
# {t("colPermission")} {t("colDescription")} {t("colGroup")} {t("colMinRank")} {t("colDepends")}
{(safePage - 1) * PAGE_SIZE + i + 1} {p.permission} {p.description || "\u2014"} {p.groupName ? ( {p.groupName} ) : ( \u2014 )}
{ e.preventDefault(); handleUpsert(new FormData(e.currentTarget)); }} style={{ display: "flex", gap: "0.5rem" }} >
{p.dependsOn ? ( {p.dependsOn} {!props.permSet.has(p.dependsOn) ? " " + t("missing") : ""} ) : ( \u2014 )}
)} {totalPages > 1 && (
{safePage} / {totalPages}
)}
); } export function PresetsSection() { const t = useTranslations("pages.admin.housekeeping"); const router = useRouter(); const [busy, setBusy] = useState(null); const handleApplyPreset = useCallback( async (preset: string) => { setBusy(preset); try { const fd = new FormData(); fd.set("preset", preset); await applyPreset(fd); toast.success(t("presetApplied")); router.refresh(); } catch { toast.error(t("errorOccurred")); } setBusy(null); }, [router, t], ); const handleClearAll = useCallback(async () => { if (!confirm(t("confirmClearAll"))) return; setBusy("__clear__"); try { await clearAllPermissions(); toast.success(t("cleared")); router.refresh(); } catch { toast.error(t("errorOccurred")); } setBusy(null); }, [router, t]); return (

{t("presets")}

{t("presetsHint")}

{(["moderator", "admin", "superadmin"] as const).map((preset) => { const labels: Record = { moderator: t("presetModerator"), admin: t("presetAdmin"), superadmin: t("presetSuperAdmin"), }; const descs: Record = { moderator: t("presetModeratorDesc"), admin: t("presetAdminDesc"), superadmin: t("presetSuperAdminDesc"), }; const loading = busy === preset; return (

{labels[preset]}

{descs[preset]}

); })}
); } export function TestSection({ permissions }: { permissions: HKPerm[] }) { const t = useTranslations("pages.admin.housekeeping"); const [testRank, setTestRank] = useState(""); const [testResult, setTestResult] = useState<{ rank: number; permissions: string[]; } | null>(null); const handleTest = useCallback(() => { const rank = Number(testRank); if (!Number.isFinite(rank) || rank < 0) return toast.error(t("invalidRank")); const matching = permissions.filter((p) => p.minRank <= rank); setTestResult({ rank, permissions: matching.map((p) => p.permission) }); toast.success(t("testDone", { count: matching.length })); }, [testRank, permissions, t]); return (

{t("testTitle")}

{t("testHint")}

setTestRank(e.target.value)} type="number" min={0} placeholder={t("rank")} className="w-[150px]" />
{testResult && (
{t("rank")} {testResult.rank}: {testResult.permissions.length}{" "} {t("permissions")}
{testResult.permissions.map((p) => (
{p}
))}
)}
); } export function ImportSection() { const t = useTranslations("pages.admin.housekeeping"); const router = useRouter(); const textareaRef = useRef(null); const [importing, setImporting] = useState(false); const handleBulkImport = useCallback( async (e: React.FormEvent) => { e.preventDefault(); const fd = new FormData(e.currentTarget); const raw = String(fd.get("json") ?? "").trim(); if (!raw) return toast.error(t("emptyInput")); try { JSON.parse(raw); } catch { return toast.error(t("invalidJson")); } setImporting(true); try { const result = await bulkImportPermissions(fd); if (result.errors.length > 0) { toast.error( t("importedWithErrors", { count: result.count, errors: result.errors.length, }), ); } else { toast.success(t("imported", { count: result.count })); } if (textareaRef.current) textareaRef.current.value = ""; router.refresh(); } catch { toast.error(t("errorOccurred")); } setImporting(false); }, [router, t], ); return (

{t("importTitle")}

{t("importHint")}