import { useMemo, useState } from "react"; import { Button, Dialog, DismissibleAlert, FormField, TableActionGroup, ToggleSwitch } from "@govoplan/core-webui"; import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react"; import type { CampaignPostboxCatalog, CampaignPostboxOrganizationUnit } from "../../../api/campaigns"; import type { CampaignFieldDefinition } from "../utils/fieldDefinitions"; import { asRecord } from "../utils/campaignView"; export type CampaignPostboxTarget = { id: string; mode: "direct" | "derived"; label?: string; postbox_id?: string; address_key?: string; template_id?: string; organization_unit_id?: string; organization_unit_field?: string; organization_unit_match?: "id" | "slug"; function_id?: string; function_field?: string; function_match?: "id" | "slug"; context_key?: string; context_field?: string; }; type Props = { open: boolean; title: string; catalog: CampaignPostboxCatalog; fields: CampaignFieldDefinition[]; targets: CampaignPostboxTarget[]; locked?: boolean; merge?: boolean; showMerge?: boolean; onSave: (targets: CampaignPostboxTarget[], merge: boolean) => void; onClose: () => void; }; export default function PostboxTargetsDialog({ open, title, catalog, fields, targets, locked = false, merge = true, showMerge = false, onSave, onClose }: Props) { const [draftTargets, setDraftTargets] = useState( () => targets.map(normalizeTarget) ); const [draftMerge, setDraftMerge] = useState(merge); const [error, setError] = useState(""); function updateTarget(index: number, patch: Partial) { setDraftTargets((current) => current.map((target, currentIndex) => currentIndex === index ? normalizeTarget({ ...target, ...patch }) : target ) ); setError(""); } function addTarget() { const firstPostbox = catalog.postboxes[0]; setDraftTargets((current) => [ ...current, { id: newTargetId(), mode: "direct", postbox_id: firstPostbox?.id ?? "" } ]); } function save() { const prepared = draftTargets.map(normalizeTarget); const invalidIndex = prepared.findIndex((target) => !targetIsComplete(target)); if (invalidIndex >= 0) { setError(`Target ${invalidIndex + 1} is incomplete.`); return; } onSave(prepared, draftMerge); } return ( } >
{showMerge && }
{error && {error}} {draftTargets.length === 0 &&
No Postbox targets configured.
}
{draftTargets.map((target, index) => updateTarget(index, patch)} onMove={(offset) => setDraftTargets((current) => moveTarget(current, index, index + offset))} onRemove={() => setDraftTargets((current) => current.filter((_, currentIndex) => currentIndex !== index))} /> )}
); } function PostboxTargetRow({ target, index, count, catalog, fields, locked, onChange, onMove, onRemove }: { target: CampaignPostboxTarget; index: number; count: number; catalog: CampaignPostboxCatalog; fields: CampaignFieldDefinition[]; locked: boolean; onChange: (patch: Partial) => void; onMove: (offset: number) => void; onRemove: () => void; }) { const selectedUnit = catalog.organization_units.find( (unit) => unit.id === target.organization_unit_id ); return (
Target {index + 1}
onChange({ label: event.target.value })} /> {target.mode === "direct" ? : }
); } function DirectTargetFields({ target, catalog, locked, onChange }: { target: CampaignPostboxTarget; catalog: CampaignPostboxCatalog; locked: boolean; onChange: (patch: Partial) => void; }) { return ( ); } function DerivedTargetFields({ target, catalog, fields, selectedUnit, locked, onChange }: { target: CampaignPostboxTarget; catalog: CampaignPostboxCatalog; fields: CampaignFieldDefinition[]; selectedUnit?: CampaignPostboxOrganizationUnit; locked: boolean; onChange: (patch: Partial) => void; }) { const organizationFields = useMemo( () => fields.filter((field) => field.type === "organization_unit" || field.type === "string"), [fields] ); const functionFields = useMemo( () => fields.filter((field) => field.type === "organization_function" || field.type === "string"), [fields] ); return ( <> onChange({ organization_unit_id: value, organization_unit_field: undefined, function_id: undefined })} onField={(value) => onChange({ organization_unit_id: undefined, organization_unit_field: value })} onMatch={(value) => onChange({ organization_unit_match: value })} /> onChange({ function_id: value, function_field: undefined })} onField={(value) => onChange({ function_id: undefined, function_field: value })} onMatch={(value) => onChange({ function_match: value })} /> {target.context_field !== undefined ? : target.context_key !== undefined && onChange({ context_key: event.target.value })} /> } ); } function DerivedReferenceField({ label, fixedValue, fieldValue, match, fixedOptions, fields, locked, fixedDisabled = false, onFixed, onField, onMatch }: { label: string; fixedValue?: string; fieldValue?: string; match?: "id" | "slug"; fixedOptions: Array<{ id: string; name: string; slug: string }>; fields: CampaignFieldDefinition[]; locked: boolean; fixedDisabled?: boolean; onFixed: (value: string) => void; onField: (value: string) => void; onMatch: (value: "id" | "slug") => void; }) { const usesField = fieldValue !== undefined; return ( <> {usesField ? <> : } ); } export function normalizePostboxTargets(value: unknown): CampaignPostboxTarget[] { if (!Array.isArray(value)) return []; return value .map(asRecord) .map((target) => normalizeTarget({ id: String(target.id ?? ""), mode: target.mode === "derived" ? "derived" : "direct", label: optionalText(target.label), postbox_id: optionalText(target.postbox_id), address_key: optionalText(target.address_key), template_id: optionalText(target.template_id), organization_unit_id: optionalText(target.organization_unit_id), organization_unit_field: optionalText(target.organization_unit_field), organization_unit_match: target.organization_unit_match === "slug" ? "slug" : "id", function_id: optionalText(target.function_id), function_field: optionalText(target.function_field), function_match: target.function_match === "slug" ? "slug" : "id", context_key: optionalText(target.context_key), context_field: optionalText(target.context_field) })) .filter((target) => Boolean(target.id)); } function normalizeTarget(target: CampaignPostboxTarget): CampaignPostboxTarget { const common = { id: target.id || newTargetId(), mode: target.mode, ...(target.label?.trim() ? { label: target.label.trim() } : {}) }; if (target.mode === "direct") { return { ...common, mode: "direct", ...(target.postbox_id ? { postbox_id: target.postbox_id } : {}), ...(!target.postbox_id && target.address_key ? { address_key: target.address_key } : {}) }; } return { ...common, mode: "derived", template_id: target.template_id ?? "", ...(target.organization_unit_field !== undefined ? { organization_unit_field: target.organization_unit_field, organization_unit_match: target.organization_unit_match ?? "id" } : { organization_unit_id: target.organization_unit_id ?? "" }), ...(target.function_field !== undefined ? { function_field: target.function_field, function_match: target.function_match ?? "id" } : { function_id: target.function_id ?? "" }), ...(target.context_field !== undefined ? { context_field: target.context_field } : target.context_key !== undefined ? { context_key: target.context_key } : {}) }; } function targetIsComplete(target: CampaignPostboxTarget): boolean { if (!target.id) return false; if (target.mode === "direct") { return Boolean(target.postbox_id || target.address_key); } return Boolean( target.template_id && (target.organization_unit_id || target.organization_unit_field) && (target.function_id || target.function_field) ); } function directTargetDefaults(id: string, catalog: CampaignPostboxCatalog): CampaignPostboxTarget { return { id, mode: "direct", postbox_id: catalog.postboxes[0]?.id ?? "" }; } function derivedTargetDefaults( id: string, catalog: CampaignPostboxCatalog, fields: CampaignFieldDefinition[] ): CampaignPostboxTarget { const unit = catalog.organization_units[0]; return { id, mode: "derived", template_id: catalog.templates[0]?.id ?? "", ...(unit ? { organization_unit_id: unit.id, function_id: unit.functions[0]?.id ?? "" } : { organization_unit_field: fields[0]?.name ?? "", organization_unit_match: "id", function_field: fields[0]?.name ?? "", function_match: "id" }) }; } function moveTarget( targets: CampaignPostboxTarget[], source: number, destination: number ): CampaignPostboxTarget[] { if (destination < 0 || destination >= targets.length) return targets; const next = [...targets]; const [target] = next.splice(source, 1); next.splice(destination, 0, target); return next; } function optionalText(value: unknown): string | undefined { const text = String(value ?? "").trim(); return text || undefined; } function newTargetId(): string { const suffix = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `postbox-${suffix}`; }