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:
162
src/components/documents/signing-details-dialog.tsx
Normal file
162
src/components/documents/signing-details-dialog.tsx
Normal 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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user