import { useEffect, useMemo, useState, type ReactNode } from "react"; import type { ApiSettings } from "../../types"; import { getPrivacyRetentionPolicy, updatePrivacyRetentionPolicy, type PrivacyRetentionLimitPermissionPatch, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type PrivacyRetentionPolicyFieldKey, type PrivacyRetentionPolicyPatch, type PrivacyRetentionPolicyScope } from "../../api/admin"; import Button from "../../components/Button"; import Card from "../../components/Card"; import DismissibleAlert from "../../components/DismissibleAlert"; import type { PolicySourcePathItem } from "../../components/PolicySourcePath"; import FieldLabel from "../../components/help/FieldLabel"; import FormField from "../../components/FormField"; import ToggleSwitch from "../../components/ToggleSwitch"; export type RetentionPolicyTargetOption = { id: string; label: string; secondary?: string | null; }; type RetentionPolicyScopeManagerProps = { settings: ApiSettings; scopeType: PrivacyRetentionPolicyScope; scopeId?: string | null; targetOptions?: RetentionPolicyTargetOption[]; targetLabel?: string; title?: string; description?: string; canWrite: boolean; locked?: boolean; }; type RetentionPolicyEditorProps = { settings: ApiSettings; scopeType: PrivacyRetentionPolicyScope; scopeId?: string | null; title?: string; description?: string; canWrite: boolean; locked?: boolean; }; type DayKey = | "raw_campaign_json_retention_days" | "generated_eml_retention_days" | "stored_report_detail_retention_days" | "mock_mailbox_retention_days" | "audit_detail_retention_days"; type FieldDefinition = { key: PrivacyRetentionPolicyFieldKey; label: string; kind: "raw-json" | "days" | "audit"; systemOnly?: boolean; }; type RawJsonValue = "inherit" | "keep" | "disable"; type AuditDetailValue = "inherit" | PrivacyRetentionPolicy["audit_detail_level"]; const auditDetailOrder: Record = { full: 0, redacted: 1, minimal: 2 }; const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = { store_raw_campaign_json: true, raw_campaign_json_retention_days: true, generated_eml_retention_days: true, stored_report_detail_retention_days: true, mock_mailbox_retention_days: true, audit_detail_retention_days: true, audit_detail_level: true }; const defaultPrivacyPolicy: PrivacyRetentionPolicy = { store_raw_campaign_json: true, raw_campaign_json_retention_days: null, generated_eml_retention_days: null, stored_report_detail_retention_days: null, mock_mailbox_retention_days: null, audit_detail_retention_days: null, audit_detail_level: "full", allow_lower_level_limits: defaultAllowLowerLevelLimits }; const fieldDefinitions: FieldDefinition[] = [ { key: "store_raw_campaign_json", label: "Raw campaign JSON", kind: "raw-json" }, { key: "raw_campaign_json_retention_days", label: "Raw campaign JSON days", kind: "days" }, { key: "generated_eml_retention_days", label: "Generated EML days", kind: "days" }, { key: "stored_report_detail_retention_days", label: "Stored report detail days", kind: "days" }, { key: "mock_mailbox_retention_days", label: "Mock mailbox days", kind: "days", systemOnly: true }, { key: "audit_detail_retention_days", label: "Audit detail days", kind: "days" }, { key: "audit_detail_level", label: "Audit detail level", kind: "audit" } ]; export function RetentionPolicyScopeManager({ settings, scopeType, scopeId = null, targetOptions = [], targetLabel = "Target", title = "Retention policy", description, canWrite, locked = false }: RetentionPolicyScopeManagerProps) { const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || ""); const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign"; const targetSelectionRequired = requiresTarget && !scopeId; const hasSelectableTarget = targetOptions.length > 0; const activeScopeId = scopeId || selectedTargetId || null; const defaultDescription = scopeType === "system" ? "System retention defaults and the fields lower levels may override." : "Local retention limits for this scope. Values inherit from the parent unless explicitly narrowed."; const targetEmptyText = `No ${pluralizeLabel(targetLabel.toLowerCase())} available`; useEffect(() => { if (scopeId) { setSelectedTargetId(scopeId); return; } if (targetOptions.length > 0 && !targetOptions.some((option) => option.id === selectedTargetId)) { setSelectedTargetId(targetOptions[0].id); return; } if (targetOptions.length === 0 && selectedTargetId) setSelectedTargetId(""); }, [scopeId, selectedTargetId, targetOptions]); return (
{targetSelectionRequired && (
)} {(!requiresTarget || Boolean(activeScopeId)) && ( )}
); } function pluralizeLabel(label: string): string { if (label.endsWith("s")) return label; if (label.endsWith("y")) return `${label.slice(0, -1)}ies`; return `${label}s`; } export function RetentionPolicyEditor({ settings, scopeType, scopeId = null, title = "Retention policy", description, canWrite, locked = false }: RetentionPolicyEditorProps) { const [policy, setPolicy] = useState({}); const [effectivePolicy, setEffectivePolicy] = useState(null); const [parentPolicy, setParentPolicy] = useState(null); const [effectivePolicySources, setEffectivePolicySources] = useState([]); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const isSystem = scopeType === "system"; const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign"; const scopeReady = !requiresTarget || Boolean(scopeId); const disabled = locked || loading || busy || !canWrite || !scopeReady; const showAllowColumn = scopeType !== "campaign"; const showEffectiveColumn = !isSystem && scopeReady; const activeParentPolicy = parentPolicy ? normalizeFullPolicy(parentPolicy) : null; const activeFullPolicy = normalizeFullPolicy({ ...defaultPrivacyPolicy, ...policy, allow_lower_level_limits: { ...defaultAllowLowerLevelLimits, ...(policy.allow_lower_level_limits ?? {}) } }); const visibleFields = useMemo(() => fieldDefinitions.filter((field) => isSystem || !field.systemOnly), [isSystem]); useEffect(() => { void loadPolicy(); }, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, scopeId]); async function loadPolicy() { setError(""); setSuccess(""); if (!scopeReady) { setPolicy({}); setEffectivePolicy(null); setParentPolicy(null); setEffectivePolicySources([]); return; } setLoading(true); try { const response = await getPrivacyRetentionPolicy(settings, scopeType, scopeId); setPolicy(isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy)); setEffectivePolicy(normalizeFullPolicy(response.effective_policy)); setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); } catch (err) { setPolicy({}); setEffectivePolicy(null); setParentPolicy(null); setEffectivePolicySources([]); setError(errorMessage(err)); } finally { setLoading(false); } } async function savePolicy() { if (!scopeReady) return; setBusy(true); setError(""); setSuccess(""); try { const payload = isSystem ? normalizeFullPolicy(policy) : normalizePatchForSave(policy, activeParentPolicy, scopeType); const response = await updatePrivacyRetentionPolicy(settings, scopeType, payload, scopeId); setPolicy(isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy)); setEffectivePolicy(normalizeFullPolicy(response.effective_policy)); setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); setSuccess("Retention policy saved."); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } function patchPolicy(patch: PrivacyRetentionPolicyPatch) { const next = { ...policy, ...patch }; if (policy.allow_lower_level_limits || patch.allow_lower_level_limits) { next.allow_lower_level_limits = { ...(policy.allow_lower_level_limits ?? {}), ...(patch.allow_lower_level_limits ?? {}) }; } setPolicy(isSystem ? normalizeFullPolicy(next) : normalizePatch(next)); } function setRawJson(value: RawJsonValue | "store" | "disabled") { if (isSystem) { patchPolicy({ store_raw_campaign_json: value === "store" }); return; } patchPolicy({ store_raw_campaign_json: value === "inherit" ? undefined : value === "keep" }); } function setRetentionDays(key: DayKey, value: string) { const trimmed = value.trim(); patchPolicy({ [key]: trimmed === "" ? (isSystem ? null : undefined) : Math.max(0, Number(trimmed) || 0) }); } function setAuditDetail(value: AuditDetailValue) { patchPolicy({ audit_detail_level: value === "inherit" ? undefined : value }); } function setAllowLowerLevelLimit(key: PrivacyRetentionPolicyFieldKey, allowed: boolean) { patchPolicy({ allow_lower_level_limits: { [key]: allowed } }); } function parentAllows(key: PrivacyRetentionPolicyFieldKey): boolean { return !activeParentPolicy || activeParentPolicy.allow_lower_level_limits[key] !== false; } function localAllowsLower(key: PrivacyRetentionPolicyFieldKey): boolean { if (isSystem) return activeFullPolicy.allow_lower_level_limits[key] !== false; const localValue = policy.allow_lower_level_limits?.[key]; if (localValue !== undefined) return localValue && parentAllows(key); return parentAllows(key); } const localPolicySummary = useMemo(() => isSystem ? "System baseline" : localPolicyCount(policy), [isSystem, policy]); const defaultDescription = isSystem ? "Set concrete system retention values. Blank day fields mean unlimited retention." : "Blank values inherit. Numeric values may only shorten the inherited retention period; audit detail may only become more restrictive."; return ( } >

{description ?? defaultDescription}

{!isSystem &&

Mock mailbox retention is currently managed at system level because mock mailbox records do not carry tenant or campaign ownership metadata.

} {error && {error}} {success && {success}}

{isSystem ? "System policy" : "Local policy"}

{localPolicySummary}
Field {isSystem ? "Value" : "Local setting"} {showEffectiveColumn && Effective policy} {showAllowColumn && Lower levels}
{visibleFields.map((field) => { const fieldLocked = !parentAllows(field.key); const fieldDisabled = disabled || fieldLocked; return (
{field.label} {fieldLocked && Locked by parent policy} {field.systemOnly && System-level cleanup scope}
{renderFieldControl(field, activeFullPolicy, policy, isSystem, fieldDisabled, setRawJson, setRetentionDays, setAuditDetail, activeParentPolicy)}
{showEffectiveColumn && (
{retentionFieldValue(field, effectivePolicy, loading)}
)} {showAllowColumn && ( setAllowLowerLevelLimit(field.key, checked)} label="Allow override" /> )}
); })}
); } type PolicySourceItem = { label: string; policy?: Record | null; }; function retentionPolicyPathHelp(field: FieldDefinition, sources: PolicySourcePathItem[], scopeType: PrivacyRetentionPolicyScope): ReactNode { const items = normalizePolicySourceItems(sources.length > 0 ? sources : retentionPolicySourcePath(scopeType)); return ; } function PolicyPathHelp({ lines }: { lines: string[] }) { return ( {lines.map((line, index) => {line})} ); } function retentionPolicyPathLines(field: FieldDefinition, items: PolicySourceItem[]): string[] { if (items.length === 0) return ["System: Default"]; const lines: string[] = []; for (const [index, item] of items.entries()) { const sourcePolicy = asRecord(item.policy); const value = retentionSourceValue(field, sourcePolicy, index === 0); const locked = asRecord(sourcePolicy.allow_lower_level_limits)[field.key] === false; lines.push(`${policyPathPrefix(index)}${item.label}: ${locked ? `${value} without override` : value}`); if (locked) break; } return lines; } function retentionSourceValue(field: FieldDefinition, sourcePolicy: Record, isSystem: boolean): string { if (Object.keys(sourcePolicy).length === 0) return isSystem ? "Default" : "Inherit"; if (!isSystem && !(field.key in sourcePolicy)) return "Inherit"; if (field.kind === "raw-json") { const value = sourcePolicy.store_raw_campaign_json; return value === false ? "Disabled" : value === true ? "Stored" : "Inherit"; } if (field.kind === "audit") { const value = sourcePolicy.audit_detail_level; return value === "full" || value === "redacted" || value === "minimal" ? auditDetailLabel(value) : "Inherit"; } const value = sourcePolicy[field.key]; return typeof value === "number" ? daysLabel(value) : value === null && isSystem ? daysLabel(null) : "Inherit"; } function normalizePolicySourceItems(items: PolicySourcePathItem[]): PolicySourceItem[] { return items.map((item) => { if (typeof item === "string") return { label: item }; return { label: item.label, policy: item.policy }; }).filter((item) => item.label.trim()); } function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function policyPathPrefix(index: number): string { return index === 0 ? "" : `${" ".repeat(index - 1)}> `; } function retentionFieldValue(field: FieldDefinition, policy: PrivacyRetentionPolicy | null, loading: boolean): string { if (!policy) return loading ? "Loading..." : "Unavailable"; if (field.kind === "raw-json") return policy.store_raw_campaign_json ? "Stored" : "Disabled"; if (field.kind === "audit") return auditDetailLabel(policy.audit_detail_level); return daysLabel(policy[field.key as DayKey]); } function renderFieldControl( field: FieldDefinition, fullPolicy: PrivacyRetentionPolicy, patchPolicy: PrivacyRetentionPolicyPatch, isSystem: boolean, disabled: boolean, setRawJson: (value: RawJsonValue | "store" | "disabled") => void, setRetentionDays: (key: DayKey, value: string) => void, setAuditDetail: (value: AuditDetailValue) => void, parentPolicy: PrivacyRetentionPolicy | null ) { if (field.kind === "raw-json") { if (isSystem) { return ; } return ; } if (field.kind === "audit") { return ; } const parentDayLimit = !isSystem && parentPolicy ? parentPolicy[field.key as DayKey] : null; return ( setRetentionDays(field.key as DayKey, value)} /> ); } function RawJsonSystemSelect({ value, disabled, onChange }: { value: "store" | "disabled"; disabled: boolean; onChange: (value: "store" | "disabled") => void }) { return ( ); } function RawJsonScopedSelect({ value, parentStoresRawJson, disabled, onChange }: { value: RawJsonValue; parentStoresRawJson: boolean; disabled: boolean; onChange: (value: RawJsonValue) => void }) { return ( ); } function RetentionDaysField({ value, disabled, placeholder, max, onChange }: { value?: number | null; disabled: boolean; placeholder: string; max?: number | null; onChange: (value: string) => void }) { function handleChange(nextValue: string) { const trimmed = nextValue.trim(); if (trimmed === "" || max === null || max === undefined) { onChange(nextValue); return; } const parsed = Number(trimmed); onChange(Number.isFinite(parsed) && parsed > max ? String(max) : nextValue); } return handleChange(event.target.value)} />; } function AuditDetailSelect({ value, includeInherit, parentValue, disabled, onChange }: { value: AuditDetailValue; includeInherit: boolean; parentValue?: PrivacyRetentionPolicy["audit_detail_level"] | null; disabled: boolean; onChange: (value: AuditDetailValue) => void }) { const minimumOrder = parentValue ? auditDetailOrder[parentValue] : 0; function optionDisabled(option: PrivacyRetentionPolicy["audit_detail_level"]): boolean { return auditDetailOrder[option] < minimumOrder; } return ( ); } function rawJsonValue(value: boolean | undefined): RawJsonValue { if (value === undefined) return "inherit"; return value ? "keep" : "disable"; } function auditDetailValue(value: PrivacyRetentionPolicyPatch["audit_detail_level"]): AuditDetailValue { return value ?? "inherit"; } function daysLabel(value?: number | null): string { return value === null || value === undefined ? "Unlimited" : `${value} day${value === 1 ? "" : "s"}`; } function auditDetailLabel(value: PrivacyRetentionPolicy["audit_detail_level"]): string { if (value === "full") return "Full"; if (value === "redacted") return "Redacted"; return "Minimal"; } function localPolicyCount(policy: PrivacyRetentionPolicyPatch): string { let count = 0; for (const field of fieldDefinitions) { if (field.systemOnly) continue; if (policy[field.key as keyof PrivacyRetentionPolicyPatch] !== undefined && policy[field.key as keyof PrivacyRetentionPolicyPatch] !== null) count += 1; } const allowCount = Object.keys(policy.allow_lower_level_limits ?? {}).length; count += allowCount; return count === 0 ? "Fully inherited" : `${count} local override${count === 1 ? "" : "s"}`; } function retentionPolicySourcePath(scopeType: PrivacyRetentionPolicyScope): string[] { if (scopeType === "system") return ["System"]; if (scopeType === "tenant") return ["System", "Tenant"]; if (scopeType === "user") return ["System", "Tenant", "User"]; if (scopeType === "group") return ["System", "Tenant", "Group"]; return ["System", "Tenant", "Owner policy", "Campaign"]; } function normalizeAllowLimits(value: PrivacyRetentionLimitPermissionPatch | PrivacyRetentionLimitPermissions | null | undefined): PrivacyRetentionLimitPermissions { return { ...defaultAllowLowerLevelLimits, ...(value ?? {}) }; } function normalizeFullPolicy(policy: PrivacyRetentionPolicyPatch | Partial | null | undefined): PrivacyRetentionPolicy { return { ...defaultPrivacyPolicy, ...(policy ?? {}), raw_campaign_json_retention_days: policy?.raw_campaign_json_retention_days ?? null, generated_eml_retention_days: policy?.generated_eml_retention_days ?? null, stored_report_detail_retention_days: policy?.stored_report_detail_retention_days ?? null, mock_mailbox_retention_days: policy?.mock_mailbox_retention_days ?? null, audit_detail_retention_days: policy?.audit_detail_retention_days ?? null, allow_lower_level_limits: normalizeAllowLimits(policy?.allow_lower_level_limits) }; } function normalizePatch(policy: PrivacyRetentionPolicyPatch | null | undefined): PrivacyRetentionPolicyPatch { const normalized: PrivacyRetentionPolicyPatch = {}; if (!policy) return normalized; if (policy.store_raw_campaign_json !== undefined) normalized.store_raw_campaign_json = policy.store_raw_campaign_json; for (const field of fieldDefinitions) { if (field.kind !== "days" || field.systemOnly) continue; const value = policy[field.key as DayKey]; if (value !== undefined && value !== null) normalized[field.key as DayKey] = Math.max(0, Number(value) || 0); } if (policy.audit_detail_level) normalized.audit_detail_level = policy.audit_detail_level; if (policy.allow_lower_level_limits) normalized.allow_lower_level_limits = { ...policy.allow_lower_level_limits }; return normalized; } function normalizePatchForSave(policy: PrivacyRetentionPolicyPatch, parentPolicy: PrivacyRetentionPolicy | null, scopeType: PrivacyRetentionPolicyScope): PrivacyRetentionPolicyPatch { const normalized = normalizePatch(policy); const allowPatch = { ...(normalized.allow_lower_level_limits ?? {}) }; for (const field of fieldDefinitions) { if (field.systemOnly || scopeType === "campaign") delete allowPatch[field.key]; if (field.systemOnly) delete normalized[field.key as keyof PrivacyRetentionPolicyPatch]; if (parentPolicy && parentPolicy.allow_lower_level_limits[field.key] === false) { delete normalized[field.key as keyof PrivacyRetentionPolicyPatch]; delete allowPatch[field.key]; } } if (Object.keys(allowPatch).length > 0) normalized.allow_lower_level_limits = allowPatch; else delete normalized.allow_lower_level_limits; return normalized; } function errorMessage(err: unknown): string { if (err instanceof Error) return err.message; return "Request failed"; }