'use client'; import { useMemo, useState } from 'react'; import Link from 'next/link'; import { ChevronDown, ChevronRight, FileText, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { EmptyState } from '@/components/ui/empty-state'; import { PageHeader } from '@/components/shared/page-header'; import { PermissionGate } from '@/components/shared/permission-gate'; import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useDocumentFolders, type FolderNode } from '@/hooks/use-document-folders'; import { FolderActionsMenu } from './folder-actions-menu'; import { FolderBreadcrumb } from './folder-breadcrumb'; import { FolderTreeSidebar } from './folder-tree-sidebar'; import { HubRootView } from './hub-root-view'; import { EntityFolderView } from './entity-folder-view'; interface HubDoc { id: string; documentType: string; title: string; status: string; createdAt: string; signers?: Array<{ id: string; signerEmail: string; signerName: string; status: string }>; } const TYPE_LABELS: Record = { eoi: 'EOI', contract: 'Contract', nda: 'NDA', reservation_agreement: 'Reservation Agreement', welcome_letter: 'Welcome Letter', handover_checklist: 'Handover', acknowledgment: 'Acknowledgment', correspondence: 'Correspondence', other: 'Other', }; const STATUS_PILL_MAP: Record = { draft: 'draft', sent: 'sent', partially_signed: 'partial', completed: 'completed', signed: 'signed', expired: 'expired', cancelled: 'cancelled', rejected: 'rejected', }; const SIGNER_STATUS_LABELS: Record = { pending: 'Pending', sent: 'Sent', signed: 'Signed', declined: 'Declined', expired: 'Expired', cancelled: 'Cancelled', }; interface DocumentsHubProps { portSlug: string; } function findInTree(nodes: FolderNode[], id: string): FolderNode | null { for (const n of nodes) { if (n.id === id) return n; const found = findInTree(n.children, id); if (found) return found; } return null; } export function DocumentsHub({ portSlug }: DocumentsHubProps) { // undefined = "All documents" (no folder selected / hub root) // null = root folder only // string = specific folder id const [selectedFolderId, setSelectedFolderId] = useState(undefined); const { data: tree = [] } = useDocumentFolders(); const selectedFolder = typeof selectedFolderId === 'string' ? findInTree(tree, selectedFolderId) : null; const isEntityFolder = selectedFolder?.systemManaged === true && selectedFolder.entityType != null && selectedFolder.entityType !== 'root' && selectedFolder.entityId != null; const handleFolderSelect = (id: string | null | undefined) => { setSelectedFolderId(id); }; return (
handleFolderSelect(undefined)} /> } />
New document } variant="gradient" /> {selectedFolderId === undefined ? ( ) : isEntityFolder ? ( ) : ( )}
); } // --------------------------------------------------------------------------- // FlatFolderListing — the original search + type-chip + document rows panel, // now scoped to a specific folder (or null for root-only). // --------------------------------------------------------------------------- interface FlatFolderListingProps { portSlug: string; folderId: string | null; } function FlatFolderListing({ portSlug, folderId }: FlatFolderListingProps) { const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState(undefined); const [expandedDocId, setExpandedDocId] = useState(null); const queryParams = useMemo(() => { const params = new URLSearchParams(); if (search) params.set('search', search); if (typeFilter) params.set('documentType', typeFilter); // folderId null = root, string = specific folder params.set('folderId', folderId ?? ''); return params; }, [search, typeFilter, folderId]); const { data: documents, isLoading } = usePaginatedQuery({ queryKey: ['documents', 'hub', 'folder', queryParams.toString()], endpoint: `/api/v1/documents?${queryParams.toString()}`, filterDefinitions: [], }); useRealtimeInvalidation({ 'document:created': [['documents']], 'document:updated': [['documents']], 'document:deleted': [['documents']], 'document:sent': [['documents']], 'document:completed': [['documents']], 'document:expired': [['documents']], 'document:cancelled': [['documents']], 'document:rejected': [['documents']], 'document:signer:signed': [['documents']], }); const renderRow = (doc: HubDoc) => { const expanded = expandedDocId === doc.id; const totalSigners = doc.signers?.length ?? 0; const signedCount = doc.signers?.filter((s) => s.status === 'signed').length ?? 0; const pillStatus = STATUS_PILL_MAP[doc.status] ?? 'pending'; const isNonSignature = [ 'welcome_letter', 'handover_checklist', 'acknowledgment', 'correspondence', ].includes(doc.documentType); return (
  • {doc.title} {TYPE_LABELS[doc.documentType] ?? doc.documentType} {isNonSignature && doc.status === 'sent' ? 'Delivered' : doc.status.replace(/_/g, ' ')} {totalSigners > 0 ? `${signedCount}/${totalSigners} signed` : '-'} {new Date(doc.createdAt).toLocaleDateString('en-GB')}
    {expanded && doc.signers && doc.signers.length > 0 ? (
      {doc.signers.map((signer) => (
    • {signer.signerName} {signer.signerEmail}
      {SIGNER_STATUS_LABELS[signer.status] ?? signer.status}
    • ))}
    ) : null}
  • ); }; return ( <>
    setSearch(e.target.value)} className="max-w-xs h-9" /> {(() => { const seenTypes = Array.from(new Set(documents.map((d) => d.documentType))).sort(); if (seenTypes.length === 0) return null; return (
    {seenTypes.map((t) => ( ))}
    ); })()}
    {isLoading ? (
      {[0, 1, 2, 3, 4].map((i) => (
    • ))}
    ) : documents.length === 0 ? ( } title="No documents in this folder" body="Create a document or move existing ones here." actions={ } /> ) : (
      {documents.map(renderRow)}
    )} ); }