feat: complete campaign wizards and retention reporting
This commit is contained in:
@@ -11,7 +11,8 @@ import {
|
||||
sendCampaignJob,
|
||||
sendUnattemptedCampaignJobs,
|
||||
type CampaignJobDetailResponse,
|
||||
type CampaignJobsResponse } from
|
||||
type CampaignJobsResponse,
|
||||
type CampaignRetentionReport } from
|
||||
"../../api/campaigns";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
@@ -103,6 +104,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
const cards = data.summary?.cards;
|
||||
const delivery = asRecord(data.summary?.delivery);
|
||||
const postboxReceipts = asRecord(data.summary?.postbox_receipts);
|
||||
const retention = data.summary?.retention;
|
||||
const rateLimit = asRecord(delivery.rate_limit);
|
||||
const imapPolicy = asRecord(delivery.imap_append_sent);
|
||||
|
||||
@@ -500,6 +502,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<div><dt>Withdrawn / expired copies</dt><dd>{String(Number(postboxReceipts.withdrawn_message_count ?? 0) + Number(postboxReceipts.expired_message_count ?? 0))}</dd></div>
|
||||
</dl>
|
||||
</Card>
|
||||
<RetentionPrivacyCard retention={retention} />
|
||||
<Card title="i18n:govoplan-campaign.explicit_delivery_actions.b35e72a4">
|
||||
<p className="muted">i18n:govoplan-campaign.these_actions_never_include_smtp_accepted_or_unr.449d0a80</p>
|
||||
<div className="button-row compact-actions">
|
||||
@@ -623,6 +626,102 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
|
||||
}
|
||||
|
||||
function RetentionPrivacyCard({ retention }: { retention?: CampaignRetentionReport }) {
|
||||
if (!retention) {
|
||||
return (
|
||||
<Card title="i18n:govoplan-campaign.retention_and_privacy.24418676">
|
||||
<p className="muted">i18n:govoplan-campaign.retention_information_is_unavailable.f68cc4d1</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const policy = retention.effective_policy;
|
||||
const evidenceRows = [
|
||||
["i18n:govoplan-campaign.raw_campaign_json.53d8522d", retention.evidence.raw_campaign_json],
|
||||
["i18n:govoplan-campaign.stored_report_detail.13a437d7", retention.evidence.stored_report_detail],
|
||||
["i18n:govoplan-campaign.generated_message_files.2d86ef64", retention.evidence.generated_eml],
|
||||
["i18n:govoplan-campaign.postbox_copies.080b404f", retention.evidence.postbox_copies]
|
||||
] as const;
|
||||
return (
|
||||
<Card title="i18n:govoplan-campaign.retention_and_privacy.24418676">
|
||||
<p>{retention.privacy_impact.summary}</p>
|
||||
{retention.policy_reason && <p className="muted">{retention.policy_reason}</p>}
|
||||
<dl className="detail-list">
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.policy_state.7e955a0d</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
status={retention.policy_status === "configured" ? "success" : retention.policy_status === "defaults" ? "warning" : "error"}
|
||||
label={humanize(retention.policy_status)}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.policy_sources.a592802f</dt>
|
||||
<dd>{retention.sources.map((source) => source.label || source.path).filter(Boolean).join(" → ") || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.raw_json_retention.19ea35f7</dt>
|
||||
<dd>{policy.store_raw_campaign_json === false ? "i18n:govoplan-campaign.do_not_retain.9fd25c4d" : retentionDuration(policy.raw_campaign_json_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.generated_message_retention.ce6366ca</dt>
|
||||
<dd>{retentionDuration(policy.generated_eml_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.report_detail_retention.68e30587</dt>
|
||||
<dd>{retentionDuration(policy.stored_report_detail_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.audit_detail.2a85f905</dt>
|
||||
<dd>{humanize(policy.audit_detail_level ?? "full")} · {retentionDuration(policy.audit_detail_retention_days)}</dd>
|
||||
</div>
|
||||
{evidenceRows.map(([label, evidence]) =>
|
||||
<div key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
status={retentionEvidenceTone(evidence?.state)}
|
||||
label={humanize(evidence?.state ?? "unavailable")}
|
||||
/>
|
||||
<span className="muted"> · {retentionEvidenceDetail(evidence)}</span>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function retentionDuration(days?: number | null): string {
|
||||
if (days === null || days === undefined) return "i18n:govoplan-campaign.no_automatic_expiry.52cb62f8";
|
||||
if (days === 0) return "i18n:govoplan-campaign.remove_when_eligible.385ff0eb";
|
||||
return i18nMessage("i18n:govoplan-campaign.value_days.2bf9b447", { value0: String(days) });
|
||||
}
|
||||
|
||||
function retentionEvidenceTone(state?: string): string {
|
||||
if (state === "retained") return "success";
|
||||
if (state === "redacted" || state === "expired") return "inactive";
|
||||
if (state === "partially_redacted" || state === "partially_expired") return "warning";
|
||||
if (state === "unavailable") return "error";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function retentionEvidenceDetail(
|
||||
evidence?: CampaignRetentionReport["evidence"][string]
|
||||
): string {
|
||||
if (!evidence) return "—";
|
||||
const counts = [
|
||||
evidence.retained_count !== undefined ? `${evidence.retained_count} retained` : "",
|
||||
evidence.expired_count !== undefined ? `${evidence.expired_count} expired` : "",
|
||||
evidence.redacted_summary_count !== undefined
|
||||
? `${evidence.redacted_summary_count}/${evidence.summary_count ?? 0} redacted`
|
||||
: "",
|
||||
evidence.currently_readable_count !== undefined ? `${evidence.currently_readable_count} readable` : "",
|
||||
evidence.withdrawn_count !== undefined ? `${evidence.withdrawn_count} withdrawn` : ""
|
||||
].filter(Boolean);
|
||||
return counts.join(", ") || (evidence.redacted_at ? formatDateTime(evidence.redacted_at) : "—");
|
||||
}
|
||||
|
||||
function PostboxTargetEvidenceSection({ targets }: { targets: unknown[] }) {
|
||||
const rows = targets.map(asRecord);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
Reference in New Issue
Block a user