import { useCallback, useEffect, useMemo, useState } from "react"; import { Search } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { fetchAdminAudit, type AuditAdminItem } from "../../api/admin"; import { Button } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn, type DataGridQueryState } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, adminErrorMessage, formatAdminDateTime as formatDateTime } from "@govoplan/core-webui"; const DEFAULT_QUERY: DataGridQueryState = { sort: { columnId: "time", direction: "desc" }, filters: {} }; export default function AdminAuditPanel({ settings, auth, systemMode = false }: { settings: ApiSettings; auth: AuthInfo; systemMode?: boolean; }) { const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [query, setQuery] = useState(DEFAULT_QUERY); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [reloadToken, setReloadToken] = useState(0); const tenantId = (auth.active_tenant ?? auth.tenant).id; const load = useCallback(async () => { setLoading(true); setError(""); try { const sortColumn = query.sort?.columnId; const response = await fetchAdminAudit(settings, { scope: systemMode ? "system" : "tenant", page, pageSize, sortBy: sortColumn && ["time", "actor", "action", "object", "tenant"].includes(sortColumn) ? sortColumn as "time" | "actor" | "action" | "object" | "tenant" : "time", sortDirection: query.sort?.direction ?? "desc", filters: query.filters }); setItems(response.items); setTotal(response.total); if (response.page !== page) setPage(response.page); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } }, [settings.accessToken, settings.apiBaseUrl, systemMode, tenantId, page, pageSize, query, reloadToken]); useEffect(() => { void load(); }, [load]); const handleQueryChange = useCallback((next: DataGridQueryState) => { setQuery((current) => { if (JSON.stringify(current) === JSON.stringify(next)) return current; setPage(1); return next; }); }, []); const columns = useMemo[]>(() => [ { id: "time", header: "Time", width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) }, { id: "actor", header: "Actor", width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "System" }, { id: "action", header: "Action", width: 250, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => row.action }, { id: "object", header: "Object", width: 300, minWidth: 180, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => `${row.object_type || "—"} ${row.object_id || ""}`.trim() }, ...(systemMode ? [{ id: "tenant", header: "Tenant context", width: 190, minWidth: 150, maxWidth: 300, resizable: true, sortable: true, filterable: true, value: (row: AuditAdminItem) => row.tenant_id || "—" }] : []), { id: "actions", header: "Actions", width: 70, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setSelected(row)} />
} ], [systemMode]); const firstShown = total === 0 ? 0 : (page - 1) * pageSize + 1; const lastShown = Math.min(total, page * pageSize); return ( <> setReloadToken((value) => value + 1)} disabled={loading}>Reload} >
row.id} emptyText="No administrative audit records found." className="admin-audit-grid" initialSort={{ columnId: "time", direction: "desc" }} pagination={{ mode: "server", page, pageSize, totalRows: total, pageSizeOptions: [10, 25, 50, 100, 250], disabled: loading, onPageChange: setPage, onPageSizeChange: (next) => { setPageSize(next); setPage(1); } }} onQueryChange={handleQueryChange} />
setSelected(null)} className="admin-dialog admin-dialog-wide" footer={}> {selected && <>
Scope
{selected.scope}
Action
{selected.action}
Actor
{selected.actor_email || "System"}
Object
{selected.object_type || "—"} {selected.object_id || ""}
Tenant context
{selected.tenant_id || "—"}
Time
{formatDateTime(selected.created_at)}
{JSON.stringify(selected.details, null, 2)}
}
); }