import { useEffect, useState } from "react"; import { ExternalLink, Upload } from "lucide-react"; import { formatDateTime as formatPlatformDateTime, formatDateTimeFromDate, mergeDeltaRows } from "@govoplan/core-webui"; import { Link } from "react-router"; import type { ApiSettings, AuthInfo } from "../../types"; import { Card } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import { DismissibleAlert, PageActionBar, PageLayout, TableActionGroup, ToggleSwitch, hasScope, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { createNewCampaign, importCampaignPackage, listCampaignsDelta, previewCampaignImport, type CampaignDeltaResponse, type CampaignImportPreview, type CampaignTransferScope } from "../../api/campaigns"; import type { CampaignListItem } from "../../types"; export default function CampaignListPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) { const navigate = useGuardedNavigate(); const [campaigns, setCampaigns] = useState([]); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); const [lastLoadedAt, setLastLoadedAt] = useState(""); const [campaignDeltaWatermark, setCampaignDeltaWatermark] = useState(null); const [importOpen, setImportOpen] = useState(false); const [importPackage, setImportPackage] = useState | null>(null); const [importPreview, setImportPreview] = useState(null); const [importScopes, setImportScopes] = useState([]); const [importIdentity, setImportIdentity] = useState({ external_id: "", name: "" }); const [importPreviewStale, setImportPreviewStale] = useState(false); const [importBusy, setImportBusy] = useState(false); const [importError, setImportError] = useState(""); const canImport = hasScope(auth, "campaigns:campaign:import") && hasScope(auth, "campaigns:campaign:create"); const canImportRecipients = hasScope(auth, "campaigns:recipient:import") && hasScope(auth, "campaigns:recipient:write"); async function load(forcedSince: string | null | undefined = campaignDeltaWatermark) { setLoading(true); setError(""); try { let nextWatermark = forcedSince ?? null; let nextCampaigns = campaigns; let response: CampaignDeltaResponse; do { response = await listCampaignsDelta(settings, { since: nextWatermark }); nextCampaigns = mergeCampaignDelta( nextCampaigns, response, Boolean(response.full && nextWatermark?.startsWith("full:campaigns:")) ); nextWatermark = response.watermark ?? null; } while (response.has_more); setCampaigns(nextCampaigns); setCampaignDeltaWatermark(nextWatermark); setLastLoadedAt(formatLoadedAt(new Date())); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } async function create() { setCreating(true); setError(""); try { const created = await createNewCampaign(settings); navigate(`/campaigns/${created.campaign.id}/wizard/create`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setCreating(false); } } function openImport() { setImportPackage(null); setImportPreview(null); setImportScopes([]); setImportIdentity({ external_id: "", name: "" }); setImportPreviewStale(false); setImportError(""); setImportOpen(true); } async function readImportFile(file: File | undefined) { if (!file) return; setImportBusy(true); setImportError(""); try { if (file.size > 25 * 1024 * 1024) throw new Error("Campaign packages larger than 25 MB must be reviewed and imported through a governed integration."); const parsed: unknown = JSON.parse(await file.text()); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Select a JSON object exported as a portable Campaign package."); const packageData = parsed as Record; const preview = await previewCampaignImport(settings, { package: packageData }); const allowedScopes = preview.selected_scopes.filter((scope) => scope !== "recipients" || canImportRecipients); setImportPackage(packageData); setImportPreview(preview); setImportScopes(allowedScopes); setImportIdentity({ external_id: preview.destination.external_id ?? "", name: preview.destination.name ?? "" }); setImportPreviewStale(allowedScopes.length !== preview.selected_scopes.length); } catch (err) { setImportPackage(null); setImportPreview(null); setImportError(err instanceof Error ? err.message : String(err)); } finally { setImportBusy(false); } } function patchImportIdentity(key: "external_id" | "name", value: string) { setImportIdentity((current) => ({ ...current, [key]: value })); setImportPreviewStale(true); } function toggleImportScope(scope: CampaignTransferScope, checked: boolean) { setImportScopes((current) => checked ? [...current, scope].filter((item, index, rows) => rows.indexOf(item) === index) : current.filter((item) => item !== scope)); setImportPreviewStale(true); } async function refreshImportPreview() { if (!importPackage || importBusy || importScopes.length === 0) return; setImportBusy(true); setImportError(""); try { const preview = await previewCampaignImport(settings, { package: importPackage, selected_scopes: importScopes, external_id: importIdentity.external_id.trim() || undefined, name: importIdentity.name.trim() || undefined }); setImportPreview(preview); setImportIdentity({ external_id: preview.destination.external_id ?? importIdentity.external_id, name: preview.destination.name ?? importIdentity.name }); setImportPreviewStale(false); } catch (err) { setImportError(err instanceof Error ? err.message : String(err)); } finally { setImportBusy(false); } } async function applyImport() { if (!importPackage || !importPreview?.compatible || !importPreview.package_sha256 || importPreviewStale || importBusy) return; setImportBusy(true); setImportError(""); try { const created = await importCampaignPackage(settings, { package: importPackage, selected_scopes: importScopes, external_id: importIdentity.external_id.trim() || undefined, name: importIdentity.name.trim() || undefined, expected_package_sha256: importPreview.package_sha256 }); setImportOpen(false); navigate(`/campaigns/${created.campaign.id}`); } catch (err) { setImportError(err instanceof Error ? err.message : String(err)); } finally { setImportBusy(false); } } useEffect(() => { setCampaignDeltaWatermark(null); load(null); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); const columns: DataGridColumn[] = [ { id: "campaign", header: "i18n:govoplan-campaign.campaign.69390e16", width: "minmax(260px, 1.8fr)", maxWidth: 720, sortable: true, filterable: true, sticky: "start", render: (campaign) =>
{campaign.name || campaign.external_id || campaign.id}
{campaign.description || campaign.external_id || campaign.id}
, value: (campaign) => `${campaign.name ?? ""} ${campaign.external_id ?? ""} ${campaign.id}` }, { id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: [ { value: "draft", label: "i18n:govoplan-campaign.draft.23d33e22" }, { value: "active", label: "i18n:govoplan-campaign.active.a733b809" }, { value: "completed", label: "i18n:govoplan-campaign.completed.1798b3ba" }, { value: "archived", label: "i18n:govoplan-campaign.archived.eddc813f" }], display: "pill" }, render: (campaign) => , value: (campaign) => campaign.status || "draft" }, { id: "current_version", header: "i18n:govoplan-campaign.current_version.7be72582", width: 170, sortable: true, filterable: true, className: "version-cell mono-small", render: (campaign) => {campaign.current_version_id ? shortId(campaign.current_version_id) : "—"}, value: (campaign) => campaign.current_version_id ?? "" }, { id: "updated", header: "i18n:govoplan-campaign.updated.f2f8570d", width: 190, sortable: true, filterable: true, filterType: "date", className: "updated-cell", render: (campaign) => formatDateTime(campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at), value: (campaign) => campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at ?? "" }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 120, sticky: "end", align: "right", render: (campaign) =>