import { useCallback, useEffect, useMemo, useState } from "react"; import { Archive, Boxes, Building2, Eye, Inbox, Pencil, Plus, RefreshCw, Rocket, Save, Trash2 } from "lucide-react"; import { ActionBlockerHint, AdminPageLayout, Button, ConfirmDialog, Dialog, DocumentationHelpLink, FormField, IconButton, MetricCard, SegmentedControl, SelectionList, SelectionListItem, StatusBadge, ToggleSwitch, i18nMessage, useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings } from "@govoplan/core-webui"; import { archivePostbox, createExactPostbox, createPostboxTemplate, listAdminPostboxes, listPostboxOrganizationTargets, listPostboxTemplates, materializePostboxTemplate, previewPostboxTemplate, publishPostboxTemplate, retirePostboxTemplate, revisePostboxTemplate, type PostboxDirectoryItem, type PostboxExactCreatePayload, type PostboxOrganizationFunction, type PostboxOrganizationStructure, type PostboxOrganizationUnit, type PostboxRoutingPolicy, type PostboxTemplate, type PostboxTemplateCreatePayload, type PostboxTemplatePreview, type PostboxTemplateRevisionPayload } from "../../api/postbox"; import { POSTBOX_ADMIN_DOCUMENTATION, POSTBOX_FIELD_DOCUMENTATION, POSTBOX_INTERFACE_I18N, postboxBusyReason } from "./interfacePatterns"; type AdminMode = "templates" | "postboxes"; type TemplateDraft = PostboxTemplateCreatePayload & { templateId: string }; type ExactDraft = PostboxExactCreatePayload; type MaterializeDraft = { templateId: string; organization_unit_id: string; function_id: string; context_key: string; }; const routingDefaults = (): PostboxRoutingPolicy => ({ linked_copy: { enabled: false, structure_id: null, relation_type_ids: [], max_depth: 1, stop_unit_id: null, stop_unit_type_id: null, target_function_type_id: null, target_template_id: null, fanout: "nearest", allowed_classifications: ["internal"], allowed_producer_modules: [], require_expiry: false, max_retention_days: null }, attention: { mode: "none", delay_minutes: null }, shared_visibility: { mode: "none" } }); const templateDefaults = (): TemplateDraft => ({ templateId: "", slug: "", name: "", description: "", function_type_id: null, scope_kind: "tenant", scope_id: null, scope_structure_id: null, scope_relation_type_ids: [], name_pattern: "{unit_name} / {function_name}", address_pattern: "{template_slug}.{unit_slug}.{function_slug}", classification: "internal", allow_vacant_delivery: true, portal_visible: false, routing_policy: routingDefaults() }); const exactDefaults = (): ExactDraft => ({ name: "", description: "", organization_unit_id: "", function_id: "", address_key: "", classification: "internal", portal_visible: false }); export default function PostboxAdminPanel({ settings, canManageBindings, canManageTemplates }: { settings: ApiSettings; canManageBindings: boolean; canManageTemplates: boolean; }) { const [mode, setMode] = useState( canManageTemplates ? "templates" : "postboxes" ); const [templates, setTemplates] = useState([]); const [postboxes, setPostboxes] = useState([]); const [units, setUnits] = useState([]); const [structures, setStructures] = useState([]); const [selectedTemplateId, setSelectedTemplateId] = useState(""); const [selectedPostboxId, setSelectedPostboxId] = useState(""); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [templateDialogOpen, setTemplateDialogOpen] = useState(false); const [templateDraft, setTemplateDraft] = useState(templateDefaults); const [templateBaseline, setTemplateBaseline] = useState(templateDefaults); const [templatePreview, setTemplatePreview] = useState(null); const [templatePreviewLoading, setTemplatePreviewLoading] = useState(false); const [exactDialogOpen, setExactDialogOpen] = useState(false); const [exactDraft, setExactDraft] = useState(exactDefaults); const [exactBaseline, setExactBaseline] = useState(exactDefaults); const [materializeDialogOpen, setMaterializeDialogOpen] = useState(false); const [materializeDraft, setMaterializeDraft] = useState({ templateId: "", organization_unit_id: "", function_id: "", context_key: "" }); const [materializeBaseline, setMaterializeBaseline] = useState({ templateId: "", organization_unit_id: "", function_id: "", context_key: "" }); const [archiveTarget, setArchiveTarget] = useState(null); const [retireTarget, setRetireTarget] = useState(null); const { requestDiscard } = useUnsavedChanges(); const selectedTemplate = useMemo( () => templates.find((template) => template.id === selectedTemplateId) ?? templates[0] ?? null, [templates, selectedTemplateId] ); const selectedPostbox = useMemo( () => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? postboxes[0] ?? null, [postboxes, selectedPostboxId] ); const functionTypes = useMemo(() => { const values = new Map(); for (const unit of units) { for (const fn of unit.functions) { if (fn.function_type_id && !values.has(fn.function_type_id)) { values.set(fn.function_type_id, fn.name); } } } return [...values].map(([id, name]) => ({ id, name })); }, [units]); const unitTypes = useMemo(() => { const values = new Map(); for (const unit of units) { if (unit.unit_type_id && !values.has(unit.unit_type_id)) { values.set(unit.unit_type_id, unit.name); } } return [...values].map(([id, example]) => ({ id, example })); }, [units]); const templateDirty = templateDialogOpen && draftKey(templateDraft) !== draftKey(templateBaseline); const exactDirty = exactDialogOpen && draftKey(exactDraft) !== draftKey(exactBaseline); const materializeDirty = materializeDialogOpen && draftKey(materializeDraft) !== draftKey(materializeBaseline); useUnsavedDraftGuard({ dirty: templateDirty || exactDirty || materializeDirty, title: "Unsaved Postbox administration draft", message: "Save or discard the open Postbox administration draft before leaving this surface.", onSave: saveActiveDraft, onDiscard: discardAdminDraft }); const load = useCallback(async () => { setLoading(true); setError(""); try { const [nextTemplates, nextPostboxes, organizationTargets] = await Promise.all([ canManageTemplates ? listPostboxTemplates(settings) : Promise.resolve([]), canManageBindings ? listAdminPostboxes(settings) : Promise.resolve([]), listPostboxOrganizationTargets(settings) ]); setTemplates(nextTemplates); setPostboxes(nextPostboxes); setUnits(organizationTargets.units); setStructures(organizationTargets.structures); setSelectedTemplateId((current) => current && nextTemplates.some((template) => template.id === current) ? current : nextTemplates[0]?.id ?? "" ); setSelectedPostboxId((current) => current && nextPostboxes.some((postbox) => postbox.id === current) ? current : nextPostboxes[0]?.id ?? "" ); } catch (loadError) { setError(errorMessage(loadError)); } finally { setLoading(false); } }, [canManageBindings, canManageTemplates, settings]); useEffect(() => { void load(); }, [load]); function openNewTemplate() { const next = templateDefaults(); setTemplatePreview(null); setTemplateDraft(next); setTemplateBaseline(next); setTemplateDialogOpen(true); } function openTemplateRevision(template: PostboxTemplate) { const revision = currentRevision(template); if (!revision) return; const next = { templateId: template.id, slug: template.slug, name: template.name, description: template.description || "", function_type_id: revision.function_type_id ?? null, scope_kind: revision.scope_kind, scope_id: revision.scope_id ?? null, scope_structure_id: revision.scope_structure_id ?? null, scope_relation_type_ids: revision.scope_relation_type_ids ?? [], name_pattern: revision.name_pattern, address_pattern: revision.address_pattern, classification: revision.classification, allow_vacant_delivery: revision.allow_vacant_delivery, portal_visible: revision.portal_visible, routing_policy: revision.routing_policy ?? routingDefaults() }; setTemplatePreview(null); setTemplateDraft(next); setTemplateBaseline(next); setTemplateDialogOpen(true); } async function saveTemplate(): Promise { setBusy(true); setError(""); setSuccess(""); try { if (templateDraft.templateId) { const currentTemplate = templates.find( (item) => item.id === templateDraft.templateId ); if (!currentTemplate) throw new Error("The template is no longer available."); await revisePostboxTemplate( settings, currentTemplate, revisionPayload(templateDraft) ); setSuccess("A new immutable template revision was created."); } else { await createPostboxTemplate(settings, { slug: templateDraft.slug, name: templateDraft.name, description: templateDraft.description || null, ...revisionPayload(templateDraft) }); setSuccess("Postbox template created as a draft."); } setTemplateBaseline(templateDraft); setTemplateDialogOpen(false); await load(); return true; } catch (actionError) { setError(errorMessage(actionError)); return false; } finally { setBusy(false); } } async function previewTemplate() { setTemplatePreviewLoading(true); setError(""); try { const preview = await previewPostboxTemplate(settings, { slug: templateDraft.slug, name: templateDraft.name, description: templateDraft.description || null, ...revisionPayload(templateDraft), template_id: templateDraft.templateId || null, limit: 200 }); setTemplatePreview(preview); } catch (actionError) { setTemplatePreview(null); setError(errorMessage(actionError)); } finally { setTemplatePreviewLoading(false); } } async function publishSelected() { if (!selectedTemplate) return; setBusy(true); setError(""); setSuccess(""); try { await publishPostboxTemplate( settings, selectedTemplate, selectedTemplate.current_revision ); setSuccess(i18nMessage("i18n:govoplan-postbox.published_revision_message", { revision: selectedTemplate.current_revision })); await load(); } catch (actionError) { setError(errorMessage(actionError)); } finally { setBusy(false); } } function openExact() { const unit = units.find((item) => item.functions.length); const next = { ...exactDefaults(), organization_unit_id: unit?.id ?? "", function_id: unit?.functions[0]?.id ?? "" }; setExactDraft(next); setExactBaseline(next); setExactDialogOpen(true); } async function saveExact(): Promise { setBusy(true); setError(""); setSuccess(""); try { await createExactPostbox(settings, { ...exactDraft, description: exactDraft.description || null, address_key: exactDraft.address_key || null }); setExactBaseline(exactDraft); setExactDialogOpen(false); setSuccess("Exact function-bound Postbox created."); await load(); return true; } catch (actionError) { setError(errorMessage(actionError)); return false; } finally { setBusy(false); } } function openMaterialize(template: PostboxTemplate) { const revision = currentRevision(template); const compatible = compatibleTargets(units, revision?.function_type_id); const unit = compatible[0]; const next = { templateId: template.id, organization_unit_id: unit?.id ?? "", function_id: unit?.functions[0]?.id ?? "", context_key: "" }; setMaterializeDraft(next); setMaterializeBaseline(next); setMaterializeDialogOpen(true); } async function materialize(): Promise { setBusy(true); setError(""); setSuccess(""); try { await materializePostboxTemplate(settings, materializeDraft.templateId, { organization_unit_id: materializeDraft.organization_unit_id, function_id: materializeDraft.function_id, context_key: materializeDraft.context_key || null }); setMaterializeBaseline(materializeDraft); setMaterializeDialogOpen(false); setSuccess("Stable Postbox address resolved and materialized."); await load(); return true; } catch (actionError) { setError(errorMessage(actionError)); return false; } finally { setBusy(false); } } async function confirmArchive() { if (!archiveTarget) return; setBusy(true); setError(""); setSuccess(""); try { await archivePostbox(settings, archiveTarget); setSuccess("Postbox archived. Messages and delivery evidence were retained."); setArchiveTarget(null); await load(); } catch (actionError) { setError(errorMessage(actionError)); } finally { setBusy(false); } } async function confirmRetire() { if (!retireTarget) return; setSelectedTemplateId(retireTarget.id); setBusy(true); setError(""); setSuccess(""); try { await retirePostboxTemplate(settings, retireTarget); setSuccess("Postbox template retired. Existing addresses remain durable."); setRetireTarget(null); await load(); } catch (actionError) { setError(errorMessage(actionError)); } finally { setBusy(false); } } function saveActiveDraft(): Promise { if (templateDirty) return saveTemplate(); if (exactDirty) return saveExact(); if (materializeDirty) return materialize(); return Promise.resolve(true); } function discardAdminDraft() { if (templateDialogOpen) { setTemplateDraft(templateBaseline); setTemplateDialogOpen(false); } if (exactDialogOpen) { setExactDraft(exactBaseline); setExactDialogOpen(false); } if (materializeDialogOpen) { setMaterializeDraft(materializeBaseline); setMaterializeDialogOpen(false); } } function closeTemplateDialog() { const close = () => { setTemplateDraft(templateBaseline); setTemplateDialogOpen(false); }; if (templateDirty) requestDiscard(close); else close(); } function closeExactDialog() { const close = () => { setExactDraft(exactBaseline); setExactDialogOpen(false); }; if (exactDirty) requestDiscard(close); else close(); } function closeMaterializeDialog() { const close = () => { setMaterializeDraft(materializeBaseline); setMaterializeDialogOpen(false); }; if (materializeDirty) requestDiscard(close); else close(); } return ( } onClick={() => requestDiscard(() => void load())} disabled={loading || busy} disabledReason={postboxBusyReason(loading, busy)} /> {mode === "templates" && canManageTemplates ? ( ) : null} {mode === "postboxes" && canManageBindings ? ( ) : null} } > setMode(value as AdminMode)} options={[ ...(canManageTemplates ? [{ id: "templates", label: "Templates" }] : []), ...(canManageBindings ? [{ id: "postboxes", label: "Postboxes" }] : []) ]} ariaLabel="Postbox administration section" /> {mode === "postboxes" && !units.some((unit) => unit.functions.length) ? ( ) : null} {mode === "templates" ? ( void publishSelected()} onRetire={() => selectedTemplate && setRetireTarget(selectedTemplate)} onMaterialize={openMaterialize} busy={busy} canManageBindings={canManageBindings} /> ) : ( )} { setTemplateDraft(draft); setTemplatePreview(null); }} onPreview={() => void previewTemplate()} onClose={closeTemplateDialog} onSave={() => void saveTemplate()} /> void saveExact()} /> item.id === materializeDraft.templateId) ?? null} units={units} busy={busy} onChange={setMaterializeDraft} onClose={closeMaterializeDialog} onSave={() => void materialize()} /> void confirmArchive()} onCancel={() => setArchiveTarget(null)} /> void confirmRetire()} onCancel={() => setRetireTarget(null)} /> ); } function TemplateWorkspace({ templates, selected, onSelect, onRevise, onPublish, onRetire, onMaterialize, busy, canManageBindings }: { templates: PostboxTemplate[]; selected: PostboxTemplate | null; onSelect: (id: string) => void; onRevise: (template: PostboxTemplate) => void; onPublish: () => void; onRetire: () => void; onMaterialize: (template: PostboxTemplate) => void; busy: boolean; canManageBindings: boolean; }) { const revision = selected ? currentRevision(selected) : null; return (
{selected && revision ? ( <>
Template

{selected.name}

{selected.description || "No description."}

{canManageBindings ? ( ) : null}
Revision
{revision.revision}{revision.published_at ? " · Published" : " · Draft"}
Function type
{revision.function_type_id || "Any function type"}
Scope
{revision.scope_kind}{revision.scope_id ? ` · ${revision.scope_id}` : ""}
Classification
{revision.classification}
Vacant delivery
{revision.allow_vacant_delivery ? "Accepted" : "Blocked"}
Hierarchy copies
{revision.routing_policy.linked_copy.enabled ? i18nMessage("i18n:govoplan-postbox.depth_label", { fanout: revision.routing_policy.linked_copy.fanout, depth: revision.routing_policy.linked_copy.max_depth }) : "Disabled"}
Vacancy escalation
{revision.routing_policy.attention.mode === "vacancy_escalation" ? i18nMessage("i18n:govoplan-postbox.minutes_label", { minutes: revision.routing_policy.attention.delay_minutes }) : "Disabled"}
Encryption
{revision.encryption_profile}
Name pattern
{revision.name_pattern}
Address pattern
{revision.address_pattern}

Immutable revisions

{selected.revisions.map((item) => (
{i18nMessage("i18n:govoplan-postbox.revision_label", { revision: item.revision })} {item.classification} · {item.scope_kind}
))}
) : (
Select a template

Published revisions lazily resolve stable unit-specific addresses.

)}
); } function PostboxWorkspace({ postboxes, selected, onSelect, onArchive, busy }: { postboxes: PostboxDirectoryItem[]; selected: PostboxDirectoryItem | null; onSelect: (id: string) => void; onArchive: (postbox: PostboxDirectoryItem) => void; busy: boolean; }) { return (
{selected ? ( <>
Postbox

{selected.name}

{selected.address}

Organization unit
{selected.organization_unit_name || "None"}
Function
{selected.function_name || "None"}
Address key
{selected.address_key}
Classification
{selected.classification}
Current holders
{selected.holder_count}
Vacancy
{selected.vacant ? "Vacant" : "Staffed"}
Context
{selected.context_key || "None"}
Template revision
{selected.template_revision_id || "Exact Postbox"}
) : (
Select a Postbox

Materialized Postboxes remain durable through vacancy and reassignment.

)}
); } function TemplateDialog({ open, draft, units, structures, templates, functionTypes, unitTypes, busy, preview, previewLoading, onChange, onPreview, onClose, onSave }: { open: boolean; draft: TemplateDraft; units: PostboxOrganizationUnit[]; structures: PostboxOrganizationStructure[]; templates: PostboxTemplate[]; functionTypes: Array<{ id: string; name: string }>; unitTypes: Array<{ id: string; example: string }>; busy: boolean; preview: PostboxTemplatePreview | null; previewLoading: boolean; onChange: (draft: TemplateDraft) => void; onPreview: () => void; onClose: () => void; onSave: () => void; }) { const isRevision = Boolean(draft.templateId); const scopeOptions = draft.scope_kind === "unit_type" ? unitTypes.map((item) => ({ id: item.id, label: `${item.id} (${item.example})` })) : units.map((unit) => ({ id: unit.id, label: unit.name })); const linkedCopy = draft.routing_policy.linked_copy; const attention = draft.routing_policy.attention; const selectedScopeStructure = structures.find( (item) => item.id === draft.scope_structure_id ); const selectedStructure = structures.find( (item) => item.id === linkedCopy.structure_id ); const updateRouting = (routing_policy: PostboxRoutingPolicy) => { onChange({ ...draft, routing_policy }); }; const updateLinkedCopy = ( next: Partial ) => { updateRouting({ ...draft.routing_policy, linked_copy: { ...linkedCopy, ...next } }); }; const valid = draft.name.trim() && draft.slug.trim() && draft.name_pattern.trim() && draft.address_pattern.trim() && (draft.scope_kind === "tenant" || Boolean(draft.scope_id)) && (draft.scope_kind !== "subtree" || Boolean(draft.scope_structure_id)) && ( !linkedCopy.enabled || Boolean( linkedCopy.structure_id && linkedCopy.target_function_type_id && linkedCopy.target_template_id && linkedCopy.allowed_classifications.length && linkedCopy.allowed_producer_modules.length ) ); return ( } >
onChange({ ...draft, name: event.target.value })} /> onChange({ ...draft, slug: event.target.value })} /> onChange({ ...draft, description: event.target.value })} /> {draft.scope_kind !== "tenant" ? ( ) :
} {draft.scope_kind === "subtree" ? ( <>
{(selectedScopeStructure?.relation_types || []) .filter((item) => item.status === "active" && item.is_hierarchical) .map((item) => ( ))} {selectedScopeStructure && !selectedScopeStructure.relation_types.some( (item) => item.status === "active" && item.is_hierarchical ) ? No active hierarchical relation type. : null} {!selectedScopeStructure ? All active hierarchical relations are used unless specific types are selected. : null}
) : null} onChange({ ...draft, name_pattern: event.target.value })} /> onChange({ ...draft, address_pattern: event.target.value })} /> onChange({ ...draft, classification: event.target.value })} />
onChange({ ...draft, allow_vacant_delivery: checked })} />
onChange({ ...draft, portal_visible: checked })} />
Hierarchy linked copies Copy to explicitly bounded function Postboxes in one selected structure.
updateLinkedCopy({ enabled, allowed_classifications: linkedCopy.allowed_classifications.length ? linkedCopy.allowed_classifications : [draft.classification] })} />
{linkedCopy.enabled ? (
updateLinkedCopy({ max_depth: Math.max(1, Math.min(20, Number(event.target.value) || 1)) })} /> updateLinkedCopy({ allowed_classifications: commaSeparated(event.target.value) })} /> updateLinkedCopy({ allowed_producer_modules: commaSeparated(event.target.value) })} />
updateLinkedCopy({ require_expiry })} />
updateLinkedCopy({ max_retention_days: event.target.value ? Math.max(1, Math.min(36500, Number(event.target.value))) : null })} />
updateRouting({ ...draft.routing_policy, attention: checked ? { mode: "vacancy_escalation", delay_minutes: attention.delay_minutes || 1440 } : { mode: "none", delay_minutes: null } })} />
{attention.mode === "vacancy_escalation" ? ( updateRouting({ ...draft.routing_policy, attention: { mode: "vacancy_escalation", delay_minutes: Math.max( 1, Math.min(43200, Number(event.target.value) || 1) ) } })} /> ) :
}
) : null}
{preview ? : null}

