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) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 12:29:25 +02:00
parent 03738bfa9a
commit 2129fbdf15

View File

@@ -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<SigningDetailsResponse>({
queryKey: ['document-signing-details', documentId],
queryFn: () =>
apiFetch<SigningDetailsResponse>(`/api/v1/documents/${documentId}/signing-details`),
enabled: Boolean(documentId) && open,
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Signing details</DialogTitle>
<DialogDescription>
Audit trail for this signed document signers and timeline.
</DialogDescription>
</DialogHeader>
{isLoading || !data ? (
<div className="flex items-center justify-center py-12 text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading
</div>
) : (
<div className="space-y-4">
<section>
<h4 className="mb-1 text-sm font-semibold">{data.data.workflow.title}</h4>
<p className="flex items-center gap-2 text-xs text-muted-foreground">
<span>Status:</span>
<StatusPill status={mapWorkflowStatus(data.data.workflow.status)}>
{data.data.workflow.status}
</StatusPill>
<span>·</span>
<span>
Created {new Date(data.data.workflow.createdAt).toLocaleString('en-GB')}
</span>
</p>
</section>
<section>
<h5 className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Signers
</h5>
<ul className="divide-y rounded border bg-muted/30">
{data.data.signers.map((s) => (
<li
key={s.id}
className="flex items-center justify-between gap-2 px-3 py-2 text-xs"
>
<div className="min-w-0">
<span className="font-medium">{s.signerName}</span>
<span className="ml-2 text-muted-foreground">{s.signerEmail}</span>
</div>
<div className="flex items-center gap-2">
{s.signedAt ? (
<span className="tabular-nums text-muted-foreground">
{new Date(s.signedAt).toLocaleDateString('en-GB')}
</span>
) : null}
<StatusPill status={mapSignerStatus(s.status)}>{s.status}</StatusPill>
</div>
</li>
))}
</ul>
</section>
<section>
<h5 className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Timeline
</h5>
<ol className="space-y-1 text-xs">
{data.data.events.map((e) => (
<li key={e.id} className="flex items-center gap-2 text-muted-foreground">
<span className="tabular-nums">
{new Date(e.createdAt).toLocaleString('en-GB')}
</span>
<span>{e.eventType.replace(/_/g, ' ')}</span>
</li>
))}
</ol>
</section>
</div>
)}
</DialogContent>
</Dialog>
);
}
function mapSignerStatus(status: string): StatusPillStatus {
const known: Record<string, StatusPillStatus> = {
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<string, StatusPillStatus> = {
pending: 'pending',
draft: 'draft',
sent: 'sent',
partial: 'partial',
completed: 'completed',
signed: 'signed',
expired: 'expired',
cancelled: 'cancelled',
declined: 'declined',
rejected: 'rejected',
};
return known[status] ?? 'pending';
}