fix(audit): frontend HIGHs — surface fetch errors, kill href=#, invalidate queries, toast over alert
R2-H10: webhook-delivery-log and audit-log-list both swallowed fetch errors silently — failed loads showed spinner forever or stale data. Both now set a loadError state, show an inline retry banner, and fire a toast.error. Same applies to audit-log loadMore. R2-H11: audit-log-card rendered as `<a href="#">` — tapping on mobile inserted `#` in the URL and scrolled to top (back-button trap). ListCard now treats `href` as optional and renders a non-link `<div>` when omitted; audit-log-card no longer passes href. R2-H12: smart-archive-dialog only invalidated ['clients'] / ['berths'] / ['interests']. Detail header kept showing Archived=false until hard reload. Now also invalidates ['clients', clientId] and removes the ['client-archive-dossier', clientId] cache so re-open re-fetches. R2-H13: client-list bulk mutation used native alert() on partial failure (blocking the page) and had no onError handler. Replaced with toast.warning / toast.success / toast.error. 1175/1175 vitest passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { type ColumnDef } from '@tanstack/react-table';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { DataTable } from '@/components/shared/data-table';
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
@@ -116,6 +117,7 @@ export function AuditLogList() {
|
||||
} | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
// Filter state - debounce text inputs.
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -149,10 +151,15 @@ export function AuditLogList() {
|
||||
|
||||
const fetchFirstPage = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
try {
|
||||
const res = await apiFetch<AuditResponse>(`/api/v1/admin/audit?${queryString}`);
|
||||
setEntries(res.data);
|
||||
setNextCursor(res.pagination.nextCursor);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed to load audit log';
|
||||
setLoadError(msg);
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -168,6 +175,8 @@ export function AuditLogList() {
|
||||
const res = await apiFetch<AuditResponse>(`/api/v1/admin/audit?${params}`);
|
||||
setEntries((prev) => [...prev, ...res.data]);
|
||||
setNextCursor(res.pagination.nextCursor);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to load more audit entries');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
@@ -466,20 +475,29 @@ export function AuditLogList() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={entries}
|
||||
isLoading={loading}
|
||||
getRowId={(row) => row.id}
|
||||
cardRender={(row) => <AuditLogCard entry={row.original} />}
|
||||
emptyState={
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground">No audit log entries found.</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{loadError && !loading && entries.length === 0 ? (
|
||||
<div className="mt-4 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm space-y-2">
|
||||
<p className="text-destructive">Couldn’t load audit log: {loadError}</p>
|
||||
<Button size="sm" variant="outline" onClick={() => void fetchFirstPage()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={entries}
|
||||
isLoading={loading}
|
||||
getRowId={(row) => row.id}
|
||||
cardRender={(row) => <AuditLogCard entry={row.original} />}
|
||||
emptyState={
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground">No audit log entries found.</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nextCursor ? (
|
||||
<div className="mt-4 flex justify-center">
|
||||
|
||||
Reference in New Issue
Block a user