'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, ArrowLeft, ArrowRight, CheckCircle2, Loader2 } 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 { Textarea } from '@/components/ui/textarea'; import { WarningCallout } from '@/components/ui/warning-callout'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface PreflightItem { clientId: string; fullName: string; stakeLevel: 'low' | 'high'; highStakesStage: string | null; blockers: string[]; summary: { berths: number; yachts: number; reservations: number; signedDocs: number }; } interface Props { open: boolean; onOpenChange: (next: boolean) => void; clientIds: string[]; onSuccess?: () => void; } type Stage = 'preflight' | 'reasons' | 'confirm'; export function BulkArchiveWizard(props: Props) { // Key-based remount: body keyed on open + clientIds so its useState // initializers re-run each time the wizard opens fresh. Replaces the // useEffect(setState, [open]) reset the Compiler flagged. return ( ); } function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Props) { const qc = useQueryClient(); const [stage, setStage] = useState('preflight'); const [reasons, setReasons] = useState>({}); const [carouselIndex, setCarouselIndex] = useState(0); const preflight = useQuery({ queryKey: ['bulk-archive-preflight', clientIds.join(',')], queryFn: () => apiFetch<{ data: PreflightItem[] }>('/api/v1/clients/bulk-archive-preflight', { method: 'POST', body: { ids: clientIds }, }).then((r) => r.data), enabled: open && clientIds.length > 0, }); const items = preflight.data ?? []; const blocked = useMemo(() => items.filter((i) => i.blockers.length > 0), [items]); const highStakes = useMemo( () => items.filter((i) => i.stakeLevel === 'high' && i.blockers.length === 0), [items], ); const lowStakes = useMemo( () => items.filter((i) => i.stakeLevel === 'low' && i.blockers.length === 0), [items], ); const archivable = useMemo(() => [...lowStakes, ...highStakes], [lowStakes, highStakes]); const allHighStakesReasoned = useMemo( () => highStakes.every((i) => (reasons[i.clientId]?.trim().length ?? 0) >= 5), [highStakes, reasons], ); const archiveMutation = useMutation({ mutationFn: () => apiFetch<{ data: { summary: { total: number; succeeded: number; failed: number } } }>( '/api/v1/clients/bulk', { method: 'POST', body: { action: 'archive', ids: archivable.map((i) => i.clientId), reasonsByClientId: reasons, }, }, ), onSuccess: (res) => { const s = res.data.summary; if (s.failed === 0) { toast.success(`${s.succeeded} client${s.succeeded === 1 ? '' : 's'} archived.`); } else { toast.warning(`${s.succeeded} of ${s.total} archived. ${s.failed} failed.`); } qc.invalidateQueries({ queryKey: ['clients'] }); onOpenChange(false); onSuccess?.(); }, onError: (err: unknown) => { toastError(err, 'Bulk archive failed'); }, }); const currentHighStakes = highStakes[carouselIndex]; return ( Bulk archive · {clientIds.length} clients Smart archive runs the same backend per client. Late-stage deals require an individual reason; everything else auto-archives with safe defaults. {preflight.isLoading ? (
Checking each client…
) : preflight.error ? (
Preflight failed:{' '} {preflight.error instanceof Error ? preflight.error.message : 'unknown error'}
) : ( <> {stage === 'preflight' && (
{lowStakes.length}
Auto-archive
{highStakes.length}
Need reason
{blocked.length}
Blocked, will skip
{blocked.length > 0 && (
Blocked
{blocked.slice(0, 5).map((b) => (
{b.fullName}: {b.blockers[0]}
))} {blocked.length > 5 &&
…and {blocked.length - 5} more
}
)}
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel reservations, leave invoices/signing requests alone. Yachts stay on the archived client. To customise per-client, archive that client individually instead.
)} {stage === 'reasons' && currentHighStakes && (
Reason {carouselIndex + 1} of {highStakes.length} {highStakes.map((_, idx) => ( = 5 ? 'bg-amber-300' : 'bg-muted' }`} /> ))}
{currentHighStakes.fullName} {currentHighStakes.highStakesStage} } > {currentHighStakes.summary.berths > 0 ? `${currentHighStakes.summary.berths} berth(s), ` : ''} {currentHighStakes.summary.signedDocs > 0 ? `${currentHighStakes.summary.signedDocs} signed doc(s), ` : ''} {currentHighStakes.summary.reservations > 0 ? `${currentHighStakes.summary.reservations} reservation(s)` : ''}