'use client'; import { useMemo, useState } from 'react'; import Link from 'next/link'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, CheckCircle2, ExternalLink, FileSignature, Loader2, RefreshCw, Upload, XCircle, } from 'lucide-react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog'; import { SigningProgress } from '@/components/documents/signing-progress'; import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useConfirmation } from '@/hooks/use-confirmation'; import { DOCUMENT_STATUS_ACTIVE, DOCUMENT_STATUS_LABELS, type DocumentStatus, } from '@/lib/labels/document-status'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/ui-store'; interface InterestContractTabProps { interestId: string; clientId: string | null; } interface DocumentRow { id: string; documentType: string; title: string; status: DocumentStatus; createdAt: string; signers?: Array<{ status: string }>; } interface DocumentSigner { id: string; signerName: string; signerEmail: string; signerRole: string; signingOrder: number; status: string; signedAt?: string | null; } const STATUS_LABELS = DOCUMENT_STATUS_LABELS; const STATUS_TONES: Record = { draft: 'bg-slate-100 text-slate-700', sent: 'bg-blue-100 text-blue-700', partially_signed: 'bg-amber-100 text-amber-800', completed: 'bg-emerald-100 text-emerald-700', expired: 'bg-rose-100 text-rose-700', cancelled: 'bg-slate-200 text-slate-600', }; const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE; /** * Dedicated Contract workspace tab. Mirrors the EOI tab pattern but * for sales contracts. Contracts differ from EOIs in that there's no * standard Documenso template — each contract is drafted custom per * deal. So the active flows are: * * 1. **Upload paper-signed copy** — the signed contract was handled * outside the system; rep uploads the PDF for the record. * * 2. **Upload draft for Documenso signing** — rep uploads the PDF * draft, configures signers + signing order + signature field * placement, then sends via Documenso. (Recipient configurator * and field-placement UI are the bigger pieces; for v1 a default * footer-anchored signature layout is used.) * * The Documents tab still shows every contract document (signed or * drafted) as a permanent history. */ export function InterestContractTab({ interestId, clientId: _clientId }: InterestContractTabProps) { const portSlug = useUIStore((s) => s.currentPortSlug); const [uploadSignedOpen, setUploadSignedOpen] = useState(false); const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false); const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({ queryKey: ['documents', { interestId, documentType: 'contract' }], queryFn: () => apiFetch<{ data: DocumentRow[] }>( `/api/v1/documents?interestId=${interestId}&documentType=contract`, ), }); const docs = docsRes?.data ?? []; const activeDoc = useMemo(() => docs.find((d) => ACTIVE_STATUSES.has(d.status)) ?? null, [docs]); const completedDocs = useMemo(() => docs.filter((d) => !ACTIVE_STATUSES.has(d.status)), [docs]); return (
{docsLoading ? ( ) : activeDoc ? ( setUploadSignedOpen(true)} /> ) : ( setUploadSignedOpen(true)} onUploadForSigning={() => setUploadForSigningOpen(true)} /> )} {completedDocs.length > 0 && (

Contract history

{completedDocs.length} {completedDocs.length === 1 ? 'document' : 'documents'}
    {completedDocs.map((d) => (
  • {d.title} {new Date(d.createdAt).toLocaleDateString()} {portSlug && ( Open )}
  • ))}
)} {/* Reuses the external-EOI upload dialog. The endpoint `/api/v1/interests/{id}/external-eoi` is EOI-specific today — for contract paper-uploads we'll need the equivalent contract endpoint (deferred to a follow-up; the dialog UI is the pattern we'll clone). For now the flow is documented as 'coming soon' rather than misrouting through EOI. */} {uploadSignedOpen && ( )} {/* Phase 4 — upload-for-Documenso-signing dialog. Multi-step (file → recipients → fields → send) backed by the Phase 3 service. Auto-detect runs after the file lands; rep can tweak placements before sending. */} {uploadForSigningOpen && ( )}
); } // ─── Active contract hero ──────────────────────────────────────────────────── function ActiveContractCard({ doc, portSlug, onUploadSigned, }: { doc: DocumentRow; portSlug: string | null; onUploadSigned: () => void; }) { const queryClient = useQueryClient(); const { confirm, dialog: confirmDialog } = useConfirmation(); const { data: signersRes, isLoading: signersLoading } = useQuery<{ data: DocumentSigner[] }>({ queryKey: ['documents', doc.id, 'signers'], queryFn: () => apiFetch<{ data: DocumentSigner[] }>(`/api/v1/documents/${doc.id}/signers`), refetchInterval: 30_000, }); const signers = signersRes?.data ?? []; const signedCount = signers.filter((s) => s.status === 'signed').length; const totalCount = signers.length; const allSigned = totalCount > 0 && signedCount === totalCount; const cancelMutation = useMutation({ mutationFn: () => apiFetch(`/api/v1/documents/${doc.id}/cancel`, { method: 'POST', body: {} }), onSuccess: () => { queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'documents' }); toast.success('Contract cancelled.'); }, onError: (err) => toastError(err), }); const remindAllMutation = useMutation({ mutationFn: () => apiFetch(`/api/v1/documents/${doc.id}/remind`, { method: 'POST', body: {} }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['documents', doc.id, 'signers'] }); toast.success('Reminder sent.'); }, onError: (err) => toastError(err), }); return (

{doc.title}

Created {new Date(doc.createdAt).toLocaleDateString()} ·{' '} {totalCount > 0 ? `${signedCount} of ${totalCount} signed` : 'No signers loaded'}

{portSlug && ( )} {!allSigned && ( )}

Signing progress

{signersLoading ? (
Loading signers…
) : signers.length === 0 ? (

The signing service hasn't reported signers yet — check back in a moment.

) : ( )}

Reminders are rate-limited (max once per 7 days per signer).

{confirmDialog}
); } // ─── Empty state ───────────────────────────────────────────────────────────── function EmptyContractState({ onUploadSigned, onUploadForSigning, }: { onUploadSigned: () => void; onUploadForSigning: () => void; }) { return (

No contract in flight for this interest

Sales contracts are drafted custom per deal. Either upload a paper-signed copy you handled externally, or upload the draft PDF and send for e-signing.

); } // ─── Helpers ───────────────────────────────────────────────────────────────── function StatusBadge({ status }: { status: DocumentRow['status'] }) { return ( {status === 'completed' && } {STATUS_LABELS[status]} ); }