import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { usePlatformModuleInstalled } from "@govoplan/core-webui"; import type { ApiSettings } from "../../types"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { PageTitle } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import LockedVersionNotice from "./components/LockedVersionNotice"; import VersionLine from "./components/VersionLine"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView"; import FieldValueInput from "./components/FieldValueInput"; import AttachmentRulesOverlay from "./components/AttachmentRulesOverlay"; import { getDraftFields } from "./utils/fieldDefinitions"; import { getIndividualAttachmentBasePaths, normalizeAttachmentBasePaths, normalizeAttachmentRules, normalizeAttachmentZipCollection, type AttachmentBasePath, type AttachmentRule, type AttachmentZipCollection } from "./utils/attachments"; import { importedRowsNeedAttachmentSource, materializeRecipientImport, type RecipientImportMode, type RecipientImportPreview } from "./utils/bulkImport"; import { buildTemplatePreviewContext } from "./utils/templatePlaceholders"; import { RecipientImportDialog } from "./RecipientDataPage"; import { addressesFromValue } from "@govoplan/core-webui"; import { DismissibleAlert, i18nMessage } from "@govoplan/core-webui"; import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid"; export default function RecipientDetailsPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) { const filesModuleInstalled = usePlatformModuleInstalled("files"); const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const [importOpen, setImportOpen] = useState(false); const [recipientDataPage, setRecipientDataPage] = useState(1); const [recipientDataPageSize, setRecipientDataPageSize] = useState(10); const version = data.currentVersion; const locked = isAuditLockedVersion(version, data.campaign?.current_version_id); const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({ settings, campaignId, version, locked, reload, setError, currentStep: "recipient-data", unsavedTitle: "i18n:govoplan-campaign.unsaved_recipient_data_changes.c810507e", unsavedMessage: "i18n:govoplan-campaign.recipient_field_values_or_attachments_have_unsav.49ab1870" }); 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(() => getIndividualAttachmentBasePaths(attachmentBasePaths), [attachmentBasePaths]); const importBasePaths = useMemo(() => normalizeAttachmentBasePaths(attachmentSection.base_paths, attachmentSection, true), [attachmentSection]); const defaultImportBasePath = useMemo(() => importBasePaths.find((basePath) => basePath.allow_individual) ?? importBasePaths[0] ?? null, [importBasePaths]); const zipConfig = useMemo(() => normalizeAttachmentZipCollection(attachmentSection.zip), [attachmentSection.zip]); 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 })); } function applyRecipientImport(preview: RecipientImportPreview, mode: RecipientImportMode) { if (locked || !draft) return; const needsAttachmentSource = importedRowsNeedAttachmentSource(preview); const currentAttachments = asRecord(draft.attachments); let nextBasePaths = normalizeAttachmentBasePaths(currentAttachments.base_paths, currentAttachments, true); let attachmentBasePath: AttachmentBasePath | null = null; if (needsAttachmentSource) { if (nextBasePaths.length === 0) { nextBasePaths = [{ id: "base-path-campaign", name: "Campaign files", path: ".", allow_individual: true, unsent_warning: false }]; } const selectedIndex = Math.max(0, nextBasePaths.findIndex((basePath) => basePath.allow_individual)); nextBasePaths = nextBasePaths.map((basePath, index) => index === selectedIndex ? { ...basePath, allow_individual: true } : basePath); attachmentBasePath = nextBasePaths[selectedIndex] ?? null; } const importedDraft = materializeRecipientImport(draft, preview, { mode, attachmentBasePath }); const nextDraft = needsAttachmentSource ? { ...importedDraft, attachments: { ...asRecord(importedDraft.attachments), base_paths: nextBasePaths, base_path: nextBasePaths[0]?.path || "." } } : importedDraft; setDraft(nextDraft); markDirty(); setImportOpen(false); } return (
i18n:govoplan-campaign.recipient_data.c2baaf10
{error && {error}} {localError && {localError}} {locked && } <> setImportOpen(true)}>i18n:govoplan-campaign.import.d6fbc9d2}> {inlineEntries.length === 0 && !source.type &&

i18n:govoplan-campaign.no_recipient_profiles_are_stored_in_the_current_.e9f9a224

} {inlineEntries.length === 0 && Boolean(source.type) && i18n:govoplan-campaign.this_campaign_references_an_external_recipient_s.0d1ae252 } {inlineEntries.length > 0 &&
String(entry.id || index)} emptyText="i18n:govoplan-campaign.no_recipient_data_found.a12be7d0" className="recipient-table-wrap recipient-data-table-wrap recipient-data-table" pagination={{ page: recipientDataPage, pageSize: recipientDataPageSize, pageSizeOptions: [10, 25, 50, 100, 250], onPageChange: setRecipientDataPage, onPageSizeChange: (pageSize) => { setRecipientDataPageSize(pageSize); setRecipientDataPage(1); } }} />
}
{importOpen && setImportOpen(false)} onImport={applyRecipientImport} /> }
); } type RecipientDataColumnContext = { settings: ApiSettings; campaignId: string; draft: Record; locked: boolean; filesModuleInstalled: boolean; fieldDefinitions: ReturnType; individualAttachmentBasePaths: ReturnType; zipConfig: AttachmentZipCollection; updateEntryAttachments: (index: number, attachments: AttachmentRule[]) => void; updateEntryField: (index: number, field: string, value: unknown) => void; }; function recipientDataColumns({ settings, campaignId, draft, locked, filesModuleInstalled, fieldDefinitions, individualAttachmentBasePaths, zipConfig, updateEntryAttachments, updateEntryField }: RecipientDataColumnContext): DataGridColumn>[] { return [ { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", render: (_entry, index) => {index + 1}, value: (_entry, index) => index + 1 }, { id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 260, resizable: true, sortable: true, filterable: true, sticky: "start", render: (entry) => {firstRecipientEmail(entry) || "i18n:govoplan-campaign.no_to_address.683350f9"} {extraRecipientCount(entry) > 0 && +{extraRecipientCount(entry)}} , value: firstRecipientEmail }, { id: "attachments", header: "i18n:govoplan-campaign.attachments.6771ade6", width: 180, filterable: true, render: (entry, index) => { const attachments = normalizeAttachmentRules(entry.attachments); return ( updateEntryAttachments(index, rules)} />); }, value: (entry) => normalizeAttachmentRules(entry.attachments).map((rule) => `${rule.label ?? ""} ${rule.file_filter ?? ""}`).join(", ") }, ...fieldDefinitions.map((field): DataGridColumn> => ({ id: `field-${field.name}`, header: field.label || field.name, width: 190, 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 ( updateEntryField(index, field.name, value)} />); }, value: (entry) => String(asRecord(entry.fields)[field.name] ?? "") }))]; } 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); }