feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
'use client';
|
|
|
|
|
|
2026-05-13 11:50:07 +02:00
|
|
|
import { useMemo, useState } from 'react';
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { AlertTriangle, Anchor, FileText, Loader2, Receipt, Ship, Users } from 'lucide-react';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
fix(audit-wave-11): dossier sweep — error-ux + webhook + storage + search + maintainability
Final pass over the unaddressed AUDIT-2026-05-12 dossiers, taking the
tractable Critical/High items from each:
error-ux-auditor (5 items)
- C2: 17 toast.error(err.message) sites swept to toastError(err, …) so
every user-visible failure carries a copy-paste Reference ID
- C3: apiFetch synthesizes a client-side correlation id when a 5xx
comes back with a non-JSON body (reverse-proxy HTML pages); message
becomes "The server is unreachable. Please try again." with code
UPSTREAM_UNREACHABLE
- C4: checkRateLimit fails OPEN when Redis is unavailable so an outage
no longer 500s login + portal sign-in; logged at warn so monitoring
catches it
- H2: StorageTimeoutError (name='TimeoutError') replaces the plain
Error throw in s3.ts withTimeout — error-classifier hints fire now
- H5: errorResponse() adopted across /api/storage/[token],
/api/public/website-inquiries, and the Documenso webhook body (drops
the "Invalid secret" reconnaissance string)
outbound-webhook-auditor (5 items)
- C1: signature is now HMAC(secret, `${ts}.${body}`) with the
timestamp surfaced as X-Webhook-Timestamp so receivers can reject
replays outside a freshness window
- C3: dead-letter with reason missing_signing_secret when secret is
null (defence-in-depth against DB tampering / future migration
mistakes)
- H2: webhooks queue bumped to maxAttempts=8 with 30 s base
exponential backoff so a 30 s receiver blip during a deploy no
longer dead-letters every in-flight event; per-queue
backoffDelayMs added to QUEUE_CONFIGS
- M1: SSRF denylist gains Oracle Cloud metadata 192.0.0.192
- M2: dispatch-time https:// assertion before fetch, so a bad DB edit
can't slip plaintext through
storage-pathing-auditor (2 items)
- H1: berth-PDF presigned-upload keys now `${portSlug}/berths/…/…`
with portSlug threaded into backend.presignUpload — engages the
filesystem-proxy port-binding `p` token verifier
- H2: presignDownloadUrl auto-derives portSlug from the key's first
segment when callers don't pass it, so all 8 download sites engage
the `p`-token guard without per-site plumbing
search-auditor (1 item)
- H3: removed dead void wantEmail; void wantPhone; pair plus the
unused looksLikeEmail helper — the bucket-reorder it was scaffolded
for was never wired
maintainability-auditor (1 item)
- M2: swept seven abandoned `void <symbol>` markers and their dead
imports across clients/bulk, interests/bulk, admin/email-templates,
admin/website-submissions, alert-rules, and notes.service
Deferred to future work (substantial refactors, schema migrations, or
multi-file UI work):
- error-ux M3-M8 (global-error.tsx, per-route loading.tsx coverage,
ErrorBanner component, /api/ready route, worker DLQ admin surface)
- maintainability C1-C4 (documents/search/notes service splits,
interest-tabs split — multi-hour refactors)
- currency C1-H5 (mixed-currency dashboard aggregation, FX history
table, rounding policy) — wait for second non-USD port
- outbound-webhook C2 (deliveries reaper job), H1 (DNS-rebind TOCTOU
with undici Agent), H3 (circuit-breaker), H5 (presigned-post-policy)
- storage-pathing C2 (orphan reaper), H3-H5 (streaming + content-type
binding)
Tests: 1315/1315 vitest ✅ ; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:27:32 +02:00
|
|
|
import { toastError } from '@/lib/api/toast-error';
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
|
|
|
|
|
interface DossierBerth {
|
|
|
|
|
berthId: string;
|
|
|
|
|
mooringNumber: string;
|
|
|
|
|
status: string;
|
2026-05-06 22:11:00 +02:00
|
|
|
linkedInterestIds: string[];
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
otherInterests: Array<{
|
|
|
|
|
interestId: string;
|
|
|
|
|
clientId: string | null;
|
|
|
|
|
clientName: string | null;
|
|
|
|
|
pipelineStage: string;
|
|
|
|
|
daysSinceUpdate: number;
|
|
|
|
|
}>;
|
|
|
|
|
}
|
|
|
|
|
interface DossierDocument {
|
|
|
|
|
documentId: string;
|
|
|
|
|
templateName: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
documensoEnvelopeId: string | null;
|
|
|
|
|
isInFlight: boolean;
|
|
|
|
|
}
|
|
|
|
|
interface DossierYacht {
|
|
|
|
|
yachtId: string;
|
|
|
|
|
name: string;
|
|
|
|
|
hullNumber: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
}
|
|
|
|
|
interface DossierReservation {
|
|
|
|
|
reservationId: string;
|
|
|
|
|
berthId: string;
|
|
|
|
|
mooringNumber: string;
|
|
|
|
|
status: string;
|
|
|
|
|
startDate: string;
|
|
|
|
|
}
|
|
|
|
|
interface DossierInvoice {
|
|
|
|
|
invoiceId: string;
|
|
|
|
|
invoiceNumber: string;
|
|
|
|
|
status: string;
|
|
|
|
|
total: string;
|
|
|
|
|
currency: string;
|
|
|
|
|
}
|
|
|
|
|
interface DossierInterest {
|
|
|
|
|
interestId: string;
|
|
|
|
|
pipelineStage: string;
|
|
|
|
|
primaryBerthMooring: string | null;
|
|
|
|
|
hasSignedEoi: boolean;
|
|
|
|
|
}
|
|
|
|
|
interface ArchiveDossier {
|
|
|
|
|
client: { id: string; fullName: string; portId: string; archivedAt: string | null };
|
|
|
|
|
stakeLevel: 'low' | 'high';
|
|
|
|
|
highStakesStage: string | null;
|
|
|
|
|
interests: DossierInterest[];
|
|
|
|
|
berths: DossierBerth[];
|
|
|
|
|
yachts: DossierYacht[];
|
|
|
|
|
companies: Array<{ companyId: string; name: string; membershipRole: string | null }>;
|
|
|
|
|
reservations: DossierReservation[];
|
|
|
|
|
invoices: DossierInvoice[];
|
|
|
|
|
documents: DossierDocument[];
|
|
|
|
|
hasPortalUser: boolean;
|
|
|
|
|
blockers: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type BerthAction = 'release' | 'retain';
|
|
|
|
|
type YachtAction = 'transfer' | 'mark_sold_away' | 'retain';
|
|
|
|
|
type ReservationAction = 'cancel' | 'transfer';
|
|
|
|
|
type InvoiceAction = 'void' | 'write_off' | 'leave';
|
|
|
|
|
type DocumentAction = 'void_documenso' | 'leave';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (next: boolean) => void;
|
|
|
|
|
clientId: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
/** Called after successful archive. */
|
|
|
|
|
onSuccess?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 11:50:07 +02:00
|
|
|
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} />
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
|
2026-05-13 11:50:07 +02:00
|
|
|
function SmartArchiveDialogShell({ open, onOpenChange, clientId, clientName, onSuccess }: Props) {
|
|
|
|
|
const qc = useQueryClient();
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
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;
|
2026-05-13 11:50:07 +02:00
|
|
|
// 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}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
|
2026-05-13 11:50:07 +02:00
|
|
|
function SmartArchiveDialogBody({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
clientId,
|
|
|
|
|
clientName,
|
|
|
|
|
onSuccess,
|
|
|
|
|
dossier,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
qc,
|
|
|
|
|
}: Props & {
|
|
|
|
|
dossier: ArchiveDossier | null;
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
error: unknown;
|
|
|
|
|
qc: ReturnType<typeof useQueryClient>;
|
|
|
|
|
}) {
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
// ─── Local decision state ────────────────────────────────────────────────
|
|
|
|
|
const [reason, setReason] = useState('');
|
|
|
|
|
const [acknowledged, setAcknowledged] = useState(false);
|
2026-05-13 11:50:07 +02:00
|
|
|
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]))
|
|
|
|
|
: {},
|
|
|
|
|
);
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
const [reservationDecisions, setReservationDecisions] = useState<
|
|
|
|
|
Record<string, ReservationAction>
|
2026-05-13 11:50:07 +02:00
|
|
|
>(() =>
|
|
|
|
|
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]))
|
|
|
|
|
: {},
|
|
|
|
|
);
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
const hasSignedDocs = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
dossier?.documents.some((d) => d.status === 'completed' || d.status === 'signed') ?? false,
|
|
|
|
|
[dossier],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const canSubmit = useMemo(() => {
|
|
|
|
|
if (!dossier) return false;
|
|
|
|
|
if (dossier.blockers.length > 0) return false;
|
|
|
|
|
if (dossier.stakeLevel === 'high' && reason.trim().length < 5) return false;
|
|
|
|
|
if (hasSignedDocs && !acknowledged) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}, [dossier, reason, hasSignedDocs, acknowledged]);
|
|
|
|
|
|
|
|
|
|
const archiveMutation = useMutation({
|
|
|
|
|
mutationFn: () => {
|
|
|
|
|
if (!dossier) throw new Error('No dossier');
|
2026-05-06 22:11:00 +02:00
|
|
|
// Pick the first linked interest for this berth from the
|
|
|
|
|
// authoritative dossier join. Berths with no linked interest for
|
|
|
|
|
// this client are skipped — sending an empty interestId would
|
|
|
|
|
// make the server-side delete silently match zero rows.
|
|
|
|
|
const berthDec = dossier.berths
|
|
|
|
|
.map((b) => {
|
|
|
|
|
const interestId = b.linkedInterestIds[0];
|
|
|
|
|
if (!interestId) return null;
|
|
|
|
|
return {
|
|
|
|
|
berthId: b.berthId,
|
|
|
|
|
interestId,
|
|
|
|
|
action: berthDecisions[b.berthId] ?? 'retain',
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter(
|
|
|
|
|
(x): x is { berthId: string; interestId: string; action: BerthAction } => x !== null,
|
|
|
|
|
);
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
return apiFetch<{ data: { releasedBerths: Array<{ mooringNumber: string }> } }>(
|
|
|
|
|
`/api/v1/clients/${clientId}/archive`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: {
|
|
|
|
|
reason,
|
|
|
|
|
acknowledgedSignedDocuments: acknowledged,
|
|
|
|
|
berthDecisions: berthDec,
|
|
|
|
|
yachtDecisions: dossier.yachts.map((y) => ({
|
|
|
|
|
yachtId: y.yachtId,
|
|
|
|
|
action: yachtDecisions[y.yachtId] ?? 'retain',
|
|
|
|
|
})),
|
|
|
|
|
reservationDecisions: dossier.reservations.map((r) => ({
|
|
|
|
|
reservationId: r.reservationId,
|
|
|
|
|
action: reservationDecisions[r.reservationId] ?? 'cancel',
|
|
|
|
|
})),
|
|
|
|
|
invoiceDecisions: dossier.invoices.map((i) => ({
|
|
|
|
|
invoiceId: i.invoiceId,
|
|
|
|
|
action: invoiceDecisions[i.invoiceId] ?? 'leave',
|
|
|
|
|
})),
|
|
|
|
|
documentDecisions: dossier.documents.map((d) => ({
|
|
|
|
|
documentId: d.documentId,
|
|
|
|
|
action: documentDecisions[d.documentId] ?? 'leave',
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (res) => {
|
|
|
|
|
const released = res.data.releasedBerths;
|
|
|
|
|
toast.success(
|
|
|
|
|
released.length > 0
|
|
|
|
|
? `${clientName} archived. ${released.length} berth${released.length === 1 ? '' : 's'} released.`
|
|
|
|
|
: `${clientName} archived.`,
|
|
|
|
|
);
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['clients'] });
|
2026-05-06 22:18:14 +02:00
|
|
|
// 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] });
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
qc.invalidateQueries({ queryKey: ['berths'] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['interests'] });
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
onSuccess?.();
|
|
|
|
|
},
|
|
|
|
|
onError: (err: unknown) => {
|
fix(audit-wave-11): dossier sweep — error-ux + webhook + storage + search + maintainability
Final pass over the unaddressed AUDIT-2026-05-12 dossiers, taking the
tractable Critical/High items from each:
error-ux-auditor (5 items)
- C2: 17 toast.error(err.message) sites swept to toastError(err, …) so
every user-visible failure carries a copy-paste Reference ID
- C3: apiFetch synthesizes a client-side correlation id when a 5xx
comes back with a non-JSON body (reverse-proxy HTML pages); message
becomes "The server is unreachable. Please try again." with code
UPSTREAM_UNREACHABLE
- C4: checkRateLimit fails OPEN when Redis is unavailable so an outage
no longer 500s login + portal sign-in; logged at warn so monitoring
catches it
- H2: StorageTimeoutError (name='TimeoutError') replaces the plain
Error throw in s3.ts withTimeout — error-classifier hints fire now
- H5: errorResponse() adopted across /api/storage/[token],
/api/public/website-inquiries, and the Documenso webhook body (drops
the "Invalid secret" reconnaissance string)
outbound-webhook-auditor (5 items)
- C1: signature is now HMAC(secret, `${ts}.${body}`) with the
timestamp surfaced as X-Webhook-Timestamp so receivers can reject
replays outside a freshness window
- C3: dead-letter with reason missing_signing_secret when secret is
null (defence-in-depth against DB tampering / future migration
mistakes)
- H2: webhooks queue bumped to maxAttempts=8 with 30 s base
exponential backoff so a 30 s receiver blip during a deploy no
longer dead-letters every in-flight event; per-queue
backoffDelayMs added to QUEUE_CONFIGS
- M1: SSRF denylist gains Oracle Cloud metadata 192.0.0.192
- M2: dispatch-time https:// assertion before fetch, so a bad DB edit
can't slip plaintext through
storage-pathing-auditor (2 items)
- H1: berth-PDF presigned-upload keys now `${portSlug}/berths/…/…`
with portSlug threaded into backend.presignUpload — engages the
filesystem-proxy port-binding `p` token verifier
- H2: presignDownloadUrl auto-derives portSlug from the key's first
segment when callers don't pass it, so all 8 download sites engage
the `p`-token guard without per-site plumbing
search-auditor (1 item)
- H3: removed dead void wantEmail; void wantPhone; pair plus the
unused looksLikeEmail helper — the bucket-reorder it was scaffolded
for was never wired
maintainability-auditor (1 item)
- M2: swept seven abandoned `void <symbol>` markers and their dead
imports across clients/bulk, interests/bulk, admin/email-templates,
admin/website-submissions, alert-rules, and notes.service
Deferred to future work (substantial refactors, schema migrations, or
multi-file UI work):
- error-ux M3-M8 (global-error.tsx, per-route loading.tsx coverage,
ErrorBanner component, /api/ready route, worker DLQ admin surface)
- maintainability C1-C4 (documents/search/notes service splits,
interest-tabs split — multi-hour refactors)
- currency C1-H5 (mixed-currency dashboard aggregation, FX history
table, rounding policy) — wait for second non-USD port
- outbound-webhook C2 (deliveries reaper job), H1 (DNS-rebind TOCTOU
with undici Agent), H3 (circuit-breaker), H5 (presigned-post-policy)
- storage-pathing C2 (orphan reaper), H3-H5 (streaming + content-type
binding)
Tests: 1315/1315 vitest ✅ ; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:27:32 +02:00
|
|
|
toastError(err, 'Archive failed');
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent className="sm:max-w-2xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Archive {clientName}</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Archive is reversible — the client can be restored from the archived list. Decide what
|
|
|
|
|
should happen to the relationships below before continuing.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
2026-05-13 11:50:07 +02:00
|
|
|
{isLoading ? (
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" aria-hidden />
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
Loading dossier…
|
|
|
|
|
</div>
|
2026-05-13 11:50:07 +02:00
|
|
|
) : error || !dossier ? (
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
<div className="py-8 text-center text-sm text-red-600">
|
2026-05-13 11:50:07 +02:00
|
|
|
Failed to load dossier: {error instanceof Error ? error.message : 'unknown error'}
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
|
|
|
|
{dossier.blockers.length > 0 && (
|
|
|
|
|
<Card className="border-red-300 bg-red-50">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium text-red-800 flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<AlertTriangle className="h-4 w-4" aria-hidden /> Cannot archive
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="text-sm text-red-700 space-y-1">
|
|
|
|
|
{dossier.blockers.map((b, i) => (
|
|
|
|
|
<p key={i}>{b}</p>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{dossier.stakeLevel === 'high' && (
|
|
|
|
|
<Card className="border-amber-300 bg-amber-50">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<AlertTriangle className="h-4 w-4" aria-hidden />
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
Late-stage deal — confirmation required
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="text-xs text-amber-900">
|
|
|
|
|
This client is at <Badge variant="secondary">{dossier.highStakesStage}</Badge>.
|
|
|
|
|
Provide a reason explaining why you’re archiving them at this stage. The
|
|
|
|
|
reason is recorded in the audit log.
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Interests + signed-doc acknowledgment */}
|
|
|
|
|
{dossier.interests.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<FileText className="h-4 w-4" aria-hidden /> Pipeline interests (
|
|
|
|
|
{dossier.interests.length})
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="text-xs space-y-1">
|
|
|
|
|
{dossier.interests.map((i) => (
|
|
|
|
|
<div key={i.interestId} className="flex items-center justify-between">
|
|
|
|
|
<span className="font-mono">{i.interestId.slice(0, 8)}</span>
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
{i.pipelineStage}
|
|
|
|
|
</Badge>
|
|
|
|
|
{i.hasSignedEoi && <Badge className="text-xs">Signed EOI</Badge>}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{hasSignedDocs && (
|
|
|
|
|
<Card className="border-blue-300 bg-blue-50">
|
|
|
|
|
<CardContent className="pt-4 text-xs text-blue-900">
|
|
|
|
|
<label className="flex items-start gap-2 cursor-pointer">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={acknowledged}
|
|
|
|
|
onChange={(e) => setAcknowledged(e.target.checked)}
|
|
|
|
|
className="mt-0.5"
|
|
|
|
|
/>
|
|
|
|
|
<span>
|
|
|
|
|
I acknowledge this client has signed legal documents. The documents will be
|
|
|
|
|
retained in the system and remain binding regardless of archive status.
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Berths */}
|
|
|
|
|
{dossier.berths.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Anchor className="h-4 w-4" aria-hidden /> Berths ({dossier.berths.length})
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
{dossier.berths.map((b) => (
|
|
|
|
|
<div key={b.berthId} className="text-xs">
|
|
|
|
|
<div className="flex items-center justify-between mb-1">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-medium">Berth {b.mooringNumber}</span>
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
{b.status}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
className="rounded border bg-background px-2 py-1 text-xs disabled:opacity-50"
|
|
|
|
|
value={berthDecisions[b.berthId] ?? 'retain'}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setBerthDecisions((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[b.berthId]: e.target.value as BerthAction,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
disabled={b.status === 'sold'}
|
|
|
|
|
>
|
|
|
|
|
<option value="retain">Retain on archived client</option>
|
|
|
|
|
<option value="release">Release to available</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
{b.status === 'sold' && (
|
|
|
|
|
<p className="text-muted-foreground italic">
|
|
|
|
|
Sold berths stay sold. Process a refund separately if needed.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{b.otherInterests.length > 0 && berthDecisions[b.berthId] === 'release' && (
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Releasing will notify the sales rep. Other interests on this berth:{' '}
|
|
|
|
|
{b.otherInterests
|
|
|
|
|
.slice(0, 3)
|
|
|
|
|
.map((o) => `${o.clientName ?? '?'} (${o.pipelineStage})`)
|
|
|
|
|
.join(', ')}
|
|
|
|
|
{b.otherInterests.length > 3 ? ` +${b.otherInterests.length - 3}` : ''}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Yachts */}
|
|
|
|
|
{dossier.yachts.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Ship className="h-4 w-4" aria-hidden /> Yachts owned ({dossier.yachts.length})
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
{dossier.yachts.map((y) => (
|
|
|
|
|
<div key={y.yachtId} className="flex items-center justify-between text-xs">
|
|
|
|
|
<span className="font-medium">{y.name}</span>
|
|
|
|
|
<select
|
|
|
|
|
className="rounded border bg-background px-2 py-1 text-xs"
|
|
|
|
|
value={yachtDecisions[y.yachtId] ?? 'retain'}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setYachtDecisions((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[y.yachtId]: e.target.value as YachtAction,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="retain">Leave on archived client</option>
|
|
|
|
|
<option value="mark_sold_away">Mark as sold-away</option>
|
|
|
|
|
{/* Transfer to another owner needs an owner picker; v1
|
|
|
|
|
offers retain + sold-away inline. Use the yacht
|
|
|
|
|
detail page to transfer to a specific owner. */}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Reservations */}
|
|
|
|
|
{dossier.reservations.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Anchor className="h-4 w-4" aria-hidden /> Active reservations (
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
{dossier.reservations.length})
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
{dossier.reservations.map((r) => (
|
|
|
|
|
<div
|
|
|
|
|
key={r.reservationId}
|
|
|
|
|
className="flex items-center justify-between text-xs"
|
|
|
|
|
>
|
|
|
|
|
<span>Berth {r.mooringNumber}</span>
|
|
|
|
|
<select
|
|
|
|
|
className="rounded border bg-background px-2 py-1 text-xs"
|
|
|
|
|
value={reservationDecisions[r.reservationId] ?? 'cancel'}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setReservationDecisions((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[r.reservationId]: e.target.value as ReservationAction,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="cancel">Cancel reservation</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Invoices */}
|
|
|
|
|
{dossier.invoices.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Receipt className="h-4 w-4" aria-hidden /> Outstanding invoices (
|
|
|
|
|
{dossier.invoices.length})
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
{dossier.invoices.map((i) => (
|
|
|
|
|
<div key={i.invoiceId} className="flex items-center justify-between text-xs">
|
|
|
|
|
<span>
|
|
|
|
|
{i.invoiceNumber} · {i.total} {i.currency}
|
|
|
|
|
</span>
|
|
|
|
|
<select
|
|
|
|
|
className="rounded border bg-background px-2 py-1 text-xs"
|
|
|
|
|
value={invoiceDecisions[i.invoiceId] ?? 'leave'}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setInvoiceDecisions((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[i.invoiceId]: e.target.value as InvoiceAction,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="leave">Leave open</option>
|
|
|
|
|
<option value="void">Void</option>
|
|
|
|
|
<option value="write_off">Write off</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
fix(audit-wave-9): copy/terminology sweep (copy-auditor)
Address the highest-impact items from the copy-auditor's CRITICAL +
HIGH + MEDIUM bands:
**C2 portal raw-status leak**
- Drop the staff-only `leadCategory` chip from the portal interests
page entirely. Privacy + optics: clients should never see "hot lead"
in their own portal. `eoiStatus` was already wrapped in
`portalSigningLabel`; only the categorical chip remained.
**C3 signing-status label drift**
- Add `src/lib/labels/document-status.ts` as the single source of
truth for the {draft, sent, partially_signed, completed, expired,
cancelled} lifecycle: labels (CRM + portal variants), StatusPill
variant, and the "active / in-flight" set.
- Wire it into interest-eoi-tab, interest-contract-tab,
interest-reservation-tab — they previously redefined identical
STATUS_LABELS / ACTIVE_STATUSES blocks per-file.
**H1 + M3 verbiage codemod**
- `Save Changes` → `Save changes` (sentence case, matches the
surrounding admin/CRM pattern).
- `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis).
Matches the project's UTF-8-elsewhere convention and reads
correctly via screen-readers.
**M1 envelope jargon → signing request**
- smart-archive-dialog: "Leave envelope pending" → "Leave signing
request pending"; "Void the signing envelope" → "Cancel the signing
request"; section header updated to match.
- document-detail: "voids the signing envelope" → "cancels the signing
request".
- bulk-archive-wizard: "leave invoices/signing envelopes alone" →
"leave invoices/signing requests alone".
- Documenso admin page intentionally keeps `envelope` (dev/integration
vocabulary).
**M5 Hot Lead casing**
- Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to
sentence case in `constants.ts` LABEL_OVERRIDES and all per-file
lead-category maps so the CRM trend (sentence case) is consistent.
**C1 surface-level rename**
- "Linked prospect (optional)" → "Linked interest (optional)" on the
berth status-change dialog.
- "Deal Documents" tab → "Interest Documents" (URL/route kept as
`/deal-documents` to avoid breaking deep links; rename deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:12:40 +02:00
|
|
|
{/* In-flight signing requests */}
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
{dossier.documents.filter((d) => d.isInFlight).length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<FileText className="h-4 w-4" aria-hidden /> In-flight signing requests
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
{dossier.documents
|
|
|
|
|
.filter((d) => d.isInFlight)
|
|
|
|
|
.map((d) => (
|
|
|
|
|
<div key={d.documentId} className="flex items-center justify-between text-xs">
|
|
|
|
|
<span>{d.templateName ?? d.documentId.slice(0, 8)}</span>
|
|
|
|
|
<select
|
|
|
|
|
className="rounded border bg-background px-2 py-1 text-xs"
|
|
|
|
|
value={documentDecisions[d.documentId] ?? 'leave'}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setDocumentDecisions((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[d.documentId]: e.target.value as DocumentAction,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
>
|
fix(audit-wave-9): copy/terminology sweep (copy-auditor)
Address the highest-impact items from the copy-auditor's CRITICAL +
HIGH + MEDIUM bands:
**C2 portal raw-status leak**
- Drop the staff-only `leadCategory` chip from the portal interests
page entirely. Privacy + optics: clients should never see "hot lead"
in their own portal. `eoiStatus` was already wrapped in
`portalSigningLabel`; only the categorical chip remained.
**C3 signing-status label drift**
- Add `src/lib/labels/document-status.ts` as the single source of
truth for the {draft, sent, partially_signed, completed, expired,
cancelled} lifecycle: labels (CRM + portal variants), StatusPill
variant, and the "active / in-flight" set.
- Wire it into interest-eoi-tab, interest-contract-tab,
interest-reservation-tab — they previously redefined identical
STATUS_LABELS / ACTIVE_STATUSES blocks per-file.
**H1 + M3 verbiage codemod**
- `Save Changes` → `Save changes` (sentence case, matches the
surrounding admin/CRM pattern).
- `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis).
Matches the project's UTF-8-elsewhere convention and reads
correctly via screen-readers.
**M1 envelope jargon → signing request**
- smart-archive-dialog: "Leave envelope pending" → "Leave signing
request pending"; "Void the signing envelope" → "Cancel the signing
request"; section header updated to match.
- document-detail: "voids the signing envelope" → "cancels the signing
request".
- bulk-archive-wizard: "leave invoices/signing envelopes alone" →
"leave invoices/signing requests alone".
- Documenso admin page intentionally keeps `envelope` (dev/integration
vocabulary).
**M5 Hot Lead casing**
- Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to
sentence case in `constants.ts` LABEL_OVERRIDES and all per-file
lead-category maps so the CRM trend (sentence case) is consistent.
**C1 surface-level rename**
- "Linked prospect (optional)" → "Linked interest (optional)" on the
berth status-change dialog.
- "Deal Documents" tab → "Interest Documents" (URL/route kept as
`/deal-documents` to avoid breaking deep links; rename deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:12:40 +02:00
|
|
|
<option value="leave">Leave signing request pending</option>
|
|
|
|
|
<option value="void_documenso">Cancel the signing request</option>
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Auto-handled summary */}
|
|
|
|
|
<Card className="border-muted">
|
|
|
|
|
<CardHeader className="pb-2">
|
|
|
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Users className="h-4 w-4" aria-hidden /> Automatically handled
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="text-xs text-muted-foreground space-y-1">
|
|
|
|
|
<p>EOI documents — retained for audit (always)</p>
|
|
|
|
|
{dossier.hasPortalUser && <p>Portal user — deactivated (login revoked)</p>}
|
|
|
|
|
{dossier.companies.length > 0 && (
|
|
|
|
|
<p>Company memberships — end-dated to today (history preserved)</p>
|
|
|
|
|
)}
|
|
|
|
|
<p>Notes, contacts, tags, addresses — survive on the archived client</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Reason field */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Reason {dossier.stakeLevel === 'high' && <span className="text-red-600">*</span>}
|
|
|
|
|
</label>
|
|
|
|
|
<Textarea
|
|
|
|
|
value={reason}
|
|
|
|
|
onChange={(e) => setReason(e.target.value)}
|
|
|
|
|
placeholder={
|
|
|
|
|
dossier.stakeLevel === 'high'
|
|
|
|
|
? `Why are you archiving this client at ${dossier.highStakesStage}?`
|
|
|
|
|
: 'Optional: why is this client being archived?'
|
|
|
|
|
}
|
|
|
|
|
className="mt-1 min-h-[60px]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
disabled={!canSubmit || archiveMutation.isPending}
|
|
|
|
|
onClick={() => archiveMutation.mutate()}
|
|
|
|
|
>
|
|
|
|
|
{archiveMutation.isPending ? (
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" aria-hidden />
|
feat(client-archive): single-client smart-archive dialog + CSP/middleware fixups
UI side of the smart-archive backend that shipped in d07f1ed.
- SmartArchiveDialog renders the dossier as a sectioned modal:
Pipeline interests, Berths (with next-in-line listed), Yachts,
Active reservations, Outstanding invoices, In-flight Documenso
envelopes, Auto-handled summary. Each section has a per-row decision
dropdown with sensible defaults (release for available/under-offer
berths, retain for sold berths and yachts, cancel for active
reservations, leave for invoices and documents).
- High-stakes deals show an amber warning panel + require a reason in
the textarea before the Archive button enables. Signed-document
acknowledgment checkbox blocks submission until checked.
- Wires into client-detail-header in place of the previous dumb
ArchiveConfirmDialog (the simple confirm dialog is kept for the
restore case until the smart-restore wizard ships).
- Pre-flight blocker banner surfaces dossier.blockers (e.g. active
reservation on a sold berth) and disables the Archive button entirely.
Two side fixes from CSP rollout:
- next.config CSP allows unpkg.com in dev so the react-grab devtool
loads. Stripped in prod via the existing isProd flag.
- middleware whitelist now passes /manifest.json + icon-*.png +
apple-touch-icon through unauthenticated, so PWA installability
isn't blocked by the auth redirect.
Bulk variant + restore wizard + hard-delete-with-email-code land in
follow-on commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:19:34 +02:00
|
|
|
) : null}
|
|
|
|
|
Archive
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|