diff --git a/public/favicon.svg b/public/favicon.svg index 6893eb1..6f40541 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/src/app/configuration/actionBuilder/ActionBuilderApi.ts b/src/app/configuration/actionBuilder/ActionBuilderApi.ts index 0366661..dd08187 100644 --- a/src/app/configuration/actionBuilder/ActionBuilderApi.ts +++ b/src/app/configuration/actionBuilder/ActionBuilderApi.ts @@ -99,7 +99,7 @@ export function submitActionPayload(payload: ActionSubmissionPayload): Promise { - return ApiClient.get(`/master-data/category-values/${categoryCode}`) + return ApiClient.get(`/master-data/${categoryCode}`) .then((res) => { if (Array.isArray(res)) { return res.map((item) => { diff --git a/src/app/configuration/actionBuilder/index.tsx b/src/app/configuration/actionBuilder/index.tsx index 90f05bb..b9ef2b8 100644 --- a/src/app/configuration/actionBuilder/index.tsx +++ b/src/app/configuration/actionBuilder/index.tsx @@ -49,7 +49,6 @@ export default function ActionBuilderPage() { const [masterLookupOptions, setMasterLookupOptions] = useState<{ label: string; value: string }[]>([ { label: 'None (Manual / Free-text)', value: '' }, - { label: 'Currencies (CURRENCIES)', value: 'CURRENCIES' }, ]); // Loading & Notification States @@ -162,7 +161,6 @@ export default function ActionBuilderPage() { })); setMasterLookupOptions([ { label: 'None (Manual / Free-text)', value: '' }, - { label: 'Currencies (CURRENCIES)', value: 'CURRENCIES' }, ...opts, ]); } diff --git a/src/app/configuration/masterData/MasterDataApi.ts b/src/app/configuration/masterData/MasterDataApi.ts index c773604..49c712a 100644 --- a/src/app/configuration/masterData/MasterDataApi.ts +++ b/src/app/configuration/masterData/MasterDataApi.ts @@ -21,11 +21,22 @@ function normalizeItems(items: any[]): MasterDataItem[] { }); } -// ─── Categories List Endpoint ─────────────────────────────────────────────── +// ─── Lookup Tables & Categories List Endpoint ───────────────────────────────── + +export function getLookupTables(): Promise { + return ApiClient.get('/master-data/lookup-tables') + .then((res) => (Array.isArray(res) ? res : [])) + .catch(() => []); +} export function getMasterCategories(): Promise { - return ApiClient.get('/master-data/categories') - .then((res) => (Array.isArray(res) ? res : [])) + return getLookupTables() + .then((tables) => { + if (Array.isArray(tables) && tables.length > 0) return tables; + return ApiClient.get('/master-data/categories') + .then((res) => (Array.isArray(res) ? res : [])) + .catch(() => []); + }) .catch(() => []); } @@ -33,13 +44,9 @@ export function getMasterCategories(): Promise { export function getCategoryValues(categoryCode: string): Promise { if (!categoryCode) return Promise.resolve([]); - return ApiClient.get(`/master-data/category-values/${categoryCode}`) + return ApiClient.get(`/master-data/${categoryCode}`) .then((res) => normalizeItems(res)) - .catch(() => - ApiClient.get(`/master-data/${categoryCode}`) - .then((res) => normalizeItems(res)) - .catch(() => []), - ); + .catch(() => []); } // ─── Generic Master Data Item CRUD ────────────────────────────────────────── diff --git a/src/app/configuration/masterData/components/MasterItemTable.tsx b/src/app/configuration/masterData/components/MasterItemTable.tsx index 4a7a236..b2d572f 100644 --- a/src/app/configuration/masterData/components/MasterItemTable.tsx +++ b/src/app/configuration/masterData/components/MasterItemTable.tsx @@ -121,11 +121,11 @@ export const MasterItemTable: React.FC = ({ - {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 + +
);