From 2129fbdf15c5ceea222fe54a23c55b909a9c22ee Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 May 2026 12:29:25 +0200 Subject: [PATCH] feat(documents): SigningDetailsDialog Modal rendering workflow + signers + events for a signed-PDF file. Wired to GET /api/v1/documents/[id]/signing-details. The "view signing details" link on signed-file rows in the Files section opens this dialog (wiring in the entity-folder view task). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../documents/signing-details-dialog.tsx | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/components/documents/signing-details-dialog.tsx diff --git a/src/components/documents/signing-details-dialog.tsx b/src/components/documents/signing-details-dialog.tsx new file mode 100644 index 00000000..71ee7019 --- /dev/null +++ b/src/components/documents/signing-details-dialog.tsx @@ -0,0 +1,162 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; +import { Loader2 } from 'lucide-react'; + +import { apiFetch } from '@/lib/api/client'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; + +interface SigningDetailsResponse { + data: { + workflow: { + id: string; + title: string; + status: string; + documentType: string; + createdAt: string; + updatedAt: string; + }; + signers: Array<{ + id: string; + signerName: string; + signerEmail: string; + signerRole: string; + status: string; + signedAt: string | null; + }>; + events: Array<{ + id: string; + eventType: string; + createdAt: string; + }>; + }; +} + +interface Props { + documentId: string | null; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function SigningDetailsDialog({ documentId, open, onOpenChange }: Props) { + const { data, isLoading } = useQuery({ + queryKey: ['document-signing-details', documentId], + queryFn: () => + apiFetch(`/api/v1/documents/${documentId}/signing-details`), + enabled: Boolean(documentId) && open, + }); + + return ( + + + + Signing details + + Audit trail for this signed document — signers and timeline. + + + {isLoading || !data ? ( +
+ + Loading… +
+ ) : ( +
+
+

{data.data.workflow.title}

+

+ Status: + + {data.data.workflow.status} + + · + + Created {new Date(data.data.workflow.createdAt).toLocaleString('en-GB')} + +

+
+ +
+
+ Signers +
+
    + {data.data.signers.map((s) => ( +
  • +
    + {s.signerName} + {s.signerEmail} +
    +
    + {s.signedAt ? ( + + {new Date(s.signedAt).toLocaleDateString('en-GB')} + + ) : null} + {s.status} +
    +
  • + ))} +
+
+ +
+
+ Timeline +
+
    + {data.data.events.map((e) => ( +
  1. + + {new Date(e.createdAt).toLocaleString('en-GB')} + + {e.eventType.replace(/_/g, ' ')} +
  2. + ))} +
+
+
+ )} +
+
+ ); +} + +function mapSignerStatus(status: string): StatusPillStatus { + const known: Record = { + pending: 'pending', + sent: 'sent', + signed: 'signed', + declined: 'declined', + expired: 'expired', + cancelled: 'cancelled', + rejected: 'rejected', + }; + return known[status] ?? 'pending'; +} + +function mapWorkflowStatus(status: string): StatusPillStatus { + const known: Record = { + pending: 'pending', + draft: 'draft', + sent: 'sent', + partial: 'partial', + completed: 'completed', + signed: 'signed', + expired: 'expired', + cancelled: 'cancelled', + declined: 'declined', + rejected: 'rejected', + }; + return known[status] ?? 'pending'; +}