'use client'; import { useState } from 'react'; import Link from 'next/link'; import type { Route } from 'next'; import { useRouter } from 'next/navigation'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { ArrowLeft, Bell, Download, Mail, Trash2, X } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { PageHeader } from '@/components/shared/page-header'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface DetailDoc { id: string; title: string; status: string; documentType: string; documensoId: string | null; signedFileId: string | null; reservationId: string | null; interestId: string | null; clientId: string | null; yachtId: string | null; companyId: string | null; createdAt: string; createdBy: string; } interface DetailSigner { id: string; signerName: string; signerEmail: string; signerRole: string; signingOrder: number; status: string; signedAt: string | null; signingUrl: string | null; } interface DetailEvent { id: string; eventType: string; createdAt: string; signerId: string | null; eventData: Record | null; } interface DetailWatcher { userId: string; addedBy: string; addedAt: string; } interface DetailResponse { data: { document: DetailDoc; signers: DetailSigner[]; events: DetailEvent[]; watchers: DetailWatcher[]; }; } const STATUS_PILL_MAP: Record = { draft: 'draft', sent: 'sent', partially_signed: 'partial', completed: 'completed', signed: 'signed', expired: 'expired', cancelled: 'cancelled', rejected: 'rejected', pending: 'pending', declined: 'declined', }; const SIGNER_PILL_MAP: Record = { pending: 'pending', signed: 'signed', declined: 'declined', }; interface DocumentDetailProps { documentId: string; portSlug: string; } export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) { const router = useRouter(); const queryClient = useQueryClient(); const [isCancelling, setIsCancelling] = useState(false); const { data, isLoading, error } = useQuery({ queryKey: ['document-detail', documentId], queryFn: () => apiFetch(`/api/v1/documents/${documentId}?detail=true`), }); useRealtimeInvalidation({ 'document:updated': [['document-detail', documentId]], 'document:sent': [['document-detail', documentId]], 'document:completed': [['document-detail', documentId]], 'document:cancelled': [['document-detail', documentId]], 'document:rejected': [['document-detail', documentId]], 'document:expired': [['document-detail', documentId]], 'document:signer:signed': [['document-detail', documentId]], 'document:signer:opened': [['document-detail', documentId]], }); if (isLoading) { return (
); } if (error || !data) { return (
Back to documents } />
); } const { document: doc, signers, events, watchers } = data.data; const handleRemind = async (signerId: string) => { try { await apiFetch(`/api/v1/documents/${documentId}/remind`, { method: 'POST', body: { signerId }, }); toast.success('Reminder sent'); queryClient.invalidateQueries({ queryKey: ['document-detail', documentId] }); } catch (err) { toastError(err); } }; const handleCancel = async () => { if (!confirm('Cancel this document? This voids the signing envelope and cannot be undone.')) return; setIsCancelling(true); try { await apiFetch(`/api/v1/documents/${documentId}/cancel`, { method: 'POST' }); toast.success('Document cancelled'); router.push(`/${portSlug}/documents`); } catch (err) { toastError(err); setIsCancelling(false); } }; const handleEmailSignedPdf = async () => { try { const draft = await apiFetch<{ data: { to: string[]; subject: string; attachments: Array<{ fileId: string }> }; }>(`/api/v1/documents/${documentId}/compose-completion-email`, { method: 'POST' }); toast.info( `Email composer prepared for ${draft.data.to.length} signer${draft.data.to.length === 1 ? '' : 's'} - opens in PR8 wizard`, ); } catch (err) { toastError(err); } }; const isInFlight = ['sent', 'partially_signed'].includes(doc.status); const isComplete = ['completed', 'signed'].includes(doc.status); const subjectLink = doc.reservationId ? { href: `/${portSlug}/berth-reservations/${doc.reservationId}`, label: 'Reservation' } : doc.interestId ? { href: `/${portSlug}/interests/${doc.interestId}`, label: 'Interest' } : doc.clientId ? { href: `/${portSlug}/clients/${doc.clientId}`, label: 'Client' } : doc.yachtId ? { href: `/${portSlug}/yachts/${doc.yachtId}`, label: 'Yacht' } : doc.companyId ? { href: `/${portSlug}/companies/${doc.companyId}`, label: 'Company' } : null; return (
{doc.status.replace(/_/g, ' ')} {signers.filter((s) => s.status === 'signed').length}/{signers.length} signed {watchers.length > 0 ? {watchers.length} watching : null} } actions={
{isComplete && doc.signedFileId ? ( <> ) : null} {isInFlight ? ( ) : null}
} variant="gradient" />
{/* Left column */}

Signers

{signers.length === 0 ? (

No signers attached.

) : (
    {signers.map((signer, idx) => (
  • {idx + 1}
    {signer.signerName}
    {signer.status}
    {signer.signerEmail} · {signer.signerRole}
    {signer.signedAt ? `Signed ${new Date(signer.signedAt).toLocaleDateString('en-GB')}` : 'Pending'}
    {signer.status === 'pending' && doc.documensoId && isInFlight ? (
    {signer.signingUrl ? ( ) : null}
    ) : null}
  • ))}
)}
{subjectLink ? (

Linked entity

{subjectLink.label} →
) : null}
{/* Right column */}

Watchers

{watchers.length === 0 ? (

No one is watching this document yet.

) : (
    {watchers.map((w) => (
  • {w.userId.slice(0, 8)}…
  • ))}
)}

Activity

{events.length === 0 ? (

No events yet.

) : (
    {events.slice(0, 12).map((e) => (
  • {e.eventType.replace(/_/g, ' ')}
    {new Date(e.createdAt).toLocaleString('en-GB')}
  • ))}
)}
); }