234 lines
10 KiB
TypeScript
234 lines
10 KiB
TypeScript
import { Pencil } from "lucide-react";
|
|
import type { ApiSettings } from "../../../types";
|
|
import type { CampaignPostboxCatalog } from "../../../api/campaigns";
|
|
import {
|
|
Button,
|
|
DataGridRowActions,
|
|
ToggleSwitch,
|
|
i18nMessage,
|
|
type DataGridColumn
|
|
} from "@govoplan/core-webui";
|
|
import AttachmentRulesOverlay from "../components/AttachmentRulesOverlay";
|
|
import FieldValueInput from "../components/FieldValueInput";
|
|
import {
|
|
normalizePostboxTargets
|
|
} from "../components/PostboxTargetsDialog";
|
|
import { buildTemplatePreviewContext } from "../utils/templatePlaceholders";
|
|
import {
|
|
getIndividualAttachmentBasePaths,
|
|
normalizeAttachmentRules,
|
|
type AttachmentRule,
|
|
type AttachmentZipCollection
|
|
} from "../utils/attachments";
|
|
import { getDraftFields } from "../utils/fieldDefinitions";
|
|
import { asRecord } from "../utils/campaignView";
|
|
import {
|
|
getEntryAddresses,
|
|
hiddenRecipientAddressMatch,
|
|
recipientAddressFilterValue,
|
|
recipientAddressSummary
|
|
} from "./RecipientAddressEditor";
|
|
|
|
export type RecipientProfileColumnContext = {
|
|
settings: ApiSettings;
|
|
campaignId: string;
|
|
draft: Record<string, unknown>;
|
|
locked: boolean;
|
|
filesModuleInstalled: boolean;
|
|
postboxModuleInstalled: boolean;
|
|
templatesModuleInstalled: boolean;
|
|
postboxCatalog: CampaignPostboxCatalog;
|
|
entries: Record<string, unknown>[];
|
|
fieldDefinitions: ReturnType<typeof getDraftFields>;
|
|
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
|
|
zipConfig: AttachmentZipCollection;
|
|
addressFilter: string;
|
|
translateText: (value: string) => string;
|
|
openAddressEditor: (index: number) => void;
|
|
openPostboxTargetEditor: (index: number) => void;
|
|
updateEntry: (index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) => void;
|
|
updateEntryAttachments: (index: number, attachments: AttachmentRule[]) => void;
|
|
updateEntryField: (index: number, field: string, value: unknown) => void;
|
|
addRecipient: (afterIndex?: number) => void;
|
|
moveEntry: (index: number, targetIndex: number) => void;
|
|
removeEntry: (index: number) => void;
|
|
};
|
|
|
|
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",
|
|
header: "#",
|
|
width: 72,
|
|
sortable: true,
|
|
filterType: "integer",
|
|
sticky: "start",
|
|
render: (_entry, index) =>
|
|
<span className="recipient-data-number">
|
|
{index + 1}
|
|
</span>,
|
|
|
|
value: (_entry, index) => index + 1
|
|
},
|
|
{
|
|
id: "recipients",
|
|
header: "Recipient(s)",
|
|
width: "minmax(320px, 1.4fr)",
|
|
maxWidth: 640,
|
|
resizable: true,
|
|
filterable: true,
|
|
render: (entry, index) => {
|
|
const summary = recipientAddressSummary(entry, translateText);
|
|
const hiddenMatch = hiddenRecipientAddressMatch(entry, addressFilter);
|
|
const content = (
|
|
<>
|
|
<span className="recipient-address-editor-main">
|
|
<span className={`recipient-data-address ${summary.empty ? "is-empty" : ""}`}>{summary.primary}</span>
|
|
{summary.badges.map((badge) =>
|
|
<span className="recipient-extra-bubble" key={badge}>{badge}</span>
|
|
)}
|
|
{hiddenMatch &&
|
|
<span className="recipient-hidden-address-match">
|
|
Matched in {hiddenMatch.label}: {hiddenMatch.address}
|
|
</span>
|
|
}
|
|
</span>
|
|
{!locked && <Pencil aria-hidden="true" />}
|
|
</>
|
|
);
|
|
if (locked) {
|
|
return <div className="recipient-address-editor-trigger is-readonly">{content}</div>;
|
|
}
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="recipient-address-editor-trigger"
|
|
onClick={() => openAddressEditor(index)}>
|
|
{content}
|
|
</button>);
|
|
},
|
|
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 || 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)",
|
|
maxWidth: 480,
|
|
resizable: true,
|
|
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
|
|
value={typeof entry.channel_policy === "string" ? entry.channel_policy : ""}
|
|
disabled={locked}
|
|
aria-label={`Recipient ${index + 1} delivery policy`}
|
|
onChange={(event) => updateEntry(index, (current) => {
|
|
const next = { ...current };
|
|
if (event.target.value) next.channel_policy = event.target.value;
|
|
else delete next.channel_policy;
|
|
return next;
|
|
})}
|
|
>
|
|
<option value="">Campaign default</option>
|
|
<option value="mail">Mail</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>
|
|
{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(" ")} ${String(asRecord(entry.print_target).target ?? "")}`
|
|
} as DataGridColumn<Record<string, unknown>>] : []),
|
|
...(individualAttachmentBasePaths.length > 0 ? [{
|
|
id: "attachments",
|
|
header: "i18n:govoplan-campaign.attachments.6771ade6",
|
|
width: 180,
|
|
filterable: true,
|
|
render: (entry, index) => {
|
|
const attachments = normalizeAttachmentRules(entry.attachments);
|
|
return (
|
|
<AttachmentRulesOverlay
|
|
title={i18nMessage("i18n:govoplan-campaign.attachments_for_recipient_value.9a8df82d", { value0: index + 1 })}
|
|
rules={attachments}
|
|
settings={settings}
|
|
campaignId={campaignId}
|
|
disabled={locked}
|
|
buttonLabel={`entries: ${attachments.length}`}
|
|
basePaths={individualAttachmentBasePaths}
|
|
zipConfig={zipConfig}
|
|
filesModuleInstalled={filesModuleInstalled}
|
|
previewContext={buildTemplatePreviewContext(draft, entry)}
|
|
onChange={(rules) => updateEntryAttachments(index, rules)} />);
|
|
|
|
|
|
},
|
|
value: (entry) => normalizeAttachmentRules(entry.attachments).map((rule) => `${rule.label ?? ""} ${rule.file_filter ?? ""}`).join(", ")
|
|
}] : []),
|
|
...fieldDefinitions.filter((field) => field.can_override !== false).map((field): DataGridColumn<Record<string, unknown>> => ({
|
|
id: `field-${field.name}`,
|
|
header: field.label || field.name,
|
|
width: 190,
|
|
minWidth: 160,
|
|
maxWidth: 360,
|
|
resizable: true,
|
|
sortable: true,
|
|
filterable: true,
|
|
filterType: field.type === "integer" ? "integer" : field.type === "double" ? "number" : field.type === "date" ? "date" : "text",
|
|
render: (entry, index) => {
|
|
const fields = asRecord(entry.fields);
|
|
return (
|
|
<FieldValueInput
|
|
className="recipient-field-input"
|
|
fieldType={field.type}
|
|
value={fields[field.name]}
|
|
disabled={locked}
|
|
onChange={(value) => updateEntryField(index, field.name, value)} />);
|
|
|
|
|
|
},
|
|
value: (entry) => String(asRecord(entry.fields)[field.name] ?? "")
|
|
})),
|
|
{
|
|
id: "actions",
|
|
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
|
width: 180,
|
|
sticky: "end",
|
|
render: (_entry, index) =>
|
|
<DataGridRowActions
|
|
disabled={locked}
|
|
onAddBelow={() => addRecipient(index)}
|
|
onRemove={() => removeEntry(index)}
|
|
onMoveUp={index > 0 ? () => moveEntry(index, index - 1) : undefined}
|
|
onMoveDown={index < entries.length - 1 ? () => moveEntry(index, index + 1) : undefined}
|
|
addLabel="i18n:govoplan-campaign.add_recipient_below.52fb47d8"
|
|
removeLabel="i18n:govoplan-campaign.remove_recipient.d1bc9f53"
|
|
moveUpLabel="i18n:govoplan-campaign.move_recipient_up.90c6bccc"
|
|
moveDownLabel="i18n:govoplan-campaign.move_recipient_down.ead5466c" />
|
|
|
|
|
|
}];
|
|
|
|
}
|