'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 { WarningCallout } from '@/components/ui/warning-callout'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface Props { open: boolean; onOpenChange: (open: boolean) => void; clientId: string; clientName: string; /** Called after successful delete, e.g. to navigate away. */ onDeleted?: () => void; } type Stage = 'intent' | 'confirm'; /** * Outer wrapper keeps the Dialog mounted (so its close animation runs); * the body only mounts when `open` is true and remounts on each * open via the `clientId` key. This avoids the open→reset-state * useEffect that React Compiler flags — fresh state per open is just * the natural mount. */ export function HardDeleteDialog(props: Props) { return ( {props.open && } ); } function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }: Props) { const qc = useQueryClient(); const [stage, setStage] = useState('intent'); const [code, setCode] = useState(''); const [typedName, setTypedName] = useState(''); const [maskedEmail, setMaskedEmail] = useState(null); const requestCode = useMutation({ mutationFn: () => apiFetch<{ data: { sentToMaskedEmail: string } }>( `/api/v1/clients/${clientId}/hard-delete-request`, { method: 'POST' }, ), 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 hardDelete = useMutation({ mutationFn: () => apiFetch<{ data: { deletedClientId: string } }>(`/api/v1/clients/${clientId}/hard-delete`, { method: 'POST', body: { code, typedName }, }), onSuccess: () => { toast.success(`${clientName} permanently deleted.`); qc.invalidateQueries({ queryKey: ['clients'] }); onOpenChange(false); onDeleted?.(); }, onError: (err: unknown) => { toastError(err, 'Delete failed'); }, }); const nameMatches = typedName.trim().toLowerCase() === clientName.trim().toLowerCase(); const codeValid = /^\d{4}$/.test(code.trim()); return ( <> Permanently delete {clientName} This permanently removes the client record and detaches all related history (signed documents, emails, files). It cannot be undone. {stage === 'intent' ? (

Permanent deletion is reserved for archived clients only. We’ll email a 4-digit confirmation code to your account address. The code expires in 10 minutes.

  • Client record + addresses, contacts, notes, tags
  • Portal user account + GDPR consent records
  • All pipeline interests + reservations for this client

What is preserved

  • Signed documents (detached from client, kept for legal history)
  • Email threads, files, reminders (detached)
  • Audit log entries
) : (
Code sent to {maskedEmail}. It expires in 10 minutes.
setCode(e.target.value.replace(/\D/g, ''))} placeholder="0000" className="font-mono tracking-[0.4em] text-center text-lg" autoComplete="off" />
setTypedName(e.target.value)} placeholder={clientName} autoComplete="off" />
)} {stage === 'intent' ? ( ) : ( )} ); }