import { useMemo } from "react"; import { Link } from "react-router-dom"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import Card from "../../components/Card"; import PageTitle from "../../components/PageTitle"; import LoadingFrame from "../../components/LoadingFrame"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import { asArray, asRecord, isAuditLockedVersion, isRecord, versionLockReason } from "./utils/campaignView"; import { getBool, getText } from "./utils/draftEditor"; import FieldValueInput from "./components/FieldValueInput"; import AttachmentRulesOverlay, { type AttachmentBasePath, type AttachmentRule } from "./components/AttachmentRulesOverlay"; import { addressesFromValue } from "../../utils/emailAddresses"; type FieldDefinition = { name: string; label: string; type: string; can_override: boolean; }; export default function RecipientDetailsPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) { const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const version = data.currentVersion; const locked = isAuditLockedVersion(version); const { draft, displayDraft, dirty, saveState, localError, patch, saveDraft } = useCampaignDraftEditor({ settings, campaignId, version, locked, reload, setError, currentStep: "recipient-data", unsavedTitle: "Unsaved recipient data changes", unsavedMessage: "Recipient field values or attachments have unsaved changes. Save them before leaving, or discard them and continue." }); const entries = asRecord(displayDraft.entries); const inlineEntries = asArray(entries.inline).map(asRecord); const source = asRecord(entries.source); const fieldDefinitions = useMemo(() => getDraftFields(displayDraft), [displayDraft]); const attachmentSection = asRecord(displayDraft.attachments); const attachmentBasePaths = useMemo(() => normalizeAttachmentBasePaths(attachmentSection.base_paths, attachmentSection), [attachmentSection]); const individualAttachmentBasePaths = useMemo(() => { const enabled = attachmentBasePaths.filter((basePath) => basePath.allow_individual); return enabled.length > 0 ? enabled : attachmentBasePaths; }, [attachmentBasePaths]); function replaceInlineEntries(nextEntries: Record[]) { patch(["entries", "inline"], nextEntries); } function updateEntry(index: number, updater: (entry: Record) => Record) { const nextEntries = inlineEntries.map((entry, currentIndex) => currentIndex === index ? updater(entry) : entry); replaceInlineEntries(nextEntries); } function updateEntryField(index: number, field: string, value: unknown) { updateEntry(index, (entry) => ({ ...entry, fields: { ...asRecord(entry.fields), [field]: value } })); } function updateEntryAttachments(index: number, attachments: AttachmentRule[]) { updateEntry(index, (entry) => ({ ...entry, attachments })); } return (
Recipient data

Version {version ? `#${version.version_number}` : "—"} · {saveState}

{error &&
{error}
} {localError &&
{localError}
} {locked &&
This version is read-only. {versionLockReason(version)}
} <> {inlineEntries.length === 0 && !source.type &&

No recipient profiles are stored in the current version yet. Add recipients first, then maintain their data here.

} {inlineEntries.length === 0 && Boolean(source.type) && (
This campaign references an external recipient source. A parsed data preview will be added when file/source preview support is implemented.
)} {inlineEntries.length > 0 && (
{fieldDefinitions.map((field) => )} {inlineEntries.slice(0, 100).map((entry, index) => { const fields = asRecord(entry.fields); const attachments = normalizeAttachmentRules(entry.attachments); return ( {fieldDefinitions.map((field) => ( ))} ); })}
# Recipient Attachments{field.label || field.name}
{index + 1} {firstRecipientEmail(entry) || "No To address"} {extraRecipientCount(entry) > 0 && +{extraRecipientCount(entry)}} updateEntryAttachments(index, rules)} /> updateEntryField(index, field.name, value)} />
)}
); } function getDraftFields(draft: Record | null): FieldDefinition[] { return asArray(draft?.fields) .map((field) => asRecord(field)) .map((field) => ({ name: getText(field, "name") || getText(field, "id"), label: getText(field, "label"), type: normalizeFieldType(getText(field, "type", "string")), can_override: getBool(field, "can_override", true) })) .filter((field) => Boolean(field.name)); } function normalizeFieldType(value: string): string { return ["integer", "double", "date", "password"].includes(value) ? value : "string"; } function firstRecipientEmail(entry: Record): string { return (addressesFromValue(entry.to)[0] ?? addressesFromValue(entry.recipient)[0] ?? addressesFromValue(entry)[0])?.email ?? ""; } function extraRecipientCount(entry: Record): number { const count = addressesFromValue(entry.to).length; return Math.max(0, count - 1); } function normalizeAttachmentBasePaths(value: unknown, attachments: Record): AttachmentBasePath[] { if (Array.isArray(value) && value.length > 0) { return value.filter(isRecord).map((basePath, index) => ({ id: getText(basePath, "id", `base-path-${index + 1}`), name: getText(basePath, "name", `Base path ${index + 1}`), source: getText(basePath, "source"), path: getText(basePath, "path", index === 0 ? getText(attachments, "base_path", ".") : "."), allow_individual: getBool(basePath, "allow_individual") })); } return [{ id: "base-path-campaign", name: "Campaign files", path: getText(attachments, "base_path", "."), allow_individual: getBool(attachments, "allow_individual", true) }]; } function normalizeAttachmentRules(value: unknown): AttachmentRule[] { if (!Array.isArray(value)) return []; return value.filter(isRecord).map((rule) => ({ ...rule })); }