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