'use client'; import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Loader2, Mail } 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 { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface Props { open: boolean; onOpenChange: (open: boolean) => void; clientIds: string[]; onDeleted?: (deletedCount: number) => void; } type Stage = 'intent' | 'confirm' | 'partial'; interface SkippedRow { clientId: string; reason: string; } /** * Key-based remount of the body when the dialog opens - fresh state per * open without an open→reset useEffect (React Compiler-safe). */ export function BulkHardDeleteDialog(props: Props) { return ( {props.open && } ); } function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props) { const qc = useQueryClient(); const [stage, setStage] = useState('intent'); const [code, setCode] = useState(''); const [typedPhrase, setTypedPhrase] = useState(''); const [maskedEmail, setMaskedEmail] = useState(null); const [skipped, setSkipped] = useState([]); const [partialDeleted, setPartialDeleted] = useState(0); const expectedPhrase = `DELETE ${clientIds.length} CLIENT${clientIds.length === 1 ? '' : 'S'}`; const requestCode = useMutation({ mutationFn: () => apiFetch<{ data: { count: number; sentToMaskedEmail: string } }>( '/api/v1/clients/bulk-hard-delete-request', { method: 'POST', body: { ids: clientIds } }, ), onSuccess: (res) => { setMaskedEmail(res.data.sentToMaskedEmail); setStage('confirm'); toast.success(`Code sent to ${res.data.sentToMaskedEmail}`); }, onError: (err: unknown) => { toastError(err, 'Failed to send code'); }, }); const bulkDelete = useMutation({ mutationFn: () => apiFetch<{ data: { deletedCount: number; skipped: SkippedRow[] }; }>('/api/v1/clients/bulk-hard-delete', { method: 'POST', body: { ids: clientIds, code, typedPhrase }, }), onSuccess: (res) => { const n = res.data.deletedCount; const skippedRows = res.data.skipped ?? []; qc.invalidateQueries({ queryKey: ['clients'] }); if (skippedRows.length === 0) { toast.success(`${n} client${n === 1 ? '' : 's'} permanently deleted.`); onOpenChange(false); onDeleted?.(n); } else { // Stay open so the operator can see exactly which IDs were // skipped and why (e.g. unarchived between preflight + execute, // already deleted by another operator). setSkipped(skippedRows); setPartialDeleted(n); setStage('partial'); toast.warning(`${n} of ${clientIds.length} deleted. ${skippedRows.length} skipped.`); } }, onError: (err: unknown) => { toastError(err, 'Bulk delete failed'); }, }); const phraseMatches = typedPhrase.trim().toUpperCase() === expectedPhrase; const codeValid = /^\d{4}$/.test(code.trim()); return ( <> Permanently delete {clientIds.length} client{clientIds.length === 1 ? '' : 's'} All selected clients must already be archived. This cannot be undone. {stage === 'intent' && (

We’ll email a 4-digit confirmation code to your account address. The code is tied to this exact set of clients and expires in 10 minutes.

For each client we delete: client record + addresses, contacts, notes, tags, portal user, GDPR records, all interests, all tenancies. Signed documents, email threads, files and reminders are detached but kept.
)} {stage === 'confirm' && (
Code sent to {maskedEmail}. Enter both fields below.
setCode(e.target.value.replace(/\D/g, ''))} placeholder="0000" className="font-mono tracking-[0.4em] text-center text-lg" autoComplete="off" />
setTypedPhrase(e.target.value)} placeholder={expectedPhrase} autoComplete="off" className="font-mono" />
)} {stage === 'partial' && (
{partialDeleted} of {clientIds.length} permanently deleted. {skipped.length} skipped - see below.
{skipped.map((row) => ( ))}
Client ID Reason
{row.clientId.slice(0, 8)}… {row.reason}
)} {stage !== 'partial' && ( )} {stage === 'intent' && ( )} {stage === 'confirm' && ( )} {stage === 'partial' && ( )} ); }