import { useMemo, useState } from 'react' import { createFileRoute } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Alert, Badge, Button, DatePicker, EmptyState, IconButton, LoadingBlock, PageHeader, SingleSelect, Table, TableBody, TableCell, TableEmpty, TableHead, TableHeaderCell, TableRow, TextInput, } from '@nstart/ui' import { ArrowsClockwiseIcon, CaretDownIcon, CaretRightIcon, DownloadIcon, } from '@phosphor-icons/react' import { buildAuditExportUrl, useAudit, useDictionaries } from '@/api/queries' import type { AuditAction, AuditEntry, AuditFilters } from '@/api/client' import { localTzOffset, parseFormDate } from '@/lib/dates' export const Route = createFileRoute('/audit')({ component: AuditPage, }) const ACTIONS: AuditAction[] = ['CREATE', 'UPDATE', 'CLOSE'] const PAGE_SIZE_OPTIONS = [ { id: '50', label: '50' }, { id: '100', label: '100' }, { id: '200', label: '200' }, ] function AuditPage() { const { t } = useTranslation() const dictionariesQuery = useDictionaries() const [filters, setFilters] = useState({ page: 0, size: 50 }) const [draft, setDraft] = useState(filters) const { data, isLoading, error, refetch, isFetching } = useAudit(filters) const dictionaryOptions = useMemo(() => { const opts: { id: string; label: string }[] = [{ id: '', label: '—' }] for (const d of dictionariesQuery.data ?? []) { opts.push({ id: d.name, label: d.displayName ?? d.name }) } return opts }, [dictionariesQuery.data]) const actionOptions = useMemo( () => [ { id: '', label: '—' }, ...ACTIONS.map((a) => ({ id: a, label: t(`audit.action.${a}`) })), ], [t], ) const apply = () => setFilters({ ...draft, page: 0 }) const reset = () => { const empty: AuditFilters = { page: 0, size: filters.size ?? 50 } setDraft(empty) setFilters(empty) } const totalPages = data?.totalPages ?? 0 const currentPage = data?.number ?? 0 return (
} /> {error ? ( {String(error)} ) : isLoading ? ( ) : !data || data.content.length === 0 ? ( ) : ( <> setFilters((f) => ({ ...f, page: Math.max(0, (f.page ?? 0) - 1) })) } onNext={() => setFilters((f) => ({ ...f, page: (f.page ?? 0) + 1 })) } onSizeChange={(size) => setFilters((f) => ({ ...f, page: 0, size })) } /> )} ) } type FilterPanelProps = { filters: AuditFilters onChange: (f: AuditFilters) => void dictionaryOptions: { id: string; label: string }[] actionOptions: { id: string; label: string }[] onApply: () => void onReset: () => void } function FilterPanel({ filters, onChange, dictionaryOptions, actionOptions, onApply, onReset, }: FilterPanelProps) { const { t } = useTranslation() const set = (k: K, v: AuditFilters[K]) => onChange({ ...filters, [k]: v }) const fromDate = parseFormDate(filters.from) const toDate = parseFormDate(filters.to) return (
set('dictionaryName', id || undefined)} /> set('action', (id as AuditAction) || '')} /> set('userId', e.target.value || undefined)} /> set('businessKey', e.target.value || undefined)} /> set( 'from', d ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String( d.getDate(), ).padStart(2, '0')}T00:00:00${localTzOffset(d)}` : undefined, ) } /> set( 'to', d ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String( d.getDate(), ).padStart(2, '0')}T23:59:59${localTzOffset(d)}` : undefined, ) } />
) } function AuditTable({ rows }: { rows: AuditEntry[] }) { const { t } = useTranslation() return ( {t('audit.col.time')} {t('audit.col.action')} {t('audit.col.dictionary')} {t('audit.col.businessKey')} {t('audit.col.user')} {t('audit.col.scope')} {t('audit.col.trace')} {t('audit.col.diff')} {rows.length === 0 ? ( {t('audit.empty')} ) : ( rows.map((r) => ) )}
) } function AuditRow({ row }: { row: AuditEntry }) { const { t } = useTranslation() const [open, setOpen] = useState(false) const time = new Date(row.eventTime) const timeLabel = time.toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }) const actionVariant = row.action === 'CREATE' ? 'success' : row.action === 'CLOSE' ? 'error' : 'info' return ( <> {timeLabel} {t(`audit.action.${row.action}`, row.action)} {row.dictionaryName ?? '—'} {row.businessKey ?? '—'} {row.userId ?? 'anonymous'} {row.userScope ? {row.userScope} : null} {row.traceId ? row.traceId.slice(0, 8) : '—'} : } label={t('audit.action.expand')} onClick={() => setOpen((v) => !v)} /> {open && (
{t('audit.diff.before')}
                  {row.payloadBefore
                    ? JSON.stringify(row.payloadBefore, null, 2)
                    : '—'}
                
{t('audit.diff.after')}
                  {row.payloadAfter
                    ? JSON.stringify(row.payloadAfter, null, 2)
                    : '—'}
                
{(row.ipAddress || row.userAgent || row.requestId) && (
{row.ipAddress && IP: {row.ipAddress}} {row.requestId && req: {row.requestId}} {row.userAgent && ( UA: {row.userAgent} )}
)}
)} ) } type PaginationProps = { page: number totalPages: number size: number onPrev: () => void onNext: () => void onSizeChange: (size: number) => void } function Pagination({ page, totalPages, size, onPrev, onNext, onSizeChange, }: PaginationProps) { const { t } = useTranslation() return (
{t('audit.page.size')} onSizeChange(Number(id))} />
{t('audit.page.of', { cur: page + 1, total: Math.max(totalPages, 1) })}
) }