feat(clients): hard-delete with email-code confirmation (single + bulk)
Permanent client deletion is now reachable from: - archived single-client detail page (icon button, gated by new admin.permanently_delete_clients perm) - archived clients list bulk action Both flows are 2-stage: request a 4-digit code (sent to operator's account email, 10min Redis TTL), then enter both code AND a typed confirmation (client name single, "DELETE N CLIENTS" bulk). Cascade strategy preserves audit trails: signed documents, email threads, files and reminders are detached but retained; addresses, contacts, notes, portal user, GDPR records, interests and reservations are deleted via FK cascade or explicit tx delete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Plus, Archive, Tag as TagIcon, TagsIcon } from 'lucide-react';
|
||||
import { Plus, Archive, Tag as TagIcon, TagsIcon, Trash2 } from 'lucide-react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -15,6 +15,8 @@ import { TableSkeleton } from '@/components/shared/loading-skeleton';
|
||||
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { TagPicker } from '@/components/shared/tag-picker';
|
||||
import { BulkHardDeleteDialog } from '@/components/clients/bulk-hard-delete-dialog';
|
||||
import { usePermissions } from '@/hooks/use-permissions';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -43,6 +45,10 @@ export function ClientList() {
|
||||
null,
|
||||
);
|
||||
const [tagChoice, setTagChoice] = useState<string[]>([]);
|
||||
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[]>([]);
|
||||
|
||||
const { can } = usePermissions();
|
||||
const canHardDelete = can('admin', 'permanently_delete_clients');
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -187,6 +193,19 @@ export function ClientList() {
|
||||
bulkMutation.mutate({ action: 'archive', ids });
|
||||
},
|
||||
},
|
||||
...(canHardDelete
|
||||
? [
|
||||
{
|
||||
label: 'Permanently delete (archived only)',
|
||||
icon: Trash2,
|
||||
variant: 'destructive' as const,
|
||||
onClick: (ids: string[]) => {
|
||||
if (ids.length === 0) return;
|
||||
setBulkDeleteIds(ids);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
cardRender={(row) => (
|
||||
<ClientCard
|
||||
@@ -272,6 +291,13 @@ export function ClientList() {
|
||||
onConfirm={() => archiveClient && archiveMutation.mutate(archiveClient.id)}
|
||||
isLoading={archiveMutation.isPending}
|
||||
/>
|
||||
|
||||
<BulkHardDeleteDialog
|
||||
open={bulkDeleteIds.length > 0}
|
||||
onOpenChange={(open) => !open && setBulkDeleteIds([])}
|
||||
clientIds={bulkDeleteIds}
|
||||
onDeleted={() => setBulkDeleteIds([])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user