'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Loader2, XCircle } 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 { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface Signer { id: string; signerName: string; signerEmail: string; signerRole: string; status: string; } interface EoiCancelDialogProps { documentId: string; signers: Signer[]; open: boolean; onOpenChange: (open: boolean) => void; } /** * Cancel-with-notify modal. Two variants by signedCount: * - 0 signed: simple confirm with optional reason. Cancel button. * - 1+ signed: list each signer with a checkbox so the rep picks * who to email. Pre-checks the signers who have signed (they're * the most-affected) — rep can opt out. * * In both cases the reason textarea is optional and (when present) * gets inlined into the cancellation email body + the audit log. * * On confirm: POST /api/v1/documents/[id]/cancel with * { reason, notifyRecipients: [signerId, ...] } * The server voids the envelope, marks status=cancelled, sends the * branded cancellation email to each picked recipient. */ export function EoiCancelDialog({ documentId, signers, open, onOpenChange }: EoiCancelDialogProps) { const queryClient = useQueryClient(); const [reason, setReason] = useState(''); const [notifyIds, setNotifyIds] = useState>(() => { // Default: pre-check the signers who have signed — they're the // recipients most likely to want to know. Pending signers can be // notified too but the rep needs to opt them in. return new Set(signers.filter((s) => s.status === 'signed').map((s) => s.id)); }); const signedCount = useMemo(() => signers.filter((s) => s.status === 'signed').length, [signers]); const cancelMutation = useMutation({ mutationFn: () => apiFetch(`/api/v1/documents/${documentId}/cancel`, { method: 'POST', body: { reason: reason.trim() || null, notifyRecipients: Array.from(notifyIds), }, }), onSuccess: () => { queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'documents' }); toast.success( notifyIds.size > 0 ? `EOI cancelled. ${notifyIds.size} signer${notifyIds.size === 1 ? '' : 's'} notified.` : 'EOI cancelled.', ); onOpenChange(false); // Reset internal state so a second open of the dialog starts clean. setReason(''); setNotifyIds(new Set()); }, onError: (err) => toastError(err), }); const toggle = (id: string) => { setNotifyIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; return ( Cancel this EOI? {signedCount === 0 ? 'No signatures have been collected yet. The signing service will be told to void this envelope.' : `${signedCount} signer${signedCount === 1 ? ' has' : 's have'} already signed. The envelope will be voided and pick the signers you want to notify by email below.`} {signedCount > 0 && (

Notify

    {signers.map((s) => (
  • toggle(s.id)} />
  • ))}

Leave all unchecked to cancel silently - no emails will be sent.

)}