import type { CampaignListItem } from "../../../types"; import type { CampaignSummary, CampaignVersionDetail, CampaignVersionListItem } from "../../../api/campaigns"; export type CampaignWorkspaceData = { campaign: CampaignListItem | null; versions: CampaignVersionListItem[]; currentVersion: CampaignVersionDetail | null; summary: CampaignSummary | null; }; export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function asRecord(value: unknown): Record { return isRecord(value) ? value : {}; } export function asArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } export function getCampaignJson(version: CampaignVersionDetail | null): Record { return version?.raw_json ?? version?.campaign_json ?? {}; } export function getCampaignSection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).campaign); } export function getRecipientsSection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).recipients); } export function getTemplateSection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).template); } export function getDeliverySection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).delivery); } export function getEntriesSection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).entries); } export function getAttachmentsSection(version: CampaignVersionDetail | null): Record { return asRecord(getCampaignJson(version).attachments); } export function getFields(version: CampaignVersionDetail | null): unknown[] { return asArray(getCampaignJson(version).fields); } export function userLockState( version: CampaignVersionDetail | CampaignVersionListItem | null ): "temporary" | "permanent" | null { if (!version) return null; if (version.user_lock_state === "temporary" || version.user_lock_state === "permanent") { return version.user_lock_state; } // Backwards compatibility for snapshots created before explicit user locks. return version.published_at ? "permanent" : null; } export function isTemporaryUserLockedVersion(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { return userLockState(version) === "temporary"; } export function isPermanentUserLockedVersion(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { return userLockState(version) === "permanent"; } export function isUserLockedVersion(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { return userLockState(version) !== null; } export function isFinalLockedVersion(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { if (!version) return false; return [ "queued", "sending", "sent", "completed", "partially_completed", "outcome_unknown", "failed", "failed_partial", "partially_sent", "archived", "cancelled" ].includes((version.workflow_state ?? "").toLowerCase()); } export function isVersionReadyForDelivery(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { if (!version?.locked_at || isUserLockedVersion(version)) return false; const validation = asRecord(version.validation_summary); return validation.ok === true; } export function canUnlockValidationVersion(version: CampaignVersionDetail | CampaignVersionListItem | null): boolean { if (!version) return false; if (isUserLockedVersion(version)) return false; if (!isVersionReadyForDelivery(version)) return false; if (isFinalLockedVersion(version)) return false; return ["approved", "built"].includes(version.workflow_state ?? ""); } export function isHistoricalCampaignVersion( version: CampaignVersionDetail | CampaignVersionListItem | null, currentVersionId?: string | null, ): boolean { return Boolean(version && currentVersionId && version.id !== currentVersionId); } export function isAuditLockedVersion( version: CampaignVersionDetail | CampaignVersionListItem | null, currentVersionId?: string | null, ): boolean { if (!version) return false; if (isHistoricalCampaignVersion(version, currentVersionId)) return true; if (version.locked_at || isUserLockedVersion(version)) return true; return isFinalLockedVersion(version); } export function versionLockReason( version: CampaignVersionDetail | CampaignVersionListItem | null, currentVersionId?: string | null, ): string { if (!version) return "No campaign version is loaded."; if (isHistoricalCampaignVersion(version, currentVersionId)) { return "Historical campaign versions are review-only. Continue work in the current version or create a new working copy after the current version becomes immutable."; } if (isTemporaryUserLockedVersion(version)) { return `Temporarily user-locked at ${formatDateTime(version.user_locked_at)}. Authorized users may unlock it or make the lock permanent.`; } if (isPermanentUserLockedVersion(version)) { return `Permanently user-locked at ${formatDateTime(version.user_locked_at ?? version.published_at)}. It cannot be unlocked; create an editable copy.`; } if (canUnlockValidationVersion(version)) { return `Temporarily locked by validation at ${formatDateTime(version.locked_at)}. Unlocking invalidates validation, build and queue state.`; } if (isFinalLockedVersion(version)) return `Permanently locked by delivery/final state: ${humanize(version.workflow_state ?? "locked")}.`; if (version.locked_at) return `Temporarily locked at ${formatDateTime(version.locked_at)}.`; return "Editable working version."; } export function currentStepLabel(version: CampaignVersionDetail | CampaignVersionListItem | null): string { if (!version) return "—"; const flow = version.current_flow || "manual"; const step = version.current_step || "not set"; return `${humanize(flow)} / ${humanize(step)}`; } export function formatDateTime(value?: string | null): string { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(undefined, { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } export function humanize(value?: string | null): string { if (!value) return "—"; return value.replace(/_/g, " ").replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); } export function summaryValue(summary: Record | null | undefined, keys: string[]): string | number { if (!summary) return "—"; for (const key of keys) { const value = summary[key]; if (typeof value === "number" || typeof value === "string") return value; } return "—"; } export function getString(record: Record, key: string, fallback = "—"): string { const value = record[key]; if (typeof value === "string" && value.trim()) return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return fallback; } export function getNestedString(record: Record, path: string[], fallback = "—"): string { let current: unknown = record; for (const part of path) { if (!isRecord(current)) return fallback; current = current[part]; } if (typeof current === "string" && current.trim()) return current; if (typeof current === "number" || typeof current === "boolean") return String(current); if (Array.isArray(current)) return current.length ? current.join(", ") : fallback; if (isRecord(current)) return stringifyPreview(current, 120); return fallback; } export function stringifyPreview(value: unknown, maxLength = 220): string { const text = typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? ""; return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text; } export function cloneCampaignJsonForCopy( source: Record, campaign: CampaignListItem | null, stamp: string ): { externalId: string; name: string; description: string; rawJson: Record } { const rawJson = JSON.parse(JSON.stringify(source)) as Record; const campaignSection = asRecord(rawJson.campaign); const baseId = String(campaignSection.id || campaign?.external_id || campaign?.id || "campaign"); const baseName = String(campaignSection.name || campaign?.name || "Campaign"); const description = String(campaignSection.description || campaign?.description || ""); const externalId = `${baseId}-copy-${stamp}`.replace(/[^a-zA-Z0-9_.-]+/g, "-").toLowerCase(); const name = `${baseName} (copy)`; rawJson.campaign = { ...campaignSection, id: externalId, name, description }; return { externalId, name, description, rawJson }; } export function timestampSlug(date = new Date()): string { return date.toISOString().slice(0, 19).replace(/[-:T]/g, ""); }