import { useState, useRef, useEffect } from 'react'; import { ArrowLeftIcon, PlusIcon, TrashIcon, MinusIcon, BookIcon, UsersIcon, GitBranchIcon, CaretDownIcon } from '@phosphor-icons/react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ApiClient } from '../../api/ApiClient'; import { CustomInput, CustomDropdown, CustomMultiSelect, CustomButton, CustomTextArea, CustomStatus, CustomRadio, CustomSwitch, CustomCheckBox, CustomLoader, Skeleton, } from '../../../components/custom'; import CustomSuccessModal from '../../../components/custom/CustomSuccessModal'; import { getJurisdictionOptions, getCohortOptions, getRuleCategoryOptions, getConditionOptions, getOperatorOptions, getActionCategoryOptions, getActionTypesByCategoryOptions, getActionTypeFields, getLookupOptions, getPolicy, createPolicy, updatePolicy, getConditionGroupsForCategory, getConditionFieldsForGroup, getFieldLookupOptions, type OptionItem, type ActionTypeField, } from '../PolicyEngineApi'; const WIDTH_GRID_MAP: Record = { full: 'col-span-12', half: 'col-span-12 md:col-span-6', third: 'col-span-12 md:col-span-4', two_thirds: 'col-span-12 md:col-span-8', }; function parseVisibilityCondition(visCond: any): { field?: string; operator?: string; value?: any } | null { if (!visCond) return null; if (typeof visCond === 'string') { try { return JSON.parse(visCond); } catch { return null; } } if (typeof visCond === 'object') { return visCond; } return null; } function isActionFieldVisible( field: ActionTypeField, fieldValues?: Record, allFields?: ActionTypeField[], fieldLookupOptionsMap?: Record ): boolean { const visCond = parseVisibilityCondition( field.visibilityConditionJson || (field as any).visibility_condition_json ); if (!visCond || !visCond.field) return true; const targetFieldCode = visCond.field; const operator = visCond.operator || 'equals'; const expectedValue = visCond.value; const targetField = allFields?.find((f) => f.fieldCode === targetFieldCode); const rawActual = fieldValues?.[targetFieldCode] !== undefined && fieldValues?.[targetFieldCode] !== null ? fieldValues[targetFieldCode] : targetField?.defaultValue; const actualValue = rawActual && typeof rawActual === 'object' && 'amount' in rawActual ? rawActual.amount : rawActual; const candidateValues: string[] = []; if (actualValue !== undefined && actualValue !== null) { if (Array.isArray(actualValue)) { actualValue.forEach((v) => candidateValues.push(String(v))); } else { candidateValues.push(String(actualValue)); } if (fieldLookupOptionsMap) { Object.values(fieldLookupOptionsMap).forEach((opts) => { opts.forEach((opt) => { if ( candidateValues.some( (c) => c.toLowerCase() === String(opt.value || '').toLowerCase() || c.toLowerCase() === String(opt.id || '').toLowerCase() || c.toLowerCase() === String(opt.code || '').toLowerCase() || c.toLowerCase() === String(opt.label || '').toLowerCase() ) ) { if (opt.value) candidateValues.push(String(opt.value)); if (opt.code) candidateValues.push(String(opt.code)); if (opt.label) candidateValues.push(String(opt.label)); if (opt.id) candidateValues.push(String(opt.id)); } }); }); } } const normalizeStr = (s: any) => String(s ?? '') .trim() .toLowerCase() .replace(/[\s_-]+/g, ''); const normExpected = normalizeStr(expectedValue); if (operator === 'equals') { if (typeof expectedValue === 'boolean') { return Boolean(actualValue) === expectedValue; } if (expectedValue === '' || expectedValue === undefined || expectedValue === null) { return actualValue === '' || actualValue === undefined || actualValue === null; } return candidateValues.some((c) => normalizeStr(c) === normExpected); } if (operator === 'not_equals') { if (typeof expectedValue === 'boolean') { return Boolean(actualValue) !== expectedValue; } return !candidateValues.some((c) => normalizeStr(c) === normExpected); } if (operator === 'contains') { if (typeof expectedValue === 'boolean') { return Boolean(actualValue) === expectedValue; } return candidateValues.some( (c) => normalizeStr(c).includes(normExpected) || normExpected.includes(normalizeStr(c)) ); } return true; } // ─── Types ─────────────────────────────────────────────────────────────────── type Condition = { id: string; condition: string; operator: string; value: string; logic: string; }; type Action = { id: string; actionCategoryId: string; actionTypeId: string; logic: string; fieldValues?: Record; }; type Rule = { id: string; category: string; priority: number; conditions: Condition[]; actions: Action[]; }; // ─── Constants ─────────────────────────────────────────────────────────────── const LOGIC_OPTIONS = [ { label: 'AND', value: 'AND' }, { label: 'OR', value: 'OR' }, ]; // Per-gate color scheme (exact Figma tokens): AND=blue, OR=yellow, NOT=red. // Fill = background per gate; border per gate; label/chevron text = #032D20 for all. const LOGIC_STYLES: Record = { AND: { bg: 'bg-[#F2FAFF]', text: 'text-[#032D20]', border: 'border-[#1A6597]' }, OR: { bg: 'bg-[#FFFDF2]', text: 'text-[#032D20]', border: 'border-[#977E1A]' }, NOT: { bg: 'bg-[#FFF2F2]', text: 'text-[#032D20]', border: 'border-[#971A1C]' }, }; // ─── Logic Gate Dropdown ───────────────────────────────────────────────────── // Colored variant of the logic selector — the shared CustomDropdown hardcodes a // white background and text color, so we render a dedicated colored control here. function LogicDropdown({ value, onChange, readOnly = false }: { value: string; onChange?: (v: string) => void; readOnly?: boolean }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (readOnly) return; const handleClickOutside = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [readOnly]); const style = LOGIC_STYLES[value] ?? LOGIC_STYLES.AND; const boxClass = `w-full h-[50px] px-4 rounded-[10px] border flex items-center justify-between gap-2 text-[14px] font-semibold transition-colors shadow-[0_1px_2px_0_rgba(0,0,0,0.05)] ${style.bg} ${style.text} ${style.border}`; // Fixed / non-editable gate (used by the Strategic Action Builder) — no menu. if (readOnly) { return
{value}
; } return (
{open && (
{LOGIC_OPTIONS.map(opt => ( ))}
)}
); } // ─── Component ─────────────────────────────────────────────────────────────── export default function AddPolicyEngine() { const navigate = useNavigate(); const { id: paramId } = useParams(); const [searchParams] = useSearchParams(); const policyId = paramId || searchParams.get('id') || ''; const isEditMode = !!policyId; const [loadingPolicy, setLoadingPolicy] = useState(false); const [isSaving, setIsSaving] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false); const [successTitle, setSuccessTitle] = useState(''); // Dynamic Options State (loaded directly from PolicyEngineApi) const [jurisdictionOptions, setJurisdictionOptions] = useState([]); const [cohortOptions, setCohortOptions] = useState([]); const [ruleCategoryOptions, setRuleCategoryOptions] = useState([]); const [operatorOptions, setOperatorOptions] = useState([]); const [categoryConditionMap, setCategoryConditionMap] = useState>({}); // Dynamic Action Builder State const [actionCategoryOptions, setActionCategoryOptions] = useState([]); const [actionTypesByCategoryMap, setActionTypesByCategoryMap] = useState>({}); const [loadingActionTypesMap, setLoadingActionTypesMap] = useState>({}); const [actionTypeFieldsMap, setActionTypeFieldsMap] = useState>({}); const [fieldLookupOptionsMap, setFieldLookupOptionsMap] = useState>({}); const [loadingActionFields, setLoadingActionFields] = useState>({}); useEffect(() => { getJurisdictionOptions().then(setJurisdictionOptions); getCohortOptions().then(setCohortOptions); getRuleCategoryOptions().then(setRuleCategoryOptions); getOperatorOptions().then(setOperatorOptions); getActionCategoryOptions().then(setActionCategoryOptions); }, []); const [conditionFieldsMetaMap, setConditionFieldsMetaMap] = useState>({}); const [conditionValueLookupMap, setConditionValueLookupMap] = useState>({}); const loadCategoryConditions = async (category: string) => { if (!category) return []; try { const groups = await getConditionGroupsForCategory(category); const allFields: OptionItem[] = []; const metaMap: Record = {}; for (const g of groups) { const fields = await getConditionFieldsForGroup(g.id || g.code); if (fields && fields.length > 0) { allFields.push({ label: g.name, value: `header_${g.id || g.code}`, groupHeader: true, disabled: true, }); fields.forEach((f: any) => { const val = f.id || f.code; allFields.push({ label: f.name, value: val, id: f.id, code: f.code, groupName: g.name, }); metaMap[val] = f; }); } } setCategoryConditionMap((prev) => ({ ...prev, [category]: allFields, })); setConditionFieldsMetaMap((prev) => ({ ...prev, ...metaMap, })); return allFields; } catch (err) { console.error('Failed to load condition groups/fields for category', err); try { const fallbackOpts = await getConditionOptions(category); setCategoryConditionMap((prev) => ({ ...prev, [category]: fallbackOpts, })); return fallbackOpts; } catch { return []; } } }; const handleCategoryChange = async (ruleId: string, category: string) => { handleUpdateRule(ruleId, { category }); if (category) { await loadCategoryConditions(category); } }; const handleConditionFieldSelect = async (ruleId: string, conditionId: string, selectedFieldIdOrCode: string) => { handleUpdateCondition(ruleId, conditionId, { condition: selectedFieldIdOrCode, value: '' }); if (selectedFieldIdOrCode) { try { const opts = await getFieldLookupOptions(selectedFieldIdOrCode); if (opts && opts.length > 0) { setConditionValueLookupMap((prev) => ({ ...prev, [selectedFieldIdOrCode]: opts, })); } } catch (err) { console.error('Failed to load lookup options for condition field', err); } } }; const handleActionCategoryChange = async (ruleId: string, actionId: string, selectedCategoryId: string) => { handleUpdateAction(ruleId, actionId, { actionCategoryId: selectedCategoryId, actionTypeId: '', fieldValues: {} }); if (!selectedCategoryId) return; if (!actionTypesByCategoryMap[selectedCategoryId]) { setLoadingActionTypesMap((prev) => ({ ...prev, [selectedCategoryId]: true })); try { const types = await getActionTypesByCategoryOptions(selectedCategoryId); setActionTypesByCategoryMap((prev) => ({ ...prev, [selectedCategoryId]: types })); } catch (err) { console.error('Failed to load action types', err); } finally { setLoadingActionTypesMap((prev) => ({ ...prev, [selectedCategoryId]: false })); } } }; const handleActionTypeChange = async (ruleId: string, actionId: string, selectedTypeId: string) => { handleUpdateAction(ruleId, actionId, { actionTypeId: selectedTypeId, fieldValues: {} }); if (!selectedTypeId) return; if (!actionTypeFieldsMap[selectedTypeId]) { setLoadingActionFields((prev) => ({ ...prev, [selectedTypeId]: true })); try { const fields = await getActionTypeFields(selectedTypeId); setActionTypeFieldsMap((prev) => ({ ...prev, [selectedTypeId]: fields })); // Pre-populate default values for the new action const initialFieldValues: Record = {}; fields.forEach((f) => { const defaultVal = f.defaultValue ?? (f as any).default_value ?? (f as any).defaultValueJson ?? (f as any).default_value_json; if (defaultVal !== undefined && defaultVal !== null) { initialFieldValues[f.fieldCode] = defaultVal; } }); handleUpdateAction(ruleId, actionId, { fieldValues: initialFieldValues }); // Pre-fetch lookup options for fields requiring lookupSource const sourcesToFetch = new Set(); fields.forEach((f) => { if (f.lookupSource) sourcesToFetch.add(f.lookupSource); }); for (const src of Array.from(sourcesToFetch)) { if (!fieldLookupOptionsMap[src]) { const opts = await getLookupOptions(src); setFieldLookupOptionsMap((prev) => ({ ...prev, [src]: opts })); } } } catch (err) { console.error('Failed to load action fields', err); } finally { setLoadingActionFields((prev) => ({ ...prev, [selectedTypeId]: false })); } } else { // Fields already loaded in map, just pre-populate default values const fields = actionTypeFieldsMap[selectedTypeId] || []; const initialFieldValues: Record = {}; fields.forEach((f) => { const defaultVal = f.defaultValue ?? (f as any).default_value ?? (f as any).defaultValueJson ?? (f as any).default_value_json; if (defaultVal !== undefined && defaultVal !== null && defaultVal !== '') { initialFieldValues[f.fieldCode] = defaultVal; } }); handleUpdateAction(ruleId, actionId, { fieldValues: initialFieldValues }); } }; const handleFieldValueChange = (ruleId: string, actionId: string, fieldCode: string, value: any) => { setRules((prevRules) => prevRules.map((r) => { if (r.id === ruleId) { return { ...r, actions: r.actions.map((a) => { if (a.id === actionId) { return { ...a, fieldValues: { ...(a.fieldValues || {}), [fieldCode]: value, }, }; } return a; }), }; } return r; }), ); }; // Policy Info State const [policyName, setPolicyName] = useState(''); const [jurisdiction, setJurisdiction] = useState<(string | number)[]>([]); const [status, setStatus] = useState<'Active' | 'Inactive' | 'Draft'>('Active'); const [description, setDescription] = useState(''); // Target Audience State const [audienceType, setAudienceType] = useState<'All Passengers' | 'Selected Cohorts'>('All Passengers'); const [selectedCohorts, setSelectedCohorts] = useState<(string | number)[]>([]); // Rule Engine State const [rules, setRules] = useState([ { id: crypto.randomUUID(), category: '', priority: 2, conditions: [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }], actions: [{ id: crypto.randomUUID(), actionCategoryId: '', actionTypeId: '', logic: 'AND', fieldValues: {} }] } ]); // Load existing policy data if editing useEffect(() => { if (!policyId) return; setLoadingPolicy(true); getPolicy(policyId) .then(async (data: any) => { if (!data) return; setPolicyName(data.policyName || data.name || ''); if (Array.isArray(data.jurisdictions)) { setJurisdiction( data.jurisdictions .map((j: any) => (typeof j === 'object' ? j.jurisdictionId || j.jurisdiction?.id || j.jurisdiction?.code || j.id : j)) .filter(Boolean), ); } else if (Array.isArray(data.jurisdictionIds)) { setJurisdiction(data.jurisdictionIds); } else { const singleJur = typeof data.jurisdiction === 'object' && data.jurisdiction !== null ? data.jurisdiction.code || data.jurisdiction.id : data.jurisdictionId || data.jurisdiction || ''; setJurisdiction(singleJur ? [singleJur] : []); } setStatus( data.status?.toLowerCase() === 'active' ? 'Active' : data.status?.toLowerCase() === 'draft' ? 'Draft' : 'Inactive' ); setDescription(data.description || ''); const isCohort = data.audienceType === 'COHORT' || data.audienceType === 'Selected Cohorts' || (Array.isArray(data.targetAudiences) && data.targetAudiences.length > 0); setAudienceType(isCohort ? 'Selected Cohorts' : 'All Passengers'); if (Array.isArray(data.targetAudiences)) { const cohortIds = data.targetAudiences .map((ta: any) => ta.targetId) .filter(Boolean); setSelectedCohorts(cohortIds); } if (Array.isArray(data.rules) && data.rules.length > 0) { const loadedRules: Rule[] = []; const ruleCatOpts = ruleCategoryOptions.length > 0 ? ruleCategoryOptions : await getRuleCategoryOptions(); for (const r of data.rules) { const rawCat = r.ruleCategory?.code || r.ruleCategory?.id || r.ruleCategoryId || r.category || ''; const matchedRuleCat = ruleCatOpts.find((c) => c.id === rawCat || c.code === rawCat || c.value === rawCat); const cat = matchedRuleCat ? matchedRuleCat.value : rawCat; if (cat) { await loadCategoryConditions(cat); } const loadedConditions: Condition[] = Array.isArray(r.conditions) && r.conditions.length > 0 ? r.conditions.map((c: any) => ({ id: c.id || crypto.randomUUID(), condition: c.fieldId || c.condition || '', operator: c.operatorId || c.operator || '', value: c.valueText || c.value || '', logic: c.logicalOperator || c.logic || 'AND', })) : [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }]; for (const c of loadedConditions) { if (c.condition) { try { const opts = await getFieldLookupOptions(c.condition); if (opts && opts.length > 0) { setConditionValueLookupMap((prev) => ({ ...prev, [c.condition]: opts })); } } catch (err) { console.error('Failed to load lookup options for condition', err); } } } const loadedActions: Action[] = []; if (Array.isArray(r.actions) && r.actions.length > 0) { for (const a of r.actions) { const actionTypeId = a.actionTypeId || ''; let actionCatId = a.actionCategoryId || a.actionType?.categoryId || a.actionType?.actionCategoryId || ''; if (!actionCatId && actionTypeId) { try { const typeInfo = await ApiClient.get(`/master-data/action-types/${actionTypeId}`); if (typeInfo?.categoryId) { actionCatId = typeInfo.categoryId; } } catch (err) { console.error('Failed to fetch action type category for edit mode', err); } } if (actionCatId) { const types = await getActionTypesByCategoryOptions(actionCatId); setActionTypesByCategoryMap((prev) => ({ ...prev, [actionCatId]: types })); } const fieldValues: Record = {}; if (Array.isArray(a.values)) { a.values.forEach((v: any) => { const curr = v.currencyCodeId || v.currencyCode; if (curr || (v.numberValue !== null && v.numberValue !== undefined && curr)) { fieldValues[v.fieldCode] = { amount: v.numberValue, currency: curr }; } else if (v.booleanValue !== null && v.booleanValue !== undefined) { fieldValues[v.fieldCode] = v.booleanValue; } else if (v.numberValue !== null && v.numberValue !== undefined) { fieldValues[v.fieldCode] = v.numberValue; } else if (v.selectedValueId) { fieldValues[v.fieldCode] = v.selectedValueId; } else if (v.textValue) { try { fieldValues[v.fieldCode] = JSON.parse(v.textValue); } catch { fieldValues[v.fieldCode] = v.textValue; } } }); } else if (a.fieldValues) { Object.assign(fieldValues, a.fieldValues); } if (actionTypeId) { const fields = await getActionTypeFields(actionTypeId); setActionTypeFieldsMap((prev) => ({ ...prev, [actionTypeId]: fields })); const sourcesToFetch = new Set(); fields.forEach((f) => { if (f.lookupSource) sourcesToFetch.add(f.lookupSource); }); for (const src of Array.from(sourcesToFetch)) { getLookupOptions(src).then((opts) => { setFieldLookupOptionsMap((prev) => ({ ...prev, [src]: opts })); }); } } loadedActions.push({ id: a.id || crypto.randomUUID(), actionCategoryId: actionCatId, actionTypeId, logic: a.logic || 'AND', fieldValues, }); } } else { loadedActions.push({ id: crypto.randomUUID(), actionCategoryId: '', actionTypeId: '', logic: 'AND', fieldValues: {}, }); } loadedRules.push({ id: r.id || crypto.randomUUID(), category: cat, priority: r.priority || 1, conditions: loadedConditions, actions: loadedActions, }); } setRules(loadedRules); } }) .catch((err) => { console.error('Failed to load policy for edit', err); }) .finally(() => setLoadingPolicy(false)); }, [policyId]); const isFormValid = (() => { if (!policyName.trim()) return false; if (audienceType === 'Selected Cohorts' && (!selectedCohorts || selectedCohorts.length === 0)) return false; if (!rules || rules.length === 0) return false; for (const rule of rules) { if (!rule.category) return false; if (!rule.conditions || rule.conditions.length === 0) return false; for (const cond of rule.conditions) { if (!cond.condition || !cond.operator || cond.value === '' || cond.value === undefined || cond.value === null) return false; } if (!rule.actions || rule.actions.length === 0) return false; for (const act of rule.actions) { if (!act.actionCategoryId || !act.actionTypeId) return false; const allFields = actionTypeFieldsMap[act.actionTypeId] || []; const visibleFields = allFields.filter((f) => isActionFieldVisible(f, act.fieldValues, allFields, fieldLookupOptionsMap) ); for (const field of visibleFields) { if (field.isRequired) { const val = act.fieldValues?.[field.fieldCode]; if (val === undefined || val === null || val === '') return false; if (Array.isArray(val) && val.length === 0) return false; if (typeof val === 'object' && !Array.isArray(val)) { if (val.amount === undefined || val.amount === null || val.amount === '') return false; } } } } } return true; })(); const isDraftValid = policyName.trim().length > 0; const handleSavePolicy = async (isDeploy: boolean) => { if (isDeploy && !isFormValid) { return; } if (!isDeploy && !isDraftValid) { return; } setIsSaving(true); try { const payload = { policyName: policyName.trim(), jurisdictionId: Array.isArray(jurisdiction) && jurisdiction.length > 0 ? String(jurisdiction[0]) : undefined, jurisdictionIds: Array.isArray(jurisdiction) ? jurisdiction.map(String) : [], description: description || undefined, status: isDeploy ? (status === 'Draft' ? 'active' : status.toLowerCase()) : 'draft', audienceType: audienceType === 'Selected Cohorts' ? 'COHORT' : 'ALL', targetAudiences: audienceType === 'Selected Cohorts' ? selectedCohorts.map((cohortId) => ({ targetType: 'COHORT', targetId: String(cohortId), })) : [], rules: rules.map((r, rIdx) => ({ ruleCategoryId: ruleCategoryOptions.find((cat) => cat.code === r.category || cat.value === r.category || cat.id === r.category)?.id || (r.category && r.category.includes('-') ? r.category : undefined), priority: r.priority || rIdx + 1, conditions: r.conditions .filter((c) => c.condition && c.operator) .map((c, cIdx) => ({ fieldId: c.condition, operatorId: c.operator, valueText: typeof c.value === 'object' ? JSON.stringify(c.value) : String(c.value ?? ''), logicalOperator: c.logic || 'AND', sequence: cIdx + 1, })), actions: r.actions .filter((a) => a.actionTypeId) .map((a, aIdx) => ({ actionTypeId: a.actionTypeId, sequence: aIdx + 1, values: Object.entries(a.fieldValues || {}).map(([fCode, val]) => { const fields = actionTypeFieldsMap[a.actionTypeId] || []; const fieldDef = fields.find((f) => f.fieldCode === fCode); const valObj: any = { fieldCode: fCode }; if (fieldDef?.id) { valObj.fieldDefinitionId = fieldDef.id; } const fieldType = fieldDef?.fieldType; if (fieldType === 'currency' || (val && typeof val === 'object' && ('amount' in val || 'currency' in val))) { const amt = typeof val === 'object' ? val.amount : val; const curr = typeof val === 'object' ? val.currency : undefined; valObj.numberValue = parseFloat(amt) || 0; if (curr) { const lookupOpts = fieldLookupOptionsMap[fieldDef?.lookupSource || 'currency'] || fieldLookupOptionsMap['currency'] || []; const matchedCurr = lookupOpts.find( (o) => o.value === curr || o.id === curr || o.code === curr ); valObj.currencyCodeId = matchedCurr?.id || (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(curr)) ? String(curr) : undefined); } } else if (fieldType === 'checkbox' || fieldType === 'switch' || typeof val === 'boolean') { valObj.booleanValue = !!val; } else if (fieldType === 'number' || fieldType === 'decimal' || fieldType === 'percentage') { valObj.numberValue = parseFloat(val) || 0; } else if (Array.isArray(val)) { valObj.textValue = JSON.stringify(val); } else if (fieldType === 'dropdown' || fieldType === 'select' || fieldDef?.lookupSource) { valObj.selectedValueId = String(val ?? ''); const lookupOpts = fieldLookupOptionsMap[fieldDef?.lookupSource || ''] || []; const matchedOpt = lookupOpts.find((o) => o.value === val || o.id === val || o.code === val); valObj.textValue = matchedOpt ? matchedOpt.label : String(val ?? ''); } else { valObj.textValue = String(val ?? ''); } return valObj; }), })), })), }; if (isEditMode) { await updatePolicy(policyId, payload); } else { await createPolicy(payload); } if (isEditMode) { } setSuccessTitle(isDeploy ? (isEditMode ? "Policy Updated Successfully." : "Policy Created Successfully.") : "Policy Saved as Draft."); setShowSuccessModal(true); } catch (err) { console.error('Failed to save policy', err); alert('Failed to save policy. Please check input values and try again.'); } finally { setIsSaving(false); } }; // ─── Handlers ────────────────────────────────────────────────────────────── // Returns the lowest priority (1-10) not already used by an existing rule. const findFreePriority = () => { const taken = rules.map(r => r.priority); for (let p = 1; p <= 10; p++) { if (!taken.includes(p)) return p; } return 1; // all 10 slots taken (>10 rules) — warning handles this edge case }; const handleAddRule = () => { setRules([...rules, { id: crypto.randomUUID(), category: '', priority: findFreePriority(), conditions: [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }], actions: [{ id: crypto.randomUUID(), actionCategoryId: '', actionTypeId: '', logic: 'AND', fieldValues: {} }] }]); }; const handleDeleteRule = (ruleId: string) => { setRules(rules.filter(r => r.id !== ruleId)); }; const handleUpdateRule = (ruleId: string, updates: Partial) => { setRules(rules.map(r => r.id === ruleId ? { ...r, ...updates } : r)); }; // Priorities used by every OTHER rule — a rule may never land on one of these. const priorityTakenByOthers = (rule: Rule) => rules.filter(r => r.id !== rule.id).map(r => r.priority); // Next free priority below the current value (down to 1), or null if none. const getPrevPriority = (rule: Rule): number | null => { const taken = priorityTakenByOthers(rule); for (let p = rule.priority - 1; p >= 1; p--) { if (!taken.includes(p)) return p; } return null; }; // Next free priority above the current value (up to 10), or null if none. const getNextPriority = (rule: Rule): number | null => { const taken = priorityTakenByOthers(rule); for (let p = rule.priority + 1; p <= 10; p++) { if (!taken.includes(p)) return p; } return null; }; const handleAddCondition = (ruleId: string) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, conditions: [...r.conditions, { id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }] }; } return r; })); }; const handleUpdateCondition = (ruleId: string, conditionId: string, updates: Partial) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, conditions: r.conditions.map(c => c.id === conditionId ? { ...c, ...updates } : c) }; } return r; })); }; const handleDeleteCondition = (ruleId: string, conditionId: string) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, conditions: r.conditions.filter(c => c.id !== conditionId) }; } return r; })); }; const handleAddAction = (ruleId: string) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, actions: [...r.actions, { id: crypto.randomUUID(), actionCategoryId: '', actionTypeId: '', logic: 'AND', fieldValues: {} }] }; } return r; })); }; const handleUpdateAction = (ruleId: string, actionId: string, updates: Partial) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, actions: r.actions.map(a => a.id === actionId ? { ...a, ...updates } : a) }; } return r; })); }; const handleDeleteAction = (ruleId: string, actionId: string) => { setRules(rules.map(r => { if (r.id === ruleId) { return { ...r, actions: r.actions.filter(a => a.id !== actionId) }; } return r; })); }; // ─── Render Helpers ──────────────────────────────────────────────────────── const CardHeader = ({ icon: Icon, title }: { icon: any, title: string }) => (

