= ({
|
- {item.value || item.label || item.name}
+ {item.label || item.value}
|
- {item.code || '—'}
+ {item.value || '—'}
|
@@ -182,11 +182,10 @@ export const MasterItemTable: React.FC = ({
diff --git a/src/app/policyEngine/PolicyEngineApi.ts b/src/app/policyEngine/PolicyEngineApi.ts
index 6dcb2fe..9a10c5b 100644
--- a/src/app/policyEngine/PolicyEngineApi.ts
+++ b/src/app/policyEngine/PolicyEngineApi.ts
@@ -45,13 +45,9 @@ function normalizeItems(items: any[]): any[] {
function fetchCategoryValues(categoryCodeOrId: string): Promise {
if (!categoryCodeOrId) return Promise.resolve([]);
- return ApiClient.get(`/master-data/category-values/${categoryCodeOrId}`)
+ return ApiClient.get(`/master-data/${categoryCodeOrId}`)
.then((res) => normalizeItems(res))
- .catch(() =>
- ApiClient.get(`/master-data/${categoryCodeOrId}`)
- .then((res) => normalizeItems(res))
- .catch(() => []),
- );
+ .catch(() => []);
}
function fetchMastersCategories(): Promise {
@@ -187,17 +183,18 @@ export function getConditionOptions(categoryCodeOrId?: string): Promise {
- return fetchCategoryValues('operator')
- .then((items) =>
- (items || [])
+ return ApiClient.get('/master-data/operators')
+ .then((res) => {
+ const items = Array.isArray(res) ? res : [];
+ return items
.filter((i) => i.isActive !== false)
.map((i) => ({
- label: i.label || i.name || i.value || '',
+ label: i.name || i.symbol || i.label || i.code || '',
value: i.id || i.code || i.value || '',
id: i.id,
code: i.code,
- })),
- )
+ }));
+ })
.catch(() => []);
}
@@ -294,3 +291,34 @@ export function updatePolicyStatus(id: string, status: string): Promise {
return ApiClient.delete(`/policy-engine/${id}`);
}
+
+// ─── 3-Level Metadata API Functions ──────────────────────────────────────────
+
+export function getConditionGroupsForCategory(categoryCodeOrId: string): Promise {
+ if (!categoryCodeOrId) return Promise.resolve([]);
+ return ApiClient.get(`/master-data/rule-categories/${categoryCodeOrId}/condition-groups`)
+ .then((res) => (Array.isArray(res) ? res : []))
+ .catch(() => []);
+}
+
+export function getConditionFieldsForGroup(groupIdOrCode: string): Promise {
+ if (!groupIdOrCode) return Promise.resolve([]);
+ return ApiClient.get(`/master-data/condition-groups/${groupIdOrCode}/fields`)
+ .then((res) => (Array.isArray(res) ? res : []))
+ .catch(() => []);
+}
+
+export function getFieldLookupOptions(fieldIdOrCode: string): Promise {
+ if (!fieldIdOrCode) return Promise.resolve([]);
+ return ApiClient.get(`/master-data/condition-fields/${fieldIdOrCode}/lookup-values`)
+ .then((res) =>
+ (res || []).map((i) => ({
+ label: i.label || i.name || i.value || '',
+ value: i.id || i.code || i.value || '',
+ id: i.id,
+ code: i.code,
+ })),
+ )
+ .catch(() => []);
+}
+
diff --git a/src/app/policyEngine/PolicyEngineTypes.ts b/src/app/policyEngine/PolicyEngineTypes.ts
index 5e4bce7..e29c4be 100644
--- a/src/app/policyEngine/PolicyEngineTypes.ts
+++ b/src/app/policyEngine/PolicyEngineTypes.ts
@@ -12,3 +12,31 @@ export interface PaginatedPolicyEngineResponse {
totalPages: number;
page: number;
}
+
+export interface ConditionGroupItem {
+ id: string;
+ code: string;
+ name: string;
+ displayOrder?: number;
+ isActive?: boolean;
+}
+
+export interface ConditionFieldItem {
+ id: string;
+ groupId: string;
+ code: string;
+ name: string;
+ lookupTable?: string;
+ dataType: 'STRING' | 'NUMBER' | 'BOOLEAN' | 'ENUM' | 'DATE';
+ operatorType: 'COMPARISON' | 'TEXT' | 'SET' | 'BOOLEAN';
+ displayOrder?: number;
+ isActive?: boolean;
+}
+
+export interface LookupOptionItem {
+ id: string;
+ code: string;
+ label: string;
+ value: string;
+ displayOrder?: number;
+}
diff --git a/src/app/policyEngine/components/AddPolicyEngine.tsx b/src/app/policyEngine/components/AddPolicyEngine.tsx
index e7a378d..9711d29 100644
--- a/src/app/policyEngine/components/AddPolicyEngine.tsx
+++ b/src/app/policyEngine/components/AddPolicyEngine.tsx
@@ -30,6 +30,9 @@ import {
getPolicy,
createPolicy,
updatePolicy,
+ getConditionGroupsForCategory,
+ getConditionFieldsForGroup,
+ getFieldLookupOptions,
type OptionItem,
type ActionTypeField,
} from '../PolicyEngineApi';
@@ -75,7 +78,6 @@ type Rule = {
const LOGIC_OPTIONS = [
{ label: 'AND', value: 'AND' },
{ label: 'OR', value: 'OR' },
- { label: 'NOT', value: 'NOT' },
];
// Per-gate color scheme (exact Figma tokens): AND=blue, OR=yellow, NOT=red.
@@ -175,15 +177,76 @@ export default function AddPolicyEngine() {
getActionCategoryOptions().then(setActionCategoryOptions);
}, []);
- const handleCategoryChange = (ruleId: string, category: string) => {
- handleUpdateRule(ruleId, { category });
- if (category && !categoryConditionMap[category]) {
- getConditionOptions(category).then((opts) => {
+ 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);
+ fields.forEach((f: any) => {
+ const val = f.id || f.code;
+ allFields.push({
+ label: `${g.name} › ${f.name}`,
+ value: val,
+ id: f.id,
+ code: f.code,
+ });
+ 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]: opts,
+ [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);
+ }
}
};
@@ -324,22 +387,33 @@ export default function AddPolicyEngine() {
const cat = matchedRuleCat ? matchedRuleCat.value : rawCat;
if (cat) {
- getConditionOptions(cat).then((opts) => {
- setCategoryConditionMap((prev) => ({ ...prev, [cat]: opts }));
- });
+ 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: 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) {
@@ -454,9 +528,9 @@ export default function AddPolicyEngine() {
targetAudiences:
audienceType === 'Selected Cohorts'
? selectedCohorts.map((cohortId) => ({
- targetType: 'COHORT',
- targetId: String(cohortId),
- }))
+ targetType: 'COHORT',
+ targetId: String(cohortId),
+ }))
: [],
rules: rules.map((r, rIdx) => ({
ruleCategoryId:
@@ -862,69 +936,99 @@ export default function AddPolicyEngine() {
Visual Condition Builder
- {rule.conditions.map((condition, cIdx) => (
-
-
- handleUpdateCondition(rule.id, condition.id, { condition: val })}
- placeholder={rule.category ? "Select Condition..." : "Select Rule Category first"}
- disabled={!rule.category}
- />
-
-
- handleUpdateCondition(rule.id, condition.id, { operator: val })}
- placeholder="Selected Option"
- />
-
-
- handleUpdateCondition(rule.id, condition.id, { value: e.target.value })}
- placeholder="Selected Option"
- />
-
- {cIdx < rule.conditions.length - 1 ? (
-
-
- handleUpdateCondition(rule.id, condition.id, { logic: val })}
+ {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}
/>
- ) : (
-
- }
- onClick={() => handleAddCondition(rule.id)}
- >
-
- Add Condition
-
-
+
+ handleUpdateCondition(rule.id, condition.id, { operator: val })}
+ placeholder="Selected Option"
+ />
- )}
-
- {rule.conditions.length > 1 && (
-
+
+ {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 && (
+
+ )}
+
-
- ))}
+ )
+ })}
diff --git a/src/app/recoveryIncidents/RecoveryIncidentsApi.ts b/src/app/recoveryIncidents/RecoveryIncidentsApi.ts
index 9f064ee..0d827fd 100644
--- a/src/app/recoveryIncidents/RecoveryIncidentsApi.ts
+++ b/src/app/recoveryIncidents/RecoveryIncidentsApi.ts
@@ -61,6 +61,10 @@ export function updateRecoveryIncident(id: string, data: Partial (`/recovery-incidents/${id}`, data);
}
+export function updateIncidentStatus(id: string, status: string): Promise {
+ return ApiClient.patch(`/recovery-incidents/${id}/status`, { status });
+}
+
export function deleteRecoveryIncident(id: string): Promise {
return ApiClient.delete(`/recovery-incidents/${id}`);
}
diff --git a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
index 40e3d1d..14c9e1d 100644
--- a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
+++ b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
@@ -180,11 +180,10 @@ export default function RecoveryIncidentsList() {
const handleStatusChange = async (incident: RecoveryIncident, text: string) => {
try {
- const { updateRecoveryIncident } = await import('../RecoveryIncidentsApi');
- await updateRecoveryIncident(incident.id, {
- status: text
- });
+ const { updateIncidentStatus, getRecoveryMetrics } = await import('../RecoveryIncidentsApi');
+ await updateIncidentStatus(incident.id, text);
fetchIncidents();
+ getRecoveryMetrics().then((data) => setMetrics(data)).catch(() => {});
} catch (error) {
console.error("Failed to update status", error);
}
diff --git a/src/app/recoveryIncidents/tabs/RecoveryPlanTab.tsx b/src/app/recoveryIncidents/tabs/RecoveryPlanTab.tsx
index afdab3c..4522487 100644
--- a/src/app/recoveryIncidents/tabs/RecoveryPlanTab.tsx
+++ b/src/app/recoveryIncidents/tabs/RecoveryPlanTab.tsx
@@ -9,7 +9,7 @@ export default function RecoveryPlanTab() {
FINANCIAL REFUND
-
+
REFUND AMOUNT
@@ -32,7 +32,7 @@ export default function RecoveryPlanTab() {
COMPENSATION & PERKS
-
+
CASH COMPENSATION
@@ -55,7 +55,7 @@ export default function RecoveryPlanTab() {
PASSENGER CARE
-
+
MEAL VOUCHERS
diff --git a/src/app/recoveryIncidents/tabs/index.tsx b/src/app/recoveryIncidents/tabs/index.tsx
index 2c627cd..b24fb9c 100644
--- a/src/app/recoveryIncidents/tabs/index.tsx
+++ b/src/app/recoveryIncidents/tabs/index.tsx
@@ -1,37 +1,62 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
-import {User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
+import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom';
import SummaryTab from './SummaryTab';
import CaseDetailsTab from './CaseDetailsTab';
import RecoveryPlanTab from './RecoveryPlanTab';
import AuditTrailTab from './AuditTrailTab';
import { SparkleIcon } from 'lucide-react';
-import { getRecoveryIncident } from '../RecoveryIncidentsApi';
+import { getRecoveryIncident, updateIncidentStatus } from '../RecoveryIncidentsApi';
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
+function getStatusVariant(status?: string): "success" | "error" | "warning" | "info" | "neutral" {
+ if (!status) return "neutral";
+ const s = status.toLowerCase();
+ if (s.includes("appr") || s.includes("active") || s.includes("success")) return "success";
+ if (s.includes("reject") || s.includes("denied") || s.includes("error")) return "error";
+ if (s.includes("pend") || s.includes("review") || s.includes("warn")) return "warning";
+ if (s.includes("new") || s.includes("info")) return "info";
+ return "neutral";
+}
+
export default function RecoveryIncidentTabs() {
const { id } = useParams();
const [activeTab, setActiveTab] = useState('Summary');
const [incident, setIncident] = useState (null);
const [loading, setLoading] = useState(true);
+ const [updating, setUpdating] = useState(false);
+
+ const fetchIncident = async () => {
+ if (!id) return;
+ setLoading(true);
+ try {
+ const data = await getRecoveryIncident(id);
+ setIncident(data);
+ } catch (err) {
+ console.error("Failed to load incident:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
useEffect(() => {
- if (id) {
- setLoading(true);
- getRecoveryIncident(id)
- .then(data => {
- setIncident(data);
- })
- .catch(err => {
- console.error("Failed to load incident:", err);
- })
- .finally(() => {
- setLoading(false);
- });
- }
+ fetchIncident();
}, [id]);
+ const handleStatusChange = async (newStatus: string) => {
+ if (!id || updating) return;
+ setUpdating(true);
+ try {
+ const updated = await updateIncidentStatus(id, newStatus);
+ setIncident(updated);
+ } catch (err) {
+ console.error("Failed to update incident status:", err);
+ } finally {
+ setUpdating(false);
+ }
+ };
+
const tabItems = [
{
id: 'Summary',
@@ -59,6 +84,8 @@ export default function RecoveryIncidentTabs() {
return Loading incident... ;
}
+ const currentStatus = incident?.status || "Pending";
+
return (
{/* Header */}
@@ -67,8 +94,11 @@ export default function RecoveryIncidentTabs() {
{incident?.recoveryCode || id}
-
-
+
{incident?.passengerName || 'Unknown'}
@@ -87,14 +117,14 @@ export default function RecoveryIncidentTabs() {
{/* Tabs Row */}
-
+
-
+
Share with Finance
@@ -109,94 +139,100 @@ export default function RecoveryIncidentTabs() {
{/* Left Column - Tab Content */}
-
+
{/* Right Column - Sidebar */}
-
- {/* Blur Overlay */}
-
- "Coming soon"
-
- {/* Sidebar Content */}
-
-
-
- AI RECOMMENDATION
-
-
-
-
- SATISFACTION PREDICT
- 84%
-
-
-
+ {/* Blur Overlay */}
+
+ "Coming soon"
+
-
-
- ESCALATION RISK
- 12%
-
-
-
+ {/* Sidebar Content */}
+
+
+
+ AI RECOMMENDATION
+
-
- NEXT RECOMMENDED ACTION
-
- Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.
-
-
- }
- className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
- >
- EXECUTE RECOMMENDATION
-
-
-
+
+
+ SATISFACTION PREDICT
+ 84%
+
+
+
+
+
+
+ ESCALATION RISK
+ 12%
+
+
+
+
+
+ NEXT RECOMMENDED ACTION
+
+ Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.
+
+
+ }
+ className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
+ >
+ EXECUTE RECOMMENDATION
+
+
+
{/* Bottom Sticky Action Bar */}
- {/* Spacer */}
-
- }
- className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100"
- >
- REJECT RECOVERY
-
- }
- className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A]"
- >
- MARK FOR REVIEW
-
- }
- className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E]"
- >
- APPROVE RECOVERY
-
-
+ {/* Spacer */}
+
+ handleStatusChange("Rejected")}
+ leftIcon={}
+ className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100 disabled:opacity-50"
+ >
+ REJECT RECOVERY
+
+ handleStatusChange("Under Review")}
+ leftIcon={}
+ className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A] disabled:opacity-50"
+ >
+ MARK FOR REVIEW
+
+ handleStatusChange("Approved")}
+ leftIcon={}
+ className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E] disabled:opacity-50"
+ >
+ APPROVE RECOVERY
+
+
);
|