fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to Sheet side=right so every detail-preview surface uses the same primitive. Document the doctrine: Sheet for side panels on both desktop and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX (currently just MoreSheet). Closes ui/ux M11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AlertTriangle, Anchor, FileText, Loader2, Receipt, Ship, Users } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -95,50 +95,96 @@ interface Props {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function SmartArchiveDialog({ open, onOpenChange, clientId, clientName, onSuccess }: Props) {
|
||||
const qc = useQueryClient();
|
||||
export function SmartArchiveDialog(props: Props) {
|
||||
// Key-based remount: body keyed on open + clientId; once the dossier
|
||||
// loads, an inner key forces the decision-defaults to seed cleanly.
|
||||
return (
|
||||
<SmartArchiveDialogShell key={props.open ? `open:${props.clientId}` : 'closed'} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function SmartArchiveDialogShell({ open, onOpenChange, clientId, clientName, onSuccess }: Props) {
|
||||
const qc = useQueryClient();
|
||||
const dossierQuery = useQuery({
|
||||
queryKey: ['client-archive-dossier', clientId],
|
||||
queryFn: () =>
|
||||
apiFetch<{ data: ArchiveDossier }>(`/api/v1/clients/${clientId}/archive-dossier`),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const dossier = dossierQuery.data?.data;
|
||||
// While the dossier is loading the body's useState initializers can't
|
||||
// derive defaults, so we delay-key the body so it mounts ONCE with the
|
||||
// right seed when the data arrives. Replaces the prior
|
||||
// useEffect(setState, [dossier]) sync that the Compiler flagged.
|
||||
return (
|
||||
<SmartArchiveDialogBody
|
||||
key={dossier ? 'loaded' : 'loading'}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
clientId={clientId}
|
||||
clientName={clientName}
|
||||
onSuccess={onSuccess}
|
||||
dossier={dossier ?? null}
|
||||
isLoading={dossierQuery.isLoading}
|
||||
error={dossierQuery.error}
|
||||
qc={qc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SmartArchiveDialogBody({
|
||||
open,
|
||||
onOpenChange,
|
||||
clientId,
|
||||
clientName,
|
||||
onSuccess,
|
||||
dossier,
|
||||
isLoading,
|
||||
error,
|
||||
qc,
|
||||
}: Props & {
|
||||
dossier: ArchiveDossier | null;
|
||||
isLoading: boolean;
|
||||
error: unknown;
|
||||
qc: ReturnType<typeof useQueryClient>;
|
||||
}) {
|
||||
// ─── Local decision state ────────────────────────────────────────────────
|
||||
const [reason, setReason] = useState('');
|
||||
const [acknowledged, setAcknowledged] = useState(false);
|
||||
const [berthDecisions, setBerthDecisions] = useState<Record<string, BerthAction>>({});
|
||||
const [yachtDecisions, setYachtDecisions] = useState<Record<string, YachtAction>>({});
|
||||
const [berthDecisions, setBerthDecisions] = useState<Record<string, BerthAction>>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(
|
||||
dossier.berths.map((berth) => [
|
||||
berth.berthId,
|
||||
berth.status === 'sold' ? ('retain' as BerthAction) : ('release' as BerthAction),
|
||||
]),
|
||||
)
|
||||
: {},
|
||||
);
|
||||
const [yachtDecisions, setYachtDecisions] = useState<Record<string, YachtAction>>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(dossier.yachts.map((y) => [y.yachtId, 'retain' as YachtAction]))
|
||||
: {},
|
||||
);
|
||||
const [reservationDecisions, setReservationDecisions] = useState<
|
||||
Record<string, ReservationAction>
|
||||
>({});
|
||||
const [invoiceDecisions, setInvoiceDecisions] = useState<Record<string, InvoiceAction>>({});
|
||||
const [documentDecisions, setDocumentDecisions] = useState<Record<string, DocumentAction>>({});
|
||||
|
||||
// Reset state when the dialog opens / closes / dossier loads.
|
||||
useEffect(() => {
|
||||
if (!open || !dossier) return;
|
||||
setReason('');
|
||||
setAcknowledged(false);
|
||||
// Sensible defaults: release all berths, retain all yachts, cancel
|
||||
// active reservations, leave invoices, leave documents alone.
|
||||
const b: Record<string, BerthAction> = {};
|
||||
for (const berth of dossier.berths) {
|
||||
// Sold berths can't be released; default to retain.
|
||||
b[berth.berthId] = berth.status === 'sold' ? 'retain' : 'release';
|
||||
}
|
||||
setBerthDecisions(b);
|
||||
setYachtDecisions(Object.fromEntries(dossier.yachts.map((y) => [y.yachtId, 'retain'])));
|
||||
setReservationDecisions(
|
||||
Object.fromEntries(dossier.reservations.map((r) => [r.reservationId, 'cancel'])),
|
||||
);
|
||||
setInvoiceDecisions(Object.fromEntries(dossier.invoices.map((i) => [i.invoiceId, 'leave'])));
|
||||
setDocumentDecisions(Object.fromEntries(dossier.documents.map((d) => [d.documentId, 'leave'])));
|
||||
}, [open, dossier]);
|
||||
|
||||
>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(
|
||||
dossier.reservations.map((r) => [r.reservationId, 'cancel' as ReservationAction]),
|
||||
)
|
||||
: {},
|
||||
);
|
||||
const [invoiceDecisions, setInvoiceDecisions] = useState<Record<string, InvoiceAction>>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(dossier.invoices.map((i) => [i.invoiceId, 'leave' as InvoiceAction]))
|
||||
: {},
|
||||
);
|
||||
const [documentDecisions, setDocumentDecisions] = useState<Record<string, DocumentAction>>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(dossier.documents.map((d) => [d.documentId, 'leave' as DocumentAction]))
|
||||
: {},
|
||||
);
|
||||
const hasSignedDocs = useMemo(
|
||||
() =>
|
||||
dossier?.documents.some((d) => d.status === 'completed' || d.status === 'signed') ?? false,
|
||||
@@ -235,15 +281,14 @@ export function SmartArchiveDialog({ open, onOpenChange, clientId, clientName, o
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{dossierQuery.isLoading ? (
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
|
||||
Loading dossier…
|
||||
</div>
|
||||
) : dossierQuery.error || !dossier ? (
|
||||
) : error || !dossier ? (
|
||||
<div className="py-8 text-center text-sm text-red-600">
|
||||
Failed to load dossier:{' '}
|
||||
{dossierQuery.error instanceof Error ? dossierQuery.error.message : 'unknown error'}
|
||||
Failed to load dossier: {error instanceof Error ? error.message : 'unknown error'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
||||
|
||||
Reference in New Issue
Block a user