import { MetricGrid } from "@govoplan/core-webui"; import { useEffect, useMemo, useState } from "react"; import { Archive, CalendarClock, Copy, Download, ExternalLink, LockKeyhole, LockOpen, Pause, Play, Trash2 } from "lucide-react"; import { Link } from "react-router"; import type { ApiSettings, AuthInfo } from "../../types"; import { FormGrid, Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import { MetricCard } from "@govoplan/core-webui"; import { PageActionBar, PageLayout } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; import { DismissibleAlert, TableActionGroup, hasScope, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { archiveCampaign, archiveCampaignVersion, copyCampaign, createCampaignSchedule, deleteCampaign, exportCampaignPackage, getCampaignLifecyclePolicy, lockCampaignVersionPermanently, lockCampaignVersionTemporarily, unlockCampaignVersionUserLock, listCampaignSchedules, setCampaignScheduleState, updateCampaignMetadata, type CampaignSchedule, type CampaignScheduleCreate, type CampaignLifecyclePolicy, type CampaignCopyOptions, type CampaignTransferScope, type CampaignVersionDetail, type CampaignVersionListItem } from "../../api/campaigns"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { asArray, asRecord, canUnlockValidationVersion, formatDateTime, getCampaignJson, isFinalLockedVersion, isPermanentUserLockedVersion, isTemporaryUserLockedVersion, isVersionReadyForDelivery, summaryValue } from "./utils/campaignView"; import { buildUndefinedPlaceholders, extractTemplatePlaceholders, recipientAddressTemplateFieldOptions } from "./utils/templatePlaceholders"; import { downloadJson, safeFileStem } from "./utils/draftEditor"; const campaignModeOptions = ["draft", "test", "send"]; type LockAction = "temporary" | "unlock" | "permanent"; type PendingLockAction = {version: CampaignVersionListItem;action: LockAction;} | null; type LifecycleAction = "archive_campaign" | "delete_campaign" | "copy_campaign" | "archive_version"; type PendingLifecycleAction = { action: LifecycleAction; policy: CampaignLifecyclePolicy; version?: CampaignVersionListItem; } | null; const defaultCopyOptions: CampaignCopyOptions = { name: "", external_id: "", include_recipients: true, include_files: true, include_shares: false, include_policies: true, include_mail_profile: true }; const defaultExportScopes: CampaignTransferScope[] = ["metadata", "template_config"]; function defaultScheduleDraft(): CampaignScheduleCreate { const start = new Date(Date.now() + 60 * 60 * 1000); start.setSeconds(0, 0); return { source_version_id: "", name: "", delivery_mode: "manual", recurrence_kind: "once", interval_count: 1, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", starts_at: localDateTimeValue(start), max_occurrences: 1, include_recipients: true, include_files: true, include_shares: false, include_policies: true, include_mail_profile: true }; } export default function CampaignOverviewPage({ settings, auth, campaignId }: {settings: ApiSettings;auth: AuthInfo;campaignId: string;}) { const navigate = useGuardedNavigate(); const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true }); const campaign = data.campaign; const [showArchivedVersions, setShowArchivedVersions] = useState(false); const archivedVersionCount = useMemo(() => data.versions.filter((version) => Boolean(version.archived_at)).length, [data.versions]); const versions = useMemo(() => data.versions.filter((version) => showArchivedVersions || !version.archived_at).sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0)), [data.versions, showArchivedVersions]); const [identity, setIdentity] = useState({ external_id: "", name: "", status: "", description: "" }); const [identityDirty, setIdentityDirty] = useState(false); const [savingIdentity, setSavingIdentity] = useState(false); const [pendingLockAction, setPendingLockAction] = useState(null); const [lockBusy, setLockBusy] = useState(false); const [pendingLifecycleAction, setPendingLifecycleAction] = useState(null); const [copyOptions, setCopyOptions] = useState(defaultCopyOptions); const [lifecycleBusy, setLifecycleBusy] = useState(false); const [exportDialogOpen, setExportDialogOpen] = useState(false); const [exportScopes, setExportScopes] = useState(defaultExportScopes); const [exportBusy, setExportBusy] = useState(false); const [message, setMessage] = useState(""); const [schedules, setSchedules] = useState([]); const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false); const [scheduleDraft, setScheduleDraft] = useState(defaultScheduleDraft); const [scheduleBusy, setScheduleBusy] = useState(false); const versionMetrics = useMemo(() => campaignVersionMetrics(data.currentVersion), [data.currentVersion]); const canArchive = Boolean(campaign) && campaign?.status !== "archived" && hasScope(auth, "campaigns:campaign:archive"); const canDelete = Boolean(campaign) && campaign?.status === "draft" && hasScope(auth, "campaigns:campaign:delete"); const canCopy = Boolean(data.currentVersion) && hasScope(auth, "campaigns:campaign:copy"); const canExport = Boolean(data.currentVersion) && hasScope(auth, "campaigns:campaign:export"); const canSchedule = Boolean(data.currentVersion) && hasScope(auth, "campaigns:campaign:schedule") && hasScope(auth, "campaigns:campaign:copy"); const canAutonomousSchedule = canSchedule && hasScope(auth, "campaigns:campaign:queue") && hasScope(auth, "campaigns:campaign:send") && hasScope(auth, "mail:profile:use"); const canExportRecipients = hasScope(auth, "campaigns:recipient:read") && hasScope(auth, "campaigns:recipient:export"); const canExportReview = hasScope(auth, "campaigns:report:read"); const canExportDelivery = canExportRecipients && hasScope(auth, "campaigns:report:export"); function openSection(section: string, fragment = "") { const params = new URLSearchParams(); if (data.currentVersion?.id) params.set("version", data.currentVersion.id); const query = params.toString(); navigate(`/campaigns/${campaignId}/${section}${query ? `?${query}` : ""}${fragment}`); } function openQueue() { const params = new URLSearchParams({ campaign: campaignId }); if (data.currentVersion?.id) params.set("version", data.currentVersion.id); navigate(`/campaigns/queue?${params.toString()}`); } useUnsavedDraftGuard({ dirty: identityDirty, onSave: saveIdentity, onDiscard: () => { if (!campaign) return; setIdentity({ external_id: campaign.external_id ?? "", name: campaign.name ?? "", status: campaign.status ?? "", description: campaign.description ?? "" }); setIdentityDirty(false); } }); useEffect(() => { if (!campaign || identityDirty) return; setIdentity({ external_id: campaign.external_id ?? "", name: campaign.name ?? "", status: campaign.status ?? "", description: campaign.description ?? "" }); }, [campaign, identityDirty]); useEffect(() => { if (!campaign?.id) { setSchedules([]); return; } let active = true; void listCampaignSchedules(settings, campaign.id).then((items) => { if (active) setSchedules(items); }).catch((err) => { if (active) setError(err instanceof Error ? err.message : String(err)); }); return () => { active = false; }; }, [campaign?.id, settings, setError]); function patchIdentity(key: keyof typeof identity, value: string) { setIdentity((current) => ({ ...current, [key]: value })); setIdentityDirty(true); setMessage(""); } async function saveIdentity(): Promise { if (!campaign || savingIdentity || !identityDirty) return false; setSavingIdentity(true); setError(""); setMessage(""); try { await updateCampaignMetadata(settings, campaign.id, { external_id: identity.external_id, name: identity.name, status: identity.status, description: identity.description }); setIdentityDirty(false); await reload(); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; } finally { setSavingIdentity(false); } } async function applyLockAction() { const pending = pendingLockAction; if (!pending || lockBusy) return; setLockBusy(true); setError(""); setMessage(""); try { if (pending.action === "temporary") { await lockCampaignVersionTemporarily(settings, campaignId, pending.version.id); setMessage(i18nMessage("i18n:govoplan-campaign.version_value_temporarily_locked.4ed4ffa7", { value0: pending.version.version_number })); } else if (pending.action === "unlock") { await unlockCampaignVersionUserLock(settings, campaignId, pending.version.id); setMessage(i18nMessage("i18n:govoplan-campaign.temporary_lock_removed_from_version_value.9a05bcfb", { value0: pending.version.version_number })); } else { await lockCampaignVersionPermanently(settings, campaignId, pending.version.id); setMessage(i18nMessage("i18n:govoplan-campaign.version_value_permanently_locked.60595f45", { value0: pending.version.version_number })); } setPendingLockAction(null); await reload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLockBusy(false); } } async function discardOverview() { if (campaign) { setIdentity({ external_id: campaign.external_id ?? "", name: campaign.name ?? "", status: campaign.status ?? "", description: campaign.description ?? "" }); setIdentityDirty(false); } await reload({ force: true }); } async function prepareLifecycleAction(action: LifecycleAction, version?: CampaignVersionListItem) { if (!campaign || lifecycleBusy || identityDirty) return; setLifecycleBusy(true); setError(""); setMessage(""); try { const policy = await getCampaignLifecyclePolicy(settings, campaign.id, version?.id); const decision = policy.actions[action]; if (!decision?.allowed) { setError(decision?.reason || "This lifecycle action is not available for the current campaign state."); return; } if (action === "copy_campaign") { setCopyOptions({ ...defaultCopyOptions, name: `${campaign.name} (copy)`, include_recipients: hasScope(auth, "campaigns:recipient:read") }); } setPendingLifecycleAction({ action, policy, version }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLifecycleBusy(false); } } async function applyLifecycleAction() { if (!campaign || !pendingLifecycleAction || lifecycleBusy) return; const pending = pendingLifecycleAction; setLifecycleBusy(true); setError(""); setMessage(""); try { if (pending.action === "archive_campaign") { await archiveCampaign(settings, campaign.id, pending.policy.state_token); setMessage("i18n:govoplan-campaign.campaign_archived.3f0ca2b7"); } else if (pending.action === "delete_campaign") { await deleteCampaign(settings, campaign.id, pending.policy.state_token); setPendingLifecycleAction(null); navigate("/campaigns"); return; } else if (pending.action === "copy_campaign" && pending.version) { const created = await copyCampaign(settings, campaign.id, pending.version.id, pending.policy.state_token, copyOptions); setPendingLifecycleAction(null); navigate(`/campaigns/${created.campaign.id}`); return; } else if (pending.action === "archive_version" && pending.version) { await archiveCampaignVersion(settings, campaign.id, pending.version.id, pending.policy.state_token); setMessage(`Version #${pending.version.version_number} archived.`); } setPendingLifecycleAction(null); await reload({ force: true }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLifecycleBusy(false); } } function openScheduleDialog() { if (!campaign || !data.currentVersion) return; setScheduleDraft({ ...defaultScheduleDraft(), source_version_id: data.currentVersion.id, name: campaign.name, include_recipients: hasScope(auth, "campaigns:recipient:read") }); setScheduleDialogOpen(true); } async function submitSchedule() { if (!campaign || scheduleBusy) return; setScheduleBusy(true); setError(""); try { const created = await createCampaignSchedule(settings, campaign.id, { ...scheduleDraft, starts_at: new Date(scheduleDraft.starts_at).toISOString(), max_occurrences: scheduleDraft.recurrence_kind === "once" ? 1 : scheduleDraft.max_occurrences }); setSchedules((current) => [created, ...current]); setScheduleDialogOpen(false); setMessage(created.delivery_mode === "autonomous" ? "Autonomous schedule created from the exact approved source execution." : "Manual schedule created. Each occurrence prepares a fresh draft for review."); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setScheduleBusy(false); } } async function toggleSchedule(schedule: CampaignSchedule) { if (!campaign || scheduleBusy) return; setScheduleBusy(true); setError(""); try { const updated = await setCampaignScheduleState( settings, campaign.id, schedule.id, !schedule.active, schedule.resource_revision ); setSchedules((current) => current.map((item) => item.id === updated.id ? updated : item)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setScheduleBusy(false); } } function toggleExportScope(scope: CampaignTransferScope, checked: boolean) { setExportScopes((current) => checked ? [...current, scope].filter((item, index, rows) => rows.indexOf(item) === index) : current.filter((item) => item !== scope)); } async function exportPortablePackage() { if (!campaign || !data.currentVersion || exportBusy || exportScopes.length === 0) return; setExportBusy(true); setError(""); try { const portablePackage = await exportCampaignPackage( settings, campaign.id, data.currentVersion.id, exportScopes ); downloadJson( `${safeFileStem(campaign.external_id || campaign.name)}-v${data.currentVersion.version_number ?? 1}.govoplan-campaign.json`, portablePackage ); setExportDialogOpen(false); setMessage("Portable Campaign package downloaded. Keep recipient or delivery packages in an approved location."); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setExportBusy(false); } } return ( i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113

} headerLoading={loading} error={error} success={message} actions={ void reload(), loading }} primaryActions={<> {canExport && } {canCopy && data.currentVersion && } {canSchedule && } } destructiveActions={<> {canDelete && } {canArchive && } } discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => void discardOverview(), disabled: loading || lockBusy }} saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => void saveIdentity(), disabled: !campaign && identityDirty, disabledReason: !campaign && identityDirty ? "The campaign identity is not available." : undefined }} />} > openSection("review", "#workflow-send") }} /> openSection("review", "#workflow-build-review") }} /> openSection("report") }} /> openSection("report") }} /> i18n:govoplan-campaign.edit_with_wizard.672a7d1a}> patchIdentity("external_id", event.target.value)} /> patchIdentity("name", event.target.value)} />