'use client'; import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { FileSignature } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DocumentList } from '@/components/documents/document-list'; import { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog'; import { FileGrid, type FileRow } from '@/components/files/file-grid'; import { FileUploadZone } from '@/components/files/file-upload-zone'; import { FilePreviewDialog } from '@/components/files/file-preview-dialog'; import { PermissionGate } from '@/components/shared/permission-gate'; import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { apiFetch } from '@/lib/api/client'; interface InterestDocumentsTabProps { interestId: string; } interface InterestData { id: string; clientId?: string | null; } /** * Documents tab — legal instruments (EOI / contract / reservation) with * full signing status, plus an Attachments section for any other file the * rep wants on the deal. Replaces the standalone Files tab — at the * interest level virtually everything is either a legal doc or rare * one-off, and a separate tab was dead weight 95% of the time. */ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps) { const queryClient = useQueryClient(); const [eoiDialogOpen, setEoiDialogOpen] = useState(false); const [previewFile, setPreviewFile] = useState(null); const { data: interest } = useQuery({ queryKey: ['interests', interestId], queryFn: () => apiFetch<{ data: InterestData }>(`/api/v1/interests/${interestId}`).then((r) => r.data), }); // Files attach at the client level (the schema has no interest_id // FK on `files`). For an interest, surface every file that belongs // to its parent client — covers the realistic case where a rep // uploaded a passport / scan / photo while working a deal. // Until the interest record loads we pass a sentinel clientId so the // server returns empty rather than the unscoped port-wide file list. const clientId = interest?.clientId ?? '__pending__'; const filesQueryKey = ['files', { clientId }] as const; const { data: files, isLoading: filesLoading } = usePaginatedQuery({ queryKey: filesQueryKey, endpoint: `/api/v1/files?clientId=${encodeURIComponent(clientId)}`, filterDefinitions: [], }); useRealtimeInvalidation({ 'file:uploaded': [filesQueryKey], 'file:updated': [filesQueryKey], 'file:deleted': [filesQueryKey], }); const handleDownload = async (file: FileRow) => { try { const res = await apiFetch<{ data: { url: string; filename: string } }>( `/api/v1/files/${file.id}/download`, ); const a = document.createElement('a'); a.href = res.data.url; a.download = res.data.filename; a.click(); } catch { // silent } }; const handleDelete = async (file: FileRow) => { if (!confirm(`Delete "${file.filename}"? This cannot be undone.`)) return; try { await apiFetch(`/api/v1/files/${file.id}`, { method: 'DELETE' }); queryClient.invalidateQueries({ queryKey: filesQueryKey }); } catch { // silent } }; const hasAttachments = files.length > 0; return (

Legal documents

No documents yet

Generate the EOI to send it for signing in one click.

} />

Attachments

{hasAttachments ? ( {files.length} file{files.length === 1 ? '' : 's'} ) : null}
{interest?.clientId ? ( { queryClient.invalidateQueries({ queryKey: filesQueryKey }); }} /> ) : null} {hasAttachments ? ( {}} onDelete={handleDelete} isLoading={filesLoading} /> ) : null}
!open && setPreviewFile(null)} fileId={previewFile?.id} fileName={previewFile?.filename} mimeType={previewFile?.mimeType ?? undefined} /> ); }