{title}

); if (loadingPolicy) { return (
{/* ─── Header Skeleton ────────────────────────────────────────────── */}
{/* ─── Body Content Skeleton ──────────────────────────────────────── */}
{/* Policy Information Card */}
{/* Target Audience Card */}
{/* Rule Engine Card */}
); } return (
{/* ─── Header ────────────────────────────────────────────────────── */}

{isEditMode ? 'Edit Policy Engine' : 'Deploy New Policy'}

Global Framework Registry
{/* ─── Body Content ──────────────────────────────────────────────── */}
{/* Policy Information Card */}
setPolicyName(e.target.value)} placeholder="Selected Option" className="!h-11" />
setStatus('Active')} /> setStatus('Inactive')} />
setDescription(e.target.value)} placeholder="Enter description..." className="!h-24 resize-none" />
{/* Target Audience Card */}
setAudienceType('All Passengers')} /> setAudienceType('Selected Cohorts')} />
{audienceType === 'Selected Cohorts' && (
)}
{/* Rule Engine Card */}
} className="!border-[#1E7D5C] !text-[#1E7D5C] hover:!bg-emerald-50/50 font-semibold" > Add Strategic Rule
{rules.map((rule, ruleIndex) => (
{/* Rule Header */}

Rule {ruleIndex + 1}

{rules.length > 1 && ( )}
{/* Rule Settings */}
handleCategoryChange(rule.id, val)} placeholder="Selected Option" />
{(() => { const prevPriority = getPrevPriority(rule); const nextPriority = getNextPriority(rule); return (
{rule.priority}
); })()} {rules.some(r => r.id !== rule.id && r.priority === rule.priority) && ( Priority already exists )}
{/* Visual Condition Builder */}

Visual Condition Builder

{rule.conditions.map((condition, cIdx) => { const fieldMeta = conditionFieldsMetaMap[condition.condition]; const lookupOpts = conditionValueLookupMap[condition.condition] || []; const isLookup = (fieldMeta && (fieldMeta.lookupTable || fieldMeta.dataType === 'ENUM')) || lookupOpts.length > 0; const isBoolean = fieldMeta?.dataType === 'BOOLEAN'; const isNumber = fieldMeta?.dataType === 'NUMBER'; return (
handleConditionFieldSelect(rule.id, condition.id, val)} placeholder={rule.category ? "Select Condition..." : "Select Rule Category first"} disabled={!rule.category} />
handleUpdateCondition(rule.id, condition.id, { operator: val })} placeholder="Selected Option" />
{isLookup ? ( handleUpdateCondition(rule.id, condition.id, { value: val })} placeholder="Select Value..." /> ) : isBoolean ? ( handleUpdateCondition(rule.id, condition.id, { value: val })} placeholder="Select..." /> ) : ( handleUpdateCondition(rule.id, condition.id, { value: e.target.value })} placeholder="Enter Value..." /> )}
{cIdx < rule.conditions.length - 1 ? (
handleUpdateCondition(rule.id, condition.id, { logic: val })} />
) : (
} onClick={() => handleAddCondition(rule.id)} > Add Condition
)}
{rule.conditions.length > 1 && ( )}
) })}
{/* Strategic Action Builder */}