Available pattern variables include template, unit, function, and optional context names or slugs. Published revisions are immutable.

); } function TemplateImpactPreview({ preview }: { preview: PostboxTemplatePreview; }) { return (
Dry-run impact No Postboxes or addresses were created.
{preview.truncated ? : null}
{preview.diagnostics.length ? (

{preview.diagnostics.join(", ")}

) : null}
{preview.targets.map((target) => (
{target.name} {target.organization_unit_name} · {target.function_name} {target.address} {target.diagnostics.length ? ( {target.diagnostics.join(", ")} ) : null}
{target.holder_count} holder{target.holder_count === 1 ? "" : "s"}
))} {!preview.targets.length ? (

The scope contains no matching active function.

) : null}
); } function ExactPostboxDialog({ open, draft, units, busy, onChange, onClose, onSave }: { open: boolean; draft: ExactDraft; units: PostboxOrganizationUnit[]; busy: boolean; onChange: (draft: ExactDraft) => void; onClose: () => void; onSave: () => void; }) { const unit = units.find((item) => item.id === draft.organization_unit_id); return ( } >
onChange({ ...draft, name: event.target.value })} /> onChange({ ...draft, address_key: event.target.value })} /> onChange({ ...draft, classification: event.target.value })} /> onChange({ ...draft, description: event.target.value })} />
onChange({ ...draft, portal_visible: checked })} />
); } function MaterializeDialog({ open, draft, template, units, busy, onChange, onClose, onSave }: { open: boolean; draft: MaterializeDraft; template: PostboxTemplate | null; units: PostboxOrganizationUnit[]; busy: boolean; onChange: (draft: MaterializeDraft) => void; onClose: () => void; onSave: () => void; }) { const revision = template ? currentRevision(template) : null; const compatible = compatibleTargets(units, revision?.function_type_id); const unit = compatible.find((item) => item.id === draft.organization_unit_id); return ( } >
onChange({ ...draft, context_key: event.target.value })} />
); } function currentRevision(template: PostboxTemplate) { return template.revisions.find((revision) => revision.revision === template.current_revision) ?? template.revisions.at(-1) ?? null; } function revisionPayload(draft: TemplateDraft): PostboxTemplateRevisionPayload { return { function_type_id: draft.function_type_id || null, scope_kind: draft.scope_kind, scope_id: draft.scope_kind === "tenant" ? null : draft.scope_id || null, scope_structure_id: draft.scope_kind === "subtree" ? draft.scope_structure_id || null : null, scope_relation_type_ids: draft.scope_kind === "subtree" ? draft.scope_relation_type_ids : [], name_pattern: draft.name_pattern, address_pattern: draft.address_pattern, classification: draft.classification, allow_vacant_delivery: draft.allow_vacant_delivery, portal_visible: draft.portal_visible, routing_policy: draft.routing_policy }; } function compatibleTargets( units: PostboxOrganizationUnit[], functionTypeId?: string | null ): PostboxOrganizationUnit[] { return units .map((unit) => ({ ...unit, functions: functionTypeId ? unit.functions.filter((fn) => fn.function_type_id === functionTypeId) : unit.functions })) .filter((unit) => unit.functions.length); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "Postbox request failed"; } function commaSeparated(value: string): string[] { return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))]; } function draftKey(value: unknown): string { return JSON.stringify(value); }