import { useCallback, useEffect, useMemo } from 'react'; import type { MutableRefObject } from 'react'; import { resolveDocumentAssetUrl } from '../asset_manager'; import { useDetailPanel } from '../app/useDetailPanel'; import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey, } from '../app/appLayoutUtils'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; type Identifier = string | number; interface DocumentLike { id?: Identifier; folder_id?: Identifier | 'root'; title?: string; [key: string]: unknown; } interface FolderNode { id: Identifier | 'root'; name?: string; parentId?: Identifier | 'root'; } type DocumentLink = { url?: string; contentType?: string | null; } | null; interface UseDetailWorkspaceArgs { documents: DocumentLike[]; selectionOrder: string[]; selectedDocumentIds: Identifier[]; documentLookup: Map; folderNodes: Map; ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise; detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>; detailFolderFetchRef: MutableRefObject>; documentLinks: Map; previewDocumentId?: Identifier | null; activePreviewId?: Identifier | null; openDocumentPreview?: (args: { documentIds: Identifier[] }) => void; handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise | boolean; handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise | boolean; handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; handleTagRemove?: (...args: unknown[]) => void; ensureAssetUrl?: EnsureAssetUrl; getDocumentAsset?: GetDocumentAsset; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void; resolveApiPath?: (path: string) => string; selectFolder?: (folderId?: Identifier | 'root') => void; tags?: unknown[]; tagLookupById?: Map | null; } interface UseDetailWorkspaceResult { detailPanelProps: DocumentInfoPanelProps; detailPanelOpen: boolean; openDetailPanel: ReturnType['openDetailPanel']; closeDetailPanel: ReturnType['closeDetailPanel']; handleDetailPanelClose: () => void; inspectDocument: (docId: Identifier | null) => void; previewActive: boolean; previewWorkspaceDocument: DocumentLike | null; documentLink: DocumentLink; resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null; resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>; } const useDetailWorkspace = ({ documents, selectionOrder, selectedDocumentIds, documentLookup, folderNodes, ensureFolderData, detailPanelControlRef, detailFolderFetchRef, documentLinks, previewDocumentId, activePreviewId, openDocumentPreview, handleDocumentTitleUpdate, handleDocumentIssuedUpdate, handleDocumentTagAdd, handleTagRemove, ensureAssetUrl, getDocumentAsset, correspondents, handleCorrespondentAdd, handleCorrespondentRemove, resolveApiPath, selectFolder, tags, tagLookupById, }: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => { const orderedSelectedDocuments = useMemo(() => { const ordered = []; const seen = new Set(); const pushDoc = (doc) => { if (doc?.id && !seen.has(doc.id)) { ordered.push(doc); seen.add(doc.id); } }; selectionOrder.forEach((key) => { if (!isDocumentRowKey(key)) { return; } const docId = getRowId(key); const doc = documentLookup.get(docId) || null; pushDoc(doc); }); selectedDocumentIds.forEach((docId) => { if (seen.has(docId)) { return; } const doc = documentLookup.get(docId) || null; pushDoc(doc); }); return ordered; }, [selectionOrder, documentLookup, selectedDocumentIds]); const { detailPanelOpen, detailPanelDocument, openDetailPanel, closeDetailPanel, } = useDetailPanel({ documentLookup, orderedSelectedDocuments, }); useEffect(() => { detailPanelControlRef.current = { open: openDetailPanel, close: closeDetailPanel, }; }, [detailPanelControlRef, openDetailPanel, closeDetailPanel]); useEffect(() => { if (!orderedSelectedDocuments.length) { return; } const visited = new Set(); orderedSelectedDocuments.forEach((doc) => { const folderId = doc?.folder_id; if (!folderId) { return; } let currentId = folderId; let guard = 0; while (currentId && currentId !== 'root' && guard < 32) { guard += 1; if (visited.has(currentId)) { break; } visited.add(currentId); const node = folderNodes.get(currentId); if (!node) { if (!detailFolderFetchRef.current.has(currentId)) { detailFolderFetchRef.current.add(currentId); ensureFolderData(currentId, { force: false, includeDocuments: false }) .catch((error) => { console.warn('Failed to preload folder metadata for detail path', currentId, error); }) .finally(() => { detailFolderFetchRef.current.delete(currentId); }); } break; } const parentId = node.parentId ?? 'root'; if (!parentId || parentId === 'root') { break; } currentId = parentId; } }); }, [orderedSelectedDocuments, folderNodes, ensureFolderData, detailFolderFetchRef]); const resolveFolderPath = useCallback( (folderId) => { if (!folderId || folderId === 'root') { return []; } const segments = []; const visited = new Set(); let currentId = folderId; let guard = 0; while (currentId && guard < 32 && !visited.has(currentId)) { guard += 1; visited.add(currentId); if (currentId === 'root') { break; } const node = folderNodes.get(currentId); if (!node) { segments.push({ id: currentId, name: '…' }); break; } segments.push({ id: node.id, name: node.name || 'Folder' }); const parentId = node.parentId ?? 'root'; if (!parentId || parentId === 'root') { segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); break; } currentId = parentId; } if (!segments.some((segment) => segment.id === 'root')) { segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); } return segments.reverse(); }, [folderNodes], ); const documentLink = useMemo( () => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null), [detailPanelDocument, documentLinks], ); const previewWorkspaceDocument = useMemo(() => { if (!previewDocumentId) { return null; } return documentLookup.get(previewDocumentId) || documents.find((doc) => doc.id === previewDocumentId) || null; }, [previewDocumentId, documentLookup, documents]); const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument); const resolveThumbnailUrlForDoc = useCallback( (doc) => resolveDocumentAssetUrl(doc, 'thumbnail', { ensureAssetUrl, getAsset: getDocumentAsset, }), [ensureAssetUrl, getDocumentAsset], ); const inspectDocument = useCallback( (documentId) => { if (!documentId) { return; } openDetailPanel({ documentIds: [documentId] }); }, [openDetailPanel], ); const handleDetailPanelClose = useCallback(() => { closeDetailPanel(); }, [closeDetailPanel]); const detailPanelProps = useMemo( () => ({ document: detailPanelDocument, tags, tagLookupById, onTagAdd: handleDocumentTagAdd, onTagRemove: handleTagRemove, documentLink, onOpenPreview: openDocumentPreview, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate, onUpdateIssued: handleDocumentIssuedUpdate, ensureAssetUrl, getDocumentAsset, correspondents, onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentRemove: handleCorrespondentRemove, resolveApiPath, onFolderNavigate: selectFolder, onClose: handleDetailPanelClose, resolveFolderPath, }), [ activePreviewId, correspondents, detailPanelDocument, ensureAssetUrl, getDocumentAsset, handleCorrespondentAdd, handleCorrespondentRemove, handleDetailPanelClose, handleDocumentTagAdd, handleDocumentIssuedUpdate, handleDocumentTitleUpdate, handleTagRemove, openDocumentPreview, resolveApiPath, resolveFolderPath, selectFolder, documentLink, tags, tagLookupById, ], ); return { detailPanelProps, detailPanelOpen, openDetailPanel, closeDetailPanel, handleDetailPanelClose, inspectDocument, previewActive, previewWorkspaceDocument, documentLink, resolveThumbnailUrlForDoc, resolveFolderPath, }; }; export default useDetailWorkspace;