Migrate Postbox interface patterns
This commit is contained in:
@@ -12,10 +12,12 @@ import {
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
AdminPageLayout,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
IconButton,
|
||||
SegmentedControl,
|
||||
@@ -23,6 +25,9 @@ import {
|
||||
SelectionListItem,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
i18nMessage,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -46,6 +51,12 @@ import {
|
||||
type PostboxTemplateCreatePayload,
|
||||
type PostboxTemplateRevisionPayload
|
||||
} from "../../api/postbox";
|
||||
import {
|
||||
POSTBOX_ADMIN_DOCUMENTATION,
|
||||
POSTBOX_FIELD_DOCUMENTATION,
|
||||
POSTBOX_INTERFACE_I18N,
|
||||
postboxBusyReason
|
||||
} from "./interfacePatterns";
|
||||
|
||||
|
||||
type AdminMode = "templates" | "postboxes";
|
||||
@@ -131,8 +142,10 @@ export default function PostboxAdminPanel({
|
||||
const [success, setSuccess] = useState("");
|
||||
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
||||
const [templateDraft, setTemplateDraft] = useState<TemplateDraft>(templateDefaults);
|
||||
const [templateBaseline, setTemplateBaseline] = useState<TemplateDraft>(templateDefaults);
|
||||
const [exactDialogOpen, setExactDialogOpen] = useState(false);
|
||||
const [exactDraft, setExactDraft] = useState<ExactDraft>(exactDefaults);
|
||||
const [exactBaseline, setExactBaseline] = useState<ExactDraft>(exactDefaults);
|
||||
const [materializeDialogOpen, setMaterializeDialogOpen] = useState(false);
|
||||
const [materializeDraft, setMaterializeDraft] = useState<MaterializeDraft>({
|
||||
templateId: "",
|
||||
@@ -140,7 +153,15 @@ export default function PostboxAdminPanel({
|
||||
function_id: "",
|
||||
context_key: ""
|
||||
});
|
||||
const [materializeBaseline, setMaterializeBaseline] = useState<MaterializeDraft>({
|
||||
templateId: "",
|
||||
organization_unit_id: "",
|
||||
function_id: "",
|
||||
context_key: ""
|
||||
});
|
||||
const [archiveTarget, setArchiveTarget] = useState<PostboxDirectoryItem | null>(null);
|
||||
const [retireTarget, setRetireTarget] = useState<PostboxTemplate | null>(null);
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
|
||||
const selectedTemplate = useMemo(
|
||||
() => templates.find((template) => template.id === selectedTemplateId) ?? templates[0] ?? null,
|
||||
@@ -170,6 +191,17 @@ export default function PostboxAdminPanel({
|
||||
}
|
||||
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);
|
||||
@@ -206,14 +238,16 @@ export default function PostboxAdminPanel({
|
||||
}, [load]);
|
||||
|
||||
function openNewTemplate() {
|
||||
setTemplateDraft(templateDefaults());
|
||||
const next = templateDefaults();
|
||||
setTemplateDraft(next);
|
||||
setTemplateBaseline(next);
|
||||
setTemplateDialogOpen(true);
|
||||
}
|
||||
|
||||
function openTemplateRevision(template: PostboxTemplate) {
|
||||
const revision = currentRevision(template);
|
||||
if (!revision) return;
|
||||
setTemplateDraft({
|
||||
const next = {
|
||||
templateId: template.id,
|
||||
slug: template.slug,
|
||||
name: template.name,
|
||||
@@ -226,11 +260,13 @@ export default function PostboxAdminPanel({
|
||||
classification: revision.classification,
|
||||
allow_vacant_delivery: revision.allow_vacant_delivery,
|
||||
routing_policy: revision.routing_policy ?? routingDefaults()
|
||||
});
|
||||
};
|
||||
setTemplateDraft(next);
|
||||
setTemplateBaseline(next);
|
||||
setTemplateDialogOpen(true);
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
async function saveTemplate(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
@@ -255,10 +291,13 @@ export default function PostboxAdminPanel({
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -275,23 +314,9 @@ export default function PostboxAdminPanel({
|
||||
selectedTemplate,
|
||||
selectedTemplate.current_revision
|
||||
);
|
||||
setSuccess(`Published revision ${selectedTemplate.current_revision}.`);
|
||||
await load();
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function retireSelected() {
|
||||
if (!selectedTemplate) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
await retirePostboxTemplate(settings, selectedTemplate);
|
||||
setSuccess("Postbox template retired. Existing addresses remain durable.");
|
||||
setSuccess(i18nMessage("i18n:govoplan-postbox.published_revision_message", {
|
||||
revision: selectedTemplate.current_revision
|
||||
}));
|
||||
await load();
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
@@ -302,15 +327,17 @@ export default function PostboxAdminPanel({
|
||||
|
||||
function openExact() {
|
||||
const unit = units.find((item) => item.functions.length);
|
||||
setExactDraft({
|
||||
const next = {
|
||||
...exactDefaults(),
|
||||
organization_unit_id: unit?.id ?? "",
|
||||
function_id: unit?.functions[0]?.id ?? ""
|
||||
});
|
||||
};
|
||||
setExactDraft(next);
|
||||
setExactBaseline(next);
|
||||
setExactDialogOpen(true);
|
||||
}
|
||||
|
||||
async function saveExact() {
|
||||
async function saveExact(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
@@ -320,11 +347,14 @@ export default function PostboxAdminPanel({
|
||||
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);
|
||||
}
|
||||
@@ -334,16 +364,18 @@ export default function PostboxAdminPanel({
|
||||
const revision = currentRevision(template);
|
||||
const compatible = compatibleTargets(units, revision?.function_type_id);
|
||||
const unit = compatible[0];
|
||||
setMaterializeDraft({
|
||||
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() {
|
||||
async function materialize(): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
@@ -353,11 +385,14 @@ export default function PostboxAdminPanel({
|
||||
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);
|
||||
}
|
||||
@@ -380,6 +415,73 @@ export default function PostboxAdminPanel({
|
||||
}
|
||||
}
|
||||
|
||||
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<boolean> {
|
||||
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 (
|
||||
<AdminPageLayout
|
||||
title="Postboxes"
|
||||
@@ -393,16 +495,18 @@ export default function PostboxAdminPanel({
|
||||
<IconButton
|
||||
label="Refresh"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void load()}
|
||||
disabled={busy}
|
||||
onClick={() => requestDiscard(() => void load())}
|
||||
disabled={loading || busy}
|
||||
disabledReason={postboxBusyReason(loading, busy)}
|
||||
/>
|
||||
<DocumentationHelpLink reference={POSTBOX_ADMIN_DOCUMENTATION} />
|
||||
{mode === "templates" && canManageTemplates ? (
|
||||
<Button variant="primary" onClick={openNewTemplate}>
|
||||
<Button variant="primary" onClick={openNewTemplate} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||
<Plus size={16} /> New template
|
||||
</Button>
|
||||
) : null}
|
||||
{mode === "postboxes" && canManageBindings ? (
|
||||
<Button variant="primary" onClick={openExact}>
|
||||
<Button variant="primary" onClick={openExact} disabled={busy || !units.some((unit) => unit.functions.length)} disabledReason={postboxBusyReason(false, busy) ?? (!units.some((unit) => unit.functions.length) ? POSTBOX_INTERFACE_I18N.organizationTargetSummary : undefined)}>
|
||||
<Plus size={16} /> Exact postbox
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -418,6 +522,22 @@ export default function PostboxAdminPanel({
|
||||
]}
|
||||
ariaLabel="Postbox administration section"
|
||||
/>
|
||||
{mode === "postboxes" && !units.some((unit) => unit.functions.length) ? (
|
||||
<ActionBlockerHint
|
||||
reason={{
|
||||
summary: POSTBOX_INTERFACE_I18N.organizationTargetSummary,
|
||||
requiredAction: POSTBOX_INTERFACE_I18N.organizationTargetAction,
|
||||
actor: POSTBOX_INTERFACE_I18N.organizationTargetActor,
|
||||
target: POSTBOX_INTERFACE_I18N.organizationTargetDestination
|
||||
}}
|
||||
labels={{
|
||||
requiredAction: POSTBOX_INTERFACE_I18N.requiredAction,
|
||||
actor: POSTBOX_INTERFACE_I18N.actor,
|
||||
target: POSTBOX_INTERFACE_I18N.destination
|
||||
}}
|
||||
documentation={POSTBOX_ADMIN_DOCUMENTATION}
|
||||
/>
|
||||
) : null}
|
||||
{mode === "templates" ? (
|
||||
<TemplateWorkspace
|
||||
templates={templates}
|
||||
@@ -425,7 +545,7 @@ export default function PostboxAdminPanel({
|
||||
onSelect={setSelectedTemplateId}
|
||||
onRevise={openTemplateRevision}
|
||||
onPublish={() => void publishSelected()}
|
||||
onRetire={() => void retireSelected()}
|
||||
onRetire={() => selectedTemplate && setRetireTarget(selectedTemplate)}
|
||||
onMaterialize={openMaterialize}
|
||||
busy={busy}
|
||||
canManageBindings={canManageBindings}
|
||||
@@ -450,7 +570,7 @@ export default function PostboxAdminPanel({
|
||||
unitTypes={unitTypes}
|
||||
busy={busy}
|
||||
onChange={setTemplateDraft}
|
||||
onClose={() => setTemplateDialogOpen(false)}
|
||||
onClose={closeTemplateDialog}
|
||||
onSave={() => void saveTemplate()}
|
||||
/>
|
||||
<ExactPostboxDialog
|
||||
@@ -459,7 +579,7 @@ export default function PostboxAdminPanel({
|
||||
units={units}
|
||||
busy={busy}
|
||||
onChange={setExactDraft}
|
||||
onClose={() => setExactDialogOpen(false)}
|
||||
onClose={closeExactDialog}
|
||||
onSave={() => void saveExact()}
|
||||
/>
|
||||
<MaterializeDialog
|
||||
@@ -469,7 +589,7 @@ export default function PostboxAdminPanel({
|
||||
units={units}
|
||||
busy={busy}
|
||||
onChange={setMaterializeDraft}
|
||||
onClose={() => setMaterializeDialogOpen(false)}
|
||||
onClose={closeMaterializeDialog}
|
||||
onSave={() => void materialize()}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
@@ -477,7 +597,7 @@ export default function PostboxAdminPanel({
|
||||
title="Archive Postbox"
|
||||
message={
|
||||
archiveTarget
|
||||
? `Archive "${archiveTarget.name}"? Its messages and delivery evidence remain retained, but the address stops accepting new delivery.`
|
||||
? i18nMessage("i18n:govoplan-postbox.archive_confirmation", { name: archiveTarget.name })
|
||||
: ""
|
||||
}
|
||||
confirmLabel="Archive"
|
||||
@@ -486,6 +606,18 @@ export default function PostboxAdminPanel({
|
||||
onConfirm={() => void confirmArchive()}
|
||||
onCancel={() => setArchiveTarget(null)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={Boolean(retireTarget)}
|
||||
title="Retire Postbox template"
|
||||
message={retireTarget
|
||||
? i18nMessage("i18n:govoplan-postbox.retire_template_confirmation", { name: retireTarget.name })
|
||||
: ""}
|
||||
confirmLabel="Retire"
|
||||
tone="danger"
|
||||
busy={busy}
|
||||
onConfirm={() => void confirmRetire()}
|
||||
onCancel={() => setRetireTarget(null)}
|
||||
/>
|
||||
</AdminPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -529,7 +661,7 @@ function TemplateWorkspace({
|
||||
<StatusBadge status={template.status} />
|
||||
</span>
|
||||
<span className="postbox-item-context">
|
||||
{template.slug} · revision {template.current_revision}
|
||||
{template.slug} · {i18nMessage("i18n:govoplan-postbox.revision_label", { revision: template.current_revision })}
|
||||
</span>
|
||||
</SelectionListItem>
|
||||
))}
|
||||
@@ -546,24 +678,41 @@ function TemplateWorkspace({
|
||||
<p>{selected.description || "No description."}</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => onRevise(selected)} disabled={busy || selected.status === "retired"}>
|
||||
<Button
|
||||
onClick={() => onRevise(selected)}
|
||||
disabled={busy || selected.status === "retired"}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (selected.status === "retired" ? POSTBOX_INTERFACE_I18N.retiredTemplate : undefined)}
|
||||
>
|
||||
<Pencil size={16} /> New revision
|
||||
</Button>
|
||||
<Button onClick={onPublish} disabled={busy || selected.status === "retired" || Boolean(revision.published_at)}>
|
||||
<Button
|
||||
onClick={onPublish}
|
||||
disabled={busy || selected.status === "retired" || Boolean(revision.published_at)}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (selected.status === "retired" ? POSTBOX_INTERFACE_I18N.retiredTemplate : revision.published_at ? POSTBOX_INTERFACE_I18N.publishedRevision : undefined)}
|
||||
>
|
||||
<Save size={16} /> Publish
|
||||
</Button>
|
||||
{canManageBindings ? (
|
||||
<Button onClick={() => onMaterialize(selected)} disabled={busy || selected.status !== "published"}>
|
||||
<Button
|
||||
onClick={() => onMaterialize(selected)}
|
||||
disabled={busy || selected.status !== "published"}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (selected.status !== "published" ? POSTBOX_INTERFACE_I18N.unpublishedTemplate : undefined)}
|
||||
>
|
||||
<Rocket size={16} /> Resolve address
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant="danger" onClick={onRetire} disabled={busy || selected.status === "retired"}>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={onRetire}
|
||||
disabled={busy || selected.status === "retired"}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (selected.status === "retired" ? POSTBOX_INTERFACE_I18N.retiredTemplate : undefined)}
|
||||
>
|
||||
<Trash2 size={16} /> Retire
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="postbox-admin-properties">
|
||||
<div><dt>Revision</dt><dd>{revision.revision}{revision.published_at ? " · published" : " · draft"}</dd></div>
|
||||
<div><dt>Revision</dt><dd>{revision.revision}{revision.published_at ? " · Published" : " · Draft"}</dd></div>
|
||||
<div><dt>Function type</dt><dd>{revision.function_type_id || "Any function type"}</dd></div>
|
||||
<div><dt>Scope</dt><dd>{revision.scope_kind}{revision.scope_id ? ` · ${revision.scope_id}` : ""}</dd></div>
|
||||
<div><dt>Classification</dt><dd>{revision.classification}</dd></div>
|
||||
@@ -572,7 +721,10 @@ function TemplateWorkspace({
|
||||
<dt>Hierarchy copies</dt>
|
||||
<dd>
|
||||
{revision.routing_policy.linked_copy.enabled
|
||||
? `${revision.routing_policy.linked_copy.fanout} · depth ${revision.routing_policy.linked_copy.max_depth}`
|
||||
? i18nMessage("i18n:govoplan-postbox.depth_label", {
|
||||
fanout: revision.routing_policy.linked_copy.fanout,
|
||||
depth: revision.routing_policy.linked_copy.max_depth
|
||||
})
|
||||
: "Disabled"}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -580,7 +732,9 @@ function TemplateWorkspace({
|
||||
<dt>Vacancy escalation</dt>
|
||||
<dd>
|
||||
{revision.routing_policy.attention.mode === "vacancy_escalation"
|
||||
? `${revision.routing_policy.attention.delay_minutes} minutes`
|
||||
? i18nMessage("i18n:govoplan-postbox.minutes_label", {
|
||||
minutes: revision.routing_policy.attention.delay_minutes
|
||||
})
|
||||
: "Disabled"}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -592,7 +746,7 @@ function TemplateWorkspace({
|
||||
<h3>Immutable revisions</h3>
|
||||
{selected.revisions.map((item) => (
|
||||
<div key={item.id}>
|
||||
<strong>Revision {item.revision}</strong>
|
||||
<strong>{i18nMessage("i18n:govoplan-postbox.revision_label", { revision: item.revision })}</strong>
|
||||
<span>{item.classification} · {item.scope_kind}</span>
|
||||
<StatusBadge
|
||||
status={item.published_at ? "published" : "draft"}
|
||||
@@ -664,6 +818,7 @@ function PostboxWorkspace({
|
||||
variant="danger"
|
||||
onClick={() => onArchive(selected)}
|
||||
disabled={busy || selected.status !== "active"}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (selected.status !== "active" ? POSTBOX_INTERFACE_I18N.archivedPostbox : undefined)}
|
||||
>
|
||||
<Archive size={16} /> Archive
|
||||
</Button>
|
||||
@@ -764,36 +919,41 @@ function TemplateDialog({
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button variant="primary" onClick={onSave} disabled={busy || !valid}>
|
||||
<Button onClick={onClose} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={busy || !valid}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (!valid ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
>
|
||||
{isRevision ? "Create revision" : "Create draft"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="postbox-form-grid two-columns">
|
||||
<FormField label="Name">
|
||||
<FormField label="Name" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.name}
|
||||
disabled={isRevision}
|
||||
onChange={(event) => onChange({ ...draft, name: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Slug">
|
||||
<FormField label="Slug" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.slug}
|
||||
disabled={isRevision}
|
||||
onChange={(event) => onChange({ ...draft, slug: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Description">
|
||||
<FormField label="Description" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.description || ""}
|
||||
disabled={isRevision}
|
||||
onChange={(event) => onChange({ ...draft, description: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Function type">
|
||||
<FormField label="Function type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.function_type_id || ""}
|
||||
onChange={(event) => onChange({
|
||||
@@ -807,7 +967,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Scope">
|
||||
<FormField label="Scope" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.scope_kind}
|
||||
onChange={(event) => {
|
||||
@@ -826,7 +986,7 @@ function TemplateDialog({
|
||||
</select>
|
||||
</FormField>
|
||||
{draft.scope_kind !== "tenant" ? (
|
||||
<FormField label="Scope target">
|
||||
<FormField label="Scope target" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.scope_id || ""}
|
||||
onChange={(event) => onChange({ ...draft, scope_id: event.target.value || null })}
|
||||
@@ -838,19 +998,19 @@ function TemplateDialog({
|
||||
</select>
|
||||
</FormField>
|
||||
) : <div />}
|
||||
<FormField label="Name pattern">
|
||||
<FormField label="Name pattern" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.name_pattern}
|
||||
onChange={(event) => onChange({ ...draft, name_pattern: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Address pattern">
|
||||
<FormField label="Address pattern" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.address_pattern}
|
||||
onChange={(event) => onChange({ ...draft, address_pattern: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Classification">
|
||||
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.classification}
|
||||
onChange={(event) => onChange({ ...draft, classification: event.target.value })}
|
||||
@@ -859,6 +1019,7 @@ function TemplateDialog({
|
||||
<div className="postbox-toggle-field">
|
||||
<ToggleSwitch
|
||||
label="Accept delivery while vacant"
|
||||
help="Messages remain institutionally retained even when no incumbent currently has access."
|
||||
checked={draft.allow_vacant_delivery}
|
||||
onChange={(checked) => onChange({ ...draft, allow_vacant_delivery: checked })}
|
||||
/>
|
||||
@@ -868,9 +1029,11 @@ function TemplateDialog({
|
||||
<div>
|
||||
<strong>Hierarchy linked copies</strong>
|
||||
<span>Copy to explicitly bounded function Postboxes in one selected structure.</span>
|
||||
<DocumentationHelpLink reference={POSTBOX_FIELD_DOCUMENTATION} />
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
label="Enable hierarchy linked copies"
|
||||
help="Copies create independent deliveries and evidence at explicitly bounded hierarchy targets."
|
||||
checked={linkedCopy.enabled}
|
||||
onChange={(enabled) => updateLinkedCopy({
|
||||
enabled,
|
||||
@@ -882,7 +1045,7 @@ function TemplateDialog({
|
||||
</div>
|
||||
{linkedCopy.enabled ? (
|
||||
<div className="postbox-form-grid two-columns">
|
||||
<FormField label="Organization structure">
|
||||
<FormField label="Organization structure" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.structure_id || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -896,7 +1059,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Relation type">
|
||||
<FormField label="Relation type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.relation_type_ids[0] || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -911,7 +1074,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Target function type">
|
||||
<FormField label="Target function type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.target_function_type_id || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -924,7 +1087,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Target Postbox template">
|
||||
<FormField label="Target Postbox template" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.target_template_id || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -939,7 +1102,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Maximum hierarchy depth">
|
||||
<FormField label="Maximum hierarchy depth" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -950,7 +1113,7 @@ function TemplateDialog({
|
||||
})}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Copy behavior">
|
||||
<FormField label="Copy behavior" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.fanout}
|
||||
onChange={(event) => {
|
||||
@@ -968,7 +1131,7 @@ function TemplateDialog({
|
||||
<option value="all">All matching ancestors</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Optional stop unit">
|
||||
<FormField label="Optional stop unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.stop_unit_id || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -981,7 +1144,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Optional stop unit type">
|
||||
<FormField label="Optional stop unit type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={linkedCopy.stop_unit_type_id || ""}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -994,7 +1157,7 @@ function TemplateDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Allowed classifications">
|
||||
<FormField label="Allowed classifications" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={linkedCopy.allowed_classifications.join(", ")}
|
||||
onChange={(event) => updateLinkedCopy({
|
||||
@@ -1002,7 +1165,7 @@ function TemplateDialog({
|
||||
})}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Authorized producer modules">
|
||||
<FormField label="Authorized producer modules" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
placeholder="campaigns, workflow"
|
||||
value={linkedCopy.allowed_producer_modules.join(", ")}
|
||||
@@ -1014,11 +1177,12 @@ function TemplateDialog({
|
||||
<div className="postbox-toggle-field">
|
||||
<ToggleSwitch
|
||||
label="Require message expiry"
|
||||
help="Reject copied deliveries that do not carry an explicit expiry boundary."
|
||||
checked={linkedCopy.require_expiry}
|
||||
onChange={(require_expiry) => updateLinkedCopy({ require_expiry })}
|
||||
/>
|
||||
</div>
|
||||
<FormField label="Maximum retention days">
|
||||
<FormField label="Maximum retention days" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -1034,6 +1198,7 @@ function TemplateDialog({
|
||||
<div className="postbox-toggle-field">
|
||||
<ToggleSwitch
|
||||
label="Escalate when the nearest target remains vacant"
|
||||
help="Schedules a separate, auditable delivery only after the configured vacancy delay."
|
||||
checked={attention.mode === "vacancy_escalation"}
|
||||
onChange={(checked) => updateRouting({
|
||||
...draft.routing_policy,
|
||||
@@ -1047,7 +1212,7 @@ function TemplateDialog({
|
||||
/>
|
||||
</div>
|
||||
{attention.mode === "vacancy_escalation" ? (
|
||||
<FormField label="Vacancy escalation delay (minutes)">
|
||||
<FormField label="Vacancy escalation delay (minutes)" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -1104,11 +1269,12 @@ function ExactPostboxDialog({
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button onClick={onClose} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={busy || !draft.name.trim() || !draft.organization_unit_id || !draft.function_id}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? ((!draft.name.trim() || !draft.organization_unit_id || !draft.function_id) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
>
|
||||
Create Postbox
|
||||
</Button>
|
||||
@@ -1116,13 +1282,13 @@ function ExactPostboxDialog({
|
||||
}
|
||||
>
|
||||
<div className="postbox-form-grid two-columns">
|
||||
<FormField label="Name">
|
||||
<FormField label="Name" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.name} onChange={(event) => onChange({ ...draft, name: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Address key">
|
||||
<FormField label="Address key" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.address_key || ""} placeholder="Generated when empty" onChange={(event) => onChange({ ...draft, address_key: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Organization unit">
|
||||
<FormField label="Organization unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.organization_unit_id}
|
||||
onChange={(event) => {
|
||||
@@ -1140,7 +1306,7 @@ function ExactPostboxDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Function">
|
||||
<FormField label="Function" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select value={draft.function_id} onChange={(event) => onChange({ ...draft, function_id: event.target.value })}>
|
||||
<option value="">Select function</option>
|
||||
{(unit?.functions || []).map((fn) => (
|
||||
@@ -1148,10 +1314,10 @@ function ExactPostboxDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Classification">
|
||||
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.classification} onChange={(event) => onChange({ ...draft, classification: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Description">
|
||||
<FormField label="Description" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.description || ""} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -1184,21 +1350,28 @@ function MaterializeDialog({
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={`Resolve address${template ? ` · ${template.name}` : ""}`}
|
||||
title={template
|
||||
? i18nMessage("i18n:govoplan-postbox.resolve_address_title", { name: template.name })
|
||||
: "Resolve address"}
|
||||
className="postbox-dialog"
|
||||
onClose={onClose}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button variant="primary" onClick={onSave} disabled={busy || !draft.organization_unit_id || !draft.function_id}>
|
||||
<Button onClick={onClose} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={busy || !draft.organization_unit_id || !draft.function_id}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? ((!draft.organization_unit_id || !draft.function_id) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
>
|
||||
Resolve address
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="postbox-form-grid">
|
||||
<FormField label="Organization unit">
|
||||
<FormField label="Organization unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.organization_unit_id}
|
||||
onChange={(event) => {
|
||||
@@ -1216,7 +1389,7 @@ function MaterializeDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Function">
|
||||
<FormField label="Function" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select value={draft.function_id} onChange={(event) => onChange({ ...draft, function_id: event.target.value })}>
|
||||
<option value="">Select function</option>
|
||||
{(unit?.functions || []).map((fn) => (
|
||||
@@ -1224,7 +1397,7 @@ function MaterializeDialog({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Optional case or service context">
|
||||
<FormField label="Optional case or service context" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.context_key} onChange={(event) => onChange({ ...draft, context_key: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -1272,3 +1445,7 @@ function errorMessage(error: unknown): string {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
DashboardWidgetList,
|
||||
DismissibleAlert,
|
||||
LoadingFrame,
|
||||
i18nMessage,
|
||||
useDashboardWidgetData,
|
||||
usePlatformLanguage,
|
||||
type ApiSettings,
|
||||
type DashboardWidgetConfiguration
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -23,6 +25,7 @@ export default function PostboxInboxWidget({
|
||||
refreshKey: number;
|
||||
configuration: DashboardWidgetConfiguration;
|
||||
}) {
|
||||
const { language } = usePlatformLanguage();
|
||||
const maxItems = numberSetting(configuration.maxItems, 5, 1, 12);
|
||||
const load = useCallback(async () => {
|
||||
const postboxes = await listPostboxes(settings);
|
||||
@@ -53,7 +56,7 @@ export default function PostboxInboxWidget({
|
||||
id: message.id,
|
||||
title: message.subject,
|
||||
detail: message.sender_label || message.producer_module || "Postbox",
|
||||
meta: new Intl.DateTimeFormat(undefined, {
|
||||
meta: new Intl.DateTimeFormat(language, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
@@ -61,7 +64,7 @@ export default function PostboxInboxWidget({
|
||||
}).format(new Date(message.delivered_at)),
|
||||
leading: <Inbox size={17} aria-hidden="true" />,
|
||||
trailing: message.attachments.length ? (
|
||||
<span title={`${message.attachments.length} attachments`}>
|
||||
<span title={i18nMessage("i18n:govoplan-postbox.attachments_label", { count: message.attachments.length })}>
|
||||
<Paperclip size={15} aria-hidden="true" />
|
||||
</span>
|
||||
) : undefined,
|
||||
@@ -70,7 +73,9 @@ export default function PostboxInboxWidget({
|
||||
/>
|
||||
<div className="dashboard-contribution-footer">
|
||||
<Link className="btn btn-secondary" to="/postbox">
|
||||
{data?.total ? `Open Postbox (${data.total} unread)` : "Open Postbox"}
|
||||
{data?.total
|
||||
? i18nMessage("i18n:govoplan-postbox.open_unread", { count: data.total })
|
||||
: "Open Postbox"}
|
||||
</Link>
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
|
||||
@@ -18,10 +18,13 @@ import {
|
||||
X
|
||||
} from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
DataGridPaginationBar,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
IconButton,
|
||||
SegmentedControl,
|
||||
@@ -30,7 +33,11 @@ import {
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
hasScope,
|
||||
i18nMessage,
|
||||
isApiError,
|
||||
usePlatformLanguage,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -49,6 +56,12 @@ import {
|
||||
type PostboxGrouping,
|
||||
type PostboxMessage
|
||||
} from "../../api/postbox";
|
||||
import {
|
||||
POSTBOX_DOCUMENTATION,
|
||||
POSTBOX_FIELD_DOCUMENTATION,
|
||||
POSTBOX_INTERFACE_I18N,
|
||||
postboxBusyReason
|
||||
} from "./interfacePatterns";
|
||||
|
||||
|
||||
type GroupingDraft = {
|
||||
@@ -114,13 +127,20 @@ export default function PostboxPage({
|
||||
const [error, setError] = useState("");
|
||||
const [groupingDialogOpen, setGroupingDialogOpen] = useState(false);
|
||||
const [groupingDraft, setGroupingDraft] = useState<GroupingDraft>(emptyGrouping);
|
||||
const [groupingBaseline, setGroupingBaseline] = useState<GroupingDraft>(emptyGrouping);
|
||||
const [deleteGroupingTarget, setDeleteGroupingTarget] = useState<GroupingDraft | null>(null);
|
||||
const [messageDialogOpen, setMessageDialogOpen] = useState(false);
|
||||
const [replyParent, setReplyParent] = useState<PostboxMessage | null>(null);
|
||||
const [messageDraft, setMessageDraft] = useState<MessageDraft>(emptyMessageDraft);
|
||||
const [messageBaseline, setMessageBaseline] = useState<MessageDraft>(emptyMessageDraft);
|
||||
const { language } = usePlatformLanguage();
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
|
||||
const canAcknowledge = hasScope(auth, "postbox:message:acknowledge");
|
||||
const canSend = hasScope(auth, "postbox:message:write");
|
||||
const canReply = hasScope(auth, "postbox:message:reply");
|
||||
const groupingDirty = groupingDialogOpen && draftKey(groupingDraft) !== draftKey(groupingBaseline);
|
||||
const messageDirty = messageDialogOpen && draftKey(messageDraft) !== draftKey(messageBaseline);
|
||||
const selectedPostbox = useMemo(
|
||||
() => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? null,
|
||||
[postboxes, selectedPostboxId]
|
||||
@@ -138,6 +158,32 @@ export default function PostboxPage({
|
||||
return postboxes.map((postbox) => postbox.id);
|
||||
}, [postboxes, selectedGrouping, selectedPostboxId]);
|
||||
const scopeKey = scopePostboxIds.join("|");
|
||||
const composeDisabledReason = postboxBusyReason(false, busy)
|
||||
?? (!canSend ? POSTBOX_INTERFACE_I18N.noSendReason : undefined)
|
||||
?? (!postboxes.length ? POSTBOX_INTERFACE_I18N.noPostbox : undefined);
|
||||
const replyDisabledReason = postboxBusyReason(false, busy)
|
||||
?? (!canReply ? POSTBOX_INTERFACE_I18N.noReplyReason : undefined)
|
||||
?? (!selectedMessage ? POSTBOX_INTERFACE_I18N.noMessage : undefined)
|
||||
?? (selectedMessage?.availability !== "available"
|
||||
? POSTBOX_INTERFACE_I18N.unavailableMessage
|
||||
: undefined);
|
||||
const acknowledgeDisabledReason = postboxBusyReason(false, busy)
|
||||
?? (!canAcknowledge ? POSTBOX_INTERFACE_I18N.noAcknowledgeReason : undefined)
|
||||
?? (!selectedMessage ? POSTBOX_INTERFACE_I18N.noMessage : undefined)
|
||||
?? (selectedMessage?.availability !== "available"
|
||||
? POSTBOX_INTERFACE_I18N.unavailableMessage
|
||||
: undefined)
|
||||
?? (selectedMessage?.acknowledged_at
|
||||
? POSTBOX_INTERFACE_I18N.alreadyAcknowledged
|
||||
: undefined);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: groupingDirty || messageDirty,
|
||||
title: "Unsaved Postbox draft",
|
||||
message: "Save or discard the open Postbox draft before leaving this surface.",
|
||||
onSave: () => groupingDirty ? saveGrouping() : submitMessage(),
|
||||
onDiscard: discardOpenDraft
|
||||
});
|
||||
|
||||
const loadDirectory = useCallback(async () => {
|
||||
setLoadingDirectory(true);
|
||||
@@ -336,22 +382,26 @@ export default function PostboxPage({
|
||||
}
|
||||
|
||||
function openNewGrouping() {
|
||||
setGroupingDraft(emptyGrouping());
|
||||
const next = emptyGrouping();
|
||||
setGroupingDraft(next);
|
||||
setGroupingBaseline(next);
|
||||
setGroupingDialogOpen(true);
|
||||
}
|
||||
|
||||
function openGrouping(grouping: PostboxGrouping) {
|
||||
setGroupingDraft({
|
||||
const next = {
|
||||
id: grouping.id,
|
||||
name: grouping.name,
|
||||
is_default: grouping.is_default,
|
||||
postbox_ids: [...grouping.postbox_ids]
|
||||
});
|
||||
};
|
||||
setGroupingDraft(next);
|
||||
setGroupingBaseline(next);
|
||||
setGroupingDialogOpen(true);
|
||||
}
|
||||
|
||||
async function saveGrouping() {
|
||||
if (!groupingDraft.name.trim()) return;
|
||||
async function saveGrouping(): Promise<boolean> {
|
||||
if (!groupingDraft.name.trim()) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const payload = {
|
||||
@@ -368,24 +418,28 @@ export default function PostboxPage({
|
||||
setSelectedScope(saved.id);
|
||||
setSelectedPostboxId("");
|
||||
setGroupingDialogOpen(false);
|
||||
setGroupingBaseline(groupingDraft);
|
||||
return true;
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeGrouping() {
|
||||
if (!groupingDraft.id) return;
|
||||
if (!deleteGroupingTarget?.id) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const existing = groupings.find((item) => item.id === groupingDraft.id);
|
||||
const existing = groupings.find((item) => item.id === deleteGroupingTarget.id);
|
||||
if (!existing) throw new Error("The grouping is no longer available.");
|
||||
await deletePostboxGrouping(settings, existing);
|
||||
setSelectedScope("all");
|
||||
setSelectedPostboxId("");
|
||||
setGroupingDialogOpen(false);
|
||||
setDeleteGroupingTarget(null);
|
||||
await loadDirectory();
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
@@ -398,30 +452,34 @@ export default function PostboxPage({
|
||||
const postbox = selectedPostbox ?? postboxes[0] ?? null;
|
||||
if (!postbox) return;
|
||||
setReplyParent(null);
|
||||
setMessageDraft({
|
||||
const next = {
|
||||
...emptyMessageDraft(),
|
||||
postbox_id: postbox.id,
|
||||
classification: postbox.classification
|
||||
});
|
||||
};
|
||||
setMessageDraft(next);
|
||||
setMessageBaseline(next);
|
||||
setMessageDialogOpen(true);
|
||||
}
|
||||
|
||||
function openReply() {
|
||||
if (!selectedMessage) return;
|
||||
setReplyParent(selectedMessage);
|
||||
setMessageDraft({
|
||||
const next = {
|
||||
...emptyMessageDraft(),
|
||||
postbox_id: selectedMessage.postbox_id,
|
||||
subject: selectedMessage.subject.toLowerCase().startsWith("re:")
|
||||
? selectedMessage.subject
|
||||
: `Re: ${selectedMessage.subject}`,
|
||||
classification: selectedMessage.classification
|
||||
});
|
||||
};
|
||||
setMessageDraft(next);
|
||||
setMessageBaseline(next);
|
||||
setMessageDialogOpen(true);
|
||||
}
|
||||
|
||||
async function submitMessage() {
|
||||
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return;
|
||||
async function submitMessage(): Promise<boolean> {
|
||||
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const participants = messageDraft.recipients
|
||||
@@ -467,13 +525,47 @@ export default function PostboxPage({
|
||||
);
|
||||
setMessages(refreshed.messages);
|
||||
setTotal(refreshed.total);
|
||||
setMessageBaseline(messageDraft);
|
||||
return true;
|
||||
} catch (actionError) {
|
||||
setError(errorMessage(actionError));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function discardOpenDraft() {
|
||||
if (groupingDialogOpen) {
|
||||
setGroupingDraft(groupingBaseline);
|
||||
setGroupingDialogOpen(false);
|
||||
}
|
||||
if (messageDialogOpen) {
|
||||
setMessageDraft(messageBaseline);
|
||||
setMessageDialogOpen(false);
|
||||
setReplyParent(null);
|
||||
}
|
||||
}
|
||||
|
||||
function closeGroupingDialog() {
|
||||
const close = () => {
|
||||
setGroupingDraft(groupingBaseline);
|
||||
setGroupingDialogOpen(false);
|
||||
};
|
||||
if (groupingDirty) requestDiscard(close);
|
||||
else close();
|
||||
}
|
||||
|
||||
function closeMessageDialog() {
|
||||
const close = () => {
|
||||
setMessageDraft(messageBaseline);
|
||||
setMessageDialogOpen(false);
|
||||
setReplyParent(null);
|
||||
};
|
||||
if (messageDirty) requestDiscard(close);
|
||||
else close();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="workspace-data-page module-entry-page postbox-page">
|
||||
<div className="postbox-shell">
|
||||
@@ -482,18 +574,22 @@ export default function PostboxPage({
|
||||
<div className="postbox-bar-title">
|
||||
<Inbox size={17} aria-hidden="true" />
|
||||
<strong>Postbox</strong>
|
||||
<DocumentationHelpLink reference={POSTBOX_DOCUMENTATION} />
|
||||
</div>
|
||||
<div className="postbox-icon-actions">
|
||||
<IconButton
|
||||
label="New unified view"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={openNewGrouping}
|
||||
disabled={busy}
|
||||
disabledReason={postboxBusyReason(false, busy)}
|
||||
/>
|
||||
<IconButton
|
||||
label="Refresh"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void loadDirectory()}
|
||||
onClick={() => requestDiscard(() => void loadDirectory())}
|
||||
disabled={loadingDirectory || busy}
|
||||
disabledReason={postboxBusyReason(loadingDirectory, busy)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,6 +626,20 @@ export default function PostboxPage({
|
||||
<Archive size={20} />
|
||||
<strong>No assigned postboxes</strong>
|
||||
<p>Postboxes appear when your account has a current matching function assignment.</p>
|
||||
<ActionBlockerHint
|
||||
reason={{
|
||||
summary: POSTBOX_INTERFACE_I18N.noPostbox,
|
||||
requiredAction: POSTBOX_INTERFACE_I18N.assignmentAction,
|
||||
actor: POSTBOX_INTERFACE_I18N.assignmentActor,
|
||||
target: POSTBOX_INTERFACE_I18N.assignmentDestination
|
||||
}}
|
||||
labels={{
|
||||
requiredAction: POSTBOX_INTERFACE_I18N.requiredAction,
|
||||
actor: POSTBOX_INTERFACE_I18N.actor,
|
||||
target: POSTBOX_INTERFACE_I18N.destination
|
||||
}}
|
||||
documentation={POSTBOX_DOCUMENTATION}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{postboxes.length ? (
|
||||
@@ -585,13 +695,15 @@ export default function PostboxPage({
|
||||
label="New message"
|
||||
icon={<Send size={16} />}
|
||||
onClick={openCompose}
|
||||
disabled={!canSend || !postboxes.length || busy}
|
||||
disabled={Boolean(composeDisabledReason)}
|
||||
disabledReason={composeDisabledReason}
|
||||
/>
|
||||
<IconButton
|
||||
label="Refresh messages"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void loadMessages()}
|
||||
onClick={() => requestDiscard(() => void loadMessages())}
|
||||
disabled={loadingMessages || busy}
|
||||
disabledReason={postboxBusyReason(loadingMessages, busy)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -679,7 +791,7 @@ export default function PostboxPage({
|
||||
>
|
||||
<span className="postbox-message-heading">
|
||||
<strong>{message.subject}</strong>
|
||||
<time>{formatDate(message.delivered_at)}</time>
|
||||
<time>{formatDate(message.delivered_at, language)}</time>
|
||||
</span>
|
||||
<span className="postbox-message-preview">
|
||||
{message.sender_label || message.producer_module || "Platform"}
|
||||
@@ -734,31 +846,15 @@ export default function PostboxPage({
|
||||
<div className="button-row compact-actions">
|
||||
<Button
|
||||
onClick={openReply}
|
||||
disabled={
|
||||
!canReply ||
|
||||
!selectedMessage ||
|
||||
selectedMessage.availability !== "available" ||
|
||||
busy
|
||||
}
|
||||
disabled={Boolean(replyDisabledReason)}
|
||||
disabledReason={replyDisabledReason}
|
||||
>
|
||||
<Reply size={16} /> Reply
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void acknowledgeSelected()}
|
||||
disabled={
|
||||
!selectedMessage ||
|
||||
selectedMessage.availability !== "available" ||
|
||||
Boolean(selectedMessage.acknowledged_at) ||
|
||||
busy
|
||||
}
|
||||
disabledReason={
|
||||
!canAcknowledge
|
||||
? "You cannot acknowledge Postbox messages."
|
||||
: selectedMessage &&
|
||||
selectedMessage.availability !== "available"
|
||||
? "Withdrawn or expired messages cannot be acknowledged."
|
||||
: undefined
|
||||
}
|
||||
disabled={Boolean(acknowledgeDisabledReason)}
|
||||
disabledReason={acknowledgeDisabledReason}
|
||||
>
|
||||
<CheckCheck size={16} /> Acknowledge
|
||||
</Button>
|
||||
@@ -789,27 +885,29 @@ export default function PostboxPage({
|
||||
open={groupingDialogOpen}
|
||||
title={groupingDraft.id ? "Edit unified view" : "New unified view"}
|
||||
className="postbox-dialog"
|
||||
onClose={() => setGroupingDialogOpen(false)}
|
||||
onClose={closeGroupingDialog}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="postbox-dialog-actions">
|
||||
{groupingDraft.id ? (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => void removeGrouping()}
|
||||
onClick={() => setDeleteGroupingTarget(groupingDraft)}
|
||||
disabled={busy}
|
||||
disabledReason={postboxBusyReason(false, busy)}
|
||||
>
|
||||
<Trash2 size={16} /> Delete
|
||||
</Button>
|
||||
) : <span />}
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => setGroupingDialogOpen(false)} disabled={busy}>
|
||||
<Button onClick={closeGroupingDialog} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void saveGrouping()}
|
||||
disabled={busy || !groupingDraft.name.trim()}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (!groupingDraft.name.trim() ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
@@ -818,7 +916,7 @@ export default function PostboxPage({
|
||||
}
|
||||
>
|
||||
<div className="postbox-form-grid">
|
||||
<FormField label="Name">
|
||||
<FormField label="Name" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={groupingDraft.name}
|
||||
onChange={(event) =>
|
||||
@@ -871,11 +969,11 @@ export default function PostboxPage({
|
||||
open={messageDialogOpen}
|
||||
title={replyParent ? "Reply" : "New message"}
|
||||
className="postbox-dialog postbox-message-dialog"
|
||||
onClose={() => setMessageDialogOpen(false)}
|
||||
onClose={closeMessageDialog}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<div className="postbox-dialog-actions end">
|
||||
<Button onClick={() => setMessageDialogOpen(false)} disabled={busy}>
|
||||
<Button onClick={closeMessageDialog} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -886,6 +984,7 @@ export default function PostboxPage({
|
||||
!messageDraft.postbox_id ||
|
||||
!messageDraft.subject.trim()
|
||||
}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? ((!messageDraft.postbox_id || !messageDraft.subject.trim()) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
>
|
||||
<Send size={16} /> Send
|
||||
</Button>
|
||||
@@ -893,7 +992,7 @@ export default function PostboxPage({
|
||||
}
|
||||
>
|
||||
<div className="postbox-compose-grid">
|
||||
<FormField label="Postbox">
|
||||
<FormField label="Postbox" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={messageDraft.postbox_id}
|
||||
disabled={Boolean(replyParent)}
|
||||
@@ -915,7 +1014,7 @@ export default function PostboxPage({
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Classification">
|
||||
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={messageDraft.classification}
|
||||
onChange={(event) =>
|
||||
@@ -937,7 +1036,7 @@ export default function PostboxPage({
|
||||
</select>
|
||||
</FormField>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Recipients">
|
||||
<FormField label="Recipients" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={messageDraft.recipients}
|
||||
placeholder="Separate addresses with commas"
|
||||
@@ -951,7 +1050,7 @@ export default function PostboxPage({
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Subject">
|
||||
<FormField label="Subject" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={messageDraft.subject}
|
||||
onChange={(event) =>
|
||||
@@ -964,7 +1063,7 @@ export default function PostboxPage({
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="postbox-compose-wide">
|
||||
<FormField label="Message">
|
||||
<FormField label="Message" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<textarea
|
||||
rows={10}
|
||||
value={messageDraft.body_text}
|
||||
@@ -979,6 +1078,19 @@ export default function PostboxPage({
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(deleteGroupingTarget)}
|
||||
title="Delete unified view"
|
||||
message={deleteGroupingTarget
|
||||
? i18nMessage("i18n:govoplan-postbox.delete_grouping_confirmation", { name: deleteGroupingTarget.name })
|
||||
: ""}
|
||||
confirmLabel="Delete"
|
||||
tone="danger"
|
||||
busy={busy}
|
||||
onConfirm={() => void removeGrouping()}
|
||||
onCancel={() => setDeleteGroupingTarget(null)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -990,6 +1102,7 @@ function MessageDetail({
|
||||
message: PostboxMessage;
|
||||
postbox?: PostboxDirectoryItem;
|
||||
}) {
|
||||
const { language } = usePlatformLanguage();
|
||||
return (
|
||||
<div className="postbox-message-detail">
|
||||
{message.availability !== "available" ? (
|
||||
@@ -1014,7 +1127,7 @@ function MessageDetail({
|
||||
<h1>{message.subject}</h1>
|
||||
<div className="postbox-detail-byline">
|
||||
<span>{message.sender_label || message.producer_module || "Platform"}</span>
|
||||
<time>{formatLongDate(message.delivered_at)}</time>
|
||||
<time>{formatLongDate(message.delivered_at, language)}</time>
|
||||
</div>
|
||||
</header>
|
||||
<section className="postbox-provenance">
|
||||
@@ -1122,10 +1235,10 @@ function classificationLabel(value: string): string {
|
||||
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
function formatDate(value: string, language: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
return new Intl.DateTimeFormat(language, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
@@ -1133,10 +1246,10 @@ function formatDate(value: string): string {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatLongDate(value: string): string {
|
||||
function formatLongDate(value: string, language: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
return new Intl.DateTimeFormat(language, {
|
||||
dateStyle: "long",
|
||||
timeStyle: "short"
|
||||
}).format(date);
|
||||
@@ -1145,3 +1258,7 @@ function formatLongDate(value: string): string {
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Postbox request failed";
|
||||
}
|
||||
|
||||
function draftKey(value: unknown): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||
|
||||
export const POSTBOX_DOCUMENTATION = {
|
||||
topicId: "postbox.function-bound-containers",
|
||||
documentationType: "user"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const POSTBOX_ADMIN_DOCUMENTATION = {
|
||||
topicId: "postbox.function-bound-containers",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const POSTBOX_FIELD_DOCUMENTATION = {
|
||||
topicId: "postbox.reference.fields-and-consequences",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const POSTBOX_INTERFACE_I18N = {
|
||||
loading: "i18n:govoplan-postbox.loading_reason",
|
||||
busy: "i18n:govoplan-postbox.busy_reason",
|
||||
noPostbox: "i18n:govoplan-postbox.no_postbox_reason",
|
||||
noMessage: "i18n:govoplan-postbox.no_message_reason",
|
||||
noSendReason: "i18n:govoplan-postbox.no_send_permission_reason",
|
||||
noReplyReason: "i18n:govoplan-postbox.no_reply_permission_reason",
|
||||
noAcknowledgeReason: "i18n:govoplan-postbox.no_acknowledge_permission_reason",
|
||||
unavailableMessage: "i18n:govoplan-postbox.unavailable_message_reason",
|
||||
alreadyAcknowledged: "i18n:govoplan-postbox.already_acknowledged_reason",
|
||||
retiredTemplate: "i18n:govoplan-postbox.retired_template_reason",
|
||||
publishedRevision: "i18n:govoplan-postbox.published_revision_reason",
|
||||
unpublishedTemplate: "i18n:govoplan-postbox.unpublished_template_reason",
|
||||
archivedPostbox: "i18n:govoplan-postbox.archived_postbox_reason",
|
||||
incompleteDraft: "i18n:govoplan-postbox.incomplete_draft_reason",
|
||||
requiredAction: "i18n:govoplan-postbox.required_action",
|
||||
actor: "i18n:govoplan-postbox.responsible_actor",
|
||||
destination: "i18n:govoplan-postbox.destination",
|
||||
assignmentAction: "i18n:govoplan-postbox.assignment_required_action",
|
||||
assignmentActor: "i18n:govoplan-postbox.assignment_responsible_actor",
|
||||
assignmentDestination: "i18n:govoplan-postbox.assignment_destination",
|
||||
organizationTargetSummary: "i18n:govoplan-postbox.organization_target_summary",
|
||||
organizationTargetAction: "i18n:govoplan-postbox.organization_target_action",
|
||||
organizationTargetActor: "i18n:govoplan-postbox.organization_target_actor",
|
||||
organizationTargetDestination: "i18n:govoplan-postbox.organization_target_destination"
|
||||
} as const;
|
||||
|
||||
export function postboxBusyReason(
|
||||
loading: boolean,
|
||||
busy: boolean
|
||||
): string | undefined {
|
||||
if (loading) return POSTBOX_INTERFACE_I18N.loading;
|
||||
if (busy) return POSTBOX_INTERFACE_I18N.busy;
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
const en = {
|
||||
"i18n:govoplan-postbox.postbox": "Postbox",
|
||||
"i18n:govoplan-postbox.postboxes": "Postboxes",
|
||||
"i18n:govoplan-postbox.postbox_inbox": "Postbox inbox",
|
||||
"i18n:govoplan-postbox.postbox_inbox_description": "Unread messages across accessible Postboxes.",
|
||||
"i18n:govoplan-postbox.communication": "Communication",
|
||||
"i18n:govoplan-postbox.maximum_messages": "Maximum messages",
|
||||
"i18n:govoplan-postbox.postbox_directory": "Postbox directory",
|
||||
"i18n:govoplan-postbox.postbox_messages": "Postbox messages",
|
||||
"i18n:govoplan-postbox.postbox_inbox_widget": "Postbox inbox widget",
|
||||
"i18n:govoplan-postbox.postbox_templates_bindings": "Postbox templates and bindings",
|
||||
"i18n:govoplan-postbox.loading_reason": "The Postbox data is still loading.",
|
||||
"i18n:govoplan-postbox.busy_reason": "Another Postbox action is still running.",
|
||||
"i18n:govoplan-postbox.no_postbox_reason": "No accessible Postbox is available for this action.",
|
||||
"i18n:govoplan-postbox.no_message_reason": "Select a message before using this action.",
|
||||
"i18n:govoplan-postbox.no_send_permission_reason": "Your account may not create Postbox messages.",
|
||||
"i18n:govoplan-postbox.no_reply_permission_reason": "Your account may not reply to Postbox messages.",
|
||||
"i18n:govoplan-postbox.no_acknowledge_permission_reason": "Your account may not acknowledge Postbox messages.",
|
||||
"i18n:govoplan-postbox.unavailable_message_reason": "Withdrawn or expired messages cannot be changed.",
|
||||
"i18n:govoplan-postbox.already_acknowledged_reason": "This message is already acknowledged.",
|
||||
"i18n:govoplan-postbox.retired_template_reason": "Retired templates cannot receive new revisions or addresses.",
|
||||
"i18n:govoplan-postbox.published_revision_reason": "This immutable revision is already published.",
|
||||
"i18n:govoplan-postbox.unpublished_template_reason": "Publish the template before resolving an address.",
|
||||
"i18n:govoplan-postbox.archived_postbox_reason": "Only active Postboxes can be archived.",
|
||||
"i18n:govoplan-postbox.incomplete_draft_reason": "Complete all required fields before saving.",
|
||||
"i18n:govoplan-postbox.required_action": "Required action",
|
||||
"i18n:govoplan-postbox.responsible_actor": "Responsible actor",
|
||||
"i18n:govoplan-postbox.destination": "Destination",
|
||||
"i18n:govoplan-postbox.assignment_required_action": "Obtain an effective assignment to a function with a Postbox.",
|
||||
"i18n:govoplan-postbox.assignment_responsible_actor": "An IDM or organization administrator",
|
||||
"i18n:govoplan-postbox.assignment_destination": "IDM function assignments or Organizations",
|
||||
"i18n:govoplan-postbox.organization_target_summary": "No organization function is available for a concrete Postbox address.",
|
||||
"i18n:govoplan-postbox.organization_target_action": "Create or activate an organization unit and function first.",
|
||||
"i18n:govoplan-postbox.organization_target_actor": "An organization administrator",
|
||||
"i18n:govoplan-postbox.organization_target_destination": "Organizations",
|
||||
"i18n:govoplan-postbox.archive_confirmation": "Archive \"{name}\"? Its messages and delivery evidence remain retained, but the address stops accepting new delivery.",
|
||||
"i18n:govoplan-postbox.delete_grouping_confirmation": "Delete the unified view \"{name}\"? Only this personal projection is removed. Source Postboxes, messages, receipts, and evidence remain unchanged.",
|
||||
"i18n:govoplan-postbox.retire_template_confirmation": "Retire \"{name}\"? Published addresses remain durable, but this template can no longer create new revisions or addresses.",
|
||||
"i18n:govoplan-postbox.published_revision_message": "Published revision {revision}.",
|
||||
"i18n:govoplan-postbox.resolve_address_title": "Resolve address · {name}",
|
||||
"i18n:govoplan-postbox.revision_label": "Revision {revision}",
|
||||
"i18n:govoplan-postbox.depth_label": "{fanout} · depth {depth}",
|
||||
"i18n:govoplan-postbox.minutes_label": "{minutes} minutes",
|
||||
"i18n:govoplan-postbox.attachments_label": "{count} attachments",
|
||||
"i18n:govoplan-postbox.open_unread": "Open Postbox ({count} unread)",
|
||||
|
||||
"Refresh": "Refresh",
|
||||
"New template": "New template",
|
||||
"Exact postbox": "Exact postbox",
|
||||
"Templates": "Templates",
|
||||
"Postboxes": "Postboxes",
|
||||
"Postbox administration section": "Postbox administration section",
|
||||
"Archive Postbox": "Archive Postbox",
|
||||
"Archive": "Archive",
|
||||
"No Postbox templates.": "No Postbox templates.",
|
||||
"Postbox templates": "Postbox templates",
|
||||
"Template": "Template",
|
||||
"No description.": "No description.",
|
||||
"New revision": "New revision",
|
||||
"Publish": "Publish",
|
||||
"Resolve address": "Resolve address",
|
||||
"Retire": "Retire",
|
||||
"Retire Postbox template": "Retire Postbox template",
|
||||
"Revision": "Revision",
|
||||
"Function type": "Function type",
|
||||
"Any function type": "Any function type",
|
||||
"Scope": "Scope",
|
||||
"Classification": "Classification",
|
||||
"Vacant delivery": "Vacant delivery",
|
||||
"Accepted": "Accepted",
|
||||
"Blocked": "Blocked",
|
||||
"Hierarchy copies": "Hierarchy copies",
|
||||
"Disabled": "Disabled",
|
||||
"Vacancy escalation": "Vacancy escalation",
|
||||
"Encryption": "Encryption",
|
||||
"Name pattern": "Name pattern",
|
||||
"Address pattern": "Address pattern",
|
||||
"Immutable revisions": "Immutable revisions",
|
||||
"Published": "Published",
|
||||
"Draft": "Draft",
|
||||
"Select a template": "Select a template",
|
||||
"Published revisions lazily resolve stable unit-specific addresses.": "Published revisions lazily resolve stable unit-specific addresses.",
|
||||
"No materialized Postboxes.": "No materialized Postboxes.",
|
||||
"Materialized Postboxes": "Materialized Postboxes",
|
||||
"Organization unit": "Organization unit",
|
||||
"Function": "Function",
|
||||
"Address key": "Address key",
|
||||
"Current holders": "Current holders",
|
||||
"Vacancy": "Vacancy",
|
||||
"Vacant": "Vacant",
|
||||
"Staffed": "Staffed",
|
||||
"Context": "Context",
|
||||
"None": "None",
|
||||
"Template revision": "Template revision",
|
||||
"Exact Postbox": "Exact Postbox",
|
||||
"Select a Postbox": "Select a Postbox",
|
||||
"Materialized Postboxes remain durable through vacancy and reassignment.": "Materialized Postboxes remain durable through vacancy and reassignment.",
|
||||
"Create template revision": "Create template revision",
|
||||
"New Postbox template": "New Postbox template",
|
||||
"Cancel": "Cancel",
|
||||
"Create revision": "Create revision",
|
||||
"Create draft": "Create draft",
|
||||
"Name": "Name",
|
||||
"Slug": "Slug",
|
||||
"Description": "Description",
|
||||
"Tenant": "Tenant",
|
||||
"One unit": "One unit",
|
||||
"Unit subtree": "Unit subtree",
|
||||
"Unit type": "Unit type",
|
||||
"Scope target": "Scope target",
|
||||
"Select target": "Select target",
|
||||
"Accept delivery while vacant": "Accept delivery while vacant",
|
||||
"Hierarchy linked copies": "Hierarchy linked copies",
|
||||
"Copy to explicitly bounded function Postboxes in one selected structure.": "Copy to explicitly bounded function Postboxes in one selected structure.",
|
||||
"Enable hierarchy linked copies": "Enable hierarchy linked copies",
|
||||
"Organization structure": "Organization structure",
|
||||
"Select structure": "Select structure",
|
||||
"Relation type": "Relation type",
|
||||
"All hierarchical relations": "All hierarchical relations",
|
||||
"Target function type": "Target function type",
|
||||
"Select function type": "Select function type",
|
||||
"Target Postbox template": "Target Postbox template",
|
||||
"Select published template": "Select published template",
|
||||
"Maximum hierarchy depth": "Maximum hierarchy depth",
|
||||
"Copy behavior": "Copy behavior",
|
||||
"Nearest matching ancestor": "Nearest matching ancestor",
|
||||
"All matching ancestors": "All matching ancestors",
|
||||
"Optional stop unit": "Optional stop unit",
|
||||
"No unit stop": "No unit stop",
|
||||
"Optional stop unit type": "Optional stop unit type",
|
||||
"No unit-type stop": "No unit-type stop",
|
||||
"Allowed classifications": "Allowed classifications",
|
||||
"Authorized producer modules": "Authorized producer modules",
|
||||
"Require message expiry": "Require message expiry",
|
||||
"Maximum retention days": "Maximum retention days",
|
||||
"Escalate when the nearest target remains vacant": "Escalate when the nearest target remains vacant",
|
||||
"Vacancy escalation delay (minutes)": "Vacancy escalation delay (minutes)",
|
||||
"Available pattern variables include template, unit, function, and optional context names or slugs. Published revisions are immutable.": "Available pattern variables include template, unit, function, and optional context names or slugs. Published revisions are immutable.",
|
||||
"New exact Postbox": "New exact Postbox",
|
||||
"Create Postbox": "Create Postbox",
|
||||
"Generated when empty": "Generated when empty",
|
||||
"Select unit": "Select unit",
|
||||
"Select function": "Select function",
|
||||
"Optional case or service context": "Optional case or service context",
|
||||
"Manage durable organization-function addresses and reusable templates.": "Manage durable organization-function addresses and reusable templates.",
|
||||
"A new immutable template revision was created.": "A new immutable template revision was created.",
|
||||
"Postbox template created as a draft.": "Postbox template created as a draft.",
|
||||
"Postbox template retired. Existing addresses remain durable.": "Postbox template retired. Existing addresses remain durable.",
|
||||
"Exact function-bound Postbox created.": "Exact function-bound Postbox created.",
|
||||
"Stable Postbox address resolved and materialized.": "Stable Postbox address resolved and materialized.",
|
||||
"Postbox archived. Messages and delivery evidence were retained.": "Postbox archived. Messages and delivery evidence were retained.",
|
||||
"The template is no longer available.": "The template is no longer available.",
|
||||
"Postbox request failed": "Postbox request failed",
|
||||
"Unsaved Postbox administration draft": "Unsaved Postbox administration draft",
|
||||
"Save or discard the open Postbox administration draft before leaving this surface.": "Save or discard the open Postbox administration draft before leaving this surface.",
|
||||
"Messages remain institutionally retained even when no incumbent currently has access.": "Messages remain institutionally retained even when no incumbent currently has access.",
|
||||
"Copies create independent deliveries and evidence at explicitly bounded hierarchy targets.": "Copies create independent deliveries and evidence at explicitly bounded hierarchy targets.",
|
||||
"Reject copied deliveries that do not carry an explicit expiry boundary.": "Reject copied deliveries that do not carry an explicit expiry boundary.",
|
||||
"Schedules a separate, auditable delivery only after the configured vacancy delay.": "Schedules a separate, auditable delivery only after the configured vacancy delay.",
|
||||
|
||||
"New unified view": "New unified view",
|
||||
"Delete unified view": "Delete unified view",
|
||||
"Unsaved Postbox draft": "Unsaved Postbox draft",
|
||||
"Save or discard the open Postbox draft before leaving this surface.": "Save or discard the open Postbox draft before leaving this surface.",
|
||||
"The grouping is no longer available.": "The grouping is no longer available.",
|
||||
"Inbox view": "Inbox view",
|
||||
"All postboxes": "All postboxes",
|
||||
"Edit unified view": "Edit unified view",
|
||||
"Loading postboxes": "Loading postboxes",
|
||||
"No assigned postboxes": "No assigned postboxes",
|
||||
"Postboxes appear when your account has a current matching function assignment.": "Postboxes appear when your account has a current matching function assignment.",
|
||||
"Assigned postboxes": "Assigned postboxes",
|
||||
"No organization": "No organization",
|
||||
"No function": "No function",
|
||||
"New message": "New message",
|
||||
"Refresh messages": "Refresh messages",
|
||||
"Search messages": "Search messages",
|
||||
"Search Postbox messages": "Search Postbox messages",
|
||||
"Clear message search": "Clear message search",
|
||||
"Message state": "Message state",
|
||||
"All": "All",
|
||||
"Unread": "Unread",
|
||||
"Read": "Read",
|
||||
"Acknowledged": "Acknowledged",
|
||||
"Loading messages": "Loading messages",
|
||||
"No messages": "No messages",
|
||||
"This view has no delivered Postbox messages.": "This view has no delivered Postbox messages.",
|
||||
"Postbox messages": "Postbox messages",
|
||||
"Withdrawn": "Withdrawn",
|
||||
"Expired": "Expired",
|
||||
"Postbox message pagination": "Postbox message pagination",
|
||||
"Message": "Message",
|
||||
"Reply": "Reply",
|
||||
"Acknowledge": "Acknowledge",
|
||||
"Message unavailable": "Message unavailable",
|
||||
"Select a message": "Select a message",
|
||||
"Source, function context, content, and evidence remain attached to the originating postbox.": "Source, function context, content, and evidence remain attached to the originating postbox.",
|
||||
"Delete": "Delete",
|
||||
"Save": "Save",
|
||||
"Default unified view": "Default unified view",
|
||||
"Source postboxes": "Source postboxes",
|
||||
"Recipients": "Recipients",
|
||||
"Separate addresses with commas": "Separate addresses with commas",
|
||||
"Subject": "Subject",
|
||||
"Send": "Send",
|
||||
"This message was withdrawn. Future access is blocked and audit metadata remains visible. Plaintext already decrypted, copied, exported, or printed cannot be retracted.": "This message was withdrawn. Future access is blocked and audit metadata remains visible. Plaintext already decrypted, copied, exported, or printed cannot be retracted.",
|
||||
"This message has expired. Its audit metadata remains visible, but its content and actions are unavailable.": "This message has expired. Its audit metadata remains visible, but its content and actions are unavailable.",
|
||||
"Platform": "Platform",
|
||||
"Source and responsibility": "Source and responsibility",
|
||||
"Organization": "Organization",
|
||||
"Address": "Address",
|
||||
"Producer": "Producer",
|
||||
"Encryption profile": "Encryption profile",
|
||||
"Key envelopes": "Key envelopes",
|
||||
"External grants": "External grants",
|
||||
"Not recorded": "Not recorded",
|
||||
"Not loaded": "Not loaded",
|
||||
"No plaintext body is available for this message.": "No plaintext body is available for this message.",
|
||||
"Participants": "Participants",
|
||||
"Evidence and attachments": "Evidence and attachments",
|
||||
"No attachment references.": "No attachment references.",
|
||||
"Current access": "Current access",
|
||||
"Platform-native message": "Platform-native message",
|
||||
"Public": "Public",
|
||||
"Internal": "Internal",
|
||||
"Confidential": "Confidential",
|
||||
"Restricted": "Restricted",
|
||||
"No unread Postbox messages.": "No unread Postbox messages.",
|
||||
"Loading unread Postbox messages": "Loading unread Postbox messages",
|
||||
"Open Postbox": "Open Postbox"
|
||||
} satisfies Record<string, string>;
|
||||
|
||||
const de = {
|
||||
...en,
|
||||
"i18n:govoplan-postbox.postbox": "Postfach",
|
||||
"i18n:govoplan-postbox.postboxes": "Postfächer",
|
||||
"i18n:govoplan-postbox.postbox_inbox": "Postfach-Eingang",
|
||||
"i18n:govoplan-postbox.postbox_inbox_description": "Ungelesene Nachrichten aus zugänglichen Postfächern.",
|
||||
"i18n:govoplan-postbox.communication": "Kommunikation",
|
||||
"i18n:govoplan-postbox.maximum_messages": "Maximale Nachrichtenanzahl",
|
||||
"i18n:govoplan-postbox.postbox_directory": "Postfachverzeichnis",
|
||||
"i18n:govoplan-postbox.postbox_messages": "Postfachnachrichten",
|
||||
"i18n:govoplan-postbox.postbox_inbox_widget": "Postfach-Eingangs-Widget",
|
||||
"i18n:govoplan-postbox.postbox_templates_bindings": "Postfachvorlagen und Zuordnungen",
|
||||
"i18n:govoplan-postbox.loading_reason": "Die Postfachdaten werden noch geladen.",
|
||||
"i18n:govoplan-postbox.busy_reason": "Eine andere Postfachaktion läuft noch.",
|
||||
"i18n:govoplan-postbox.no_postbox_reason": "Für diese Aktion ist kein zugängliches Postfach verfügbar.",
|
||||
"i18n:govoplan-postbox.no_message_reason": "Wählen Sie zuerst eine Nachricht aus.",
|
||||
"i18n:govoplan-postbox.no_send_permission_reason": "Ihr Konto darf keine Postfachnachrichten erstellen.",
|
||||
"i18n:govoplan-postbox.no_reply_permission_reason": "Ihr Konto darf nicht auf Postfachnachrichten antworten.",
|
||||
"i18n:govoplan-postbox.no_acknowledge_permission_reason": "Ihr Konto darf Postfachnachrichten nicht bestätigen.",
|
||||
"i18n:govoplan-postbox.unavailable_message_reason": "Zurückgezogene oder abgelaufene Nachrichten können nicht geändert werden.",
|
||||
"i18n:govoplan-postbox.already_acknowledged_reason": "Diese Nachricht wurde bereits bestätigt.",
|
||||
"i18n:govoplan-postbox.retired_template_reason": "Stillgelegte Vorlagen können keine neuen Revisionen oder Adressen erhalten.",
|
||||
"i18n:govoplan-postbox.published_revision_reason": "Diese unveränderliche Revision ist bereits veröffentlicht.",
|
||||
"i18n:govoplan-postbox.unpublished_template_reason": "Veröffentlichen Sie die Vorlage, bevor Sie eine Adresse auflösen.",
|
||||
"i18n:govoplan-postbox.archived_postbox_reason": "Nur aktive Postfächer können archiviert werden.",
|
||||
"i18n:govoplan-postbox.incomplete_draft_reason": "Füllen Sie vor dem Speichern alle Pflichtfelder aus.",
|
||||
"i18n:govoplan-postbox.required_action": "Erforderliche Aktion",
|
||||
"i18n:govoplan-postbox.responsible_actor": "Verantwortliche Stelle",
|
||||
"i18n:govoplan-postbox.destination": "Ziel",
|
||||
"i18n:govoplan-postbox.assignment_required_action": "Eine wirksame Zuordnung zu einer Funktion mit Postfach erhalten.",
|
||||
"i18n:govoplan-postbox.assignment_responsible_actor": "IDM- oder Organisationsadministration",
|
||||
"i18n:govoplan-postbox.assignment_destination": "IDM-Funktionszuordnungen oder Organisationen",
|
||||
"i18n:govoplan-postbox.organization_target_summary": "Für eine konkrete Postfachadresse ist keine Organisationsfunktion verfügbar.",
|
||||
"i18n:govoplan-postbox.organization_target_action": "Erstellen oder aktivieren Sie zuerst eine Organisationseinheit und Funktion.",
|
||||
"i18n:govoplan-postbox.organization_target_actor": "Organisationsadministration",
|
||||
"i18n:govoplan-postbox.organization_target_destination": "Organisationen",
|
||||
"i18n:govoplan-postbox.archive_confirmation": "\"{name}\" archivieren? Nachrichten und Zustellnachweise bleiben erhalten, die Adresse nimmt jedoch keine neuen Zustellungen an.",
|
||||
"i18n:govoplan-postbox.delete_grouping_confirmation": "Die zusammengeführte Ansicht \"{name}\" löschen? Nur diese persönliche Projektion wird entfernt. Quellpostfächer, Nachrichten, Bestätigungen und Nachweise bleiben unverändert.",
|
||||
"i18n:govoplan-postbox.retire_template_confirmation": "\"{name}\" stilllegen? Veröffentlichte Adressen bleiben dauerhaft, diese Vorlage kann jedoch keine neuen Revisionen oder Adressen mehr erstellen.",
|
||||
"i18n:govoplan-postbox.published_revision_message": "Revision {revision} veröffentlicht.",
|
||||
"i18n:govoplan-postbox.resolve_address_title": "Adresse auflösen · {name}",
|
||||
"i18n:govoplan-postbox.revision_label": "Revision {revision}",
|
||||
"i18n:govoplan-postbox.depth_label": "{fanout} · Tiefe {depth}",
|
||||
"i18n:govoplan-postbox.minutes_label": "{minutes} Minuten",
|
||||
"i18n:govoplan-postbox.attachments_label": "{count} Anhänge",
|
||||
"i18n:govoplan-postbox.open_unread": "Postfach öffnen ({count} ungelesen)",
|
||||
|
||||
"Refresh": "Aktualisieren",
|
||||
"New template": "Neue Vorlage",
|
||||
"Exact postbox": "Konkretes Postfach",
|
||||
"Templates": "Vorlagen",
|
||||
"Postboxes": "Postfächer",
|
||||
"Postbox administration section": "Postfach-Verwaltungsbereich",
|
||||
"Archive Postbox": "Postfach archivieren",
|
||||
"Archive": "Archivieren",
|
||||
"No Postbox templates.": "Keine Postfachvorlagen vorhanden.",
|
||||
"Postbox templates": "Postfachvorlagen",
|
||||
"Template": "Vorlage",
|
||||
"No description.": "Keine Beschreibung.",
|
||||
"New revision": "Neue Revision",
|
||||
"Publish": "Veröffentlichen",
|
||||
"Resolve address": "Adresse auflösen",
|
||||
"Retire": "Stilllegen",
|
||||
"Retire Postbox template": "Postfachvorlage stilllegen",
|
||||
"Revision": "Revision",
|
||||
"Function type": "Funktionstyp",
|
||||
"Any function type": "Beliebiger Funktionstyp",
|
||||
"Scope": "Geltungsbereich",
|
||||
"Classification": "Klassifizierung",
|
||||
"Vacant delivery": "Zustellung bei Vakanz",
|
||||
"Accepted": "Angenommen",
|
||||
"Blocked": "Blockiert",
|
||||
"Hierarchy copies": "Hierarchiekopien",
|
||||
"Disabled": "Deaktiviert",
|
||||
"Vacancy escalation": "Vakanzeskalation",
|
||||
"Encryption": "Verschlüsselung",
|
||||
"Name pattern": "Namensmuster",
|
||||
"Address pattern": "Adressmuster",
|
||||
"Immutable revisions": "Unveränderliche Revisionen",
|
||||
"Published": "Veröffentlicht",
|
||||
"Draft": "Entwurf",
|
||||
"Select a template": "Vorlage auswählen",
|
||||
"Published revisions lazily resolve stable unit-specific addresses.": "Veröffentlichte Revisionen lösen stabile einheitsspezifische Adressen bei Bedarf auf.",
|
||||
"No materialized Postboxes.": "Keine materialisierten Postfächer.",
|
||||
"Materialized Postboxes": "Materialisierte Postfächer",
|
||||
"Organization unit": "Organisationseinheit",
|
||||
"Function": "Funktion",
|
||||
"Address key": "Adressschlüssel",
|
||||
"Current holders": "Aktuelle Inhaber",
|
||||
"Vacancy": "Vakanz",
|
||||
"Vacant": "Unbesetzt",
|
||||
"Staffed": "Besetzt",
|
||||
"Context": "Kontext",
|
||||
"None": "Keiner",
|
||||
"Template revision": "Vorlagenrevision",
|
||||
"Exact Postbox": "Konkretes Postfach",
|
||||
"Select a Postbox": "Postfach auswählen",
|
||||
"Materialized Postboxes remain durable through vacancy and reassignment.": "Materialisierte Postfächer bleiben bei Vakanz und Neuzuordnung dauerhaft bestehen.",
|
||||
"Create template revision": "Vorlagenrevision erstellen",
|
||||
"New Postbox template": "Neue Postfachvorlage",
|
||||
"Cancel": "Abbrechen",
|
||||
"Create revision": "Revision erstellen",
|
||||
"Create draft": "Entwurf erstellen",
|
||||
"Name": "Name",
|
||||
"Slug": "Kurzname",
|
||||
"Description": "Beschreibung",
|
||||
"Tenant": "Mandant",
|
||||
"One unit": "Eine Einheit",
|
||||
"Unit subtree": "Teilbaum einer Einheit",
|
||||
"Unit type": "Einheitstyp",
|
||||
"Scope target": "Ziel des Geltungsbereichs",
|
||||
"Select target": "Ziel auswählen",
|
||||
"Accept delivery while vacant": "Zustellung bei Vakanz annehmen",
|
||||
"Hierarchy linked copies": "Verknüpfte Hierarchiekopien",
|
||||
"Copy to explicitly bounded function Postboxes in one selected structure.": "In ausdrücklich begrenzte Funktionspostfächer einer ausgewählten Struktur kopieren.",
|
||||
"Enable hierarchy linked copies": "Verknüpfte Hierarchiekopien aktivieren",
|
||||
"Organization structure": "Organisationsstruktur",
|
||||
"Select structure": "Struktur auswählen",
|
||||
"Relation type": "Beziehungstyp",
|
||||
"All hierarchical relations": "Alle hierarchischen Beziehungen",
|
||||
"Target function type": "Zielfunktionstyp",
|
||||
"Select function type": "Funktionstyp auswählen",
|
||||
"Target Postbox template": "Ziel-Postfachvorlage",
|
||||
"Select published template": "Veröffentlichte Vorlage auswählen",
|
||||
"Maximum hierarchy depth": "Maximale Hierarchietiefe",
|
||||
"Copy behavior": "Kopierverhalten",
|
||||
"Nearest matching ancestor": "Nächster passender Vorfahr",
|
||||
"All matching ancestors": "Alle passenden Vorfahren",
|
||||
"Optional stop unit": "Optionale Stoppeinheit",
|
||||
"No unit stop": "Keine Stoppeinheit",
|
||||
"Optional stop unit type": "Optionaler Stoppeinheitstyp",
|
||||
"No unit-type stop": "Kein Einheitstyp-Stopp",
|
||||
"Allowed classifications": "Zulässige Klassifizierungen",
|
||||
"Authorized producer modules": "Autorisierte Erzeugermodule",
|
||||
"Require message expiry": "Ablauf der Nachricht verlangen",
|
||||
"Maximum retention days": "Maximale Aufbewahrungstage",
|
||||
"Escalate when the nearest target remains vacant": "Eskalieren, wenn das nächste Ziel unbesetzt bleibt",
|
||||
"Vacancy escalation delay (minutes)": "Verzögerung der Vakanzeskalation (Minuten)",
|
||||
"Available pattern variables include template, unit, function, and optional context names or slugs. Published revisions are immutable.": "Verfügbare Mustervariablen umfassen Vorlage, Einheit, Funktion und optionale Kontextnamen oder Kurznamen. Veröffentlichte Revisionen sind unveränderlich.",
|
||||
"New exact Postbox": "Neues konkretes Postfach",
|
||||
"Create Postbox": "Postfach erstellen",
|
||||
"Generated when empty": "Wird leer automatisch erzeugt",
|
||||
"Select unit": "Einheit auswählen",
|
||||
"Select function": "Funktion auswählen",
|
||||
"Optional case or service context": "Optionaler Fall- oder Dienstkontext",
|
||||
"Manage durable organization-function addresses and reusable templates.": "Dauerhafte Organisationsfunktionsadressen und wiederverwendbare Vorlagen verwalten.",
|
||||
"A new immutable template revision was created.": "Eine neue unveränderliche Vorlagenrevision wurde erstellt.",
|
||||
"Postbox template created as a draft.": "Postfachvorlage wurde als Entwurf erstellt.",
|
||||
"Postbox template retired. Existing addresses remain durable.": "Postfachvorlage wurde stillgelegt. Bestehende Adressen bleiben dauerhaft.",
|
||||
"Exact function-bound Postbox created.": "Konkretes funktionsgebundenes Postfach wurde erstellt.",
|
||||
"Stable Postbox address resolved and materialized.": "Stabile Postfachadresse wurde aufgelöst und materialisiert.",
|
||||
"Postbox archived. Messages and delivery evidence were retained.": "Postfach wurde archiviert. Nachrichten und Zustellnachweise blieben erhalten.",
|
||||
"The template is no longer available.": "Die Vorlage ist nicht mehr verfügbar.",
|
||||
"Postbox request failed": "Postfachanfrage fehlgeschlagen",
|
||||
"Unsaved Postbox administration draft": "Ungespeicherter Postfach-Verwaltungsentwurf",
|
||||
"Save or discard the open Postbox administration draft before leaving this surface.": "Speichern oder verwerfen Sie den offenen Postfach-Verwaltungsentwurf, bevor Sie diesen Bereich verlassen.",
|
||||
"Messages remain institutionally retained even when no incumbent currently has access.": "Nachrichten bleiben institutionell erhalten, auch wenn aktuell kein Funktionsinhaber Zugriff hat.",
|
||||
"Copies create independent deliveries and evidence at explicitly bounded hierarchy targets.": "Kopien erzeugen eigenständige Zustellungen und Nachweise an ausdrücklich begrenzten Hierarchiezielen.",
|
||||
"Reject copied deliveries that do not carry an explicit expiry boundary.": "Kopierte Zustellungen ohne ausdrückliche Ablaufgrenze ablehnen.",
|
||||
"Schedules a separate, auditable delivery only after the configured vacancy delay.": "Plant erst nach der konfigurierten Vakanzverzögerung eine separate, prüfbare Zustellung.",
|
||||
|
||||
"New unified view": "Neue zusammengeführte Ansicht",
|
||||
"Delete unified view": "Zusammengeführte Ansicht löschen",
|
||||
"Unsaved Postbox draft": "Ungespeicherter Postfachentwurf",
|
||||
"Save or discard the open Postbox draft before leaving this surface.": "Speichern oder verwerfen Sie den offenen Postfachentwurf, bevor Sie diesen Bereich verlassen.",
|
||||
"The grouping is no longer available.": "Die zusammengeführte Ansicht ist nicht mehr verfügbar.",
|
||||
"Inbox view": "Eingangsansicht",
|
||||
"All postboxes": "Alle Postfächer",
|
||||
"Edit unified view": "Zusammengeführte Ansicht bearbeiten",
|
||||
"Loading postboxes": "Postfächer werden geladen",
|
||||
"No assigned postboxes": "Keine zugewiesenen Postfächer",
|
||||
"Postboxes appear when your account has a current matching function assignment.": "Postfächer erscheinen, wenn Ihr Konto eine aktuelle passende Funktionszuordnung hat.",
|
||||
"Assigned postboxes": "Zugewiesene Postfächer",
|
||||
"No organization": "Keine Organisation",
|
||||
"No function": "Keine Funktion",
|
||||
"New message": "Neue Nachricht",
|
||||
"Refresh messages": "Nachrichten aktualisieren",
|
||||
"Search messages": "Nachrichten suchen",
|
||||
"Search Postbox messages": "Postfachnachrichten suchen",
|
||||
"Clear message search": "Nachrichtensuche leeren",
|
||||
"Message state": "Nachrichtenstatus",
|
||||
"All": "Alle",
|
||||
"Unread": "Ungelesen",
|
||||
"Read": "Gelesen",
|
||||
"Acknowledged": "Bestätigt",
|
||||
"Loading messages": "Nachrichten werden geladen",
|
||||
"No messages": "Keine Nachrichten",
|
||||
"This view has no delivered Postbox messages.": "Diese Ansicht enthält keine zugestellten Postfachnachrichten.",
|
||||
"Postbox messages": "Postfachnachrichten",
|
||||
"Withdrawn": "Zurückgezogen",
|
||||
"Expired": "Abgelaufen",
|
||||
"Postbox message pagination": "Seitennavigation der Postfachnachrichten",
|
||||
"Message": "Nachricht",
|
||||
"Reply": "Antworten",
|
||||
"Acknowledge": "Bestätigen",
|
||||
"Message unavailable": "Nachricht nicht verfügbar",
|
||||
"Select a message": "Nachricht auswählen",
|
||||
"Source, function context, content, and evidence remain attached to the originating postbox.": "Quelle, Funktionskontext, Inhalt und Nachweise bleiben dem Ursprungspostfach zugeordnet.",
|
||||
"Delete": "Löschen",
|
||||
"Save": "Speichern",
|
||||
"Default unified view": "Standardansicht",
|
||||
"Source postboxes": "Quellpostfächer",
|
||||
"Recipients": "Empfänger",
|
||||
"Separate addresses with commas": "Adressen durch Kommas trennen",
|
||||
"Subject": "Betreff",
|
||||
"Send": "Senden",
|
||||
"This message was withdrawn. Future access is blocked and audit metadata remains visible. Plaintext already decrypted, copied, exported, or printed cannot be retracted.": "Diese Nachricht wurde zurückgezogen. Künftiger Zugriff ist gesperrt und Prüfmetadaten bleiben sichtbar. Bereits entschlüsselter, kopierter, exportierter oder gedruckter Klartext kann nicht zurückgerufen werden.",
|
||||
"This message has expired. Its audit metadata remains visible, but its content and actions are unavailable.": "Diese Nachricht ist abgelaufen. Ihre Prüfmetadaten bleiben sichtbar, Inhalt und Aktionen sind jedoch nicht verfügbar.",
|
||||
"Platform": "Plattform",
|
||||
"Source and responsibility": "Quelle und Verantwortung",
|
||||
"Organization": "Organisation",
|
||||
"Address": "Adresse",
|
||||
"Producer": "Erzeuger",
|
||||
"Encryption profile": "Verschlüsselungsprofil",
|
||||
"Key envelopes": "Schlüsselumschläge",
|
||||
"External grants": "Externe Freigaben",
|
||||
"Not recorded": "Nicht erfasst",
|
||||
"Not loaded": "Nicht geladen",
|
||||
"No plaintext body is available for this message.": "Für diese Nachricht ist kein Klartextinhalt verfügbar.",
|
||||
"Participants": "Beteiligte",
|
||||
"Evidence and attachments": "Nachweise und Anhänge",
|
||||
"No attachment references.": "Keine Anhangsreferenzen.",
|
||||
"Current access": "Aktueller Zugriff",
|
||||
"Platform-native message": "Plattformeigene Nachricht",
|
||||
"Public": "Öffentlich",
|
||||
"Internal": "Intern",
|
||||
"Confidential": "Vertraulich",
|
||||
"Restricted": "Beschränkt",
|
||||
"No unread Postbox messages.": "Keine ungelesenen Postfachnachrichten.",
|
||||
"Loading unread Postbox messages": "Ungelesene Postfachnachrichten werden geladen",
|
||||
"Open Postbox": "Postfach öffnen"
|
||||
} satisfies Record<keyof typeof en, string>;
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||
+19
-12
@@ -5,6 +5,7 @@ import {
|
||||
type DashboardWidgetsUiCapability,
|
||||
type PlatformWebModule
|
||||
} from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import PostboxInboxWidget from "./features/postbox/PostboxInboxWidget";
|
||||
import "./styles/postbox.css";
|
||||
|
||||
@@ -14,16 +15,21 @@ const PostboxAdminPanel = lazy(
|
||||
() => import("./features/postbox/PostboxAdminPanel")
|
||||
);
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
};
|
||||
|
||||
const readScope = ["postbox:postbox:read"];
|
||||
const postboxDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||
widgets: [
|
||||
{
|
||||
id: "postbox.inbox",
|
||||
surfaceId: "postbox.widget.inbox",
|
||||
title: "Postbox inbox",
|
||||
description: "Unread messages across accessible Postboxes.",
|
||||
title: "i18n:govoplan-postbox.postbox_inbox",
|
||||
description: "i18n:govoplan-postbox.postbox_inbox_description",
|
||||
moduleId: "postbox",
|
||||
category: "Communication",
|
||||
category: "i18n:govoplan-postbox.communication",
|
||||
order: 55,
|
||||
defaultVisible: false,
|
||||
defaultSize: "medium",
|
||||
@@ -36,7 +42,7 @@ const postboxDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||
configurationFields: [
|
||||
{
|
||||
id: "maxItems",
|
||||
label: "Maximum messages",
|
||||
label: "i18n:govoplan-postbox.maximum_messages",
|
||||
kind: "number",
|
||||
min: 1,
|
||||
max: 12,
|
||||
@@ -60,7 +66,7 @@ const postboxAdminSections: AdminSectionsUiCapability = {
|
||||
id: "postbox",
|
||||
moduleId: "postbox",
|
||||
kind: "management",
|
||||
label: "Postboxes",
|
||||
label: "i18n:govoplan-postbox.postboxes",
|
||||
group: "TENANT",
|
||||
order: 45,
|
||||
surfaceId: "postbox.admin.templates",
|
||||
@@ -80,8 +86,8 @@ const postboxAdminSections: AdminSectionsUiCapability = {
|
||||
|
||||
export const postboxModule: PlatformWebModule = {
|
||||
id: "postbox",
|
||||
label: "Postbox",
|
||||
version: "0.1.0",
|
||||
label: "i18n:govoplan-postbox.postbox",
|
||||
version: "0.1.2",
|
||||
dependencies: ["identity", "organizations", "idm"],
|
||||
optionalDependencies: [
|
||||
"access",
|
||||
@@ -95,10 +101,11 @@ export const postboxModule: PlatformWebModule = {
|
||||
"views",
|
||||
"workflow"
|
||||
],
|
||||
translations,
|
||||
navItems: [
|
||||
{
|
||||
to: "/postbox",
|
||||
label: "Postbox",
|
||||
label: "i18n:govoplan-postbox.postbox",
|
||||
iconName: "inbox",
|
||||
anyOf: readScope,
|
||||
order: 58
|
||||
@@ -119,28 +126,28 @@ export const postboxModule: PlatformWebModule = {
|
||||
id: "postbox.inbox.directory",
|
||||
moduleId: "postbox",
|
||||
kind: "section",
|
||||
label: "Postbox directory",
|
||||
label: "i18n:govoplan-postbox.postbox_directory",
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
id: "postbox.inbox.messages",
|
||||
moduleId: "postbox",
|
||||
kind: "section",
|
||||
label: "Postbox messages",
|
||||
label: "i18n:govoplan-postbox.postbox_messages",
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
id: "postbox.widget.inbox",
|
||||
moduleId: "postbox",
|
||||
kind: "section",
|
||||
label: "Postbox inbox widget",
|
||||
label: "i18n:govoplan-postbox.postbox_inbox_widget",
|
||||
order: 25
|
||||
},
|
||||
{
|
||||
id: "postbox.admin.templates",
|
||||
moduleId: "postbox",
|
||||
kind: "section",
|
||||
label: "Postbox templates and bindings",
|
||||
label: "i18n:govoplan-postbox.postbox_templates_bindings",
|
||||
order: 30
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user