Migrate Postbox interface patterns
This commit is contained in:
@@ -73,6 +73,10 @@ Frontend package:
|
|||||||
|
|
||||||
Platform RBAC, module capability contracts, and governance rules are documented in `govoplan-core/docs/`.
|
Platform RBAC, module capability contracts, and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|
||||||
|
The module's interface archetypes, consequence classes, contextual-help
|
||||||
|
contracts, and accessibility evidence are recorded in
|
||||||
|
[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md).
|
||||||
|
|
||||||
## Current implementation
|
## Current implementation
|
||||||
|
|
||||||
The first usable slice includes immutable template revisions, stable lazy
|
The first usable slice includes immutable template revisions, stable lazy
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Postbox Interface Pattern Migration
|
||||||
|
|
||||||
|
This document records the bounded migration of Postbox-owned WebUI surfaces to
|
||||||
|
the GovOPlaN interface pattern language. Core owns shared controls and host
|
||||||
|
shells. Postbox owns function-bound addresses, messages, receipts, delivery
|
||||||
|
evidence, personal inbox projections, and reusable address templates.
|
||||||
|
|
||||||
|
## Surface Inventory
|
||||||
|
|
||||||
|
| Surface | Archetype | Consequence class | Contract |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/postbox` directory | Directory and hierarchy-context selector | Change query projection | Shared selection list, explicit assignment blocker, contextual help, guarded reload |
|
||||||
|
| `/postbox` messages | Searchable work queue | Read, acknowledge, reply, or author | Shared filters/pagination/status/alerts, explained unavailable actions, privacy-safe retained states |
|
||||||
|
| Message detail and composer | Record detail and consequential editor | Deliver institutional message | Source/function provenance, classification boundary, guarded draft, stable field help |
|
||||||
|
| Unified-view dialog | Personal configuration editor | Change or delete query projection | Shared dialog/toggle/confirmation; deletion never changes source messages or evidence |
|
||||||
|
| `postbox.admin.templates` | Versioned definition library | Publish or retire immutable template revision | Shared admin layout, durable consequence explanations, guarded editor, contextual help |
|
||||||
|
| Materialized Postbox administration | Repeated administration | Create or archive durable address | Organization/function selector, prerequisite blocker, destructive confirmation, retained-evidence explanation |
|
||||||
|
| `postbox.widget.inbox` | Dashboard summary | Navigate to unread work | Shared widget loading/error/list contract and localized accessible attachment metadata |
|
||||||
|
|
||||||
|
## Consequence And Availability Rules
|
||||||
|
|
||||||
|
- A Postbox address belongs to an organization function, not to an account.
|
||||||
|
Current effective IDM assignments determine access without changing the
|
||||||
|
durable message or address record.
|
||||||
|
- Publishing freezes an immutable template revision. Retiring a template stops
|
||||||
|
future revision and materialization work but leaves existing addresses
|
||||||
|
intact.
|
||||||
|
- Archiving an address stops new delivery while preserving messages, receipts,
|
||||||
|
access events, and delivery evidence.
|
||||||
|
- Unified inbox views are personal projections. Deleting one does not move or
|
||||||
|
delete source Postboxes, messages, acknowledgements, or evidence.
|
||||||
|
- Hierarchy copies are separate, bounded deliveries. Vacancy escalation is a
|
||||||
|
separately scheduled and auditable route, not an implicit personal grant.
|
||||||
|
- Withdrawal and expiry prevent future content access while retaining only the
|
||||||
|
metadata the current actor may inspect. Already exported or printed plaintext
|
||||||
|
cannot be retracted.
|
||||||
|
- Disabled actions state whether the blocker is loading, another operation, a
|
||||||
|
missing permission, a missing assignment, an unavailable message, or a
|
||||||
|
lifecycle constraint.
|
||||||
|
|
||||||
|
## State And Accessibility Evidence
|
||||||
|
|
||||||
|
The module uses Core admin/page shells, selection lists, pagination, dialogs,
|
||||||
|
confirmations, alerts, status badges, action blockers, contextual field help,
|
||||||
|
disabled reasons, and unsaved-change guards. Shared dialogs retain keyboard
|
||||||
|
focus and return behavior. Existing responsive workspace CSS keeps directory,
|
||||||
|
message list, and detail regions bounded and independently scrollable.
|
||||||
|
|
||||||
|
English and German catalogues cover module metadata, accessible attributes,
|
||||||
|
workspaces, dialogs, fields, lifecycle states, and dashboard output. Dates use
|
||||||
|
the selected platform locale. Manifest topics publish stable route, surface,
|
||||||
|
field, blocker, privacy, and consequence references. Focused backend and WebUI
|
||||||
|
tests pin these contracts without importing optional sibling modules.
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ from govoplan_core.core.module_guards import (
|
|||||||
persistent_table_uninstall_guard,
|
persistent_table_uninstall_guard,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
|
DocumentationLink,
|
||||||
DocumentationTopic,
|
DocumentationTopic,
|
||||||
FrontendModule,
|
FrontendModule,
|
||||||
FrontendRoute,
|
FrontendRoute,
|
||||||
@@ -375,8 +376,8 @@ manifest = ModuleManifest(
|
|||||||
"reassignment. Current access combines a generic Postbox "
|
"reassignment. Current access combines a generic Postbox "
|
||||||
"permission with effective IDM assignment context. Templates "
|
"permission with effective IDM assignment context. Templates "
|
||||||
"can lazily materialize unit-specific addresses, while exact "
|
"can lazily materialize unit-specific addresses, while exact "
|
||||||
"postboxes cover exceptional responsibilities. The first "
|
"postboxes cover exceptional responsibilities. Plaintext "
|
||||||
"Plaintext Postboxes remain available without Encryption. A "
|
"Postboxes remain available without Encryption. A "
|
||||||
"server-envelope profile stores message bodies as ciphertext and "
|
"server-envelope profile stores message bodies as ciphertext and "
|
||||||
"uses the optional Encryption capability for authorized reads. "
|
"uses the optional Encryption capability for authorized reads. "
|
||||||
"External ciphertext profiles retain producer-managed references "
|
"External ciphertext profiles retain producer-managed references "
|
||||||
@@ -393,8 +394,102 @@ manifest = ModuleManifest(
|
|||||||
"files",
|
"files",
|
||||||
"notifications",
|
"notifications",
|
||||||
),
|
),
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Postbox inbox",
|
||||||
|
href="/postbox",
|
||||||
|
kind="runtime",
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Postbox directory API",
|
||||||
|
href="/api/v1/postbox/directory",
|
||||||
|
kind="api",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
"kind": "guide",
|
||||||
|
"help_contexts": [
|
||||||
|
"postbox.inbox.directory",
|
||||||
|
"postbox.inbox.messages",
|
||||||
|
"postbox.widget.inbox",
|
||||||
|
"postbox.admin.templates",
|
||||||
|
"postbox.blocker.assignment",
|
||||||
|
"postbox.state.unavailable",
|
||||||
|
],
|
||||||
|
"privacy_notes": [
|
||||||
|
"Directory access is derived from current effective function assignments.",
|
||||||
|
"Unavailable message states reveal only retained audit metadata permitted to the current actor.",
|
||||||
|
"Subjects, participants, routing facts, and attachment references remain observable metadata.",
|
||||||
|
],
|
||||||
|
},
|
||||||
order=35,
|
order=35,
|
||||||
),
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="postbox.reference.fields-and-consequences",
|
||||||
|
title="Postbox fields and consequences",
|
||||||
|
summary=(
|
||||||
|
"Reference for address, template, routing, message, grouping, "
|
||||||
|
"classification, retention, and lifecycle fields."
|
||||||
|
),
|
||||||
|
body=(
|
||||||
|
"A Postbox address is durable and bound to an organization function. "
|
||||||
|
"Template revisions are immutable after publication; retiring a template "
|
||||||
|
"does not remove materialized addresses. Archiving an address stops new "
|
||||||
|
"delivery while retaining messages and evidence. Unified inbox views are "
|
||||||
|
"personal projections only and never move or delete source messages. "
|
||||||
|
"Classification limits eligible delivery and hierarchy-copy targets. "
|
||||||
|
"Hierarchy copies are independent deliveries with their own evidence; "
|
||||||
|
"vacancy escalation is delayed and separately auditable. Message expiry "
|
||||||
|
"or withdrawal blocks future content access but cannot retract plaintext "
|
||||||
|
"already copied, exported, or printed."
|
||||||
|
),
|
||||||
|
layer="configured",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("administrator", "user", "campaign_manager"),
|
||||||
|
related_modules=(
|
||||||
|
"organizations",
|
||||||
|
"idm",
|
||||||
|
"policy",
|
||||||
|
"audit",
|
||||||
|
"encryption",
|
||||||
|
),
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Postbox administration",
|
||||||
|
href="/admin?section=postbox",
|
||||||
|
kind="runtime",
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Postbox concept",
|
||||||
|
href="docs/POSTBOX_CONCEPT.md",
|
||||||
|
kind="source",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
"kind": "reference",
|
||||||
|
"help_contexts": [
|
||||||
|
"postbox.field.address",
|
||||||
|
"postbox.field.function",
|
||||||
|
"postbox.field.scope",
|
||||||
|
"postbox.field.classification",
|
||||||
|
"postbox.field.retention",
|
||||||
|
"postbox.field.hierarchy-routing",
|
||||||
|
"postbox.field.recipients",
|
||||||
|
"postbox.action.archive",
|
||||||
|
"postbox.action.retire-template",
|
||||||
|
"postbox.action.delete-grouping",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"publish_template": "Freezes an immutable address and routing revision for future materialization.",
|
||||||
|
"retire_template": "Stops new revisions and materialization while retaining existing addresses.",
|
||||||
|
"archive_postbox": "Stops new delivery while retaining messages, receipts, and evidence.",
|
||||||
|
"delete_grouping": "Deletes only the personal projection; source Postboxes and messages remain unchanged.",
|
||||||
|
"route_copy": "Creates a separately retained delivery and evidence record at each bounded target.",
|
||||||
|
"withdraw_or_expire": "Blocks future content access while retaining permitted audit metadata.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order=36,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
architecture=declared_module_architecture(
|
architecture=declared_module_architecture(
|
||||||
layer="communication_participation",
|
layer="communication_participation",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_postbox.backend.manifest import manifest
|
||||||
|
|
||||||
|
|
||||||
|
class PostboxInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_route_and_contributed_surfaces_remain_declared(self) -> None:
|
||||||
|
frontend = manifest.frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
self.assertEqual(
|
||||||
|
{"/postbox"},
|
||||||
|
{route.path for route in frontend.routes}, # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"postbox.admin.templates",
|
||||||
|
"postbox.inbox.directory",
|
||||||
|
"postbox.inbox.messages",
|
||||||
|
"postbox.widget.inbox",
|
||||||
|
},
|
||||||
|
{surface.id for surface in frontend.view_surfaces}, # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
self.assertTrue(all(item.icon == "inbox" for item in manifest.nav_items))
|
||||||
|
|
||||||
|
def test_topics_publish_stable_help_privacy_and_consequence_metadata(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in manifest.documentation}
|
||||||
|
self.assertIn("postbox.function-bound-containers", topics)
|
||||||
|
self.assertIn("postbox.reference.fields-and-consequences", topics)
|
||||||
|
|
||||||
|
guide = topics["postbox.function-bound-containers"]
|
||||||
|
self.assertIn("postbox.inbox.messages", guide.metadata["help_contexts"])
|
||||||
|
self.assertIn("postbox.state.unavailable", guide.metadata["help_contexts"])
|
||||||
|
self.assertGreaterEqual(len(guide.metadata["privacy_notes"]), 3)
|
||||||
|
|
||||||
|
reference = topics["postbox.reference.fields-and-consequences"]
|
||||||
|
self.assertIn("postbox.field.classification", reference.metadata["help_contexts"])
|
||||||
|
self.assertIn("postbox.action.delete-grouping", reference.metadata["help_contexts"])
|
||||||
|
self.assertIn("archive_postbox", reference.metadata["consequence_classes"])
|
||||||
|
self.assertIn("withdraw_or_expire", reference.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
+2
-1
@@ -14,7 +14,8 @@
|
|||||||
"./styles/postbox.css": "./src/styles/postbox.css"
|
"./styles/postbox.css": "./src/styles/postbox.css"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:ui-structure": "node scripts/test-postbox-page-structure.mjs"
|
"test:ui-structure": "node scripts/test-postbox-page-structure.mjs",
|
||||||
|
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.14",
|
"@govoplan/core-webui": "^0.1.14",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
function source(path) {
|
||||||
|
return readFileSync(new URL(path, import.meta.url), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = source("../src/features/postbox/PostboxPage.tsx");
|
||||||
|
const admin = source("../src/features/postbox/PostboxAdminPanel.tsx");
|
||||||
|
const widget = source("../src/features/postbox/PostboxInboxWidget.tsx");
|
||||||
|
const patterns = source("../src/features/postbox/interfacePatterns.ts");
|
||||||
|
const moduleSource = source("../src/module.ts");
|
||||||
|
const translations = source("../src/i18n/generatedTranslations.ts");
|
||||||
|
|
||||||
|
assert(page.includes("DocumentationHelpLink") && admin.includes("DocumentationHelpLink"), "Inbox and administration expose contextual documentation");
|
||||||
|
assert(page.includes("ActionBlockerHint") && admin.includes("ActionBlockerHint"), "Assignment and organization prerequisites identify action, actor, and destination");
|
||||||
|
assert(page.includes("disabledReason") && admin.includes("disabledReason"), "Unavailable Postbox actions explain their state");
|
||||||
|
assert(page.includes("useUnsavedDraftGuard") && admin.includes("useUnsavedDraftGuard"), "Message, grouping, template, and address drafts use the shared guard");
|
||||||
|
assert(page.includes("delete_grouping_confirmation") && page.includes("ConfirmDialog"), "Deleting a unified view confirms that source records remain unchanged");
|
||||||
|
assert(admin.includes("archive_confirmation") && admin.includes("retire_template_confirmation"), "Address and template lifecycle actions use shared destructive confirmation");
|
||||||
|
assert(patterns.includes('topicId: "postbox.function-bound-containers"') && patterns.includes('topicId: "postbox.reference.fields-and-consequences"'), "Postbox uses manifest-backed help references");
|
||||||
|
assert(moduleSource.includes('version: "0.1.2"') && moduleSource.includes("generatedTranslations"), "WebUI metadata matches the module release and registers translations");
|
||||||
|
assert(translations.includes('"i18n:govoplan-postbox.unavailable_message_reason"'), "Access-sensitive unavailable states are localized");
|
||||||
|
assert(widget.includes("usePlatformLanguage") && widget.includes("i18nMessage"), "Widget dates and dynamic accessible labels follow the platform locale");
|
||||||
|
assert(!page.includes("window.confirm") && !admin.includes("window.confirm"), "Postbox does not use browser-native consequential confirmation");
|
||||||
|
|
||||||
|
console.log("Postbox surfaces satisfy the recorded interface pattern-language contract.");
|
||||||
|
|
||||||
@@ -12,10 +12,12 @@ import {
|
|||||||
Trash2
|
Trash2
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
AdminPageLayout,
|
AdminPageLayout,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
@@ -23,6 +25,9 @@ import {
|
|||||||
SelectionListItem,
|
SelectionListItem,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
|
i18nMessage,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings
|
type ApiSettings
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +51,12 @@ import {
|
|||||||
type PostboxTemplateCreatePayload,
|
type PostboxTemplateCreatePayload,
|
||||||
type PostboxTemplateRevisionPayload
|
type PostboxTemplateRevisionPayload
|
||||||
} from "../../api/postbox";
|
} from "../../api/postbox";
|
||||||
|
import {
|
||||||
|
POSTBOX_ADMIN_DOCUMENTATION,
|
||||||
|
POSTBOX_FIELD_DOCUMENTATION,
|
||||||
|
POSTBOX_INTERFACE_I18N,
|
||||||
|
postboxBusyReason
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
type AdminMode = "templates" | "postboxes";
|
type AdminMode = "templates" | "postboxes";
|
||||||
@@ -131,8 +142,10 @@ export default function PostboxAdminPanel({
|
|||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
||||||
const [templateDraft, setTemplateDraft] = useState<TemplateDraft>(templateDefaults);
|
const [templateDraft, setTemplateDraft] = useState<TemplateDraft>(templateDefaults);
|
||||||
|
const [templateBaseline, setTemplateBaseline] = useState<TemplateDraft>(templateDefaults);
|
||||||
const [exactDialogOpen, setExactDialogOpen] = useState(false);
|
const [exactDialogOpen, setExactDialogOpen] = useState(false);
|
||||||
const [exactDraft, setExactDraft] = useState<ExactDraft>(exactDefaults);
|
const [exactDraft, setExactDraft] = useState<ExactDraft>(exactDefaults);
|
||||||
|
const [exactBaseline, setExactBaseline] = useState<ExactDraft>(exactDefaults);
|
||||||
const [materializeDialogOpen, setMaterializeDialogOpen] = useState(false);
|
const [materializeDialogOpen, setMaterializeDialogOpen] = useState(false);
|
||||||
const [materializeDraft, setMaterializeDraft] = useState<MaterializeDraft>({
|
const [materializeDraft, setMaterializeDraft] = useState<MaterializeDraft>({
|
||||||
templateId: "",
|
templateId: "",
|
||||||
@@ -140,7 +153,15 @@ export default function PostboxAdminPanel({
|
|||||||
function_id: "",
|
function_id: "",
|
||||||
context_key: ""
|
context_key: ""
|
||||||
});
|
});
|
||||||
|
const [materializeBaseline, setMaterializeBaseline] = useState<MaterializeDraft>({
|
||||||
|
templateId: "",
|
||||||
|
organization_unit_id: "",
|
||||||
|
function_id: "",
|
||||||
|
context_key: ""
|
||||||
|
});
|
||||||
const [archiveTarget, setArchiveTarget] = useState<PostboxDirectoryItem | null>(null);
|
const [archiveTarget, setArchiveTarget] = useState<PostboxDirectoryItem | null>(null);
|
||||||
|
const [retireTarget, setRetireTarget] = useState<PostboxTemplate | null>(null);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
const selectedTemplate = useMemo(
|
const selectedTemplate = useMemo(
|
||||||
() => templates.find((template) => template.id === selectedTemplateId) ?? templates[0] ?? null,
|
() => templates.find((template) => template.id === selectedTemplateId) ?? templates[0] ?? null,
|
||||||
@@ -170,6 +191,17 @@ export default function PostboxAdminPanel({
|
|||||||
}
|
}
|
||||||
return [...values].map(([id, example]) => ({ id, example }));
|
return [...values].map(([id, example]) => ({ id, example }));
|
||||||
}, [units]);
|
}, [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 () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -206,14 +238,16 @@ export default function PostboxAdminPanel({
|
|||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
function openNewTemplate() {
|
function openNewTemplate() {
|
||||||
setTemplateDraft(templateDefaults());
|
const next = templateDefaults();
|
||||||
|
setTemplateDraft(next);
|
||||||
|
setTemplateBaseline(next);
|
||||||
setTemplateDialogOpen(true);
|
setTemplateDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTemplateRevision(template: PostboxTemplate) {
|
function openTemplateRevision(template: PostboxTemplate) {
|
||||||
const revision = currentRevision(template);
|
const revision = currentRevision(template);
|
||||||
if (!revision) return;
|
if (!revision) return;
|
||||||
setTemplateDraft({
|
const next = {
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
slug: template.slug,
|
slug: template.slug,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
@@ -226,11 +260,13 @@ export default function PostboxAdminPanel({
|
|||||||
classification: revision.classification,
|
classification: revision.classification,
|
||||||
allow_vacant_delivery: revision.allow_vacant_delivery,
|
allow_vacant_delivery: revision.allow_vacant_delivery,
|
||||||
routing_policy: revision.routing_policy ?? routingDefaults()
|
routing_policy: revision.routing_policy ?? routingDefaults()
|
||||||
});
|
};
|
||||||
|
setTemplateDraft(next);
|
||||||
|
setTemplateBaseline(next);
|
||||||
setTemplateDialogOpen(true);
|
setTemplateDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTemplate() {
|
async function saveTemplate(): Promise<boolean> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
@@ -255,10 +291,13 @@ export default function PostboxAdminPanel({
|
|||||||
});
|
});
|
||||||
setSuccess("Postbox template created as a draft.");
|
setSuccess("Postbox template created as a draft.");
|
||||||
}
|
}
|
||||||
|
setTemplateBaseline(templateDraft);
|
||||||
setTemplateDialogOpen(false);
|
setTemplateDialogOpen(false);
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -275,23 +314,9 @@ export default function PostboxAdminPanel({
|
|||||||
selectedTemplate,
|
selectedTemplate,
|
||||||
selectedTemplate.current_revision
|
selectedTemplate.current_revision
|
||||||
);
|
);
|
||||||
setSuccess(`Published revision ${selectedTemplate.current_revision}.`);
|
setSuccess(i18nMessage("i18n:govoplan-postbox.published_revision_message", {
|
||||||
await load();
|
revision: selectedTemplate.current_revision
|
||||||
} 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.");
|
|
||||||
await load();
|
await load();
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
@@ -302,15 +327,17 @@ export default function PostboxAdminPanel({
|
|||||||
|
|
||||||
function openExact() {
|
function openExact() {
|
||||||
const unit = units.find((item) => item.functions.length);
|
const unit = units.find((item) => item.functions.length);
|
||||||
setExactDraft({
|
const next = {
|
||||||
...exactDefaults(),
|
...exactDefaults(),
|
||||||
organization_unit_id: unit?.id ?? "",
|
organization_unit_id: unit?.id ?? "",
|
||||||
function_id: unit?.functions[0]?.id ?? ""
|
function_id: unit?.functions[0]?.id ?? ""
|
||||||
});
|
};
|
||||||
|
setExactDraft(next);
|
||||||
|
setExactBaseline(next);
|
||||||
setExactDialogOpen(true);
|
setExactDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveExact() {
|
async function saveExact(): Promise<boolean> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
@@ -320,11 +347,14 @@ export default function PostboxAdminPanel({
|
|||||||
description: exactDraft.description || null,
|
description: exactDraft.description || null,
|
||||||
address_key: exactDraft.address_key || null
|
address_key: exactDraft.address_key || null
|
||||||
});
|
});
|
||||||
|
setExactBaseline(exactDraft);
|
||||||
setExactDialogOpen(false);
|
setExactDialogOpen(false);
|
||||||
setSuccess("Exact function-bound Postbox created.");
|
setSuccess("Exact function-bound Postbox created.");
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -334,16 +364,18 @@ export default function PostboxAdminPanel({
|
|||||||
const revision = currentRevision(template);
|
const revision = currentRevision(template);
|
||||||
const compatible = compatibleTargets(units, revision?.function_type_id);
|
const compatible = compatibleTargets(units, revision?.function_type_id);
|
||||||
const unit = compatible[0];
|
const unit = compatible[0];
|
||||||
setMaterializeDraft({
|
const next = {
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
organization_unit_id: unit?.id ?? "",
|
organization_unit_id: unit?.id ?? "",
|
||||||
function_id: unit?.functions[0]?.id ?? "",
|
function_id: unit?.functions[0]?.id ?? "",
|
||||||
context_key: ""
|
context_key: ""
|
||||||
});
|
};
|
||||||
|
setMaterializeDraft(next);
|
||||||
|
setMaterializeBaseline(next);
|
||||||
setMaterializeDialogOpen(true);
|
setMaterializeDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function materialize() {
|
async function materialize(): Promise<boolean> {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
@@ -353,11 +385,14 @@ export default function PostboxAdminPanel({
|
|||||||
function_id: materializeDraft.function_id,
|
function_id: materializeDraft.function_id,
|
||||||
context_key: materializeDraft.context_key || null
|
context_key: materializeDraft.context_key || null
|
||||||
});
|
});
|
||||||
|
setMaterializeBaseline(materializeDraft);
|
||||||
setMaterializeDialogOpen(false);
|
setMaterializeDialogOpen(false);
|
||||||
setSuccess("Stable Postbox address resolved and materialized.");
|
setSuccess("Stable Postbox address resolved and materialized.");
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
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 (
|
return (
|
||||||
<AdminPageLayout
|
<AdminPageLayout
|
||||||
title="Postboxes"
|
title="Postboxes"
|
||||||
@@ -393,16 +495,18 @@ export default function PostboxAdminPanel({
|
|||||||
<IconButton
|
<IconButton
|
||||||
label="Refresh"
|
label="Refresh"
|
||||||
icon={<RefreshCw size={16} />}
|
icon={<RefreshCw size={16} />}
|
||||||
onClick={() => void load()}
|
onClick={() => requestDiscard(() => void load())}
|
||||||
disabled={busy}
|
disabled={loading || busy}
|
||||||
|
disabledReason={postboxBusyReason(loading, busy)}
|
||||||
/>
|
/>
|
||||||
|
<DocumentationHelpLink reference={POSTBOX_ADMIN_DOCUMENTATION} />
|
||||||
{mode === "templates" && canManageTemplates ? (
|
{mode === "templates" && canManageTemplates ? (
|
||||||
<Button variant="primary" onClick={openNewTemplate}>
|
<Button variant="primary" onClick={openNewTemplate} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||||
<Plus size={16} /> New template
|
<Plus size={16} /> New template
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{mode === "postboxes" && canManageBindings ? (
|
{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
|
<Plus size={16} /> Exact postbox
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -418,6 +522,22 @@ export default function PostboxAdminPanel({
|
|||||||
]}
|
]}
|
||||||
ariaLabel="Postbox administration section"
|
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" ? (
|
{mode === "templates" ? (
|
||||||
<TemplateWorkspace
|
<TemplateWorkspace
|
||||||
templates={templates}
|
templates={templates}
|
||||||
@@ -425,7 +545,7 @@ export default function PostboxAdminPanel({
|
|||||||
onSelect={setSelectedTemplateId}
|
onSelect={setSelectedTemplateId}
|
||||||
onRevise={openTemplateRevision}
|
onRevise={openTemplateRevision}
|
||||||
onPublish={() => void publishSelected()}
|
onPublish={() => void publishSelected()}
|
||||||
onRetire={() => void retireSelected()}
|
onRetire={() => selectedTemplate && setRetireTarget(selectedTemplate)}
|
||||||
onMaterialize={openMaterialize}
|
onMaterialize={openMaterialize}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
canManageBindings={canManageBindings}
|
canManageBindings={canManageBindings}
|
||||||
@@ -450,7 +570,7 @@ export default function PostboxAdminPanel({
|
|||||||
unitTypes={unitTypes}
|
unitTypes={unitTypes}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onChange={setTemplateDraft}
|
onChange={setTemplateDraft}
|
||||||
onClose={() => setTemplateDialogOpen(false)}
|
onClose={closeTemplateDialog}
|
||||||
onSave={() => void saveTemplate()}
|
onSave={() => void saveTemplate()}
|
||||||
/>
|
/>
|
||||||
<ExactPostboxDialog
|
<ExactPostboxDialog
|
||||||
@@ -459,7 +579,7 @@ export default function PostboxAdminPanel({
|
|||||||
units={units}
|
units={units}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onChange={setExactDraft}
|
onChange={setExactDraft}
|
||||||
onClose={() => setExactDialogOpen(false)}
|
onClose={closeExactDialog}
|
||||||
onSave={() => void saveExact()}
|
onSave={() => void saveExact()}
|
||||||
/>
|
/>
|
||||||
<MaterializeDialog
|
<MaterializeDialog
|
||||||
@@ -469,7 +589,7 @@ export default function PostboxAdminPanel({
|
|||||||
units={units}
|
units={units}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onChange={setMaterializeDraft}
|
onChange={setMaterializeDraft}
|
||||||
onClose={() => setMaterializeDialogOpen(false)}
|
onClose={closeMaterializeDialog}
|
||||||
onSave={() => void materialize()}
|
onSave={() => void materialize()}
|
||||||
/>
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -477,7 +597,7 @@ export default function PostboxAdminPanel({
|
|||||||
title="Archive Postbox"
|
title="Archive Postbox"
|
||||||
message={
|
message={
|
||||||
archiveTarget
|
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"
|
confirmLabel="Archive"
|
||||||
@@ -486,6 +606,18 @@ export default function PostboxAdminPanel({
|
|||||||
onConfirm={() => void confirmArchive()}
|
onConfirm={() => void confirmArchive()}
|
||||||
onCancel={() => setArchiveTarget(null)}
|
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>
|
</AdminPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -529,7 +661,7 @@ function TemplateWorkspace({
|
|||||||
<StatusBadge status={template.status} />
|
<StatusBadge status={template.status} />
|
||||||
</span>
|
</span>
|
||||||
<span className="postbox-item-context">
|
<span className="postbox-item-context">
|
||||||
{template.slug} · revision {template.current_revision}
|
{template.slug} · {i18nMessage("i18n:govoplan-postbox.revision_label", { revision: template.current_revision })}
|
||||||
</span>
|
</span>
|
||||||
</SelectionListItem>
|
</SelectionListItem>
|
||||||
))}
|
))}
|
||||||
@@ -546,24 +678,41 @@ function TemplateWorkspace({
|
|||||||
<p>{selected.description || "No description."}</p>
|
<p>{selected.description || "No description."}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<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
|
<Pencil size={16} /> New revision
|
||||||
</Button>
|
</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
|
<Save size={16} /> Publish
|
||||||
</Button>
|
</Button>
|
||||||
{canManageBindings ? (
|
{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
|
<Rocket size={16} /> Resolve address
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : 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
|
<Trash2 size={16} /> Retire
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dl className="postbox-admin-properties">
|
<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>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>Scope</dt><dd>{revision.scope_kind}{revision.scope_id ? ` · ${revision.scope_id}` : ""}</dd></div>
|
||||||
<div><dt>Classification</dt><dd>{revision.classification}</dd></div>
|
<div><dt>Classification</dt><dd>{revision.classification}</dd></div>
|
||||||
@@ -572,7 +721,10 @@ function TemplateWorkspace({
|
|||||||
<dt>Hierarchy copies</dt>
|
<dt>Hierarchy copies</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{revision.routing_policy.linked_copy.enabled
|
{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"}
|
: "Disabled"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -580,7 +732,9 @@ function TemplateWorkspace({
|
|||||||
<dt>Vacancy escalation</dt>
|
<dt>Vacancy escalation</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{revision.routing_policy.attention.mode === "vacancy_escalation"
|
{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"}
|
: "Disabled"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -592,7 +746,7 @@ function TemplateWorkspace({
|
|||||||
<h3>Immutable revisions</h3>
|
<h3>Immutable revisions</h3>
|
||||||
{selected.revisions.map((item) => (
|
{selected.revisions.map((item) => (
|
||||||
<div key={item.id}>
|
<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>
|
<span>{item.classification} · {item.scope_kind}</span>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={item.published_at ? "published" : "draft"}
|
status={item.published_at ? "published" : "draft"}
|
||||||
@@ -664,6 +818,7 @@ function PostboxWorkspace({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onArchive(selected)}
|
onClick={() => onArchive(selected)}
|
||||||
disabled={busy || selected.status !== "active"}
|
disabled={busy || selected.status !== "active"}
|
||||||
|
disabledReason={postboxBusyReason(false, busy) ?? (selected.status !== "active" ? POSTBOX_INTERFACE_I18N.archivedPostbox : undefined)}
|
||||||
>
|
>
|
||||||
<Archive size={16} /> Archive
|
<Archive size={16} /> Archive
|
||||||
</Button>
|
</Button>
|
||||||
@@ -764,36 +919,41 @@ function TemplateDialog({
|
|||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<div className="button-row compact-actions">
|
<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 || !valid}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={busy || !valid}
|
||||||
|
disabledReason={postboxBusyReason(false, busy) ?? (!valid ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||||
|
>
|
||||||
{isRevision ? "Create revision" : "Create draft"}
|
{isRevision ? "Create revision" : "Create draft"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="postbox-form-grid two-columns">
|
<div className="postbox-form-grid two-columns">
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.name}
|
value={draft.name}
|
||||||
disabled={isRevision}
|
disabled={isRevision}
|
||||||
onChange={(event) => onChange({ ...draft, name: event.target.value })}
|
onChange={(event) => onChange({ ...draft, name: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Slug">
|
<FormField label="Slug" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.slug}
|
value={draft.slug}
|
||||||
disabled={isRevision}
|
disabled={isRevision}
|
||||||
onChange={(event) => onChange({ ...draft, slug: event.target.value })}
|
onChange={(event) => onChange({ ...draft, slug: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Description">
|
<FormField label="Description" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.description || ""}
|
value={draft.description || ""}
|
||||||
disabled={isRevision}
|
disabled={isRevision}
|
||||||
onChange={(event) => onChange({ ...draft, description: event.target.value })}
|
onChange={(event) => onChange({ ...draft, description: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Function type">
|
<FormField label="Function type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.function_type_id || ""}
|
value={draft.function_type_id || ""}
|
||||||
onChange={(event) => onChange({
|
onChange={(event) => onChange({
|
||||||
@@ -807,7 +967,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Scope">
|
<FormField label="Scope" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.scope_kind}
|
value={draft.scope_kind}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -826,7 +986,7 @@ function TemplateDialog({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
{draft.scope_kind !== "tenant" ? (
|
{draft.scope_kind !== "tenant" ? (
|
||||||
<FormField label="Scope target">
|
<FormField label="Scope target" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.scope_id || ""}
|
value={draft.scope_id || ""}
|
||||||
onChange={(event) => onChange({ ...draft, scope_id: event.target.value || null })}
|
onChange={(event) => onChange({ ...draft, scope_id: event.target.value || null })}
|
||||||
@@ -838,19 +998,19 @@ function TemplateDialog({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
) : <div />}
|
) : <div />}
|
||||||
<FormField label="Name pattern">
|
<FormField label="Name pattern" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.name_pattern}
|
value={draft.name_pattern}
|
||||||
onChange={(event) => onChange({ ...draft, name_pattern: event.target.value })}
|
onChange={(event) => onChange({ ...draft, name_pattern: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Address pattern">
|
<FormField label="Address pattern" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.address_pattern}
|
value={draft.address_pattern}
|
||||||
onChange={(event) => onChange({ ...draft, address_pattern: event.target.value })}
|
onChange={(event) => onChange({ ...draft, address_pattern: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Classification">
|
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={draft.classification}
|
value={draft.classification}
|
||||||
onChange={(event) => onChange({ ...draft, classification: event.target.value })}
|
onChange={(event) => onChange({ ...draft, classification: event.target.value })}
|
||||||
@@ -859,6 +1019,7 @@ function TemplateDialog({
|
|||||||
<div className="postbox-toggle-field">
|
<div className="postbox-toggle-field">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Accept delivery while vacant"
|
label="Accept delivery while vacant"
|
||||||
|
help="Messages remain institutionally retained even when no incumbent currently has access."
|
||||||
checked={draft.allow_vacant_delivery}
|
checked={draft.allow_vacant_delivery}
|
||||||
onChange={(checked) => onChange({ ...draft, allow_vacant_delivery: checked })}
|
onChange={(checked) => onChange({ ...draft, allow_vacant_delivery: checked })}
|
||||||
/>
|
/>
|
||||||
@@ -868,9 +1029,11 @@ function TemplateDialog({
|
|||||||
<div>
|
<div>
|
||||||
<strong>Hierarchy linked copies</strong>
|
<strong>Hierarchy linked copies</strong>
|
||||||
<span>Copy to explicitly bounded function Postboxes in one selected structure.</span>
|
<span>Copy to explicitly bounded function Postboxes in one selected structure.</span>
|
||||||
|
<DocumentationHelpLink reference={POSTBOX_FIELD_DOCUMENTATION} />
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Enable hierarchy linked copies"
|
label="Enable hierarchy linked copies"
|
||||||
|
help="Copies create independent deliveries and evidence at explicitly bounded hierarchy targets."
|
||||||
checked={linkedCopy.enabled}
|
checked={linkedCopy.enabled}
|
||||||
onChange={(enabled) => updateLinkedCopy({
|
onChange={(enabled) => updateLinkedCopy({
|
||||||
enabled,
|
enabled,
|
||||||
@@ -882,7 +1045,7 @@ function TemplateDialog({
|
|||||||
</div>
|
</div>
|
||||||
{linkedCopy.enabled ? (
|
{linkedCopy.enabled ? (
|
||||||
<div className="postbox-form-grid two-columns">
|
<div className="postbox-form-grid two-columns">
|
||||||
<FormField label="Organization structure">
|
<FormField label="Organization structure" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.structure_id || ""}
|
value={linkedCopy.structure_id || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -896,7 +1059,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Relation type">
|
<FormField label="Relation type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.relation_type_ids[0] || ""}
|
value={linkedCopy.relation_type_ids[0] || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -911,7 +1074,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Target function type">
|
<FormField label="Target function type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.target_function_type_id || ""}
|
value={linkedCopy.target_function_type_id || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -924,7 +1087,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Target Postbox template">
|
<FormField label="Target Postbox template" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.target_template_id || ""}
|
value={linkedCopy.target_template_id || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -939,7 +1102,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Maximum hierarchy depth">
|
<FormField label="Maximum hierarchy depth" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -950,7 +1113,7 @@ function TemplateDialog({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Copy behavior">
|
<FormField label="Copy behavior" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.fanout}
|
value={linkedCopy.fanout}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -968,7 +1131,7 @@ function TemplateDialog({
|
|||||||
<option value="all">All matching ancestors</option>
|
<option value="all">All matching ancestors</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Optional stop unit">
|
<FormField label="Optional stop unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.stop_unit_id || ""}
|
value={linkedCopy.stop_unit_id || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -981,7 +1144,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Optional stop unit type">
|
<FormField label="Optional stop unit type" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={linkedCopy.stop_unit_type_id || ""}
|
value={linkedCopy.stop_unit_type_id || ""}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -994,7 +1157,7 @@ function TemplateDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Allowed classifications">
|
<FormField label="Allowed classifications" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={linkedCopy.allowed_classifications.join(", ")}
|
value={linkedCopy.allowed_classifications.join(", ")}
|
||||||
onChange={(event) => updateLinkedCopy({
|
onChange={(event) => updateLinkedCopy({
|
||||||
@@ -1002,7 +1165,7 @@ function TemplateDialog({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Authorized producer modules">
|
<FormField label="Authorized producer modules" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
placeholder="campaigns, workflow"
|
placeholder="campaigns, workflow"
|
||||||
value={linkedCopy.allowed_producer_modules.join(", ")}
|
value={linkedCopy.allowed_producer_modules.join(", ")}
|
||||||
@@ -1014,11 +1177,12 @@ function TemplateDialog({
|
|||||||
<div className="postbox-toggle-field">
|
<div className="postbox-toggle-field">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Require message expiry"
|
label="Require message expiry"
|
||||||
|
help="Reject copied deliveries that do not carry an explicit expiry boundary."
|
||||||
checked={linkedCopy.require_expiry}
|
checked={linkedCopy.require_expiry}
|
||||||
onChange={(require_expiry) => updateLinkedCopy({ require_expiry })}
|
onChange={(require_expiry) => updateLinkedCopy({ require_expiry })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormField label="Maximum retention days">
|
<FormField label="Maximum retention days" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -1034,6 +1198,7 @@ function TemplateDialog({
|
|||||||
<div className="postbox-toggle-field">
|
<div className="postbox-toggle-field">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Escalate when the nearest target remains vacant"
|
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"}
|
checked={attention.mode === "vacancy_escalation"}
|
||||||
onChange={(checked) => updateRouting({
|
onChange={(checked) => updateRouting({
|
||||||
...draft.routing_policy,
|
...draft.routing_policy,
|
||||||
@@ -1047,7 +1212,7 @@ function TemplateDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{attention.mode === "vacancy_escalation" ? (
|
{attention.mode === "vacancy_escalation" ? (
|
||||||
<FormField label="Vacancy escalation delay (minutes)">
|
<FormField label="Vacancy escalation delay (minutes)" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -1104,11 +1269,12 @@ function ExactPostboxDialog({
|
|||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<div className="button-row compact-actions">
|
<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
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={busy || !draft.name.trim() || !draft.organization_unit_id || !draft.function_id}
|
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
|
Create Postbox
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1116,13 +1282,13 @@ function ExactPostboxDialog({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="postbox-form-grid two-columns">
|
<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 })} />
|
<input value={draft.name} onChange={(event) => onChange({ ...draft, name: event.target.value })} />
|
||||||
</FormField>
|
</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 })} />
|
<input value={draft.address_key || ""} placeholder="Generated when empty" onChange={(event) => onChange({ ...draft, address_key: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Organization unit">
|
<FormField label="Organization unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.organization_unit_id}
|
value={draft.organization_unit_id}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -1140,7 +1306,7 @@ function ExactPostboxDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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 })}>
|
<select value={draft.function_id} onChange={(event) => onChange({ ...draft, function_id: event.target.value })}>
|
||||||
<option value="">Select function</option>
|
<option value="">Select function</option>
|
||||||
{(unit?.functions || []).map((fn) => (
|
{(unit?.functions || []).map((fn) => (
|
||||||
@@ -1148,10 +1314,10 @@ function ExactPostboxDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Classification">
|
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input value={draft.classification} onChange={(event) => onChange({ ...draft, classification: event.target.value })} />
|
<input value={draft.classification} onChange={(event) => onChange({ ...draft, classification: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Description">
|
<FormField label="Description" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input value={draft.description || ""} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
<input value={draft.description || ""} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@@ -1184,21 +1350,28 @@ function MaterializeDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
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"
|
className="postbox-dialog"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<div className="button-row compact-actions">
|
<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.organization_unit_id || !draft.function_id}>
|
<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
|
Resolve address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="postbox-form-grid">
|
<div className="postbox-form-grid">
|
||||||
<FormField label="Organization unit">
|
<FormField label="Organization unit" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.organization_unit_id}
|
value={draft.organization_unit_id}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -1216,7 +1389,7 @@ function MaterializeDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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 })}>
|
<select value={draft.function_id} onChange={(event) => onChange({ ...draft, function_id: event.target.value })}>
|
||||||
<option value="">Select function</option>
|
<option value="">Select function</option>
|
||||||
{(unit?.functions || []).map((fn) => (
|
{(unit?.functions || []).map((fn) => (
|
||||||
@@ -1224,7 +1397,7 @@ function MaterializeDialog({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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 })} />
|
<input value={draft.context_key} onChange={(event) => onChange({ ...draft, context_key: event.target.value })} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@@ -1272,3 +1445,7 @@ function errorMessage(error: unknown): string {
|
|||||||
function commaSeparated(value: string): string[] {
|
function commaSeparated(value: string): string[] {
|
||||||
return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
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,
|
DashboardWidgetList,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
|
i18nMessage,
|
||||||
useDashboardWidgetData,
|
useDashboardWidgetData,
|
||||||
|
usePlatformLanguage,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type DashboardWidgetConfiguration
|
type DashboardWidgetConfiguration
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
@@ -23,6 +25,7 @@ export default function PostboxInboxWidget({
|
|||||||
refreshKey: number;
|
refreshKey: number;
|
||||||
configuration: DashboardWidgetConfiguration;
|
configuration: DashboardWidgetConfiguration;
|
||||||
}) {
|
}) {
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
const maxItems = numberSetting(configuration.maxItems, 5, 1, 12);
|
const maxItems = numberSetting(configuration.maxItems, 5, 1, 12);
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
const postboxes = await listPostboxes(settings);
|
const postboxes = await listPostboxes(settings);
|
||||||
@@ -53,7 +56,7 @@ export default function PostboxInboxWidget({
|
|||||||
id: message.id,
|
id: message.id,
|
||||||
title: message.subject,
|
title: message.subject,
|
||||||
detail: message.sender_label || message.producer_module || "Postbox",
|
detail: message.sender_label || message.producer_module || "Postbox",
|
||||||
meta: new Intl.DateTimeFormat(undefined, {
|
meta: new Intl.DateTimeFormat(language, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -61,7 +64,7 @@ export default function PostboxInboxWidget({
|
|||||||
}).format(new Date(message.delivered_at)),
|
}).format(new Date(message.delivered_at)),
|
||||||
leading: <Inbox size={17} aria-hidden="true" />,
|
leading: <Inbox size={17} aria-hidden="true" />,
|
||||||
trailing: message.attachments.length ? (
|
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" />
|
<Paperclip size={15} aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
) : undefined,
|
) : undefined,
|
||||||
@@ -70,7 +73,9 @@ export default function PostboxInboxWidget({
|
|||||||
/>
|
/>
|
||||||
<div className="dashboard-contribution-footer">
|
<div className="dashboard-contribution-footer">
|
||||||
<Link className="btn btn-secondary" to="/postbox">
|
<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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ import {
|
|||||||
X
|
X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
|
ConfirmDialog,
|
||||||
DataGridPaginationBar,
|
DataGridPaginationBar,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
@@ -30,7 +33,11 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
i18nMessage,
|
||||||
isApiError,
|
isApiError,
|
||||||
|
usePlatformLanguage,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo
|
type AuthInfo
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
@@ -49,6 +56,12 @@ import {
|
|||||||
type PostboxGrouping,
|
type PostboxGrouping,
|
||||||
type PostboxMessage
|
type PostboxMessage
|
||||||
} from "../../api/postbox";
|
} from "../../api/postbox";
|
||||||
|
import {
|
||||||
|
POSTBOX_DOCUMENTATION,
|
||||||
|
POSTBOX_FIELD_DOCUMENTATION,
|
||||||
|
POSTBOX_INTERFACE_I18N,
|
||||||
|
postboxBusyReason
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
type GroupingDraft = {
|
type GroupingDraft = {
|
||||||
@@ -114,13 +127,20 @@ export default function PostboxPage({
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [groupingDialogOpen, setGroupingDialogOpen] = useState(false);
|
const [groupingDialogOpen, setGroupingDialogOpen] = useState(false);
|
||||||
const [groupingDraft, setGroupingDraft] = useState<GroupingDraft>(emptyGrouping);
|
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 [messageDialogOpen, setMessageDialogOpen] = useState(false);
|
||||||
const [replyParent, setReplyParent] = useState<PostboxMessage | null>(null);
|
const [replyParent, setReplyParent] = useState<PostboxMessage | null>(null);
|
||||||
const [messageDraft, setMessageDraft] = useState<MessageDraft>(emptyMessageDraft);
|
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 canAcknowledge = hasScope(auth, "postbox:message:acknowledge");
|
||||||
const canSend = hasScope(auth, "postbox:message:write");
|
const canSend = hasScope(auth, "postbox:message:write");
|
||||||
const canReply = hasScope(auth, "postbox:message:reply");
|
const canReply = hasScope(auth, "postbox:message:reply");
|
||||||
|
const groupingDirty = groupingDialogOpen && draftKey(groupingDraft) !== draftKey(groupingBaseline);
|
||||||
|
const messageDirty = messageDialogOpen && draftKey(messageDraft) !== draftKey(messageBaseline);
|
||||||
const selectedPostbox = useMemo(
|
const selectedPostbox = useMemo(
|
||||||
() => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? null,
|
() => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? null,
|
||||||
[postboxes, selectedPostboxId]
|
[postboxes, selectedPostboxId]
|
||||||
@@ -138,6 +158,32 @@ export default function PostboxPage({
|
|||||||
return postboxes.map((postbox) => postbox.id);
|
return postboxes.map((postbox) => postbox.id);
|
||||||
}, [postboxes, selectedGrouping, selectedPostboxId]);
|
}, [postboxes, selectedGrouping, selectedPostboxId]);
|
||||||
const scopeKey = scopePostboxIds.join("|");
|
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 () => {
|
const loadDirectory = useCallback(async () => {
|
||||||
setLoadingDirectory(true);
|
setLoadingDirectory(true);
|
||||||
@@ -336,22 +382,26 @@ export default function PostboxPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNewGrouping() {
|
function openNewGrouping() {
|
||||||
setGroupingDraft(emptyGrouping());
|
const next = emptyGrouping();
|
||||||
|
setGroupingDraft(next);
|
||||||
|
setGroupingBaseline(next);
|
||||||
setGroupingDialogOpen(true);
|
setGroupingDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGrouping(grouping: PostboxGrouping) {
|
function openGrouping(grouping: PostboxGrouping) {
|
||||||
setGroupingDraft({
|
const next = {
|
||||||
id: grouping.id,
|
id: grouping.id,
|
||||||
name: grouping.name,
|
name: grouping.name,
|
||||||
is_default: grouping.is_default,
|
is_default: grouping.is_default,
|
||||||
postbox_ids: [...grouping.postbox_ids]
|
postbox_ids: [...grouping.postbox_ids]
|
||||||
});
|
};
|
||||||
|
setGroupingDraft(next);
|
||||||
|
setGroupingBaseline(next);
|
||||||
setGroupingDialogOpen(true);
|
setGroupingDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveGrouping() {
|
async function saveGrouping(): Promise<boolean> {
|
||||||
if (!groupingDraft.name.trim()) return;
|
if (!groupingDraft.name.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -368,24 +418,28 @@ export default function PostboxPage({
|
|||||||
setSelectedScope(saved.id);
|
setSelectedScope(saved.id);
|
||||||
setSelectedPostboxId("");
|
setSelectedPostboxId("");
|
||||||
setGroupingDialogOpen(false);
|
setGroupingDialogOpen(false);
|
||||||
|
setGroupingBaseline(groupingDraft);
|
||||||
|
return true;
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeGrouping() {
|
async function removeGrouping() {
|
||||||
if (!groupingDraft.id) return;
|
if (!deleteGroupingTarget?.id) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
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.");
|
if (!existing) throw new Error("The grouping is no longer available.");
|
||||||
await deletePostboxGrouping(settings, existing);
|
await deletePostboxGrouping(settings, existing);
|
||||||
setSelectedScope("all");
|
setSelectedScope("all");
|
||||||
setSelectedPostboxId("");
|
setSelectedPostboxId("");
|
||||||
setGroupingDialogOpen(false);
|
setGroupingDialogOpen(false);
|
||||||
|
setDeleteGroupingTarget(null);
|
||||||
await loadDirectory();
|
await loadDirectory();
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
@@ -398,30 +452,34 @@ export default function PostboxPage({
|
|||||||
const postbox = selectedPostbox ?? postboxes[0] ?? null;
|
const postbox = selectedPostbox ?? postboxes[0] ?? null;
|
||||||
if (!postbox) return;
|
if (!postbox) return;
|
||||||
setReplyParent(null);
|
setReplyParent(null);
|
||||||
setMessageDraft({
|
const next = {
|
||||||
...emptyMessageDraft(),
|
...emptyMessageDraft(),
|
||||||
postbox_id: postbox.id,
|
postbox_id: postbox.id,
|
||||||
classification: postbox.classification
|
classification: postbox.classification
|
||||||
});
|
};
|
||||||
|
setMessageDraft(next);
|
||||||
|
setMessageBaseline(next);
|
||||||
setMessageDialogOpen(true);
|
setMessageDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReply() {
|
function openReply() {
|
||||||
if (!selectedMessage) return;
|
if (!selectedMessage) return;
|
||||||
setReplyParent(selectedMessage);
|
setReplyParent(selectedMessage);
|
||||||
setMessageDraft({
|
const next = {
|
||||||
...emptyMessageDraft(),
|
...emptyMessageDraft(),
|
||||||
postbox_id: selectedMessage.postbox_id,
|
postbox_id: selectedMessage.postbox_id,
|
||||||
subject: selectedMessage.subject.toLowerCase().startsWith("re:")
|
subject: selectedMessage.subject.toLowerCase().startsWith("re:")
|
||||||
? selectedMessage.subject
|
? selectedMessage.subject
|
||||||
: `Re: ${selectedMessage.subject}`,
|
: `Re: ${selectedMessage.subject}`,
|
||||||
classification: selectedMessage.classification
|
classification: selectedMessage.classification
|
||||||
});
|
};
|
||||||
|
setMessageDraft(next);
|
||||||
|
setMessageBaseline(next);
|
||||||
setMessageDialogOpen(true);
|
setMessageDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitMessage() {
|
async function submitMessage(): Promise<boolean> {
|
||||||
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return;
|
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
const participants = messageDraft.recipients
|
const participants = messageDraft.recipients
|
||||||
@@ -467,13 +525,47 @@ export default function PostboxPage({
|
|||||||
);
|
);
|
||||||
setMessages(refreshed.messages);
|
setMessages(refreshed.messages);
|
||||||
setTotal(refreshed.total);
|
setTotal(refreshed.total);
|
||||||
|
setMessageBaseline(messageDraft);
|
||||||
|
return true;
|
||||||
} catch (actionError) {
|
} catch (actionError) {
|
||||||
setError(errorMessage(actionError));
|
setError(errorMessage(actionError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
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 (
|
return (
|
||||||
<main className="workspace-data-page module-entry-page postbox-page">
|
<main className="workspace-data-page module-entry-page postbox-page">
|
||||||
<div className="postbox-shell">
|
<div className="postbox-shell">
|
||||||
@@ -482,18 +574,22 @@ export default function PostboxPage({
|
|||||||
<div className="postbox-bar-title">
|
<div className="postbox-bar-title">
|
||||||
<Inbox size={17} aria-hidden="true" />
|
<Inbox size={17} aria-hidden="true" />
|
||||||
<strong>Postbox</strong>
|
<strong>Postbox</strong>
|
||||||
|
<DocumentationHelpLink reference={POSTBOX_DOCUMENTATION} />
|
||||||
</div>
|
</div>
|
||||||
<div className="postbox-icon-actions">
|
<div className="postbox-icon-actions">
|
||||||
<IconButton
|
<IconButton
|
||||||
label="New unified view"
|
label="New unified view"
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
onClick={openNewGrouping}
|
onClick={openNewGrouping}
|
||||||
|
disabled={busy}
|
||||||
|
disabledReason={postboxBusyReason(false, busy)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Refresh"
|
label="Refresh"
|
||||||
icon={<RefreshCw size={16} />}
|
icon={<RefreshCw size={16} />}
|
||||||
onClick={() => void loadDirectory()}
|
onClick={() => requestDiscard(() => void loadDirectory())}
|
||||||
disabled={loadingDirectory || busy}
|
disabled={loadingDirectory || busy}
|
||||||
|
disabledReason={postboxBusyReason(loadingDirectory, busy)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -530,6 +626,20 @@ export default function PostboxPage({
|
|||||||
<Archive size={20} />
|
<Archive size={20} />
|
||||||
<strong>No assigned postboxes</strong>
|
<strong>No assigned postboxes</strong>
|
||||||
<p>Postboxes appear when your account has a current matching function assignment.</p>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{postboxes.length ? (
|
{postboxes.length ? (
|
||||||
@@ -585,13 +695,15 @@ export default function PostboxPage({
|
|||||||
label="New message"
|
label="New message"
|
||||||
icon={<Send size={16} />}
|
icon={<Send size={16} />}
|
||||||
onClick={openCompose}
|
onClick={openCompose}
|
||||||
disabled={!canSend || !postboxes.length || busy}
|
disabled={Boolean(composeDisabledReason)}
|
||||||
|
disabledReason={composeDisabledReason}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Refresh messages"
|
label="Refresh messages"
|
||||||
icon={<RefreshCw size={16} />}
|
icon={<RefreshCw size={16} />}
|
||||||
onClick={() => void loadMessages()}
|
onClick={() => requestDiscard(() => void loadMessages())}
|
||||||
disabled={loadingMessages || busy}
|
disabled={loadingMessages || busy}
|
||||||
|
disabledReason={postboxBusyReason(loadingMessages, busy)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -679,7 +791,7 @@ export default function PostboxPage({
|
|||||||
>
|
>
|
||||||
<span className="postbox-message-heading">
|
<span className="postbox-message-heading">
|
||||||
<strong>{message.subject}</strong>
|
<strong>{message.subject}</strong>
|
||||||
<time>{formatDate(message.delivered_at)}</time>
|
<time>{formatDate(message.delivered_at, language)}</time>
|
||||||
</span>
|
</span>
|
||||||
<span className="postbox-message-preview">
|
<span className="postbox-message-preview">
|
||||||
{message.sender_label || message.producer_module || "Platform"}
|
{message.sender_label || message.producer_module || "Platform"}
|
||||||
@@ -734,31 +846,15 @@ export default function PostboxPage({
|
|||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<Button
|
<Button
|
||||||
onClick={openReply}
|
onClick={openReply}
|
||||||
disabled={
|
disabled={Boolean(replyDisabledReason)}
|
||||||
!canReply ||
|
disabledReason={replyDisabledReason}
|
||||||
!selectedMessage ||
|
|
||||||
selectedMessage.availability !== "available" ||
|
|
||||||
busy
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Reply size={16} /> Reply
|
<Reply size={16} /> Reply
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void acknowledgeSelected()}
|
onClick={() => void acknowledgeSelected()}
|
||||||
disabled={
|
disabled={Boolean(acknowledgeDisabledReason)}
|
||||||
!selectedMessage ||
|
disabledReason={acknowledgeDisabledReason}
|
||||||
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
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<CheckCheck size={16} /> Acknowledge
|
<CheckCheck size={16} /> Acknowledge
|
||||||
</Button>
|
</Button>
|
||||||
@@ -789,27 +885,29 @@ export default function PostboxPage({
|
|||||||
open={groupingDialogOpen}
|
open={groupingDialogOpen}
|
||||||
title={groupingDraft.id ? "Edit unified view" : "New unified view"}
|
title={groupingDraft.id ? "Edit unified view" : "New unified view"}
|
||||||
className="postbox-dialog"
|
className="postbox-dialog"
|
||||||
onClose={() => setGroupingDialogOpen(false)}
|
onClose={closeGroupingDialog}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<div className="postbox-dialog-actions">
|
<div className="postbox-dialog-actions">
|
||||||
{groupingDraft.id ? (
|
{groupingDraft.id ? (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => void removeGrouping()}
|
onClick={() => setDeleteGroupingTarget(groupingDraft)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
|
disabledReason={postboxBusyReason(false, busy)}
|
||||||
>
|
>
|
||||||
<Trash2 size={16} /> Delete
|
<Trash2 size={16} /> Delete
|
||||||
</Button>
|
</Button>
|
||||||
) : <span />}
|
) : <span />}
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<Button onClick={() => setGroupingDialogOpen(false)} disabled={busy}>
|
<Button onClick={closeGroupingDialog} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void saveGrouping()}
|
onClick={() => void saveGrouping()}
|
||||||
disabled={busy || !groupingDraft.name.trim()}
|
disabled={busy || !groupingDraft.name.trim()}
|
||||||
|
disabledReason={postboxBusyReason(false, busy) ?? (!groupingDraft.name.trim() ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
@@ -818,7 +916,7 @@ export default function PostboxPage({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="postbox-form-grid">
|
<div className="postbox-form-grid">
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={groupingDraft.name}
|
value={groupingDraft.name}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -871,11 +969,11 @@ export default function PostboxPage({
|
|||||||
open={messageDialogOpen}
|
open={messageDialogOpen}
|
||||||
title={replyParent ? "Reply" : "New message"}
|
title={replyParent ? "Reply" : "New message"}
|
||||||
className="postbox-dialog postbox-message-dialog"
|
className="postbox-dialog postbox-message-dialog"
|
||||||
onClose={() => setMessageDialogOpen(false)}
|
onClose={closeMessageDialog}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<div className="postbox-dialog-actions end">
|
<div className="postbox-dialog-actions end">
|
||||||
<Button onClick={() => setMessageDialogOpen(false)} disabled={busy}>
|
<Button onClick={closeMessageDialog} disabled={busy} disabledReason={postboxBusyReason(false, busy)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -886,6 +984,7 @@ export default function PostboxPage({
|
|||||||
!messageDraft.postbox_id ||
|
!messageDraft.postbox_id ||
|
||||||
!messageDraft.subject.trim()
|
!messageDraft.subject.trim()
|
||||||
}
|
}
|
||||||
|
disabledReason={postboxBusyReason(false, busy) ?? ((!messageDraft.postbox_id || !messageDraft.subject.trim()) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||||
>
|
>
|
||||||
<Send size={16} /> Send
|
<Send size={16} /> Send
|
||||||
</Button>
|
</Button>
|
||||||
@@ -893,7 +992,7 @@ export default function PostboxPage({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="postbox-compose-grid">
|
<div className="postbox-compose-grid">
|
||||||
<FormField label="Postbox">
|
<FormField label="Postbox" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={messageDraft.postbox_id}
|
value={messageDraft.postbox_id}
|
||||||
disabled={Boolean(replyParent)}
|
disabled={Boolean(replyParent)}
|
||||||
@@ -915,7 +1014,7 @@ export default function PostboxPage({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Classification">
|
<FormField label="Classification" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={messageDraft.classification}
|
value={messageDraft.classification}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -937,7 +1036,7 @@ export default function PostboxPage({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="postbox-compose-wide">
|
<div className="postbox-compose-wide">
|
||||||
<FormField label="Recipients">
|
<FormField label="Recipients" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={messageDraft.recipients}
|
value={messageDraft.recipients}
|
||||||
placeholder="Separate addresses with commas"
|
placeholder="Separate addresses with commas"
|
||||||
@@ -951,7 +1050,7 @@ export default function PostboxPage({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<div className="postbox-compose-wide">
|
<div className="postbox-compose-wide">
|
||||||
<FormField label="Subject">
|
<FormField label="Subject" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={messageDraft.subject}
|
value={messageDraft.subject}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -964,7 +1063,7 @@ export default function PostboxPage({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<div className="postbox-compose-wide">
|
<div className="postbox-compose-wide">
|
||||||
<FormField label="Message">
|
<FormField label="Message" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={10}
|
rows={10}
|
||||||
value={messageDraft.body_text}
|
value={messageDraft.body_text}
|
||||||
@@ -979,6 +1078,19 @@ export default function PostboxPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -990,6 +1102,7 @@ function MessageDetail({
|
|||||||
message: PostboxMessage;
|
message: PostboxMessage;
|
||||||
postbox?: PostboxDirectoryItem;
|
postbox?: PostboxDirectoryItem;
|
||||||
}) {
|
}) {
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="postbox-message-detail">
|
<div className="postbox-message-detail">
|
||||||
{message.availability !== "available" ? (
|
{message.availability !== "available" ? (
|
||||||
@@ -1014,7 +1127,7 @@ function MessageDetail({
|
|||||||
<h1>{message.subject}</h1>
|
<h1>{message.subject}</h1>
|
||||||
<div className="postbox-detail-byline">
|
<div className="postbox-detail-byline">
|
||||||
<span>{message.sender_label || message.producer_module || "Platform"}</span>
|
<span>{message.sender_label || message.producer_module || "Platform"}</span>
|
||||||
<time>{formatLongDate(message.delivered_at)}</time>
|
<time>{formatLongDate(message.delivered_at, language)}</time>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<section className="postbox-provenance">
|
<section className="postbox-provenance">
|
||||||
@@ -1122,10 +1235,10 @@ function classificationLabel(value: string): string {
|
|||||||
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
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);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return new Intl.DateTimeFormat(language, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -1133,10 +1246,10 @@ function formatDate(value: string): string {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLongDate(value: string): string {
|
function formatLongDate(value: string, language: string): string {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return new Intl.DateTimeFormat(language, {
|
||||||
dateStyle: "long",
|
dateStyle: "long",
|
||||||
timeStyle: "short"
|
timeStyle: "short"
|
||||||
}).format(date);
|
}).format(date);
|
||||||
@@ -1145,3 +1258,7 @@ function formatLongDate(value: string): string {
|
|||||||
function errorMessage(error: unknown): string {
|
function errorMessage(error: unknown): string {
|
||||||
return error instanceof Error ? error.message : "Postbox request failed";
|
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 DashboardWidgetsUiCapability,
|
||||||
type PlatformWebModule
|
type PlatformWebModule
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import PostboxInboxWidget from "./features/postbox/PostboxInboxWidget";
|
import PostboxInboxWidget from "./features/postbox/PostboxInboxWidget";
|
||||||
import "./styles/postbox.css";
|
import "./styles/postbox.css";
|
||||||
|
|
||||||
@@ -14,16 +15,21 @@ const PostboxAdminPanel = lazy(
|
|||||||
() => import("./features/postbox/PostboxAdminPanel")
|
() => import("./features/postbox/PostboxAdminPanel")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: generatedTranslations.en,
|
||||||
|
de: generatedTranslations.de
|
||||||
|
};
|
||||||
|
|
||||||
const readScope = ["postbox:postbox:read"];
|
const readScope = ["postbox:postbox:read"];
|
||||||
const postboxDashboardWidgets: DashboardWidgetsUiCapability = {
|
const postboxDashboardWidgets: DashboardWidgetsUiCapability = {
|
||||||
widgets: [
|
widgets: [
|
||||||
{
|
{
|
||||||
id: "postbox.inbox",
|
id: "postbox.inbox",
|
||||||
surfaceId: "postbox.widget.inbox",
|
surfaceId: "postbox.widget.inbox",
|
||||||
title: "Postbox inbox",
|
title: "i18n:govoplan-postbox.postbox_inbox",
|
||||||
description: "Unread messages across accessible Postboxes.",
|
description: "i18n:govoplan-postbox.postbox_inbox_description",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
category: "Communication",
|
category: "i18n:govoplan-postbox.communication",
|
||||||
order: 55,
|
order: 55,
|
||||||
defaultVisible: false,
|
defaultVisible: false,
|
||||||
defaultSize: "medium",
|
defaultSize: "medium",
|
||||||
@@ -36,7 +42,7 @@ const postboxDashboardWidgets: DashboardWidgetsUiCapability = {
|
|||||||
configurationFields: [
|
configurationFields: [
|
||||||
{
|
{
|
||||||
id: "maxItems",
|
id: "maxItems",
|
||||||
label: "Maximum messages",
|
label: "i18n:govoplan-postbox.maximum_messages",
|
||||||
kind: "number",
|
kind: "number",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 12,
|
max: 12,
|
||||||
@@ -60,7 +66,7 @@ const postboxAdminSections: AdminSectionsUiCapability = {
|
|||||||
id: "postbox",
|
id: "postbox",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
kind: "management",
|
kind: "management",
|
||||||
label: "Postboxes",
|
label: "i18n:govoplan-postbox.postboxes",
|
||||||
group: "TENANT",
|
group: "TENANT",
|
||||||
order: 45,
|
order: 45,
|
||||||
surfaceId: "postbox.admin.templates",
|
surfaceId: "postbox.admin.templates",
|
||||||
@@ -80,8 +86,8 @@ const postboxAdminSections: AdminSectionsUiCapability = {
|
|||||||
|
|
||||||
export const postboxModule: PlatformWebModule = {
|
export const postboxModule: PlatformWebModule = {
|
||||||
id: "postbox",
|
id: "postbox",
|
||||||
label: "Postbox",
|
label: "i18n:govoplan-postbox.postbox",
|
||||||
version: "0.1.0",
|
version: "0.1.2",
|
||||||
dependencies: ["identity", "organizations", "idm"],
|
dependencies: ["identity", "organizations", "idm"],
|
||||||
optionalDependencies: [
|
optionalDependencies: [
|
||||||
"access",
|
"access",
|
||||||
@@ -95,10 +101,11 @@ export const postboxModule: PlatformWebModule = {
|
|||||||
"views",
|
"views",
|
||||||
"workflow"
|
"workflow"
|
||||||
],
|
],
|
||||||
|
translations,
|
||||||
navItems: [
|
navItems: [
|
||||||
{
|
{
|
||||||
to: "/postbox",
|
to: "/postbox",
|
||||||
label: "Postbox",
|
label: "i18n:govoplan-postbox.postbox",
|
||||||
iconName: "inbox",
|
iconName: "inbox",
|
||||||
anyOf: readScope,
|
anyOf: readScope,
|
||||||
order: 58
|
order: 58
|
||||||
@@ -119,28 +126,28 @@ export const postboxModule: PlatformWebModule = {
|
|||||||
id: "postbox.inbox.directory",
|
id: "postbox.inbox.directory",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Postbox directory",
|
label: "i18n:govoplan-postbox.postbox_directory",
|
||||||
order: 10
|
order: 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "postbox.inbox.messages",
|
id: "postbox.inbox.messages",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Postbox messages",
|
label: "i18n:govoplan-postbox.postbox_messages",
|
||||||
order: 20
|
order: 20
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "postbox.widget.inbox",
|
id: "postbox.widget.inbox",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Postbox inbox widget",
|
label: "i18n:govoplan-postbox.postbox_inbox_widget",
|
||||||
order: 25
|
order: 25
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "postbox.admin.templates",
|
id: "postbox.admin.templates",
|
||||||
moduleId: "postbox",
|
moduleId: "postbox",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Postbox templates and bindings",
|
label: "i18n:govoplan-postbox.postbox_templates_bindings",
|
||||||
order: 30
|
order: 30
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user