document types

This commit is contained in:
2025-10-31 00:13:16 +01:00
parent 3941ec61d3
commit 813ce24aeb
19 changed files with 1714 additions and 43 deletions
+172
View File
@@ -37,6 +37,32 @@ const sortCorrespondents = (entries = []) =>
}))
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
const resolveDocumentType = (doc) => {
if (!doc) {
return null;
}
const type = doc.document_type;
const fallbackId = doc.document_type_id ?? null;
if (!type) {
return null;
}
if (typeof type === 'string') {
const name = type.trim();
if (!name) {
return null;
}
return { id: fallbackId, name };
}
if (typeof type === 'object') {
const name = (type.name || type.label || '').trim();
if (!name) {
return null;
}
return { id: type.id ?? fallbackId, name };
}
return null;
};
const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
<div className="correspondent-list">
{entries.length ? (
@@ -302,6 +328,11 @@ const DetailPanel = ({
correspondents = [],
onCorrespondentAdd,
onCorrespondentRemove,
documentTypes = [],
onDocumentTypeSet,
onDocumentTypeClear,
onBulkDocumentTypeSet,
onBulkDocumentTypeClear,
resolveApiPath,
onFolderNavigate = null,
resolveFolderPath = null,
@@ -590,6 +621,23 @@ const DetailPanel = ({
});
}, [availableCorrespondents]);
const documentTypeOptions = useMemo(() => {
const seen = new Set();
return (documentTypes || [])
.map((entry) => (entry?.name || '').trim())
.filter((name) => {
if (!name) return false;
const lower = name.toLowerCase();
if (seen.has(lower)) {
return false;
}
seen.add(lower);
return true;
});
}, [documentTypes]);
const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]);
const singleCorrespondents = useMemo(() => {
if (!singleDoc) return [];
return sortCorrespondents(singleDoc.correspondents || []);
@@ -725,6 +773,42 @@ const DetailPanel = ({
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
}, [selectedDocuments]);
const bulkDocumentTypes = useMemo(() => {
if (!selectedDocuments.length) {
return [];
}
const map = new Map();
selectedDocuments.forEach((doc) => {
const type = resolveDocumentType(doc);
if (!type) return;
const key = type.id ?? type.name.toLowerCase();
if (!map.has(key)) {
map.set(key, { id: type.id ?? null, name: type.name, count: 0 });
}
map.get(key).count += 1;
});
return [...map.values()].sort((a, b) => (a.name || '').localeCompare(b.name || ''));
}, [selectedDocuments]);
const bulkDocumentTypeSummary = useMemo(() => {
if (!bulkDocumentTypes.length) {
return '';
}
return bulkDocumentTypes
.map((entry) => {
if (!entry?.name) {
return null;
}
const suffix = entry.count === selectedDocuments.length ? '' : ` (${entry.count})`;
return `${entry.name}${suffix}`;
})
.filter(Boolean)
.join(', ');
}, [bulkDocumentTypes, selectedDocuments.length]);
const handleBulkCorrespondentRemove = useCallback(
(entry) => {
if (!entry?.id) return;
@@ -1092,6 +1176,53 @@ const DetailPanel = ({
);
})}
</div>
<div className="detail-field">
<div className="detail-field__label">Document type</div>
<div className="detail-field__value">
{singleDocumentType?.name ? (
<span>{singleDocumentType.name}</span>
) : (
<span className="meta">None assigned.</span>
)}
{singleDocumentType && onDocumentTypeClear ? (
<button
type="button"
className="secondary"
onClick={() => onDocumentTypeClear?.({ documentId: singleDoc.id })}
>
Clear
</button>
) : null}
</div>
{onDocumentTypeSet ? (
<form
className="inline"
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const input = form.elements.documentType;
const value = input?.value?.trim();
if (!value) {
return;
}
onDocumentTypeSet?.({ document: singleDoc, name: value, input });
}}
>
<input
name="documentType"
placeholder="Assign or create type"
list="document-type-catalog-single"
defaultValue=""
/>
<button type="submit">Set</button>
<datalist id="document-type-catalog-single">
{documentTypeOptions.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</form>
) : null}
</div>
<TagSection
title="Tags"
tags={tagsForDoc.map((tag) => ({
@@ -1216,6 +1347,47 @@ const DetailPanel = ({
datalistOptions={tags}
className="bulk-tags"
/>
<div className="detail-field">
<div className="detail-field__label">Document type</div>
<div className="detail-field__value">
<span>{bulkDocumentTypeSummary || 'None assigned.'}</span>
{onBulkDocumentTypeClear && bulkDocumentTypes.length > 0 ? (
<button
type="button"
className="secondary"
onClick={() => onBulkDocumentTypeClear?.({ documentIds })}
>
Clear
</button>
) : null}
</div>
{onBulkDocumentTypeSet ? (
<form
className="inline"
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const input = form.elements.documentType;
const value = input?.value?.trim();
if (!value) {
return;
}
onBulkDocumentTypeSet?.({ name: value, input, documentIds });
}}
>
<input
name="documentType"
placeholder="Assign or create type"
list="document-type-catalog-bulk"
defaultValue=""
/>
<button type="submit">Set</button>
<datalist id="document-type-catalog-bulk">
{documentTypeOptions.map((name) => (<option key={name} value={name} />))}
</datalist>
</form>
) : null}
</div>
<CorrespondentSection
title="Correspondents"
entries={bulkCorrespondents}