import { useState } from "react"; import type { ApiSettings } from "../../../types"; import Button from "../../../components/Button"; import ConfirmDialog from "../../../components/ConfirmDialog"; import DismissibleAlert from "../../../components/DismissibleAlert"; import { forkCampaignVersion, lockCampaignVersionPermanently, unlockCampaignVersionUserLock, unlockCampaignVersionValidation, type CampaignVersionDetail, type CampaignVersionListItem, } from "../../../api/campaigns"; import { canUnlockValidationVersion, formatDateTime, isFinalLockedVersion, isHistoricalCampaignVersion, isPermanentUserLockedVersion, isTemporaryUserLockedVersion, } from "../utils/campaignView"; type LockedVersionNoticeProps = { settings: ApiSettings; campaignId: string; version: CampaignVersionDetail | CampaignVersionListItem | null; currentVersionId?: string | null; reload: () => Promise; message?: string; }; type ConfirmAction = "unlock-validation" | "unlock-user" | "permanent" | null; export default function LockedVersionNotice({ settings, campaignId, version, currentVersionId, reload, message }: LockedVersionNoticeProps) { const [busy, setBusy] = useState(false); const [localError, setLocalError] = useState(""); const [localMessage, setLocalMessage] = useState(""); const [confirmAction, setConfirmAction] = useState(null); const historicalVersion = isHistoricalCampaignVersion(version, currentVersionId); const validationLock = !historicalVersion && canUnlockValidationVersion(version); const temporaryUserLock = !historicalVersion && isTemporaryUserLockedVersion(version); const permanentUserLock = isPermanentUserLockedVersion(version); const finalLock = isFinalLockedVersion(version); const canCreateEditableCopy = !historicalVersion && (permanentUserLock || finalLock); const presentation = lockPresentation(version, { historicalVersion, validationLock, temporaryUserLock, permanentUserLock, finalLock, }); async function runAction(action: Exclude) { if (!version || busy) return; setBusy(true); setLocalError(""); setLocalMessage(""); try { if (action === "unlock-validation") { await unlockCampaignVersionValidation(settings, campaignId, version.id); setLocalMessage("Validation lock removed. Validation, build and generated queue state were invalidated."); } else if (action === "unlock-user") { await unlockCampaignVersionUserLock(settings, campaignId, version.id); setLocalMessage("Temporary user lock removed. The version is editable again."); } else { await lockCampaignVersionPermanently(settings, campaignId, version.id); setLocalMessage("Version permanently locked. Future changes require an editable copy."); } await reload(); } catch (err) { setLocalError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function createEditableCopy() { if (!version || busy) return; setBusy(true); setLocalError(""); setLocalMessage(""); try { const result = await forkCampaignVersion(settings, campaignId, version.id, { current_flow: "manual", current_step: version.current_step ?? null, }); setLocalMessage(`Created editable version #${result.version.version_number}.`); await reload(); } catch (err) { setLocalError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } return (
{presentation.title}{" "} {presentation.description} {message && {message}} {presentation.info && {presentation.info}} {localMessage && {localMessage}} {localError && {localError}}
{validationLock && ( )} {temporaryUserLock && ( <> )} {canCreateEditableCopy && ( )}
setConfirmAction(null)} onConfirm={() => { const action = confirmAction; setConfirmAction(null); if (action) void runAction(action); }} />
); } type LockFlags = { historicalVersion: boolean; validationLock: boolean; temporaryUserLock: boolean; permanentUserLock: boolean; finalLock: boolean; }; function lockPresentation( version: CampaignVersionDetail | CampaignVersionListItem | null, flags: LockFlags, ): { kind: string; title: string; description: string; info: string } { if (flags.historicalVersion) { return { kind: "historical", title: "Historical version.", description: "This version is review-only. Only the campaign's current working version may be edited in place.", info: `Version #${version?.version_number ?? "—"}.`, }; } if (flags.temporaryUserLock) { return { kind: "temporary-user", title: "Temporarily locked version.", description: "Campaign data is read-only, but an authorized user may unlock it or make the lock permanent.", info: version?.user_locked_at ? `Locked ${formatDateTime(version.user_locked_at)}.` : "", }; } if (flags.permanentUserLock) { return { kind: "permanent-user", title: "Permanently locked version.", description: "This user-requested snapshot cannot be unlocked. Create an editable copy for future changes.", info: version?.user_locked_at || version?.published_at ? `Locked ${formatDateTime(version.user_locked_at ?? version.published_at)}.` : "", }; } if (flags.validationLock) { return { kind: "validation", title: "Temporarily locked after validation.", description: "This protects the validated build input. Unlocking invalidates validation, build and generated queue state.", info: version?.locked_at ? `Validated ${formatDateTime(version.locked_at)}.` : "", }; } if (flags.finalLock) { return { kind: "delivery", title: "Permanently locked delivery snapshot.", description: "Delivery has started or the version is in a final state, so it cannot be unlocked in place.", info: version?.locked_at ? `Locked ${formatDateTime(version.locked_at)}.` : "", }; } return { kind: "other", title: "Locked version.", description: "This version is read-only. Create an editable copy to continue working.", info: version?.locked_at ? `Locked ${formatDateTime(version.locked_at)}.` : "", }; } function confirmDialogTitle(action: ConfirmAction): string { if (action === "unlock-validation") return "Unlock validation?"; if (action === "unlock-user") return "Unlock temporary lock?"; if (action === "permanent") return "Lock permanently?"; return "Confirm action"; } function confirmDialogMessage(action: ConfirmAction): string { if (action === "unlock-validation") { return "This makes the version editable and clears validation, build results, review acknowledgement and generated jobs for this version."; } if (action === "unlock-user") { return "This removes the reversible user lock. Campaign data and existing workflow results are not otherwise changed."; } if (action === "permanent") { return "This lock cannot be removed by any role. The version remains available for review and audit, but future changes require an editable copy."; } return "Continue?"; } function confirmDialogLabel(action: ConfirmAction): string { if (action === "unlock-validation") return "Unlock validation"; if (action === "unlock-user") return "Unlock"; if (action === "permanent") return "Lock permanently"; return "Confirm"; }