'use client'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { Download, FileDown, Loader2, Mail } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { usePermissions } from '@/hooks/use-permissions'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface ExportRow { id: string; status: 'pending' | 'building' | 'ready' | 'sent' | 'failed'; storageKey: string | null; sizeBytes: number | null; createdAt: string; readyAt: string | null; sentAt: string | null; sentTo: string | null; error: string | null; } interface ListResp { data: ExportRow[]; } const STATUS_VARIANT: Record = { pending: 'outline', building: 'outline', ready: 'secondary', sent: 'secondary', failed: 'destructive', }; export function GdprExportButton({ clientId }: { clientId: string }) { const { can, isSuperAdmin } = usePermissions(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const [emailToClient, setEmailToClient] = useState(false); const [emailOverride, setEmailOverride] = useState(''); const allowed = isSuperAdmin || can('admin', 'manage_settings'); const queryKey = ['gdpr-exports', clientId]; const { data, isLoading } = useQuery({ queryKey, queryFn: () => apiFetch(`/api/v1/clients/${clientId}/gdpr-export`), enabled: open && allowed, // Poll only when the user is watching AND a job is in flight. GDPR // exports take ~30s; 15s is the rule-of-thumb minimum that doesn't // burn CPU. When everything's already settled, stop polling. refetchInterval: (q) => { if (!open || !allowed) return false; const rows = q.state.data?.data ?? []; const hasInFlight = rows.some((r) => r.status === 'pending' || r.status === 'building'); return hasInFlight ? 15_000 : false; }, }); const request = useMutation({ mutationFn: () => apiFetch(`/api/v1/clients/${clientId}/gdpr-export`, { method: 'POST', body: { emailToClient, emailOverride: emailOverride.trim() || null, }, }), onSuccess: () => { toast.success('Export queued - refresh in ~30 seconds'); qc.invalidateQueries({ queryKey }); setEmailOverride(''); }, onError: (err: unknown) => { toastError(err); }, }); if (!allowed) return null; async function downloadById(exportId: string) { try { const res = await apiFetch<{ data: { url: string } }>( `/api/v1/clients/${clientId}/gdpr-export/${exportId}`, ); window.open(res.data.url, '_blank', 'noopener'); } catch (err) { toastError(err); } } const rows = data?.data ?? []; return ( Personal data export Bundles every record we hold about this client (profile, contacts, addresses, yachts, companies, interests, reservations, invoices, documents, audit log) into a ZIP with JSON and HTML copies. Used to satisfy GDPR Article 15 access requests.
setEmailToClient(v === true)} />

Sends a 7-day signed download link to the client's primary email - or to the override below.

{emailToClient ? ( setEmailOverride(e.target.value)} className="h-8 text-sm" /> ) : null}

Recent exports

{isLoading ? (

Loading…

) : rows.length === 0 ? (

No exports yet.

) : (
    {rows.map((r) => (
  • {r.status}
    Requested {format(new Date(r.createdAt), 'MMM d, yyyy HH:mm')}
    {r.sentTo ? (
    Sent to {r.sentTo}
    ) : null} {r.error ? (
    {r.error}
    ) : null}
    {(r.status === 'ready' || r.status === 'sent') && r.storageKey ? ( ) : null}
  • ))}
)}
); }