Files
zemion c51fc180fb
Module Package Release / publish-packages (push) Successful in 12s
Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
2026-09-08 01:32:26 +02:00

78 lines
4.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { Button, DescriptionList, Dialog, DismissibleAlert, MetricCard, MetricGrid, i18nMessage } from "@govoplan/core-webui";
import type { ApiSettings } from "../../../types";
import { getCampaignDeliveryProgress, type CampaignDeliveryProgress } from "../../../api/campaigns";
export type DeliveryProgressRequestState = "running" | "finished" | "interrupted";
/** Only this small dialog polls; the workspace, attachments and report stay still. */
export default function CampaignDeliveryProgressDialog({ settings, campaignId, versionId, kind, requestState, requestError, onClose }: {
settings: ApiSettings;
campaignId: string;
versionId: string;
kind: "smtp" | "imap";
requestState: DeliveryProgressRequestState;
requestError?: string;
onClose: () => void;
}) {
const scope = JSON.stringify([settings.apiBaseUrl, settings.apiKey, settings.accessToken, campaignId, versionId]);
const [snapshot, setSnapshot] = useState<{ scope: string; progress: CampaignDeliveryProgress } | null>(null);
const [pollError, setPollError] = useState(false);
const progress = snapshot?.scope === scope ? snapshot.progress : null;
const running = requestState === "running";
useEffect(() => {
let stopped = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const controller = new AbortController();
setPollError(false);
async function poll() {
let succeeded = false;
try {
const result = await getCampaignDeliveryProgress(settings, campaignId, versionId, controller.signal);
if (stopped) return;
if (result.campaign_id !== campaignId || result.version_id !== versionId) throw new Error("Progress context changed");
setSnapshot({ scope, progress: result });
setPollError(false);
succeeded = true;
} catch {
if (stopped) return;
// A read failure never retries or changes the authoritative send/append.
setPollError(true);
}
if (!stopped && (requestState !== "finished" || !succeeded)) timer = setTimeout(() => void poll(), 2000);
}
void poll();
return () => { stopped = true; controller.abort(); if (timer) clearTimeout(timer); };
}, [scope, requestState]);
const counts = progress?.[kind];
const accepted = progress ? kind === "smtp" ? progress.smtp.accepted : progress.imap.appended : null;
const label = kind === "smtp" ? "i18n:govoplan-campaign.delivery_progress_smtp" : "i18n:govoplan-campaign.delivery_progress_imap";
return <Dialog open title={label} closeDisabled={running} closeOnBackdrop={false} onClose={onClose}
footer={<Button variant="primary" disabled={running} onClick={onClose}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
<p role="status" aria-live="polite"><strong>{counts
? i18nMessage("i18n:govoplan-campaign.delivery_progress_processed", { value0: counts.processed, value1: counts.total })
: "i18n:govoplan-campaign.delivery_progress_loading"}</strong></p>
<p className="muted small-note">{running ? "i18n:govoplan-campaign.delivery_progress_running"
: requestState === "finished" ? "i18n:govoplan-campaign.delivery_progress_finished"
: "i18n:govoplan-campaign.delivery_progress_interrupted"}</p>
{pollError && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.delivery_progress_stale</DismissibleAlert>}
{requestError && <DismissibleAlert tone="warning" dismissible={false}>{requestError}</DismissibleAlert>}
<MetricGrid columns={3} density="compact" minimum="compact">
<MetricCard label={kind === "smtp" ? "i18n:govoplan-campaign.delivery_progress_accepted" : "i18n:govoplan-campaign.delivery_progress_appended"} value={accepted ?? "—"} density="compact" />
<MetricCard label="i18n:govoplan-campaign.delivery_progress_active" value={counts?.active ?? "—"} density="compact" />
<MetricCard label="i18n:govoplan-campaign.delivery_progress_pending" value={counts?.pending ?? "—"} density="compact" />
<MetricCard label="i18n:govoplan-campaign.delivery_progress_failed" value={counts?.failed ?? "—"} density="compact" />
<MetricCard label="i18n:govoplan-campaign.delivery_progress_unknown" value={counts?.outcome_unknown ?? "—"} density="compact" />
<MetricCard label="i18n:govoplan-campaign.delivery_progress_excluded" value={counts?.excluded ?? "—"} density="compact" />
</MetricGrid>
<DescriptionList variant="inline">
{kind === "smtp" && <><div><dt>i18n:govoplan-campaign.delivery_progress_paused</dt><dd>{progress?.smtp.paused ?? "—"}</dd></div>
<div><dt>i18n:govoplan-campaign.delivery_progress_cancelled</dt><dd>{progress?.smtp.cancelled ?? "—"}</dd></div></>}
<div><dt>i18n:govoplan-campaign.delivery_progress_updated</dt><dd>{progress?.generated_at ? new Date(progress.generated_at).toLocaleTimeString() : "—"}</dd></div>
</DescriptionList>
<p className="muted small-note">i18n:govoplan-campaign.delivery_progress_scope</p>
</Dialog>;
}