chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:18 +02:00
parent 57f6066bf7
commit 52e6ed49c5
66 changed files with 8868 additions and 4072 deletions
@@ -10,7 +10,7 @@ import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { DismissibleAlert, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
import {
lockCampaignVersionPermanently,
@@ -18,8 +18,8 @@ import {
unlockCampaignVersionUserLock,
updateCampaignMetadata,
type CampaignVersionDetail,
type CampaignVersionListItem,
} from "../../api/campaigns";
type CampaignVersionListItem } from
"../../api/campaigns";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import {
asArray,
@@ -31,15 +31,15 @@ import {
isPermanentUserLockedVersion,
isTemporaryUserLockedVersion,
isVersionReadyForDelivery,
summaryValue,
} from "./utils/campaignView";
summaryValue } from
"./utils/campaignView";
import { buildUndefinedPlaceholders, extractTemplatePlaceholders, recipientAddressTemplateFieldOptions } from "./utils/templatePlaceholders";
const campaignModeOptions = ["draft", "test", "send"];
type LockAction = "temporary" | "unlock" | "permanent";
type PendingLockAction = { version: CampaignVersionListItem; action: LockAction } | null;
type PendingLockAction = {version: CampaignVersionListItem;action: LockAction;} | null;
export default function CampaignOverviewPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
export default function CampaignOverviewPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true });
const campaign = data.campaign;
const versions = useMemo(() => data.versions.slice().sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0)), [data.versions]);
@@ -51,13 +51,28 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
const [message, setMessage] = useState("");
const versionMetrics = useMemo(() => campaignVersionMetrics(data.currentVersion), [data.currentVersion]);
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 ?? "",
description: campaign.description ?? ""
});
}, [campaign, identityDirty]);
@@ -67,8 +82,8 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
setMessage("");
}
async function saveIdentity() {
if (!campaign || savingIdentity || !identityDirty) return;
async function saveIdentity(): Promise<boolean> {
if (!campaign || savingIdentity || !identityDirty) return false;
setSavingIdentity(true);
setError("");
setMessage("");
@@ -77,12 +92,14 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
external_id: identity.external_id,
name: identity.name,
status: identity.status,
description: identity.description,
description: identity.description
});
setIdentityDirty(false);
await reload();
return true;
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return false;
} finally {
setSavingIdentity(false);
}
@@ -97,13 +114,13 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
try {
if (pending.action === "temporary") {
await lockCampaignVersionTemporarily(settings, campaignId, pending.version.id);
setMessage(`Version #${pending.version.version_number} temporarily locked.`);
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(`Temporary lock removed from version #${pending.version.version_number}.`);
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(`Version #${pending.version.version_number} permanently locked.`);
setMessage(i18nMessage("i18n:govoplan-campaign.version_value_permanently_locked.60595f45", { value0: pending.version.version_number }));
}
setPendingLockAction(null);
await reload();
@@ -118,70 +135,70 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>{campaign?.name || "Overview"}</PageTitle>
<p className="mono-small">Campaign overview · version-independent identity and version history</p>
<PageTitle loading={loading}>{campaign?.name || "i18n:govoplan-campaign.overview.0efc2e6b"}</PageTitle>
<p className="mono-small">i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113</p>
</div>
<div className="button-row compact-actions">
<Button onClick={reload} disabled={loading || savingIdentity || lockBusy}>Reload</Button>
<Link to="wizard/create"><Button>Edit with wizard</Button></Link>
<Button variant="primary" onClick={() => void saveIdentity()} disabled={!campaign || !identityDirty || savingIdentity}>{savingIdentity ? "Saving…" : "Save"}</Button>
<Button onClick={reload} disabled={loading || savingIdentity || lockBusy}>i18n:govoplan-campaign.reload.cce71553</Button>
<Link to="wizard/create"><Button>i18n:govoplan-campaign.edit_with_wizard.672a7d1a</Button></Link>
<Button variant="primary" onClick={() => void saveIdentity()} disabled={!campaign || !identityDirty || savingIdentity}>{savingIdentity ? "i18n:govoplan-campaign.saving.56a2285c" : "i18n:govoplan-campaign.save.efc007a3"}</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading campaign overview">
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_campaign_overview.ffa1adf0">
<div className="metric-grid campaign-overview-metrics current-version-metrics">
<MetricCard label="Version" value={data.currentVersion?.version_number ? `#${data.currentVersion.version_number}` : "—"} tone="neutral" />
<MetricCard label="Fields" value={versionMetrics.fieldCount} tone="info" />
<MetricCard label="Recipients" value={versionMetrics.recipientCount} tone="neutral" detail="Active inline recipients" />
<MetricCard label="Template health" value={versionMetrics.templateHealthValue} tone={versionMetrics.templateHealthTone} detail={versionMetrics.templateHealthDetail} />
<MetricCard label="i18n:govoplan-campaign.version.2da600bf" value={data.currentVersion?.version_number ? `#${data.currentVersion.version_number}` : "—"} tone="neutral" />
<MetricCard label="i18n:govoplan-campaign.fields.e8b68527" value={versionMetrics.fieldCount} tone="info" />
<MetricCard label="i18n:govoplan-campaign.recipients.78cbf8eb" value={versionMetrics.recipientCount} tone="neutral" detail="i18n:govoplan-campaign.active_inline_recipients.8ba58f6e" />
<MetricCard label="i18n:govoplan-campaign.template_health.22e14b59" value={versionMetrics.templateHealthValue} tone={versionMetrics.templateHealthTone} detail={versionMetrics.templateHealthDetail} />
</div>
<div className="metric-grid campaign-overview-metrics">
<MetricCard label="Queueable" value={data.summary?.cards?.queueable ?? "—"} tone="good" detail="Ready or warning" />
<MetricCard label="Needs attention" value={data.summary?.cards?.needs_attention ?? "—"} tone="warning" detail="Review first" />
<MetricCard label="Sent" value={data.summary?.cards?.sent ?? "—"} tone="info" detail="SMTP success" />
<MetricCard label="Failed" value={data.summary?.cards?.failed ?? "—"} tone="danger" detail="SMTP failures" />
<MetricCard label="i18n:govoplan-campaign.queueable.ea776f8d" value={data.summary?.cards?.queueable ?? "—"} tone="good" detail="i18n:govoplan-campaign.ready_or_warning.4dcce676" />
<MetricCard label="i18n:govoplan-campaign.needs_attention.a126722e" value={data.summary?.cards?.needs_attention ?? "—"} tone="warning" detail="i18n:govoplan-campaign.review_first.741ac781" />
<MetricCard label="i18n:govoplan-campaign.sent.35f49dcf" value={data.summary?.cards?.sent ?? "—"} tone="info" detail="i18n:govoplan-campaign.smtp_success.3591a856" />
<MetricCard label="i18n:govoplan-campaign.failed.09fef5d8" value={data.summary?.cards?.failed ?? "—"} tone="danger" detail="i18n:govoplan-campaign.smtp_failures.00b33b85" />
</div>
<Card title="Current version state" actions={<Link
to={`send?version=${campaign?.current_version_id}`}
className={`btn btn-primary`}
aria-label={`Open curent version`}
title={`Open curent version`}
>
Open
<Card title="i18n:govoplan-campaign.current_version_state.f581778f" actions={<Link
to={`send?version=${campaign?.current_version_id}`}
className={`btn btn-primary`}
aria-label={i18nMessage("i18n:govoplan-campaign.open_curent_version.cc1cd678", {})}
title={i18nMessage("i18n:govoplan-campaign.open_curent_version.cc1cd678", {})}>
i18n:govoplan-campaign.open.cf9b7706
</Link>}>
<div className="summary-grid overview-summary-grid">
<SummaryTile label="Validation errors" value={summaryValue(data.currentVersion?.validation_summary, ["error_count", "errors", "blocked"])} />
<SummaryTile label="Warnings" value={summaryValue(data.currentVersion?.validation_summary, ["warning_count", "warnings"])} />
<SummaryTile label="Built messages" value={summaryValue(data.currentVersion?.build_summary, ["built_count", "built", "messages_built"])} />
<SummaryTile label="Jobs total" value={data.summary?.cards?.jobs_total ?? "—"} />
<SummaryTile label="i18n:govoplan-campaign.validation_errors.e54ca4fe" value={summaryValue(data.currentVersion?.validation_summary, ["error_count", "errors", "blocked"])} />
<SummaryTile label="i18n:govoplan-campaign.warnings.1430f976" value={summaryValue(data.currentVersion?.validation_summary, ["warning_count", "warnings"])} />
<SummaryTile label="i18n:govoplan-campaign.built_messages.1fb804f2" value={summaryValue(data.currentVersion?.build_summary, ["built_count", "built", "messages_built"])} />
<SummaryTile label="i18n:govoplan-campaign.jobs_total.98da65bc" value={data.summary?.cards?.jobs_total ?? "—"} />
</div>
</Card>
<Card title="Campaign identity" collapsible>
<Card title="i18n:govoplan-campaign.campaign_identity.a00ca574" collapsible>
<div className="form-grid campaign-identity-grid">
<FormField label="Campaign ID">
<FormField label="i18n:govoplan-campaign.campaign_id.4c4ed79e">
<input value={identity.external_id} onChange={(event) => patchIdentity("external_id", event.target.value)} />
</FormField>
<FormField label="Mode">
<FormField label="i18n:govoplan-campaign.mode.a7b93d21">
<select value={identity.status} onChange={(event) => patchIdentity("status", event.target.value)}>
{campaignModeOptions.map((option) => <option key={option} value={option}>{option}</option>)}
</select>
</FormField>
<FormField label="Name">
<FormField label="i18n:govoplan-campaign.name.709a2322">
<input value={identity.name} onChange={(event) => patchIdentity("name", event.target.value)} />
</FormField>
<FormField label="Description">
<FormField label="i18n:govoplan-campaign.description.55f8ebc8">
<textarea rows={4} value={identity.description} onChange={(event) => patchIdentity("description", event.target.value)} />
</FormField>
</div>
</Card>
<Card title="Version history" collapsible>
<Card title="i18n:govoplan-campaign.version_history.91f86581" collapsible>
<div className="admin-table-surface">
<DataGrid
id={`campaign-${campaignId}-versions`}
@@ -189,10 +206,10 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
columns={versionColumns(setPendingLockAction, campaign?.current_version_id)}
getRowKey={(version) => version.id}
initialSort={{ columnId: "version", direction: "desc" }}
emptyText="No versions found."
emptyText="i18n:govoplan-campaign.no_versions_found.a8284e9e"
className="version-history-table"
rowClassName={(version) => version.id === data.currentVersion?.id ? "current-version-row" : undefined}
/>
rowClassName={(version) => version.id === data.currentVersion?.id ? "current-version-row" : undefined} />
</div>
</Card>
</LoadingFrame>
@@ -205,10 +222,10 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
tone={pendingLockAction?.action === "unlock" ? "default" : "danger"}
busy={lockBusy}
onCancel={() => setPendingLockAction(null)}
onConfirm={() => void applyLockAction()}
/>
</div>
);
onConfirm={() => void applyLockAction()} />
</div>);
}
type TemplateHealthTone = "neutral" | "good" | "warning" | "danger" | "info";
@@ -253,10 +270,10 @@ function campaignVersionMetrics(version: CampaignVersionDetail | null): Campaign
templateHealthValue: `${score}/3`,
templateHealthTone: score === 3 ? "good" : score === 2 ? "warning" : "danger",
templateHealthDetail: [
subjectOk ? "Subject OK" : "Subject missing",
bodyOk ? "Body OK" : "Body missing",
placeholdersOk ? "Placeholders OK" : `${undefinedPlaceholders.length} undefined`
].join(" · ")
subjectOk ? "i18n:govoplan-campaign.subject_ok.3dbdc3ed" : "i18n:govoplan-campaign.subject_missing.f545bf65",
bodyOk ? "i18n:govoplan-campaign.body_ok.cdb33005" : "i18n:govoplan-campaign.body_missing.bd58622f",
placeholdersOk ? "i18n:govoplan-campaign.placeholders_ok.3a07f3bc" : `${undefinedPlaceholders.length} undefined`].
join(" · ")
};
}
@@ -270,106 +287,106 @@ function textValue(value: unknown, fallback = ""): string {
function versionColumns(setPendingLockAction: (action: PendingLockAction) => void, currentVersionId?: string | null): DataGridColumn<CampaignVersionListItem>[] {
return [
{ id: "version", header: "Version", width: 110, sortable: true, filterable: true, filterType: "integer", sticky: "start", render: (version) => `#${version.version_number}`, value: (version) => version.version_number ?? 0 },
{ id: "state", header: "State", width: 140, sortable: true, filterable: true, columnType: "from-list", list: { options: ["editing", "validated", "built", "approved", "queued", "sending", "sent", "completed", "partially_completed", "outcome_unknown", "failed", "partially_sent", "failed_partial", "cancelled", "archived"].map((value) => ({ value, label: value.replace(/_/g, " ") })), display: "pill" }, render: (version) => <StatusBadge status={version.workflow_state ?? "editing"} />, value: (version) => version.workflow_state ?? "editing" },
{ id: "lock", header: "Lock", width: 190, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Current working version", label: "Current working version" }, { value: "Historical version", label: "Historical version" }, { value: "Temporarily locked", label: "Temporarily locked" }, { value: "Permanently locked", label: "Permanently locked" }, { value: "Delivery locked", label: "Delivery locked" }] }, render: (version) => versionLockLabel(version, currentVersionId), value: (version) => versionLockLabel(version, currentVersionId) },
{ id: "validation", header: "Validation", width: 170, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Not validated", label: "Not validated" }, { value: "Valid", label: "Valid" }, { value: "Validation issues", label: "Validation issues" }] }, render: validationLabel, value: validationLabel },
{ id: "build", header: "Build", width: 140, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Not built", label: "Not built" }, { value: "Built", label: "Built" }, { value: "Build issues", label: "Build issues" }] }, render: buildLabel, value: buildLabel },
{ id: "updated", header: "Updated", width: 190, sortable: true, filterable: true, filterType: "date", render: (version) => formatDateTime(version.updated_at), value: (version) => version.updated_at ?? "" },
{
id: "actions",
header: "Actions",
width: 260,
sticky: "end",
render: (version) => {
const isCurrent = version.id === currentVersionId;
return (
<div className="button-row compact-actions">
{ id: "version", header: "i18n:govoplan-campaign.version.2da600bf", width: 110, sortable: true, filterable: true, filterType: "integer", sticky: "start", render: (version) => `#${version.version_number}`, value: (version) => version.version_number ?? 0 },
{ id: "state", header: "i18n:govoplan-campaign.state.a7250206", width: 140, sortable: true, filterable: true, columnType: "from-list", list: { options: ["editing", "validated", "built", "approved", "queued", "sending", "sent", "completed", "partially_completed", "outcome_unknown", "failed", "partially_sent", "failed_partial", "cancelled", "archived"].map((value) => ({ value, label: value.replace(/_/g, " ") })), display: "pill" }, render: (version) => <StatusBadge status={version.workflow_state ?? "editing"} />, value: (version) => version.workflow_state ?? "editing" },
{ id: "lock", header: "i18n:govoplan-campaign.lock.891ebccd", width: 190, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Current working version", label: "i18n:govoplan-campaign.current_working_version.eda99d70" }, { value: "Historical version", label: "i18n:govoplan-campaign.historical_version.8880931f" }, { value: "Temporarily locked", label: "i18n:govoplan-campaign.temporarily_locked.1716dc95" }, { value: "Permanently locked", label: "i18n:govoplan-campaign.permanently_locked.327d59fd" }, { value: "Delivery locked", label: "i18n:govoplan-campaign.delivery_locked.2b664305" }] }, render: (version) => versionLockLabel(version, currentVersionId), value: (version) => versionLockLabel(version, currentVersionId) },
{ id: "validation", header: "i18n:govoplan-campaign.validation.dd74d182", width: 170, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Not validated", label: "i18n:govoplan-campaign.not_validated.11bb4178" }, { value: "Valid", label: "i18n:govoplan-campaign.valid.a4aefa35" }, { value: "Validation issues", label: "i18n:govoplan-campaign.validation_issues.528305b1" }] }, render: validationLabel, value: validationLabel },
{ id: "build", header: "i18n:govoplan-campaign.build.bbd80cf7", width: 140, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "Not built", label: "i18n:govoplan-campaign.not_built.88bbe87a" }, { value: "Built", label: "i18n:govoplan-campaign.built.a6ad3f82" }, { value: "Build issues", label: "i18n:govoplan-campaign.build_issues.3e03bf8e" }] }, render: buildLabel, value: buildLabel },
{ id: "updated", header: "i18n:govoplan-campaign.updated.f2f8570d", width: 190, sortable: true, filterable: true, filterType: "date", render: (version) => formatDateTime(version.updated_at), value: (version) => version.updated_at ?? "" },
{
id: "actions",
header: "i18n:govoplan-campaign.actions.c3cd636a",
width: 260,
sticky: "end",
render: (version) => {
const isCurrent = version.id === currentVersionId;
return (
<div className="button-row compact-actions">
<Link
to={`send?version=${version.id}`}
className={`btn ${isCurrent ? "btn-primary" : "btn-secondary"} admin-icon-button`}
aria-label={`Open version ${version.version_number}`}
title={`Open version ${version.version_number}`}
>
to={`send?version=${version.id}`}
className={`btn ${isCurrent ? "btn-primary" : "btn-secondary"} admin-icon-button`}
aria-label={i18nMessage("i18n:govoplan-campaign.open_version_value.7ef53546", { value0: version.version_number })}
title={i18nMessage("i18n:govoplan-campaign.open_version_value.7ef53546", { value0: version.version_number })}>
<ExternalLink aria-hidden="true" />
</Link>
{isCurrent && (isTemporaryUserLockedVersion(version) ? (
<>
<Button onClick={() => setPendingLockAction({ version, action: "unlock" })}>Unlock</Button>
<Button variant="danger" onClick={() => setPendingLockAction({ version, action: "permanent" })}>Lock permanently</Button>
</>
) : !isPermanentUserLockedVersion(version) && !isFinalLockedVersion(version) && !canUnlockValidationVersion(version) && !version.locked_at ? (
<Button
className="admin-icon-button"
onClick={() => setPendingLockAction({ version, action: "temporary" })}
aria-label={`Temporarily lock version ${version.version_number}`}
title="Temporarily lock version"
>
{isCurrent && (isTemporaryUserLockedVersion(version) ?
<>
<Button onClick={() => setPendingLockAction({ version, action: "unlock" })}>i18n:govoplan-campaign.unlock.1526a17e</Button>
<Button variant="danger" onClick={() => setPendingLockAction({ version, action: "permanent" })}>i18n:govoplan-campaign.lock_permanently.cc0ce9e7</Button>
</> :
!isPermanentUserLockedVersion(version) && !isFinalLockedVersion(version) && !canUnlockValidationVersion(version) && !version.locked_at ?
<Button
className="admin-icon-button"
onClick={() => setPendingLockAction({ version, action: "temporary" })}
aria-label={i18nMessage("i18n:govoplan-campaign.temporarily_lock_version_value.8019e581", { value0: version.version_number })}
title="i18n:govoplan-campaign.temporarily_lock_version.82b31149">
<LockKeyhole aria-hidden="true" />
</Button>
) : null)}
</div>
);
},
},
];
</Button> :
null)}
</div>);
}
}];
}
function versionLockLabel(version: CampaignVersionListItem, currentVersionId?: string | null): string {
if (currentVersionId && version.id !== currentVersionId) return "Historical · review-only";
if (isTemporaryUserLockedVersion(version)) return "Temporary user lock";
if (isPermanentUserLockedVersion(version)) return "Permanent user lock";
if (isFinalLockedVersion(version)) return "Permanent delivery lock";
if (canUnlockValidationVersion(version)) return "Temporary validation lock";
if (version.locked_at) return "Temporarily locked";
return "Editable";
if (currentVersionId && version.id !== currentVersionId) return "i18n:govoplan-campaign.historical_review_only.5afffe82";
if (isTemporaryUserLockedVersion(version)) return "i18n:govoplan-campaign.temporary_user_lock.c2bda6a9";
if (isPermanentUserLockedVersion(version)) return "i18n:govoplan-campaign.permanent_user_lock.9d5d8959";
if (isFinalLockedVersion(version)) return "i18n:govoplan-campaign.permanent_delivery_lock.07932206";
if (canUnlockValidationVersion(version)) return "i18n:govoplan-campaign.temporary_validation_lock.fdc5545d";
if (version.locked_at) return "i18n:govoplan-campaign.temporarily_locked.1716dc95";
return "i18n:govoplan-campaign.editable.b91faec0";
}
function validationLabel(version: CampaignVersionListItem): string {
const validation = version.validation_summary ?? {};
if (validation.ok === true && isVersionReadyForDelivery(version)) return "Passed";
if (validation.ok === false) return "Needs attention";
if (validation.ok === true) return "Passed, unavailable while user-locked";
return "Not validated";
if (validation.ok === true && isVersionReadyForDelivery(version)) return "i18n:govoplan-campaign.passed.271d60f4";
if (validation.ok === false) return "i18n:govoplan-campaign.needs_attention.a126722e";
if (validation.ok === true) return "i18n:govoplan-campaign.passed_unavailable_while_user_locked.d6771ff4";
return "i18n:govoplan-campaign.not_validated.11bb4178";
}
function buildLabel(version: CampaignVersionListItem): string {
const build = version.build_summary ?? {};
return String(build.built_count ?? build.ready_count ?? "Not built");
return String(build.built_count ?? build.ready_count ?? "i18n:govoplan-campaign.not_built.88bbe87a");
}
function lockDialogTitle(pending: PendingLockAction): string {
if (pending?.action === "temporary") return "Temporarily lock version?";
if (pending?.action === "unlock") return "Unlock version?";
if (pending?.action === "permanent") return "Lock version permanently?";
return "Confirm lock action";
if (pending?.action === "temporary") return "i18n:govoplan-campaign.temporarily_lock_version.8ccc5708";
if (pending?.action === "unlock") return "i18n:govoplan-campaign.unlock_version.ebd2fd9a";
if (pending?.action === "permanent") return "i18n:govoplan-campaign.lock_version_permanently.a8625753";
return "i18n:govoplan-campaign.confirm_lock_action.617986ee";
}
function lockDialogMessage(pending: PendingLockAction): string {
if (pending?.action === "temporary") {
return "This makes the version read-only without making it final. An authorized user may unlock it later or make the lock permanent.";
return "i18n:govoplan-campaign.this_makes_the_version_read_only_without_making_.9d86667b";
}
if (pending?.action === "unlock") {
return "This removes the temporary user lock and makes the version editable again. Existing validation/build state is otherwise retained.";
return "i18n:govoplan-campaign.this_removes_the_temporary_user_lock_and_makes_t.fdabd47a";
}
if (pending?.action === "permanent") {
return "This lock cannot be removed by any role. The version remains reviewable for audit purposes; future changes require an editable copy.";
return "i18n:govoplan-campaign.this_lock_cannot_be_removed_by_any_role_the_vers.14ae9b80";
}
return "Continue?";
return "i18n:govoplan-campaign.continue.4e6302ed";
}
function lockDialogLabel(pending: PendingLockAction): string {
if (pending?.action === "temporary") return "Lock temporarily";
if (pending?.action === "unlock") return "Unlock";
if (pending?.action === "permanent") return "Lock permanently";
return "Confirm";
if (pending?.action === "temporary") return "i18n:govoplan-campaign.lock_temporarily.3f91d346";
if (pending?.action === "unlock") return "i18n:govoplan-campaign.unlock.1526a17e";
if (pending?.action === "permanent") return "i18n:govoplan-campaign.lock_permanently.cc0ce9e7";
return "i18n:govoplan-campaign.confirm.04a21221";
}
function SummaryTile({ label, value }: { label: string; value: string | number }) {
function SummaryTile({ label, value }: {label: string;value: string | number;}) {
return (
<div className="summary-tile">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
</div>);
}