import { useCallback, useMemo } from 'react'; import { isPlainObject } from '../../utils/typeGuards'; type ApiClient = { post: (path: string, body?: unknown) => Promise<{ data: unknown }>; delete: (path: string) => Promise<{ data: unknown }>; }; interface CorrespondentOption { id?: string | number; name?: string; [key: string]: unknown; } type Identifier = string | number; interface UseDocumentCorrespondentActionsArgs { apiClient: ApiClient; correspondents: CorrespondentOption[]; handleCorrespondentCreate: (payload: { name: string }) => Promise; notifyApiError: (error: unknown, fallback: string) => void; setStatusMessage: (message: string, variant?: string) => void; updateDocumentCaches?: ( id: Identifier, updater: (doc: { correspondents?: CorrespondentOption[] } | null) => { correspondents?: CorrespondentOption[] } | null, ) => void; } const useDocumentCorrespondentActions = ({ apiClient, correspondents, handleCorrespondentCreate, notifyApiError, setStatusMessage, updateDocumentCaches, }: UseDocumentCorrespondentActionsArgs) => { const correspondentLookupByName = useMemo(() => { const map = new Map(); correspondents.forEach((correspondent) => { if (correspondent?.name) { map.set(correspondent.name.toLowerCase(), correspondent); } }); return map; }, [correspondents]); const handleDocumentCorrespondentAttach = useCallback( async ( { documentId, correspondentId }: { documentId: Identifier; correspondentId: Identifier }, { notify = true }: { notify?: boolean } = {}, ) => { if (documentId == null || correspondentId == null) { throw new Error('Missing document or correspondent.'); } try { await apiClient.post(`/documents/${documentId}/correspondents`, { assignments: [{ correspondent_id: correspondentId }], replace: false, }); if (updateDocumentCaches) { const correspondent = correspondents.find((entry) => entry?.id === correspondentId) || null; updateDocumentCaches(documentId, (doc) => { if (!doc) { return doc; } const current = Array.isArray(doc.correspondents) ? doc.correspondents : []; if (current.some((entry) => entry?.id === correspondentId)) { return doc; } const nextEntry = correspondent ? { id: correspondent.id, name: correspondent.name } : { id: correspondentId }; return { ...doc, correspondents: [...current, nextEntry] }; }); } if (notify) { setStatusMessage('Correspondent assigned.', 'success'); } return true; } catch (error) { const message = error.response?.data?.error || 'Failed to assign correspondent.'; notifyApiError(error, message); throw new Error(message); } }, [apiClient, correspondents, notifyApiError, setStatusMessage, updateDocumentCaches], ); const handleCorrespondentRemove = useCallback( async ( { documentId, correspondentId }: { documentId: Identifier; correspondentId: Identifier }, { notify = true }: { notify?: boolean } = {}, ) => { if (documentId == null || correspondentId == null) { throw new Error('Missing document or correspondent.'); } try { await apiClient.delete(`/documents/${documentId}/correspondents/${correspondentId}`); if (updateDocumentCaches) { updateDocumentCaches(documentId, (doc) => { if (!doc || !Array.isArray(doc.correspondents)) { return doc; } const filtered = doc.correspondents.filter((entry) => entry?.id !== correspondentId); return filtered.length === doc.correspondents.length ? doc : { ...doc, correspondents: filtered }; }); } if (notify) { setStatusMessage('Correspondent removed.', 'success'); } return true; } catch (error) { const message = error.response?.data?.error || 'Failed to remove correspondent.'; notifyApiError(error, message); throw new Error(message); } }, [apiClient, notifyApiError, setStatusMessage, updateDocumentCaches], ); const normalizeOption = ( option: CorrespondentOption | string | null, ): CorrespondentOption | null => { if (!option) { return null; } if (isPlainObject(option) && 'id' in option) { return option as CorrespondentOption; } if (typeof option === 'string') { const trimmed = option.trim(); if (trimmed) { return { id: null, name: trimmed }; } } return null; }; const handleCorrespondentAdd = useCallback( async ({ document, name, input = null, option = null }: { document?: { id?: string | number }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => { if (!document?.id) { throw new Error('Missing document for correspondent assignment.'); } const trimmed = name?.trim?.() || ''; if (!trimmed) { setStatusMessage('Correspondent name is required.', 'error'); return; } let target = correspondentLookupByName.get(trimmed.toLowerCase()) || normalizeOption(option); if (!target) { try { target = await handleCorrespondentCreate({ name: trimmed }); } catch { return; } } if (!target?.id) { setStatusMessage('Unable to resolve correspondent.', 'error'); return; } try { await handleDocumentCorrespondentAttach({ documentId: document.id, correspondentId: target.id, }); if (input) { input.value = ''; } } catch (error) { setStatusMessage('Failed to assign correspondent.', 'error'); console.error('[documents] assign correspondent failed', error); } }, [ correspondentLookupByName, handleCorrespondentCreate, handleDocumentCorrespondentAttach, setStatusMessage, ], ); return { correspondentLookupByName, handleDocumentCorrespondentAttach, handleCorrespondentRemove, handleCorrespondentAdd, }; }; export default useDocumentCorrespondentActions;