'use client'; import type { AdminAuditDto, AdminAuditQuery } from '@impuestos/contracts'; import { useQuery } from '@tanstack/react-query'; import { useState, type FormEvent } from 'react'; import { Button } from '@/components/ui/button'; import { EmptyState } from '@/components/ui/empty-state'; import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; import { Table, Td, Th, Tr } from '@/components/ui/table'; import { useT } from '@/i18n/t'; import { api } from '@/lib/api'; const EMPTY: AdminAuditQuery = {}; /** * Flow H: read only, filterable, exportable. The action is shown as the code it is stored * as rather than a translated phrase: it is the same token the filter takes, and an * operator searching a log wants to match what they see. */ export function AuditScreen() { const t = useT(); const [draft, setDraft] = useState>({}); const [filters, setFilters] = useState(EMPTY); const [pages, setPages] = useState([]); const [cursor, setCursor] = useState(undefined); const audit = useQuery({ queryKey: ['admin', 'audit', filters, cursor], queryFn: ({ signal }) => api.listAudit({ ...filters, ...(cursor ? { cursor } : {}) }, signal), }); // Pages accumulate, so "show more" appends rather than replacing what is on screen. const rows = cursor ? [...pages, ...(audit.data?.items ?? [])] : (audit.data?.items ?? []); function apply(event: FormEvent) { event.preventDefault(); setPages([]); setCursor(undefined); setFilters(clean(draft)); } function clear() { setDraft({}); setPages([]); setCursor(undefined); setFilters(EMPTY); } return (

{t('admin.audit.title')}

{t('admin.audit.export')}
{( [ ['actor', 'admin.audit.filterActor', 'text'], ['action', 'admin.audit.filterAction', 'text'], ['subject', 'admin.audit.filterSubject', 'text'], ['from', 'admin.audit.filterFrom', 'date'], ['to', 'admin.audit.filterTo', 'date'], ] as const ).map(([key, label, type]) => (
setDraft({ ...draft, [key]: event.target.value })} className="h-9 w-44 rounded-lg text-sm" autoComplete="off" />
))} {/* Grouped, so the two buttons wrap together rather than one at a time. */}
{audit.data ? (

{t('admin.audit.total', { count: audit.data.total })}

) : null} {audit.isPending && rows.length === 0 ? : null} {audit.isError ? (

{t('common.error.generic')}

) : null} {audit.data && rows.length === 0 ? : null} {rows.length > 0 ? ( {rows.map((row) => ( ))}
{t('admin.audit.col.when')} {t('admin.audit.col.actor')} {t('admin.audit.col.action')} {t('admin.audit.col.subject')} {t('admin.audit.col.resource')} {t('admin.audit.col.ip')}
{row.createdAt.slice(0, 16).replace('T', ' ')} {row.actorEmail ?? row.actorUserId} {row.action} {row.subjectEmail ?? t('common.none')} {row.resource} {row.ip ?? t('common.none')}
) : null} {audit.data?.cursor ? ( ) : null}
); } function clean(draft: Record): AdminAuditQuery { const out: Record = {}; for (const [key, value] of Object.entries(draft)) { if (value.trim().length > 0) out[key] = value.trim(); } return out as AdminAuditQuery; }