'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 { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog'; import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog'; import { SigningProgress } from '@/components/documents/signing-progress'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/ui-store'; interface InterestEoiTabProps { interestId: string; /** Used by the generate dialog to deep-link to the client's record. */ clientId: string | null; } interface DocumentRow { id: string; documentType: string; title: string; status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; 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: Record = { draft: 'Draft', sent: 'Awaiting signatures', partially_signed: 'Partially signed', completed: 'Signed', expired: 'Expired', cancelled: 'Cancelled', }; 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 = new Set(['draft', 'sent', 'partially_signed']); /** * Dedicated EOI workspace tab. The user's "where do I generate / track * the EOI for this deal" surface, separate from the generic Documents * tab (which is the long-tail history of every document the interest * has accumulated, including signed past EOIs). * * Layout: * - In-flight EOI hero (signing progress + reminders) when an active * EOI document exists for the interest * - "Generate EOI" CTA when none is in flight * - History strip of past completed/cancelled EOIs * * The actual generate flow opens `EoiGenerateDialog` which now shows * the resolved EoiContext (real values that will be filled) rather * than just a checklist of which fields exist. */ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) { const portSlug = useUIStore((s) => s.currentPortSlug); const [generateOpen, setGenerateOpen] = useState(false); const [uploadSignedOpen, setUploadSignedOpen] = useState(false); const { data: docsRes, isLoading: docsLoading } = useQuery<{ data: DocumentRow[] }>({ queryKey: ['documents', { interestId, documentType: 'eoi' }], queryFn: () => apiFetch<{ data: DocumentRow[] }>( `/api/v1/documents?interestId=${interestId}&documentType=eoi`, ), }); 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)} /> ) : ( setGenerateOpen(true)} onUploadSigned={() => setUploadSignedOpen(true)} /> )} {/* History strip — completed + cancelled EOIs from earlier in the deal's life. Quiet and skimmable; the active document above carries the day-to-day attention. */} {completedDocs.length > 0 && (

EOI history

{completedDocs.length} {completedDocs.length === 1 ? 'document' : 'documents'}
    {completedDocs.map((d) => (
  • {d.title} {new Date(d.createdAt).toLocaleDateString()} {portSlug && ( Open )}
  • ))}
)}
); } // ─── In-flight EOI hero ────────────────────────────────────────────────────── function ActiveEoiCard({ doc, portSlug, onUploadSigned, }: { doc: DocumentRow; portSlug: string | null; onUploadSigned: () => void; }) { const queryClient = useQueryClient(); 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('EOI 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 ? (

Documenso hasn't reported signers yet — check back in a moment.

) : ( )}

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

); } // ─── Empty state ───────────────────────────────────────────────────────────── function EmptyEoiState({ onGenerate, onUploadSigned, }: { onGenerate: () => void; onUploadSigned: () => void; }) { return (

No EOI in flight for this interest

Generate the EOI to send it for signing — Documenso handles the signing chain. You can also upload a paper-signed copy if it was signed outside the system.

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