Implement governed hybrid campaign delivery

This commit is contained in:
2026-08-02 13:58:37 +02:00
parent b38597f2be
commit 7733265cc8
40 changed files with 2531 additions and 122 deletions
@@ -36,6 +36,7 @@ export type RecipientProfileColumnContext = {
locked: boolean;
filesModuleInstalled: boolean;
postboxModuleInstalled: boolean;
templatesModuleInstalled: boolean;
postboxCatalog: CampaignPostboxCatalog;
entries: Record<string, unknown>[];
fieldDefinitions: ReturnType<typeof getDraftFields>;
@@ -53,7 +54,7 @@ export type RecipientProfileColumnContext = {
removeEntry: (index: number) => void;
};
export function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, addressFilter, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
export function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, templatesModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, addressFilter, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
return [
{
id: "number",
@@ -109,7 +110,7 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f
value: recipientAddressFilterValue
},
{ id: "active", header: "i18n:govoplan-campaign.active.a733b809", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "active", label: "i18n:govoplan-campaign.active.a733b809" }, { value: "inactive", label: "i18n:govoplan-campaign.inactive.09af574c" }] }, render: (entry, index) => <ToggleSwitch label="i18n:govoplan-campaign.active.a733b809" checked={entry.active !== false} disabled={locked} onChange={(checked) => updateEntry(index, (current) => ({ ...current, active: checked }))} />, value: (entry) => entry.active !== false ? "active" : "inactive" },
...(postboxModuleInstalled ? [{
...(postboxModuleInstalled || templatesModuleInstalled || entries.some((entry) => Boolean(entry.channel_policy || entry.print_target || normalizePostboxTargets(entry.postbox_targets).length)) ? [{
id: "delivery",
header: "Delivery",
width: "minmax(260px, 0.9fr)",
@@ -118,6 +119,7 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f
filterable: true,
render: (entry, index) => {
const targets = normalizePostboxTargets(entry.postbox_targets);
const printTarget = asRecord(entry.print_target);
return (
<div className="campaign-recipient-delivery-cell">
<select
@@ -133,21 +135,31 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f
>
<option value="">Campaign default</option>
<option value="mail">Mail</option>
<option value="postbox">Postbox</option>
<option value="mail_and_postbox">Mail and Postbox</option>
<option value="mail_then_postbox">Mail, then Postbox fallback</option>
<option value="postbox_then_mail">Postbox, then Mail fallback</option>
<option value="postbox" disabled={!postboxModuleInstalled}>Postbox</option>
<option value="print" disabled={!templatesModuleInstalled}>Print</option>
<option value="mail_and_postbox" disabled={!postboxModuleInstalled}>Mail and Postbox</option>
<option value="mail_then_postbox" disabled={!postboxModuleInstalled}>Mail, then Postbox fallback</option>
<option value="postbox_then_mail" disabled={!postboxModuleInstalled}>Postbox, then Mail fallback</option>
<option value="mail_then_print" disabled={!templatesModuleInstalled}>Mail, then print fallback</option>
<option value="postbox_then_print" disabled={!postboxModuleInstalled || !templatesModuleInstalled}>Postbox, then print fallback</option>
</select>
<Button
disabled={locked || !postboxCatalog.available}
onClick={() => openPostboxTargetEditor(index)}
>
Postboxes ({targets.length})
</Button>
{postboxModuleInstalled && (
<Button
disabled={locked || !postboxCatalog.available}
onClick={() => openPostboxTargetEditor(index)}
>
Postboxes ({targets.length})
</Button>
)}
{printTarget.target && (
<span className="muted small-note" title={String(printTarget.target)}>
{printTarget.channel === "internal_mail" ? "Internal mail" : "Postal"}: {String(printTarget.target)}
</span>
)}
</div>
);
},
value: (entry) => `${String(entry.channel_policy ?? "default")} ${normalizePostboxTargets(entry.postbox_targets).map((target) => target.label ?? target.postbox_id ?? target.template_id ?? "").join(" ")}`
value: (entry) => `${String(entry.channel_policy ?? "default")} ${normalizePostboxTargets(entry.postbox_targets).map((target) => target.label ?? target.postbox_id ?? target.template_id ?? "").join(" ")} ${String(asRecord(entry.print_target).target ?? "")}`
} as DataGridColumn<Record<string, unknown>>] : []),
...(individualAttachmentBasePaths.length > 0 ? [{
id: "attachments",