"use client"; /* Data & audit log: backups, CSV import, wipe, start fresh, and the audit log. */ import { useEffect, useState } from "react"; import { useSnap } from "@/lib/client"; import { MonoNum } from "@/components/portal"; import { CSV_TEMPLATES, parseCsv } from "@/lib/csv"; import { daysBetween, fmtDate } from "@/lib/compute"; import { Msg, SectionHead, useSaver } from "./common"; import { useBackupExport } from "./backup"; import AuditLog from "./AuditLog"; export default function DataAudit({ importKind, scrollAudit }: { importKind?: string; scrollAudit: boolean }) { const { s, isAdmin, mutate } = useSnap(); const { msg, say } = useSaver(); const { bkBusy, exportBackup } = useBackupExport(say); const [impKind, setImpKind] = useState(importKind && CSV_TEMPLATES[importKind] ? importKind : "catalog"); const [impBusy, setImpBusy] = useState(false); const [wipe, setWipe] = useState(""); const [reset, setReset] = useState(""); const [resetBusy, setResetBusy] = useState(false); useEffect(() => { if (importKind && CSV_TEMPLATES[importKind]) { setImpKind(importKind); requestAnimationFrame(() => document.getElementById("import")?.scrollIntoView({ block: "start" })); } }, [importKind]); const lastBk = s.settings.lastBackup; const bkDays = lastBk ? daysBetween(lastBk, s.today) : null; const bkStale = !lastBk || (bkDays ?? 0) > 7; /* parseCsv throws on a malformed file to stop a half-import; its words are what the admin needs. */ async function importFile(file: File) { setImpBusy(true); say("import", "Importing…"); try { const rows = parseCsv(await file.text()); if (!rows.length) { say("import", "No rows found — check the header row."); return; } const r = await mutate<{ created: number; updated: number; skipped: number; styles?: number; errors: string[] }>("import.rows", { kind: impKind, rows }); if (!r.ok) { say("import", r.error); return; } const x = r.result; say("import", `${CSV_TEMPLATES[impKind].name}: ${x.created} created, ${x.updated} updated, ${x.skipped} skipped.${x.styles ? ` ${x.styles} uniform ${x.styles === 1 ? "style" : "styles"} set.` : ""}` + (x.errors.length ? "\n" + x.errors.join("\n") : "")); } catch (e) { say("import", (e as Error)?.message || "That file couldn’t be read as a CSV. Nothing was imported."); } finally { setImpBusy(false); } } async function restore(file: File) { if (!confirm("Restore this backup? It replaces ALL data in this facility (catalogue, staff, orders, issues, stock, approvals). Users are kept.")) return; say("backup", "Restoring…"); try { const data = JSON.parse(await file.text()); const r = await mutate<{ photosSkipped: number }>("backup.restore", data); if (!r.ok) { say("backup", r.error); return; } const skipped = r.result?.photosSkipped || 0; say("backup", skipped ? `Backup restored. ${skipped} photo${skipped === 1 ? "" : "s"} in the file did not come back; keep the file.` : "Backup restored."); } catch (e) { say("backup", "Import failed — " + (e as Error).message); } } function template(kind: string) { const t = CSV_TEMPLATES[kind]; const a = document.createElement("a"); a.href = "data:text/csv;charset=utf-8," + encodeURIComponent(t.headers + "\n" + t.example + "\n"); a.download = `threadcount-${kind}-template.csv`; a.click(); } const backupLine = (