diff --git a/public/nitro-assets/bundled/pet/dog.nitro b/public/nitro-assets/bundled/pet/dog.nitro new file mode 100644 index 00000000..ab51611d Binary files /dev/null and b/public/nitro-assets/bundled/pet/dog.nitro differ diff --git a/public/swf/gordon/dog.swf b/public/swf/gordon/dog.swf new file mode 100644 index 00000000..91b2ee39 Binary files /dev/null and b/public/swf/gordon/dog.swf differ diff --git a/src/actions/admin-housekeeping.ts b/src/actions/admin-housekeeping.ts index 7953a395..0feba44c 100644 --- a/src/actions/admin-housekeeping.ts +++ b/src/actions/admin-housekeeping.ts @@ -131,9 +131,6 @@ export async function bulkImportPermissions(formData: FormData): Promise { await requirePermission(PERMS.SETTINGS_EDIT); const raw = sanitize(String(formData.get("json") ?? "")); - const errors: string[] = []; - let count = 0; - if (!raw) return; let parsed: unknown; @@ -157,7 +154,6 @@ export async function bulkImportPermissions(formData: FormData): Promise { const dependsOn = sanitize(String(item.dependsOn ?? "")) || null; if (!permission || !Number.isFinite(minRank) || minRank < 0) { - errors.push("Invalid entry: " + JSON.stringify(item)); continue; } @@ -180,9 +176,8 @@ export async function bulkImportPermissions(formData: FormData): Promise { updatedBy: userId ?? undefined, }, }); - count++; } catch { - errors.push("Failed to upsert: " + permission); + // Failed to upsert } } @@ -881,17 +876,7 @@ export async function testRankPermission(formData: FormData): Promise { }); const granted = perms.map((p) => p.permission); - // Result is displayed via the revalidated page - revalidatePath( - "/admin/housekeeping?tab=test&result=" + - encodeURIComponent(JSON.stringify({ rank, permissions: granted })), - ); + revalidatePath("/admin/housekeeping"); } -export async function getAuditHistory(): Promise { - await requirePermission(PERMS.SETTINGS_VIEW); - const { getAuditLogs } = await import("@/lib/services/audit"); - const result = await getAuditLogs({ search: "housekeeping", perPage: 50 }); - return result.rows; -} diff --git a/src/app/admin/housekeeping/client.tsx b/src/app/admin/housekeeping/client.tsx index c36176f7..e3634e1d 100644 --- a/src/app/admin/housekeeping/client.tsx +++ b/src/app/admin/housekeeping/client.tsx @@ -1,8 +1,8 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { applyPreset, @@ -25,34 +25,30 @@ interface HKPerm { dependsOn: string | null; } -interface ManageClientProps { +interface SharedProps { permissions: HKPerm[]; groups: string[]; groupCounts: Record; permSet: Set; } -export function ManageClient({ - permissions, - groups, - groupCounts, - permSet, -}: ManageClientProps) { +export function ManageSection(props: SharedProps) { const t = useTranslations("pages.admin.housekeeping"); const router = useRouter(); - const [search, setSearch] = useState(""); - const [groupFilter, setGroupFilter] = useState(""); + 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 permissions.filter((p) => { + return props.permissions.filter((p) => { if (search && !p.permission.toLowerCase().includes(search.toLowerCase())) return false; if (groupFilter && (p.groupName ?? "") !== groupFilter) return false; return true; }); - }, [permissions, search, groupFilter]); + }, [props.permissions, search, groupFilter]); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); @@ -61,6 +57,20 @@ export function ManageClient({ 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 { @@ -77,6 +87,7 @@ export function ManageClient({ const handleDelete = useCallback( async (id: string) => { if (!confirm(t("confirmDelete"))) return; + setPendingDel(id); try { const fd = new FormData(); fd.set("id", id); @@ -86,99 +97,22 @@ export function ManageClient({ } catch { toast.error(t("errorOccurred")); } + setPendingDel(null); }, [router, t], ); - 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")); - } - - try { - await bulkImportPermissions(fd); - toast.success(t("imported")); - (e.currentTarget.querySelector( - "textarea", - ) as HTMLTextAreaElement)!.value = ""; - router.refresh(); - } catch { - toast.error(t("errorOccurred")); - } - }, - [router, t], - ); - - const handleApplyPreset = useCallback( - async (preset: string) => { - try { - const fd = new FormData(); - fd.set("preset", preset); - await applyPreset(fd); - toast.success(t("presetApplied")); - router.refresh(); - } catch { - toast.error(t("errorOccurred")); - } - }, - [router, t], - ); - - const handleClearAll = useCallback(async () => { - if (!confirm(t("confirmClearAll"))) return; - try { - await clearAllPermissions(); - toast.success(t("cleared")); - router.refresh(); - } catch { - toast.error(t("errorOccurred")); - } - }, [router, t]); - - const [testRank, setTestRank] = useState(""); - const [testResult, setTestResult] = useState<{ - rank: number; - permissions: string[]; - } | null>(null); - const [testing, setTesting] = useState(false); - - const handleTest = useCallback(async () => { - const rank = Number(testRank); - if (!Number.isFinite(rank) || rank < 0) - return toast.error(t("invalidRank")); - setTesting(true); - try { - const fd = new FormData(); - fd.set("testRank", testRank); - await testRankPermission(fd); - // The action revalidates the page; we also show result client-side - const matching = permissions.filter((p) => p.minRank <= rank); - setTestResult({ rank, permissions: matching.map((p) => p.permission) }); - toast.success(t("testDone", { count: matching.length })); - } catch { - toast.error(t("errorOccurred")); - } - setTesting(false); - }, [testRank, permissions, t]); - return (
- {/* ── Manage tab ── */}
{ - setSearch(e.target.value); + const v = e.target.value; + setSearch(v); setPage(1); + syncUrl(v, groupFilter); }} placeholder={t("searchPermission")} className="flex-1 min-w-[200px]" @@ -186,15 +120,17 @@ export function ManageClient({ @@ -202,7 +138,9 @@ export function ManageClient({
-

{t("addPermission")}

+

+ {t("addPermission")} +

{ e.preventDefault(); @@ -232,7 +170,7 @@ export function ManageClient({ /> 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} -
- ))} -
-
- )} - - - {/* ── Import ── */} -
-

{t("importTitle")}

-

- {t("importHint")} -

- -
-