Strategic Action Builder

{rule.actions.map((action, aIdx) => (
{/* Action Category & Action Type Selectors */}
handleActionCategoryChange(rule.id, action.id, val)} placeholder="Select Category..." />
handleActionTypeChange(rule.id, action.id, val)} placeholder={action.actionCategoryId ? "Select Action Type..." : "Select Category first"} disabled={!action.actionCategoryId || loadingActionTypesMap[action.actionCategoryId]} />
{aIdx < rule.actions.length - 1 ? (
) : (
} onClick={() => handleAddAction(rule.id)} > Add
)}
{rule.actions.length > 1 && ( )}
{/* Dynamic Action Fields Container */} {action.actionTypeId && (
{loadingActionFields[action.actionTypeId] ? (
) : (() => { const allFields = actionTypeFieldsMap[action.actionTypeId] || []; const visibleFields = allFields.filter((f) => isActionFieldVisible(f, action.fieldValues, allFields, fieldLookupOptionsMap) ); if (allFields.length === 0) { return

No dynamic fields configured for this Action Type.

; } if (visibleFields.length === 0) { return null; } // Group visible fields by section / group name const groupedSectionsMap = new Map(); visibleFields.forEach((field) => { const secName = field.section?.trim() || ''; const fields = groupedSectionsMap.get(secName) || []; fields.push(field); groupedSectionsMap.set(secName, fields); }); return (
{Array.from(groupedSectionsMap.entries()).map(([secName, secFields]) => (
{secName && (
{secName}
)}
{secFields.map((field) => { const widthClass = WIDTH_GRID_MAP[field.width || 'full'] || 'col-span-12'; const rawVal = action.fieldValues?.[field.fieldCode]; const defaultVal = field.defaultValue ?? (field as any).default_value ?? (field as any).defaultValueJson ?? (field as any).default_value_json; const effectiveVal = rawVal !== undefined && rawVal !== null ? rawVal : defaultVal; const lookupOpts = fieldLookupOptionsMap[field.lookupSource || ''] || []; const helpText = field.helpText || (field as any).help_text; const valRules = field.validationJson || (field as any).validation_json; const getBoolVal = (v: any) => { if (typeof v === 'boolean') return v; if (typeof v === 'string') { return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes'; } return !!v; }; const getMultiVal = (v: any): string[] => { if (Array.isArray(v)) return v; if (typeof v === 'string') { if (v.trim().startsWith('[')) { try { const p = JSON.parse(v); if (Array.isArray(p)) return p; } catch { } } if (v.includes(',')) return v.split(',').map((s) => s.trim()); return v.trim() ? [v.trim()] : []; } return []; }; const getCurrencyVal = (v: any) => { let currObj = v; if (typeof currObj === 'string' && currObj.trim().startsWith('{')) { try { currObj = JSON.parse(currObj); } catch { } } const amt = typeof currObj === 'object' && currObj !== null && 'amount' in currObj ? currObj.amount : typeof currObj === 'object' && currObj !== null ? '' : currObj ?? ''; const curr = typeof currObj === 'object' && currObj !== null && 'currency' in currObj ? currObj.currency : undefined; return { amount: amt, currency: curr }; }; return (
{field.fieldType === 'textarea' ? ( handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)} placeholder={field.placeholder || 'Enter details...'} /> ) : field.fieldType === 'currency' ? (() => { const currParsed = getCurrencyVal(effectiveVal); return (
handleFieldValueChange(rule.id, action.id, field.fieldCode, { ...(currParsed || {}), amount: e.target.value, }) } placeholder={field.placeholder || 'Amount'} />
handleFieldValueChange(rule.id, action.id, field.fieldCode, { ...(currParsed || {}), currency: val, }) } />
); })() : field.fieldType === 'dropdown' ? ( handleFieldValueChange(rule.id, action.id, field.fieldCode, val)} placeholder={field.placeholder || 'Select option...'} /> ) : field.fieldType === 'multi_select' ? ( handleFieldValueChange(rule.id, action.id, field.fieldCode, vals)} placeholder={field.placeholder || 'Select multiple options...'} /> ) : field.fieldType === 'checkbox' ? ( handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.checked)} label={field.fieldName} /> ) : field.fieldType === 'switch' ? (
handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.checked)} /> {getBoolVal(effectiveVal) ? 'Enabled' : 'Disabled'}
) : ( handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)} placeholder={field.placeholder || 'Enter value...'} min={ field.fieldType === 'date' ? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0] : field.fieldType === 'datetime' ? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16) : undefined } /> )} {helpText && (

{helpText}

)}
); })}
))}
); })()}
)}
))}
))}
{/* ─── Sticky Footer ─────────────────────────────────────────────── */}
Status:
handleSavePolicy(false)} disabled={isSaving || !isDraftValid} > {isSaving ? 'Saving...' : 'Save Draft'} navigate('/policy-engine')} disabled={isSaving} > Cancel Policy handleSavePolicy(true)} disabled={isSaving || !isFormValid} > {isSaving ? 'Deploying...' : isEditMode ? 'Update & Deploy Policy' : 'Deploy Policy'}
{ setShowSuccessModal(false); navigate('/policy-engine'); }} title={successTitle} label="POLICY NAME" cohortName={policyName} cohortStatus={status} cohortDescription={description} />
); }