194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
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<CorrespondentOption | null>;
|
|
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<string, CorrespondentOption>();
|
|
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;
|