chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:18 +02:00
parent 57f6066bf7
commit 52e6ed49c5
66 changed files with 8868 additions and 4072 deletions

View File

@@ -4,7 +4,8 @@ import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { StatusBadge, i18nMessage } from "@govoplan/core-webui";
import { usePlatformLanguage } from "@govoplan/core-webui";
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
@@ -25,93 +26,98 @@ type TemplateRecord = {
type TemplateDetailSection = "overview" | "content" | "fields" | "preview" | "usage" | "versions";
const templateRecords: TemplateRecord[] = [
{
id: "monthly-statement",
name: "Monthly statement",
description: "Reusable subject and body for monthly statement mailings.",
type: "Plain text",
status: "ready",
fields: ["recipient_name", "period", "amount"],
updatedAt: "2026-06-08 16:42",
usedBy: "2 campaigns",
subject: "Your statement for {{period}}",
body: "Hello {{recipient_name}},\n\nplease find your statement for {{period}} attached.",
versions: 4
},
{
id: "deadline-reminder",
name: "Deadline reminder",
description: "Short reminder template with one deadline field.",
type: "Plain text",
status: "draft",
fields: ["recipient_name", "deadline"],
updatedAt: "2026-06-07 11:18",
usedBy: "Not used yet",
subject: "Reminder: {{deadline}}",
body: "Hello {{recipient_name}},\n\nthis is a reminder that the deadline is {{deadline}}.",
versions: 1
},
{
id: "attachment-notice",
name: "Attachment notice",
description: "Generic note for campaigns where every recipient receives a file.",
type: "HTML-ready",
status: "ready",
fields: ["recipient_name", "file_label", "contact_email"],
updatedAt: "2026-06-05 09:05",
usedBy: "1 campaign",
subject: "Documents for {{recipient_name}}",
body: "Hello {{recipient_name}},\n\nyour {{file_label}} is attached. Please contact {{contact_email}} if anything is missing.",
versions: 3
}
];
{
id: "monthly-statement",
name: "i18n:govoplan-campaign.monthly_statement.74029609",
description: "i18n:govoplan-campaign.reusable_subject_and_body_for_monthly_statement_.9754240d",
type: "plain_text",
status: "ready",
fields: ["recipient_name", "period", "amount"],
updatedAt: "2026-06-08 16:42",
usedBy: "i18n:govoplan-campaign.2_campaigns.35b84804",
subject: "i18n:govoplan-campaign.subject_monthly_statement",
body: "i18n:govoplan-campaign.hello_value_please_find_your_statement_for_value.48d94596",
versions: 4
},
{
id: "deadline-reminder",
name: "i18n:govoplan-campaign.deadline_reminder.84977a7f",
description: "i18n:govoplan-campaign.short_reminder_template_with_one_deadline_field.5f15d110",
type: "plain_text",
status: "draft",
fields: ["recipient_name", "deadline"],
updatedAt: "2026-06-07 11:18",
usedBy: "i18n:govoplan-campaign.not_used_yet.962b7bbd",
subject: "i18n:govoplan-campaign.subject_deadline_reminder",
body: "i18n:govoplan-campaign.hello_value_this_is_a_reminder_that_the_deadline.c4fbdbd0",
versions: 1
},
{
id: "attachment-notice",
name: "i18n:govoplan-campaign.attachment_notice.b73a59fe",
description: "i18n:govoplan-campaign.generic_note_for_campaigns_where_every_recipient.f21254fa",
type: "html_ready",
status: "ready",
fields: ["recipient_name", "file_label", "contact_email"],
updatedAt: "2026-06-05 09:05",
usedBy: "i18n:govoplan-campaign.1_campaign.ccd70074",
subject: "i18n:govoplan-campaign.subject_documents_for_recipient",
body: "i18n:govoplan-campaign.hello_value_your_value_is_attached_please_contac.da32415e",
versions: 3
}];
const templateSubnav = (onBack: () => void): ModuleSubnavGroup<TemplateDetailSection>[] => [
{
items: [{ actionId: "template-library", label: "← Template library", primary: true, onClick: onBack }]
},
{
title: "TEMPLATE",
items: [
{ id: "overview", label: "Overview" },
{ id: "content", label: "Content" },
{ id: "fields", label: "Fields" },
{ id: "preview", label: "Preview" },
{ id: "usage", label: "Usage" },
{ id: "versions", label: "Versions" }
]
}
];
{
items: [{ actionId: "template-library", label: "i18n:govoplan-campaign.template_library.6742c898", primary: true, onClick: onBack }]
},
{
title: "i18n:govoplan-campaign.template.4c31d4ef",
items: [
{ id: "overview", label: "i18n:govoplan-campaign.overview.0efc2e6b" },
{ id: "content", label: "i18n:govoplan-campaign.content.4f9be057" },
{ id: "fields", label: "i18n:govoplan-campaign.fields.e8b68527" },
{ id: "preview", label: "i18n:govoplan-campaign.preview.f1fbb2b4" },
{ id: "usage", label: "i18n:govoplan-campaign.usage.0bb18642" },
{ id: "versions", label: "i18n:govoplan-campaign.versions.a239107e" }]
}];
function templateLibraryColumns(openTemplate: (templateId: string) => void): DataGridColumn<TemplateRecord>[] {
return [
{ id: "template", header: "Template", width: "minmax(240px, 1.4fr)", sortable: true, filterable: true, sticky: "start", render: (template) => <div className="module-title-cell"><strong>{template.name}</strong><span>{template.description}</span></div>, value: (template) => `${template.name} ${template.description}` },
{ id: "type", header: "Type", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Campaign", label: "Campaign" }, { value: "Notification", label: "Notification" }, { value: "Attachment", label: "Attachment" }] }, value: (template) => template.type },
{ id: "fields", header: "Fields", width: 240, filterable: true, render: (template) => <div className="chip-row compact-chip-row">{template.fields.slice(0, 3).map((field) => <span key={field} className="field-chip">{field}</span>)}</div>, value: (template) => template.fields.join(", ") },
{ id: "updated", header: "Updated", width: 170, sortable: true, filterable: true, filterType: "date", render: (template) => <span className="muted small-text">{template.updatedAt}</span>, value: (template) => template.updatedAt },
{ id: "status", header: "Status", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "archived", label: "Archived" }], display: "pill" }, render: (template) => <StatusBadge status={template.status} />, value: (template) => template.status },
{
id: "actions",
header: "Actions",
width: 70,
sticky: "end",
align: "right",
render: (template) => (
<Button className="admin-icon-button" onClick={() => openTemplate(template.id)} aria-label={`Open ${template.name}`} title={`Open ${template.name}`}>
{ id: "template", header: "i18n:govoplan-campaign.template.3ec1ae06", width: "minmax(240px, 1.4fr)", sortable: true, filterable: true, sticky: "start", render: (template) => <div className="module-title-cell"><strong>{template.name}</strong><span>{template.description}</span></div>, value: (template) => `${template.name} ${template.description}` },
{ id: "type", header: "i18n:govoplan-campaign.type.3deb7456", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "plain_text", label: "i18n:govoplan-campaign.plain_text_template_type" }, { value: "html_ready", label: "i18n:govoplan-campaign.html_ready_template_type" }] }, value: (template) => template.type },
{ id: "fields", header: "i18n:govoplan-campaign.fields.e8b68527", width: 240, filterable: true, render: (template) => <div className="chip-row compact-chip-row">{template.fields.slice(0, 3).map((field) => <span key={field} className="field-chip">{field}</span>)}</div>, value: (template) => template.fields.join(", ") },
{ id: "updated", header: "i18n:govoplan-campaign.updated.f2f8570d", width: 170, sortable: true, filterable: true, filterType: "date", render: (template) => <span className="muted small-text">{template.updatedAt}</span>, value: (template) => template.updatedAt },
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "draft", label: "i18n:govoplan-campaign.draft.23d33e22" }, { value: "active", label: "i18n:govoplan-campaign.active.a733b809" }, { value: "archived", label: "i18n:govoplan-campaign.archived.eddc813f" }], display: "pill" }, render: (template) => <StatusBadge status={template.status} />, value: (template) => template.status },
{
id: "actions",
header: "i18n:govoplan-campaign.actions.c3cd636a",
width: 70,
sticky: "end",
align: "right",
render: (template) =>
<Button className="admin-icon-button" onClick={() => openTemplate(template.id)} aria-label={i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: template.name })} title={i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: template.name })}>
<ExternalLink />
</Button>
)
}
];
}];
}
function templateFieldColumns(): DataGridColumn<{ id: string; field: string; source: string; required: string }>[] {
function templateTypeLabel(type: string): string {
if (type === "html_ready") return "i18n:govoplan-campaign.html_ready_template_type";
return "i18n:govoplan-campaign.plain_text_template_type";
}
function templateFieldColumns(): DataGridColumn<{id: string;field: string;source: string;required: string;}>[] {
return [
{ id: "field", header: "Field", width: "minmax(180px, 1fr)", sortable: true, filterable: true, sticky: "start", render: (row) => <code>{row.field}</code>, value: (row) => row.field },
{ id: "source", header: "Source", width: 190, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Detected placeholder", label: "Detected placeholder" }] }, value: (row) => row.source },
{ id: "required", header: "Required", width: 120, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Yes", label: "Yes" }, { value: "No", label: "No" }] }, value: (row) => row.required },
{ id: "actions", header: "Actions", width: 130, sticky: "end", render: () => <Button disabled>Configure</Button> }
];
{ id: "field", header: "i18n:govoplan-campaign.field.c326a466", width: "minmax(180px, 1fr)", sortable: true, filterable: true, sticky: "start", render: (row) => <code>{row.field}</code>, value: (row) => row.field },
{ id: "source", header: "i18n:govoplan-campaign.source.6da13add", width: 190, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "detected_placeholder", label: "i18n:govoplan-campaign.detected_placeholder.094b5214" }] }, value: (row) => row.source },
{ id: "required", header: "i18n:govoplan-campaign.required.eed6bfb4", width: 120, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "yes", label: "i18n:govoplan-campaign.yes.5397e058" }, { value: "no", label: "i18n:govoplan-campaign.no.816c52fd" }] }, value: (row) => row.required },
{ id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 130, sticky: "end", render: () => <Button disabled>i18n:govoplan-campaign.configure.792c81a4</Button> }];
}
export default function TemplatesPage() {
@@ -139,8 +145,8 @@ export default function TemplatesPage() {
<p>{selectedTemplate.description}</p>
</div>
<div className="button-row compact-actions">
<Button disabled>Duplicate</Button>
<Button variant="primary" disabled>Use in campaign</Button>
<Button disabled>i18n:govoplan-campaign.duplicate.972d5737</Button>
<Button variant="primary" disabled>i18n:govoplan-campaign.use_in_campaign.779163bc</Button>
</div>
</div>
@@ -152,133 +158,144 @@ export default function TemplatesPage() {
{active === "versions" && <TemplateVersions template={selectedTemplate} />}
</div>
</section>
</div>
);
</div>);
}
return (
<div className="content-pad workspace-data-page module-entry-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>Templates</PageTitle>
<p>Reusable message templates. Open a template to edit content, fields, preview and usage.</p>
<PageTitle>i18n:govoplan-campaign.templates.f25b700e</PageTitle>
<p>i18n:govoplan-campaign.reusable_message_templates_open_a_template_to_ed.792e2afb</p>
</div>
<div className="button-row compact-actions">
<Button disabled>Import</Button>
<Button variant="primary" disabled>Export</Button>
<Button disabled>i18n:govoplan-campaign.import.d6fbc9d2</Button>
<Button variant="primary" disabled>i18n:govoplan-campaign.export.f3e4fadb</Button>
</div>
</div>
<Card
title={
<div className="module-card-heading">
<h2>All templates</h2>
<span>Last loaded: local demo data</span>
<div className="module-card-heading">
<h2>i18n:govoplan-campaign.all_templates.0bba114c</h2>
<span>i18n:govoplan-campaign.last_loaded_local_demo_data.62ee2f5d</span>
</div>
}
actions={<Button disabled>Refresh</Button>}
>
actions={<Button disabled>i18n:govoplan-campaign.refresh.56e3badc</Button>}>
<DataGrid
id="template-library"
rows={templateRecords}
columns={templateLibraryColumns(openTemplate)}
getRowKey={(template) => template.id}
emptyText="No templates found."
className="compact-table-wrap module-table-wrap module-entry-table"
/>
emptyText="i18n:govoplan-campaign.no_templates_found.326abcdd"
className="compact-table-wrap module-table-wrap module-entry-table" />
</Card>
</div>
);
</div>);
}
function TemplateOverview({ template }: { template: TemplateRecord }) {
function TemplateOverview({ template }: {template: TemplateRecord;}) {
return (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Template status">
<Card title="i18n:govoplan-campaign.template_status.8bec97cf">
<dl className="detail-list compact-detail-list">
<div><dt>Status</dt><dd><StatusBadge status={template.status} /></dd></div>
<div><dt>Type</dt><dd>{template.type}</dd></div>
<div><dt>Updated</dt><dd>{template.updatedAt}</dd></div>
<div><dt>Versions</dt><dd>{template.versions}</dd></div>
<div><dt>i18n:govoplan-campaign.status.bae7d5be</dt><dd><StatusBadge status={template.status} /></dd></div>
<div><dt>i18n:govoplan-campaign.type.3deb7456</dt><dd>{templateTypeLabel(template.type)}</dd></div>
<div><dt>i18n:govoplan-campaign.updated.f2f8570d</dt><dd>{template.updatedAt}</dd></div>
<div><dt>i18n:govoplan-campaign.versions.a239107e</dt><dd>{template.versions}</dd></div>
</dl>
</Card>
<Card title="Compatibility notes">
<p className="muted">Campaigns can reuse this template, but each campaign still needs a field matching check because campaign fields and template placeholders can drift.</p>
<Card title="i18n:govoplan-campaign.compatibility_notes.7d648a6d">
<p className="muted">i18n:govoplan-campaign.campaigns_can_reuse_this_template_but_each_campa.bdb25009</p>
<div className="placeholder-stack">
<span>Subject placeholders are detected</span>
<span>Body placeholders are compared with campaign fields</span>
<span>A mapping wizard can be added later</span>
<span>i18n:govoplan-campaign.subject_placeholders_are_detected.96663276</span>
<span>i18n:govoplan-campaign.body_placeholders_are_compared_with_campaign_fie.bbd1d67f</span>
<span>i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3</span>
</div>
</Card>
</div>
);
</div>);
}
function TemplateContent({ template }: { template: TemplateRecord }) {
function TemplateContent({ template }: {template: TemplateRecord;}) {
const { translateText } = usePlatformLanguage();
const subject = translateText(template.subject);
const body = translateText(template.body);
return (
<Card title="Template content" actions={<Button disabled>Save changes</Button>}>
<Card title="i18n:govoplan-campaign.template_content.48630575" actions={<Button disabled>i18n:govoplan-campaign.save_changes.179359b3</Button>}>
<div className="form-grid compact responsive-form-grid">
<label className="form-field"><FieldLabel className="form-label" help="Read-only subject stored in this reusable template record.">Subject</FieldLabel><input value={template.subject} readOnly /></label>
<label className="form-field full-span"><FieldLabel className="form-label" help="Read-only body stored in this reusable template record.">Body</FieldLabel><textarea rows={10} value={template.body} readOnly /></label>
<label className="form-field"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_subject_stored_in_this_reusable_templa.549ae246">i18n:govoplan-campaign.subject.8d183dbd</FieldLabel><input value={subject} readOnly /></label>
<label className="form-field full-span"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_body_stored_in_this_reusable_template_.98fbf178">i18n:govoplan-campaign.body.718a7e8a</FieldLabel><textarea rows={10} value={body} readOnly /></label>
</div>
</Card>
);
</Card>);
}
function TemplateFields({ template }: { template: TemplateRecord }) {
function TemplateFields({ template }: {template: TemplateRecord;}) {
return (
<Card title="Template fields" actions={<Button disabled>Add field hint</Button>}>
<Card title="i18n:govoplan-campaign.template_fields.e6d7d8ac" actions={<Button disabled>i18n:govoplan-campaign.add_field_hint.b5029047</Button>}>
<DataGrid
id={`template-${template.id}-fields`}
rows={template.fields.map((field) => ({ id: field, field, source: "Detected placeholder", required: "Yes" }))}
rows={template.fields.map((field) => ({ id: field, field, source: "detected_placeholder", required: "yes" }))}
columns={templateFieldColumns()}
getRowKey={(field) => field.id}
emptyText="No fields detected."
className="compact-table-wrap module-table-wrap module-table"
/>
</Card>
);
emptyText="i18n:govoplan-campaign.no_fields_detected.0cb30100"
className="compact-table-wrap module-table-wrap module-table" />
</Card>);
}
function TemplatePreview({ template }: { template: TemplateRecord }) {
const preview = template.body
.replace(/\{\{recipient_name\}\}/g, "Jane Example")
.replace(/\{\{period\}\}/g, "May 2026")
.replace(/\{\{amount\}\}/g, "123.45 EUR")
.replace(/\{\{deadline\}\}/g, "30 June 2026")
.replace(/\{\{file_label\}\}/g, "statement")
.replace(/\{\{contact_email\}\}/g, "support@example.org");
function TemplatePreview({ template }: {template: TemplateRecord;}) {
const { translateText } = usePlatformLanguage();
const subject = translateText(template.subject);
const body = translateText(template.body);
const sampleRecipientName = translateText("i18n:govoplan-campaign.sample_recipient_name");
const samplePeriod = translateText("i18n:govoplan-campaign.sample_period");
const sampleAmount = translateText("i18n:govoplan-campaign.sample_amount");
const sampleDeadline = translateText("i18n:govoplan-campaign.sample_deadline");
const sampleFileLabel = translateText("i18n:govoplan-campaign.sample_file_label");
const preview = body.
replace(/\{\{recipient_name\}\}/g, sampleRecipientName).
replace(/\{\{period\}\}/g, samplePeriod).
replace(/\{\{amount\}\}/g, sampleAmount).
replace(/\{\{deadline\}\}/g, sampleDeadline).
replace(/\{\{file_label\}\}/g, sampleFileLabel).
replace(/\{\{contact_email\}\}/g, "support@example.org");
return (
<Card title="Preview" actions={<Button disabled>Change sample data</Button>}>
<Card title="i18n:govoplan-campaign.preview.f1fbb2b4" actions={<Button disabled>i18n:govoplan-campaign.change_sample_data.e6903542</Button>}>
<div className="message-preview">
<strong>{template.subject.replace(/\{\{period\}\}/g, "May 2026").replace(/\{\{deadline\}\}/g, "30 June 2026").replace(/\{\{recipient_name\}\}/g, "Jane Example")}</strong>
<strong>{subject.replace(/\{\{period\}\}/g, samplePeriod).replace(/\{\{deadline\}\}/g, sampleDeadline).replace(/\{\{recipient_name\}\}/g, sampleRecipientName)}</strong>
<pre>{preview}</pre>
</div>
</Card>
);
</Card>);
}
function TemplateUsage({ template }: { template: TemplateRecord }) {
function TemplateUsage({ template }: {template: TemplateRecord;}) {
return (
<Card title="Usage">
<p className="muted">This view will list campaigns using the template once the backend template model is available.</p>
<Card title="i18n:govoplan-campaign.usage.0bb18642">
<p className="muted">i18n:govoplan-campaign.this_view_will_list_campaigns_using_the_template.116f7ee0</p>
<dl className="detail-list compact-detail-list">
<div><dt>Currently used by</dt><dd>{template.usedBy}</dd></div>
<div><dt>Safe to edit</dt><dd>Requires versioning once templates are shared</dd></div>
<div><dt>i18n:govoplan-campaign.currently_used_by.a065cfbe</dt><dd>{template.usedBy}</dd></div>
<div><dt>i18n:govoplan-campaign.safe_to_edit.57b64df5</dt><dd>i18n:govoplan-campaign.requires_versioning_once_templates_are_shared.e020b221</dd></div>
</dl>
</Card>
);
</Card>);
}
function TemplateVersions({ template }: { template: TemplateRecord }) {
function TemplateVersions({ template }: {template: TemplateRecord;}) {
return (
<Card title="Versions and import/export" actions={<div className="button-row compact-actions"><Button disabled>Import</Button><Button disabled>Export</Button></div>}>
<Card title="i18n:govoplan-campaign.versions_and_import_export.cc05cb43" actions={<div className="button-row compact-actions"><Button disabled>i18n:govoplan-campaign.import.d6fbc9d2</Button><Button disabled>i18n:govoplan-campaign.export.f3e4fadb</Button></div>}>
<div className="placeholder-stack">
<span>{template.versions} local versions in the planned model</span>
<span>Sent campaigns should keep a fixed template snapshot</span>
<span>Draft campaigns can update to a newer template version later</span>
<span>{template.versions} i18n:govoplan-campaign.local_versions_in_the_planned_model.c1bf26cb</span>
<span>i18n:govoplan-campaign.sent_campaigns_should_keep_a_fixed_template_snap.167d56c2</span>
<span>i18n:govoplan-campaign.draft_campaigns_can_update_to_a_newer_template_v.0f854608</span>
</div>
</Card>
);
</Card>);
}