import { i18nMessage } from "../../i18n/LanguageContext"; 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 LoadingFrame from "../../components/LoadingFrame"; import { PolicyRow, PolicySection, PolicyTable } from "../../components/PolicyTable"; import type { PolicySourcePathItem } from "../../components/PolicySourcePath"; import FormField from "../../components/FormField"; import ToggleSwitch from "../../components/ToggleSwitch"; import { useUnsavedDraftGuard } from "../../components/UnsavedChangesGuard"; import { privacyRetentionLocalAllowsLowerLevel, privacyRetentionParentAllowsField } from "./policyLogic"; 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: "i18n:govoplan-core.raw_campaign_json.4ca2265b", kind: "raw-json" }, { key: "raw_campaign_json_retention_days", label: "i18n:govoplan-core.raw_campaign_json_days.48805a59", kind: "days" }, { key: "generated_eml_retention_days", label: "i18n:govoplan-core.generated_eml_days.bcb273d9", kind: "days" }, { key: "stored_report_detail_retention_days", label: "i18n:govoplan-core.stored_report_detail_days.296f6dab", kind: "days" }, { key: "mock_mailbox_retention_days", label: "i18n:govoplan-core.mock_mailbox_days.a0cf3209", kind: "days", systemOnly: true }, { key: "audit_detail_retention_days", label: "i18n:govoplan-core.audit_detail_days.21c84dd1", kind: "days" }, { key: "audit_detail_level", label: "i18n:govoplan-core.audit_detail_level.b0565260", kind: "audit" }]; export function RetentionPolicyScopeManager({ settings, scopeType, scopeId = null, targetOptions = [], targetLabel = "i18n:govoplan-core.target.61ad50a9", title = "i18n:govoplan-core.retention_policy.962fb418", 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" ? "i18n:govoplan-core.system_retention_defaults_and_the_fields_lower_l.90a9e923" : "i18n:govoplan-core.local_retention_limits_for_this_scope_values_inh.6114dade"; const targetEmptyText = i18nMessage("i18n:govoplan-core.no_value_available", { value0: targetPluralLabel(scopeType, targetLabel) }); 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 targetPluralLabel(scopeType: PrivacyRetentionPolicyScope, fallback: string): string { if (scopeType === "user") return "i18n:govoplan-core.users"; if (scopeType === "group") return "i18n:govoplan-core.groups"; if (scopeType === "campaign") return "i18n:govoplan-core.campaigns.01a23a28"; return fallback; } export function RetentionPolicyEditor({ settings, scopeType, scopeId = null, title = "i18n:govoplan-core.retention_policy.962fb418", description, canWrite, locked = false }: RetentionPolicyEditorProps) { const [policy, setPolicy] = useState({}); const [effectivePolicy, setEffectivePolicy] = useState(null); const [parentPolicy, setParentPolicy] = useState(null); const [effectivePolicySources, setEffectivePolicySources] = useState([]); const [savedPolicyKey, setSavedPolicyKey] = 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]); const policyDirty = scopeReady && Boolean(savedPolicyKey) && policyDraftKey(policy, isSystem) !== savedPolicyKey; useUnsavedDraftGuard({ dirty: policyDirty, onSave: savePolicy, onDiscard: () => { if (!savedPolicyKey) return; setPolicy(JSON.parse(savedPolicyKey) as PrivacyRetentionPolicyPatch); } }); 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); const loadedPolicy = isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy); setPolicy(loadedPolicy); setSavedPolicyKey(policyDraftKey(loadedPolicy, isSystem)); setEffectivePolicy(normalizeFullPolicy(response.effective_policy)); setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); } catch (err) { setPolicy({}); setSavedPolicyKey(""); setEffectivePolicy(null); setParentPolicy(null); setEffectivePolicySources([]); setError(errorMessage(err)); } finally { setLoading(false); } } async function savePolicy(): Promise { if (!scopeReady) return false; setBusy(true); setError(""); setSuccess(""); try { const payload = isSystem ? normalizeFullPolicy(policy) : normalizePatchForSave(policy, activeParentPolicy, scopeType); const response = await updatePrivacyRetentionPolicy(settings, scopeType, payload, scopeId); const savedPolicy = isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy); setPolicy(savedPolicy); setSavedPolicyKey(policyDraftKey(savedPolicy, isSystem)); setEffectivePolicy(normalizeFullPolicy(response.effective_policy)); setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); setSuccess("i18n:govoplan-core.retention_policy_saved.eb577758"); return true; } catch (err) { setError(errorMessage(err)); return false; } 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 privacyRetentionParentAllowsField(activeParentPolicy, key); } function localAllowsLower(key: PrivacyRetentionPolicyFieldKey): boolean { return privacyRetentionLocalAllowsLowerLevel(isSystem ? activeFullPolicy : policy, activeParentPolicy, key, isSystem); } const localPolicySummary = useMemo(() => isSystem ? "i18n:govoplan-core.system_baseline.219ce334" : localPolicyCount(policy), [isSystem, policy]); const defaultDescription = isSystem ? "i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627" : "i18n:govoplan-core.blank_values_inherit_numeric_values_may_only_sho.1a96ce70"; return ( }>

{description ?? defaultDescription}

{!isSystem &&

i18n:govoplan-core.mock_mailbox_retention_is_currently_managed_at_s.8c6a8ec7

} {error && {error}} {success && {success}} {localPolicySummary}}> {visibleFields.map((field) => { const fieldLocked = !parentAllows(field.key); const fieldDisabled = disabled || fieldLocked; return ( {fieldLocked && i18n:govoplan-core.locked_by_parent_policy.66f587c1} {field.systemOnly && i18n:govoplan-core.system_level_cleanup_scope.2bce57b5} } control={renderFieldControl(field, activeFullPolicy, policy, isSystem, fieldDisabled, setRawJson, setRetentionDays, setAuditDetail, activeParentPolicy)} effective={showEffectiveColumn ? retentionFieldValue(field, effectivePolicy, loading) : undefined} effectiveHelp={showEffectiveColumn ? retentionPolicyPathHelp(field, effectivePolicySources, scopeType) : undefined} allowControl={showAllowColumn ? setAllowLowerLevelLimit(field.key, checked)} label="i18n:govoplan-core.allow_override.ffa6e9a0" /> : undefined} />); })}
); } function policyDraftKey(policy: PrivacyRetentionPolicyPatch, isSystem: boolean): string { return JSON.stringify(isSystem ? normalizeFullPolicy(policy) : normalizePatch(policy)); } 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 ["i18n:govoplan-core.system_default.1f06f3ed"]; 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 ? "i18n:govoplan-core.default.808d7dca" : "i18n:govoplan-core.inherit.18f99833"; if (!isSystem && !(field.key in sourcePolicy)) return "i18n:govoplan-core.inherit.18f99833"; if (field.kind === "raw-json") { const value = sourcePolicy.store_raw_campaign_json; return value === false ? "i18n:govoplan-core.disabled.f4f4473d" : value === true ? "i18n:govoplan-core.stored.b2b53ee4" : "i18n:govoplan-core.inherit.18f99833"; } if (field.kind === "audit") { const value = sourcePolicy.audit_detail_level; return value === "full" || value === "redacted" || value === "minimal" ? auditDetailLabel(value) : "i18n:govoplan-core.inherit.18f99833"; } const value = sourcePolicy[field.key]; return typeof value === "number" ? daysLabel(value) : value === null && isSystem ? daysLabel(null) : "i18n:govoplan-core.inherit.18f99833"; } 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 ? "i18n:govoplan-core.loading.b04ba49f" : "i18n:govoplan-core.unavailable.2c9c1f79"; if (field.kind === "raw-json") return policy.store_raw_campaign_json ? "i18n:govoplan-core.stored.b2b53ee4" : "i18n:govoplan-core.disabled.f4f4473d"; 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 ? "i18n:govoplan-core.unlimited.b8bef37b" : `${value} day${value === 1 ? "" : "s"}`; } function auditDetailLabel(value: PrivacyRetentionPolicy["audit_detail_level"]): string { if (value === "full") return "i18n:govoplan-core.full.10b28a8c"; if (value === "redacted") return "i18n:govoplan-core.redacted.8780785a"; return "i18n:govoplan-core.minimal.a711cca9"; } 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 ? "i18n:govoplan-core.fully_inherited.5e48132a" : `${count} local override${count === 1 ? "" : "s"}`; } function retentionPolicySourcePath(scopeType: PrivacyRetentionPolicyScope): string[] { if (scopeType === "system") return ["i18n:govoplan-core.system.bc0792d8"]; if (scopeType === "tenant") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78"]; if (scopeType === "user") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.user.9f8a2389"]; if (scopeType === "group") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.group.171a0606"]; return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.owner_policy.1e8df143", "i18n:govoplan-core.campaign.69390e16"]; } 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 "i18n:govoplan-core.request_failed.9fcda32c"; }