diff --git a/src/components/admin/audit/audit-log-card.tsx b/src/components/admin/audit/audit-log-card.tsx index 6bf08be..a9ede4d 100644 --- a/src/components/admin/audit/audit-log-card.tsx +++ b/src/components/admin/audit/audit-log-card.tsx @@ -93,7 +93,6 @@ export function AuditLogCard({ entry }: AuditLogCardProps) { return ( diff --git a/src/components/admin/audit/audit-log-list.tsx b/src/components/admin/audit/audit-log-list.tsx index bb1e2eb..e736de5 100644 --- a/src/components/admin/audit/audit-log-list.tsx +++ b/src/components/admin/audit/audit-log-list.tsx @@ -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(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(`/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(`/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} -
- row.id} - cardRender={(row) => } - emptyState={ -
-

No audit log entries found.

-
- } - /> -
+ {loadError && !loading && entries.length === 0 ? ( +
+

Couldn’t load audit log: {loadError}

+ +
+ ) : ( +
+ row.id} + cardRender={(row) => } + emptyState={ +
+

No audit log entries found.

+
+ } + /> +
+ )} {nextCursor ? (
diff --git a/src/components/admin/webhooks/webhook-delivery-log.tsx b/src/components/admin/webhooks/webhook-delivery-log.tsx index d96add9..290a45c 100644 --- a/src/components/admin/webhooks/webhook-delivery-log.tsx +++ b/src/components/admin/webhooks/webhook-delivery-log.tsx @@ -42,6 +42,7 @@ export function WebhookDeliveryLog({ webhookId }: Props) { const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); const [retrying, setRetrying] = useState(null); const { can } = usePermissions(); const canReplay = can('admin', 'manage_webhooks'); @@ -63,14 +64,17 @@ export function WebhookDeliveryLog({ webhookId }: Props) { async function load(p: number) { setLoading(true); + setLoadError(null); try { const result = await apiFetch<{ data: Delivery[]; total: number }>( `/api/v1/admin/webhooks/${webhookId}/deliveries?page=${p}&limit=25`, ); setDeliveries(result.data); setTotal(result.total); - } catch { - // ignore + } catch (err) { + const msg = err instanceof Error ? err.message : 'Failed to load deliveries'; + setLoadError(msg); + toast.error(msg); } finally { setLoading(false); } @@ -85,6 +89,17 @@ export function WebhookDeliveryLog({ webhookId }: Props) { return

Loading deliveries...

; } + if (loadError && deliveries.length === 0) { + return ( +
+

Couldn’t load deliveries: {loadError}

+ +
+ ); + } + if (!loading && deliveries.length === 0) { return

No deliveries yet.

; } diff --git a/src/components/clients/client-list.tsx b/src/components/clients/client-list.tsx index 3485019..c587f75 100644 --- a/src/components/clients/client-list.tsx +++ b/src/components/clients/client-list.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { useParams } from 'next/navigation'; import { Plus, Archive, Tag as TagIcon, TagsIcon, Trash2 } from 'lucide-react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/shared/data-table'; @@ -102,9 +103,14 @@ export function ClientList() { queryClient.invalidateQueries({ queryKey: ['clients'] }); const s = res.data.summary; if (s.failed > 0) { - alert(`${s.succeeded} of ${s.total} succeeded. ${s.failed} failed.`); + toast.warning(`${s.succeeded} of ${s.total} succeeded. ${s.failed} failed.`); + } else if (s.succeeded > 0) { + toast.success(`${s.succeeded} client${s.succeeded === 1 ? '' : 's'} updated.`); } }, + onError: (err: unknown) => { + toast.error(err instanceof Error ? err.message : 'Bulk action failed'); + }, }); const columns = getClientColumns({ diff --git a/src/components/clients/smart-archive-dialog.tsx b/src/components/clients/smart-archive-dialog.tsx index 2e92d80..dfa25ef 100644 --- a/src/components/clients/smart-archive-dialog.tsx +++ b/src/components/clients/smart-archive-dialog.tsx @@ -209,6 +209,11 @@ export function SmartArchiveDialog({ open, onOpenChange, clientId, clientName, o : `${clientName} archived.`, ); qc.invalidateQueries({ queryKey: ['clients'] }); + // Invalidate the single-client query AND the dossier so detail + // pages re-fetch (header now shows Archived badge) and a re-open + // of the dialog re-fetches a fresh dossier. + qc.invalidateQueries({ queryKey: ['clients', clientId] }); + qc.removeQueries({ queryKey: ['client-archive-dossier', clientId] }); qc.invalidateQueries({ queryKey: ['berths'] }); qc.invalidateQueries({ queryKey: ['interests'] }); onOpenChange(false); diff --git a/src/components/shared/list-card.tsx b/src/components/shared/list-card.tsx index c72e0a6..ae4a6f7 100644 --- a/src/components/shared/list-card.tsx +++ b/src/components/shared/list-card.tsx @@ -7,8 +7,9 @@ import { type ReactNode } from 'react'; import { cn } from '@/lib/utils'; interface ListCardProps { - /** Detail-page URL the card navigates to when tapped. */ - href: string; + /** Detail-page URL the card navigates to when tapped. Omit to render a + * non-navigating card (audit log entries, read-only rows). */ + href?: string; /** * Optional Tailwind background class painted on a 3px vertical strip on the * left edge - used to encode pipeline stage / status / category at a glance. @@ -41,6 +42,12 @@ export function ListCard({ className, children, }: ListCardProps) { + const innerClassName = cn( + 'block p-3', + accentClassName && 'pl-4', + 'rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', + ); + return (
) : null} - - {children} - + {href ? ( + + {children} + + ) : ( +
+ {children} +
+ )} {actions ?
{actions}
: null}
);