import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EditIcon, IconX, PlusIcon } from '../ui/icons'; import QuickAddMenu from '../ui/QuickAddMenu'; import { getTagColorStyle } from '../utils/colors'; import { describeDocumentSummary } from './documentSummary'; const formatDate = (value) => { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return null; } return date.toLocaleDateString(); }; const toDateInputValue = (value) => { if (!value) { return ''; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return ''; } const timezoneOffset = date.getTimezoneOffset(); const localDate = new Date(date.getTime() - timezoneOffset * 60000); return localDate.toISOString().slice(0, 10); }; const toIssuedTimestamp = (dateString, fallback) => { if (!dateString) { return null; } const base = fallback ? new Date(fallback) : new Date(); if (Number.isNaN(base.getTime())) { return null; } const [year, month, day] = dateString.split('-').map((part) => Number.parseInt(part, 10)); if (!year || !month || !day) { return null; } const candidate = new Date(base); candidate.setUTCFullYear(year, month - 1, day); return candidate.toISOString(); }; export const sortCorrespondents = (entries = []) => entries .filter((entry) => entry && entry.name) .map(({ id, name, count }) => ({ id, name, count })) .sort((a, b) => a.name.localeCompare(b.name)); export const buildCorrespondentOptions = (entries = []) => { const seen = new Set(); return entries.reduce((options, entry) => { const name = typeof entry?.name === 'string' ? entry.name.trim() : ''; if (!name) { return options; } const key = name.toLowerCase(); if (seen.has(key)) { return options; } seen.add(key); options.push(name); return options; }, []); }; const normalizeOptions = (options) => (Array.isArray(options) ? options : []); export const TagSection = ({ tags = [], onRemove, onAdd, emptyMessage = 'No tags yet.', addPlaceholder = 'Add or create tag', addButtonLabel = 'Add', datalistOptions = [], className, }) => { const handleCreate = useCallback( (label) => onAdd?.({ value: label, input: null }), [onAdd], ); const handleSelect = useCallback( (option) => { if (!onAdd) return; const label = (option && typeof option === 'object' && option.label) || (typeof option === 'string' ? option : ''); if (!label) { return; } onAdd({ value: label, option }); }, [onAdd], ); const normalizedOptions = useMemo(() => normalizeOptions(datalistOptions), [datalistOptions]); const containerClass = className ? `tag-list ${className}` : 'tag-list'; const showQuickAdd = Boolean(onAdd); return (
{tags.map((tag) => { const key = tag.id ?? tag.label; const style = getTagColorStyle(tag.color); return ( {tag.label} {onRemove ? ( ) : null} ); })} {showQuickAdd ? ( handleSelect(normalized || original)} placeholder={addPlaceholder} createLabel={addButtonLabel} triggerAriaLabel="Add tag" triggerTitle={addButtonLabel} triggerClassName="quick-add__chip quick-add__trigger" triggerContent={( )} /> ) : null} {!tags.length && !showQuickAdd ? {emptyMessage} : null}
); }; export const CorrespondentSection = ({ entries = [], onRemove, onAdd, showCount = false, addPlaceholder = 'Add or create correspondent', addButtonLabel = 'Add', datalistOptions = [], className, }) => { const handleCreate = useCallback( (name) => onAdd?.({ name, input: null }), [onAdd], ); const handleSelect = useCallback( (original, normalized) => { if (!onAdd) return; const source = normalized && typeof normalized === 'object' ? normalized : original; const resolvedName = (source && typeof source.name === 'string' && source.name.trim()) || (typeof source === 'string' ? source.trim() : '') || (source && typeof source.label === 'string' ? source.label.trim() : ''); if (!resolvedName) { return; } const payload = source && typeof source === 'object' ? { ...source, name: resolvedName } : { id: null, name: resolvedName }; onAdd({ name: resolvedName, option: payload, input: null }); }, [onAdd], ); const normalizedOptions = useMemo(() => normalizeOptions(datalistOptions), [datalistOptions]); const hasEntries = entries && entries.length > 0; const showQuickAdd = Boolean(onAdd); const containerClass = className ? `correspondent-list ${className}` : 'correspondent-list'; return (
{hasEntries ? entries.map((entry) => { const key = entry.id ?? entry.name; return ( {entry.name} {showCount && entry.count ? ` (${entry.count})` : ''} {onRemove ? ( ) : null} ); }) : !showQuickAdd && No correspondents yet.} {showQuickAdd ? ( handleSelect(original, normalized)} placeholder={addPlaceholder} createLabel={addButtonLabel} triggerAriaLabel="Add correspondent" triggerTitle={addButtonLabel} triggerClassName="quick-add__chip quick-add__trigger" triggerContent={( )} /> ) : null}
); }; const DocumentSummarySection = ({ document, tagLookupById = new Map(), tagOptions = [], onTagAdd, onTagRemove, correspondents, correspondentOptions = [], onCorrespondentAdd, onCorrespondentRemove, onUpdateTitle, onUpdateIssued }) => { const summary = useMemo(() => { if (!document) { return { title: '', originalName: '', sizeLabel: '—', pageCount: null, }; } return describeDocumentSummary(document); }, [document]); const issuedDateLabel = useMemo(() => formatDate(document?.issued_at), [document?.issued_at]); const editableTitle = Boolean(document && onUpdateTitle); const editableIssued = Boolean(document && onUpdateIssued); const resolvedTags = useMemo(() => { if (!Array.isArray(document?.tags)) { return []; } return document.tags.map((tag) => ({ id: tag.id, label: tag.label, color: tag.color ?? tagLookupById.get(tag.id)?.color ?? null, })); }, [document?.tags, tagLookupById]); const resolvedCorrespondents = useMemo(() => { if (Array.isArray(correspondents) && correspondents.length) { return correspondents; } return sortCorrespondents(document?.correspondents || []); }, [correspondents, document?.correspondents]); const metaRows = useMemo(() => { const rows = []; const currentVersionNumber = document?.current_version?.version_number; if (Number.isFinite(currentVersionNumber)) { rows.push({ key: 'current-version', label: 'Current version', value: `#${currentVersionNumber}`, }); } if (summary.sizeLabel && summary.sizeLabel !== '—') { rows.push({ key: 'size', label: 'Size', value: summary.sizeLabel }); } if (Number.isFinite(summary.pageCount)) { rows.push({ key: 'pages', label: 'Pages', value: String(summary.pageCount) }); } return rows; }, [document?.current_version?.version_number, summary]); const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); const [titleError, setTitleError] = useState(null); const [isTitleEditing, setIsTitleEditing] = useState(false); const [issuedDraft, setIssuedDraft] = useState(''); const [issuedSaving, setIssuedSaving] = useState(false); const [issuedError, setIssuedError] = useState(null); const [isIssuedEditing, setIsIssuedEditing] = useState(false); useEffect(() => { setIsTitleEditing(false); setTitleDraft(''); setTitleError(null); setTitleSaving(false); setIsIssuedEditing(false); setIssuedDraft(''); setIssuedError(null); setIssuedSaving(false); }, [document?.id]); const startTitleEdit = useCallback(() => { if (!editableTitle || !document) return; setIsTitleEditing(true); setTitleDraft(document.title); setTitleError(null); }, [document, editableTitle]); const cancelTitleEdit = useCallback(() => { setIsTitleEditing(false); setTitleDraft(''); setTitleError(null); setTitleSaving(false); }, []); const submitTitleEdit = useCallback( async (event) => { event.preventDefault(); if (!editableTitle || !document) return; const trimmed = titleDraft.trim(); if (!trimmed) { setTitleError('Title cannot be empty.'); return; } setTitleSaving(true); try { const ok = await onUpdateTitle(document.id, trimmed); if (ok) { cancelTitleEdit(); } else { setTitleError('Failed to update title.'); } } finally { setTitleSaving(false); } }, [cancelTitleEdit, document, editableTitle, onUpdateTitle, titleDraft], ); const startIssuedEdit = useCallback(() => { if (!editableIssued || !document) return; setIsIssuedEditing(true); setIssuedDraft(toDateInputValue(document.issued_at)); setIssuedError(null); }, [document, editableIssued]); const cancelIssuedEdit = useCallback(() => { setIsIssuedEditing(false); setIssuedDraft(''); setIssuedError(null); setIssuedSaving(false); }, []); const submitIssuedEdit = useCallback( async (event) => { event.preventDefault(); if (!editableIssued || !document) return; const normalizedValue = issuedDraft ? toIssuedTimestamp(issuedDraft, document.issued_at) : null; setIssuedSaving(true); try { const ok = await onUpdateIssued(document.id, normalizedValue); if (ok) { cancelIssuedEdit(); } else { setIssuedError('Failed to update issued date.'); } } finally { setIssuedSaving(false); } }, [cancelIssuedEdit, document, editableIssued, issuedDraft, onUpdateIssued], ); if (!document) { return null; } return (
{editableTitle && isTitleEditing ? (
{ setTitleDraft(event.target.value); if (titleError) { setTitleError(null); } }} onKeyDown={(event) => { if (event.key === 'Escape') { event.preventDefault(); cancelTitleEdit(); } }} aria-label="Document title" autoFocus disabled={titleSaving} />
) : ( <>

{summary.title}

{editableTitle ? ( ) : null} )}
{titleError ?
{titleError}
: null}
Issued: {editableIssued && isIssuedEditing ? (
{ setIssuedDraft(event.target.value); if (issuedError) { setIssuedError(null); } }} aria-label="Issued on" disabled={issuedSaving} />
) : ( <> {issuedDateLabel || 'Not set'} {editableIssued ? ( ) : null} )}
{issuedError ?
{issuedError}
: null} {metaRows.map((row) => (
{row.label}: {row.value}
))}
onTagRemove(document.id, tag.id) : undefined } onAdd={ onTagAdd ? ({ value, option }) => onTagAdd(document, value, { option }) : undefined } datalistOptions={tagOptions} /> onCorrespondentRemove({ documentId: document.id, correspondentId: entry.id, }) : undefined } onAdd={ onCorrespondentAdd ? ({ name, option }) => onCorrespondentAdd({ document, name, option, }) : undefined } showCount datalistOptions={correspondentOptions} />
); }; export default DocumentSummarySection;