diff --git a/src/components/clients/bulk-hard-delete-dialog.tsx b/src/components/clients/bulk-hard-delete-dialog.tsx index 611fb67c..54e23f9a 100644 --- a/src/components/clients/bulk-hard-delete-dialog.tsx +++ b/src/components/clients/bulk-hard-delete-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Loader2, Mail } from 'lucide-react'; import { toast } from 'sonner'; @@ -32,7 +32,21 @@ interface SkippedRow { reason: string; } -export function BulkHardDeleteDialog({ open, onOpenChange, clientIds, onDeleted }: Props) { +/** + * 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(''); @@ -43,17 +57,6 @@ export function BulkHardDeleteDialog({ open, onOpenChange, clientIds, onDeleted const expectedPhrase = `DELETE ${clientIds.length} CLIENT${clientIds.length === 1 ? '' : 'S'}`; - useEffect(() => { - if (open) { - setStage('intent'); - setCode(''); - setTypedPhrase(''); - setMaskedEmail(null); - setSkipped([]); - setPartialDeleted(0); - } - }, [open]); - const requestCode = useMutation({ mutationFn: () => apiFetch<{ data: { count: number; sentToMaskedEmail: string } }>( @@ -105,147 +108,145 @@ export function BulkHardDeleteDialog({ open, onOpenChange, clientIds, onDeleted 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. - - + <> + + + + 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 reservations. Signed documents, email threads, - files and reminders are detached but kept. + {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 reservations. Signed documents, email threads, + files and reminders are detached but kept. +
+
+ )} + + {stage === 'confirm' && ( +
+
+ +
+ Code sent to {maskedEmail}. Enter both fields + below.
- )} - - {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" - /> -
+
+ + 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. -
-
- - - - - + {stage === 'partial' && ( +
+
+ {partialDeleted} of {clientIds.length} permanently deleted. {skipped.length} skipped — + see below. +
+
+
Client IDReason
+ + + + + + + + {skipped.map((row) => ( + + + - - - {skipped.map((row) => ( - - - - - ))} - -
Client IDReason
+ {row.clientId.slice(0, 8)}… + {row.reason}
- {row.clientId.slice(0, 8)}… - {row.reason}
-
+ ))} + +
- )} +
+ )} - - {stage !== 'partial' && ( - - )} - {stage === 'intent' && ( - - )} - {stage === 'confirm' && ( - - )} - {stage === 'partial' && ( - - )} - - -
+ + {stage !== 'partial' && ( + + )} + {stage === 'intent' && ( + + )} + {stage === 'confirm' && ( + + )} + {stage === 'partial' && ( + + )} + + ); } diff --git a/src/components/clients/hard-delete-dialog.tsx b/src/components/clients/hard-delete-dialog.tsx index 5c219a28..77ee20c1 100644 --- a/src/components/clients/hard-delete-dialog.tsx +++ b/src/components/clients/hard-delete-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Loader2, Mail } from 'lucide-react'; import { toast } from 'sonner'; @@ -29,22 +29,30 @@ interface Props { type Stage = 'intent' | 'confirm'; -export function HardDeleteDialog({ open, onOpenChange, clientId, clientName, onDeleted }: Props) { +/** + * 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); - useEffect(() => { - if (open) { - setStage('intent'); - setCode(''); - setTypedName(''); - setMaskedEmail(null); - } - }, [open]); - const requestCode = useMutation({ mutationFn: () => apiFetch<{ data: { sentToMaskedEmail: string } }>( @@ -82,127 +90,125 @@ export function HardDeleteDialog({ open, onOpenChange, clientId, clientName, onD 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. - - + <> + + + + 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. + {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. +

+
+

+ What gets deleted

-
-

- What gets deleted -

-
    -
  • 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
  • -
-
+
    +
  • 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. -
- +
+ ) : ( +
+
+ +
+
+ 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" - /> +
- )} +
+ + 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' ? ( + - {stage === 'intent' ? ( - - ) : ( - - )} - - -
+ ) : ( + + )} + + ); }