From 1c562adf29ce37ce6af098f1e01112ca94f22094 Mon Sep 17 00:00:00 2001 From: Syed Waseem Date: Wed, 19 Aug 2026 16:32:47 +0530 Subject: [PATCH] feat: implement recovery incident management and simulation modules with supporting types and UI components --- src/AppRoutes.tsx | 29 +- .../RecoveryIncidentsTypes.ts | 1 + .../components/AddRecoveryIncidents.tsx | 1 + .../components/RecoveryIncidentsList.tsx | 2 +- .../disruptionMockService.ts | 2 +- src/app/recoveryIncidents/tabs/SummaryTab.tsx | 2 +- src/app/simulation/SimulationApi.ts | 11 + src/app/simulation/SimulationTypes.ts | 30 + .../components/SimulationTerminal.tsx | 730 ++++++++++++++++++ src/app/simulation/index.tsx | 5 + src/layout/AppHeader.tsx | 1 + 11 files changed, 793 insertions(+), 21 deletions(-) create mode 100644 src/app/simulation/SimulationApi.ts create mode 100644 src/app/simulation/SimulationTypes.ts create mode 100644 src/app/simulation/components/SimulationTerminal.tsx create mode 100644 src/app/simulation/index.tsx diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index dd48103..32da98b 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -2,24 +2,16 @@ import { lazy, Suspense } from "react"; import { Route, Routes, Navigate } from "react-router-dom"; import Layout from "./layout/AppLayout"; -const HomePage = lazy(() => import("./app/dashboard")); -const CohortManage = lazy(() => import("./app/cohartManage")); -const PolicyEngineList = lazy( - () => import("./app/policyEngine/components/PolicyEngineList"), -); -const AddPolicyEngine = lazy( - () => import("./app/policyEngine/components/AddPolicyEngine"), -); -const RecoveryIncidentsList = lazy( - () => import("./app/recoveryIncidents/components/RecoveryIncidentsList"), -); -const RecoveryIncidentTabs = lazy( - () => import("./app/recoveryIncidents/tabs/index"), -); -const AuditLogsList = lazy( - () => import("./app/auditLogs/components/AuditLogsList"), -); -const ConfigurationPage = lazy(() => import("./app/configuration")); + +const HomePage = lazy(() => import('./app/dashboard')) +const CohortManage = lazy(() => import('./app/cohartManage')) +const PolicyEngineList = lazy(() => import('./app/policyEngine/components/PolicyEngineList')) +const AddPolicyEngine = lazy(() => import('./app/policyEngine/components/AddPolicyEngine')) +const RecoveryIncidentsList = lazy(() => import('./app/recoveryIncidents/components/RecoveryIncidentsList')) +const RecoveryIncidentTabs = lazy(() => import('./app/recoveryIncidents/tabs/index')) +const AuditLogsList = lazy(() => import('./app/auditLogs/components/AuditLogsList')) +const ConfigurationPage = lazy(() => import('./app/configuration')) +const SimulationPage = lazy(() => import('./app/simulation')) function AppRoutes() { return ( @@ -27,6 +19,7 @@ function AppRoutes() { } /> + } /> } /> } /> } /> diff --git a/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts b/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts index 0c4255d..02a38a9 100644 --- a/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts +++ b/src/app/recoveryIncidents/RecoveryIncidentsTypes.ts @@ -57,6 +57,7 @@ export interface RecoveryIncident { status?: string; value: string; isPerksClaimed?: boolean; + recoverySource?: string; isGroupHeader?: boolean; evaluation?: IncidentEvaluation; } diff --git a/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx b/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx index 93c65d9..55f1a47 100644 --- a/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx +++ b/src/app/recoveryIncidents/components/AddRecoveryIncidents.tsx @@ -336,6 +336,7 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR jurisdiction: formData.jurisdiction || undefined, delayDuration: formData.delayDuration ? Number(formData.delayDuration) : undefined, scenario: formData.scenario || undefined, + recoverySource: existingInc ? (existingInc.recoverySource || 'Manual Disruption Entry') : 'Manual Disruption Entry', }; if (existingInc) { diff --git a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx index 1c61399..4607cf6 100644 --- a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx +++ b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx @@ -102,7 +102,7 @@ export default function RecoveryIncidentsList() { // Filters const [search, setSearch] = useState(""); - const [isGrouped, setIsGrouped] = useState(true); + const [isGrouped, setIsGrouped] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const [isModalOpen, setIsModalOpen] = useState(false); const [editingIncident, setEditingIncident] = useState(null); diff --git a/src/app/recoveryIncidents/disruptionMockService.ts b/src/app/recoveryIncidents/disruptionMockService.ts index f67429b..7e9215a 100644 --- a/src/app/recoveryIncidents/disruptionMockService.ts +++ b/src/app/recoveryIncidents/disruptionMockService.ts @@ -328,7 +328,7 @@ export function getMockFlightNumbers(): { label: string; value: string; descript export function getMockDisruptionByFlightNumber(query: string): MockDisruption | undefined { if (!query) return undefined; const upper = query.trim().toUpperCase(); - + // 1. Direct flight number match const flightKey = Object.keys(MOCK_DISRUPTIONS).find((k) => k.toUpperCase() === upper); if (flightKey) return MOCK_DISRUPTIONS[flightKey]; diff --git a/src/app/recoveryIncidents/tabs/SummaryTab.tsx b/src/app/recoveryIncidents/tabs/SummaryTab.tsx index 8664d26..1a90d20 100644 --- a/src/app/recoveryIncidents/tabs/SummaryTab.tsx +++ b/src/app/recoveryIncidents/tabs/SummaryTab.tsx @@ -47,7 +47,7 @@ export default function SummaryTab({ incident }: SummaryTabProps) { Recovery Source - Policy Evaluation Engine + {incident?.recoverySource || 'Policy Evaluation Engine'} diff --git a/src/app/simulation/SimulationApi.ts b/src/app/simulation/SimulationApi.ts new file mode 100644 index 0000000..ed4a380 --- /dev/null +++ b/src/app/simulation/SimulationApi.ts @@ -0,0 +1,11 @@ +import { ApiClient } from '../api/ApiClient'; + +// ─── Evaluation Engine & Simulation API ────────────────────────────────────────── + +export function evaluateIncidentPolicy(input: any): Promise { + return ApiClient.post('/policy-engine/evaluate', input); +} + +export function evaluateBatchSimulation(inputs: any[]): Promise { + return ApiClient.post('/policy-engine/evaluate-batch', inputs); +} diff --git a/src/app/simulation/SimulationTypes.ts b/src/app/simulation/SimulationTypes.ts new file mode 100644 index 0000000..eb4ece4 --- /dev/null +++ b/src/app/simulation/SimulationTypes.ts @@ -0,0 +1,30 @@ +export interface SimulatedPassenger { + id: string; + name: string; + pnr: string; + tier: string; + eligibility: 'ELIGIBLE' | 'INELIGIBLE' | 'REVIEW'; + refund: string; + comp: string; + statusBadges: string[]; + cabinClass?: string; + originalCabin?: string; + actualCabin?: string; + passengerType?: string; + nationality?: string; + specialAssistance?: string; +} + +export interface SimulationFormState { + category: 'Flight' | 'Travel' | 'Ancillary'; + scenario: string; + scenarioSubType: string; + flightNumber: string; + airline: string; + origin: string; + destination: string; + flightDistance: string; + arrDelay: string; + depDelay: string; + jurisdiction: string; +} diff --git a/src/app/simulation/components/SimulationTerminal.tsx b/src/app/simulation/components/SimulationTerminal.tsx new file mode 100644 index 0000000..ca80447 --- /dev/null +++ b/src/app/simulation/components/SimulationTerminal.tsx @@ -0,0 +1,730 @@ +import { useState, useMemo, useEffect } from 'react'; +import { + MagnifyingGlassIcon, + FloppyDiskIcon, + CaretLeftIcon, + CaretRightIcon, + CheckCircleIcon, + AirplaneTiltIcon, +} from '@phosphor-icons/react'; +import { useNavigate } from 'react-router-dom'; +import { + CustomInput, + CustomDropdown, + CustomButton, + CustomTabs, +} from '../../../components/custom'; +import type { TabItem } from '../../../components/custom/CustomTabs'; +import type { Option } from '../../../components/custom/CustomDropdown'; +import type { SimulatedPassenger, SimulationFormState } from '../SimulationTypes'; +import { createRecoveryIncident } from '../../recoveryIncidents/RecoveryIncidentsApi'; +import { getCategoryValues } from '../../configuration/masterData/MasterDataApi'; +import { evaluateBatchSimulation } from '../SimulationApi'; + +const CATEGORY_TABS: TabItem[] = [ + { id: 'Flight', label: 'Flight', content: null }, + { id: 'Travel', label: 'Travel', content: null }, + { id: 'Ancillary', label: 'Ancillary', content: null }, +]; + +const SIMULATION_PASSENGER_POOL: Array> = [ + { + id: 'p-1', + name: 'Alexander Wright', + pnr: 'PNR-A1', + tier: 'Platinum', + cabinClass: 'First Class', + originalCabin: 'First Class', + actualCabin: 'First Class', + passengerType: 'VIP Adult', + nationality: 'British', + }, + { + id: 'p-2', + name: 'Sarah Jenkins', + pnr: 'PNR-A2', + tier: 'Platinum', + cabinClass: 'First Class', + originalCabin: 'First Class', + actualCabin: 'First Class', + passengerType: 'High Value Adult', + nationality: 'British', + }, + { + id: 'p-3', + name: 'The Miller Family', + pnr: 'PNR-B3', + tier: 'Gold', + cabinClass: 'Business Class', + originalCabin: 'Business Class', + actualCabin: 'Business Class', + passengerType: 'Family Group', + nationality: 'German', + }, + { + id: 'p-4', + name: 'Marcus Chen', + pnr: 'PNR-C4', + tier: 'Silver', + cabinClass: 'Business Class', + originalCabin: 'Business Class', + actualCabin: 'Business Class', + passengerType: 'Adult', + nationality: 'American', + }, +]; + +const SCENARIO_OPTIONS: Option[] = [ + { label: 'e.g. Passenger Compensation', value: 'e.g. Passenger Compensation' }, + { label: 'Flight Delay Disruption', value: 'Flight Delay Disruption' }, + { label: 'Flight Cancellation', value: 'Flight Cancellation' }, + { label: 'Denied Boarding / Involuntary Bumping', value: 'Denied Boarding' }, + { label: 'Missed Connection Delay', value: 'Missed Connection' }, +]; + +const SCENARIO_SUBTYPE_OPTIONS: Option[] = [ + { label: 'e.g. Passenger Compensation', value: 'e.g. Passenger Compensation' }, + { label: 'Delay > 3 Hours (Long Haul)', value: 'Delay > 3 Hours' }, + { label: 'Delay > 4 Hours (Standard)', value: 'Delay > 4 Hours' }, + { label: 'Short Notice Cancellation (< 14 Days)', value: 'Short Notice Cancellation' }, + { label: 'Involuntary Cabin Downgrade', value: 'Involuntary Downgrade' }, +]; + +const DISTANCE_OPTIONS: Option[] = [ + { label: '1500', value: '1500' }, + { label: '3500', value: '3500' }, + { label: '6200', value: '6200' }, + { label: '8500', value: '8500' }, + { label: '11200', value: '11200' }, +]; + +export default function SimulationTerminal() { + const navigate = useNavigate(); + + // Form State + const [formData, setFormData] = useState({ + category: 'Flight', + scenario: 'e.g. Passenger Compensation', + scenarioSubType: 'e.g. Passenger Compensation', + flightNumber: 'LH450', + airline: 'Lufthansa', + origin: 'FRA', + destination: 'JFK', + flightDistance: '6200', + arrDelay: '240', + depDelay: '180', + jurisdiction: '', + }); + + const [jurisdictionOptions, setJurisdictionOptions] = useState([]); + const [passengers, setPassengers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [isSimulating, setIsSimulating] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [savedSuccessCount, setSavedSuccessCount] = useState(null); + + // Load Master Data Jurisdictions + useEffect(() => { + let isMounted = true; + getCategoryValues('jurisdiction') + .then((items) => { + if (!isMounted) return; + if (Array.isArray(items) && items.length > 0) { + const activeOptions: Option[] = items + .filter((m) => m.isActive !== false) + .map((m) => ({ + label: m.label || m.name || m.value, + value: m.label || m.value || m.name, + })); + if (activeOptions.length > 0) { + setJurisdictionOptions(activeOptions); + setFormData((prev) => ({ + ...prev, + jurisdiction: prev.jurisdiction || String(activeOptions[0].value), + })); + } + } + }) + .catch((err) => { + console.error('Failed to load master data jurisdictions:', err); + }); + + return () => { + isMounted = false; + }; + }, []); + + // Pagination State + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 4; + + const handleInputChange = (field: keyof SimulationFormState, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // Run Simulation Handler with real Cohort & Policy Engine evaluation + const handleRunSimulation = async () => { + setIsSimulating(true); + try { + const delay = parseInt(formData.arrDelay, 10) || 240; + + // Build payload for each simulated passenger in the pool + const evaluationPayloads = SIMULATION_PASSENGER_POOL.map((p) => ({ + recoveryCode: `SIM-${p.id}`, + passengerName: p.name, + pnr: p.pnr, + loyaltyTier: p.tier, + passengerType: p.passengerType, + nationality: p.nationality, + cabinClass: p.cabinClass, + originalCabin: p.originalCabin, + actualCabin: p.actualCabin, + flightNumber: formData.flightNumber || 'LH450', + flightRoute: `${formData.origin || 'FRA'} → ${formData.destination || 'JFK'}`, + origin: formData.origin || 'FRA', + destination: formData.destination || 'JFK', + date: new Date().toISOString(), + category: formData.category, + scenario: formData.scenarioSubType || formData.scenario, + jurisdiction: formData.jurisdiction, + delayDuration: delay, + })); + + // Call the live Policy & Cohort Engine backend + const results = await evaluateBatchSimulation(evaluationPayloads); + + const simulatedPassengers: SimulatedPassenger[] = SIMULATION_PASSENGER_POOL.map((p, index) => { + const res = Array.isArray(results) ? results[index] : null; + + let computedComp = '--'; + let computedRefund = '--'; + const badges: string[] = []; + let eligibility: 'ELIGIBLE' | 'INELIGIBLE' = 'INELIGIBLE'; + + if (res) { + const isMatched = res.decision === 'MATCHED'; + eligibility = isMatched ? 'ELIGIBLE' : 'INELIGIBLE'; + + // Extract actions resolved by Policy Engine + const rawActions = Array.isArray(res.actions) ? res.actions : []; + const normalizedActions = Array.isArray(res.normalizedActions) ? res.normalizedActions : []; + + // Find refund action if any + const refundAction = + rawActions.find( + (a: any) => + (a.category && /refund/i.test(a.category)) || + (a.actionTypeCode && /REFUND/i.test(a.actionTypeCode)) || + (a.actionName && /refund/i.test(a.actionName)) + ) || + normalizedActions.find( + (a: any) => + (a.category && /refund/i.test(a.category)) || + (a.actionType && /refund/i.test(a.actionType)) + ); + + if (refundAction) { + const refundAmt = + refundAction.amount || + refundAction.configuration?.['Refund Amount'] || + refundAction.configuration?.['Amount'] || + ''; + const refundCurr = refundAction.currency || refundAction.configuration?.['Currency'] || 'USD'; + computedRefund = refundAmt ? `${refundAmt} ${refundCurr}`.trim() : 'Applied'; + } + + // Find compensation action if any + const compAction = + rawActions.find( + (a: any) => + (a.category && /compensation|payout/i.test(a.category)) || + (a.actionTypeCode && /COMP|PAYOUT/i.test(a.actionTypeCode)) || + (a.actionName && /comp|cash|payout/i.test(a.actionName)) + ) || + normalizedActions.find( + (a: any) => + (a.category && /compensation|payout/i.test(a.category)) || + (a.actionType && /comp|cash|payout/i.test(a.actionType)) + ); + + if (compAction) { + const amt = + compAction.amount || + compAction.configuration?.['Compensation Amount'] || + compAction.configuration?.['Amount'] || + ''; + const curr = compAction.currency || compAction.configuration?.['Currency'] || 'EUR'; + computedComp = amt ? `${amt} ${curr}`.trim() : 'Applied'; + } + + // Extract ONLY applied action types for STATUS badges (strictly from policy engine, no hardcoded fallbacks) + rawActions.forEach((act: any) => { + const actionName = act.actionName || act.actionTypeCode || act.category; + if (actionName && !badges.includes(actionName)) { + badges.push(actionName); + } + }); + + if (badges.length === 0) { + normalizedActions.forEach((act: any) => { + const actionTitle = act.title || act.actionType || act.category; + if (actionTitle && !badges.includes(actionTitle)) { + badges.push(actionTitle); + } + }); + } + } + + return { + ...p, + refund: computedRefund, + comp: computedComp, + statusBadges: badges, + eligibility, + }; + }); + + setPassengers(simulatedPassengers); + setCurrentPage(1); + } catch (err) { + console.error('Failed to run live policy evaluation:', err); + const simulatedPassengers: SimulatedPassenger[] = SIMULATION_PASSENGER_POOL.map((p) => ({ + ...p, + refund: '--', + comp: '--', + statusBadges: [], + eligibility: 'INELIGIBLE', + })); + setPassengers(simulatedPassengers); + setCurrentPage(1); + } finally { + setIsSimulating(false); + } + }; + + // Save to Recovery Database + const handleSaveAsRecovery = async () => { + try { + setIsSaving(true); + const promises = passengers.map((p) => { + const payload = { + recoveryCode: `REC-${Math.floor(100000 + Math.random() * 900000)}`, + passengerName: p.name, + pnr: p.pnr, + loyaltyTier: p.tier, + passengerType: p.passengerType, + nationality: p.nationality, + cabinClass: p.cabinClass, + originalCabin: p.originalCabin, + actualCabin: p.actualCabin, + flightNumber: formData.flightNumber || 'LH450', + flightRoute: `${formData.origin || 'FRA'} → ${formData.destination || 'JFK'}`, + origin: formData.origin || 'FRA', + destination: formData.destination || 'JFK', + date: new Date().toISOString(), + category: formData.scenario.includes('Compensation') ? 'Passenger Care' : 'Flight Ops', + scenario: formData.scenarioSubType || 'Flight Delay', + jurisdiction: formData.jurisdiction, + delayDuration: parseInt(formData.arrDelay, 10) || 240, + status: 'Pending', + value: p.comp, + isPerksClaimed: false, + recoverySource: 'Simulation Engine', + }; + return createRecoveryIncident(payload); + }); + + await Promise.all(promises); + setSavedSuccessCount(passengers.length); + } catch (err) { + console.error('Failed to save simulated cases:', err); + alert('Unable to save simulation manifest. Please try again.'); + } finally { + setIsSaving(false); + } + }; + + // Filter passengers based on search + const filteredPassengers = useMemo(() => { + if (!searchQuery.trim()) return passengers; + const q = searchQuery.toLowerCase(); + return passengers.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.pnr.toLowerCase().includes(q) || + p.tier.toLowerCase().includes(q) + ); + }, [passengers, searchQuery]); + + const totalPages = Math.ceil(filteredPassengers.length / pageSize) || 1; + const paginatedPassengers = filteredPassengers.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize + ); + + return ( +
+ {/* 2-Column Grid Layout */} +
+ {/* Left Column: Assessment Setup */} +
+

+ Assessment Setup +

+ + {/* Segmented Category Tabs */} + handleInputChange('category', tabId as SimulationFormState['category'])} + tabListClassName="w-full !bg-[#F4F7F6] !p-1 !rounded-xl !border !border-gray-200/40 !shadow-none !gap-0" + tabClassName="!flex-1 !py-2 !text-xs !font-semibold !rounded-lg !px-2 !shadow-none" + activeTabClassName="!bg-[#143D30] !text-white !shadow-sm" + contentClassName="!hidden" + /> + + {/* Form Fields using Custom Components */} +
+ {/* Scenario */} + handleInputChange('scenario', val)} + searchable={false} + size="sm" + /> + + {/* Scenario Sub-Type */} + handleInputChange('scenarioSubType', val)} + searchable={false} + size="sm" + /> + + {/* Flight No & Airline */} +
+ handleInputChange('flightNumber', e.target.value)} + placeholder="LH450" + size="sm" + /> + handleInputChange('airline', e.target.value)} + placeholder="Lufthansa" + size="sm" + /> +
+ + {/* Origin & Dest */} +
+ handleInputChange('origin', e.target.value)} + placeholder="FRA" + size="sm" + /> + handleInputChange('destination', e.target.value)} + placeholder="JFK" + size="sm" + /> +
+ + {/* Flight Distance (KM) */} + handleInputChange('flightDistance', val)} + searchable={false} + size="sm" + /> + + {/* Arr Delay (Mins) & Dep Delay */} +
+ handleInputChange('arrDelay', e.target.value)} + placeholder="240" + size="sm" + /> + handleInputChange('depDelay', e.target.value)} + placeholder="180" + size="sm" + /> +
+ + {/* Jurisdiction */} + handleInputChange('jurisdiction', val)} + searchable={false} + size="sm" + /> +
+ + {/* Action Button using CustomButton */} +
+ + Run Manifest Simulation + +
+
+ + {/* Right Column: Assessment Manifest */} +
+
+ {/* Header */} +
+

+ Assessment Manifest +

+

+ Real-time recovery outcomes. +

+
+ + {passengers.length === 0 ? ( + /* Clean Empty Placeholder (No Search bar, No Save as Recovery button, No Table headers) */ +
+
+ +
+

+ No Simulation Manifest Generated +

+

+ Configure the flight disruption parameters under Assessment Setup and click "Run Manifest Simulation" to evaluate passenger recovery entitlements and policy rules. +

+ + +
+ ) : ( + <> + {/* Action Bar (Search & Save as Recovery) */} +
+
+ { + setSearchQuery(e.target.value); + setCurrentPage(1); + }} + leftIcon={} + size="sm" + containerClassName="!gap-0" + /> +
+ + } + onClick={handleSaveAsRecovery} + className="!border-[#1B9869] !text-[#1B9869] hover:!bg-[#1B9869]/5 !rounded-xl !px-4 !py-2 shrink-0 font-semibold text-xs" + > + Save as Recovery + +
+ + {/* Manifest Table */} +
+ + + + + + + + + + + + {paginatedPassengers.length > 0 ? ( + paginatedPassengers.map((passenger) => ( + + {/* Passenger */} + + + {/* Eligibility */} + + + {/* Refund */} + + + {/* Comp */} + + + {/* Status Badges */} + + + )) + ) : ( + + + + )} + +
+ PASSENGER + + ELIGIBILITY + + REFUND + + COMP + + STATUS +
+
+ + {passenger.name} + + + {passenger.pnr} • {passenger.tier} + +
+
+ + {passenger.eligibility} + + + {passenger.refund} + + {passenger.comp} + + {passenger.statusBadges && passenger.statusBadges.length > 0 ? ( +
+ {passenger.statusBadges.map((badge, idx) => ( + + {badge} + + ))} +
+ ) : ( + -- + )} +
+ No matching passengers found in this manifest simulation. +
+
+ + {/* Footer & Pagination */} +
+ + Showing {filteredPassengers.length > 0 ? (currentPage - 1) * pageSize + 1 : 0} to{' '} + {Math.min(currentPage * pageSize, filteredPassengers.length)} of{' '} + {filteredPassengers.length} entries + + +
+ + + + {filteredPassengers.length > 0 ? `${currentPage}/${totalPages}` : '0/0'} + + + +
+
+ + )} +
+
+
+ + {/* Success Modal after Saving Incidents */} + {savedSuccessCount !== null && ( +
+
+
+ +
+ +
+

+ Manifest Saved to Recovery Database +

+

+ Successfully created {savedSuccessCount} recovery incident case(s) for flight{' '} + {formData.flightNumber} with + active entitlement rules. +

+
+ +
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/app/simulation/index.tsx b/src/app/simulation/index.tsx new file mode 100644 index 0000000..504c9b9 --- /dev/null +++ b/src/app/simulation/index.tsx @@ -0,0 +1,5 @@ +import SimulationTerminal from './components/SimulationTerminal'; + +export default function SimulationPage() { + return ; +} diff --git a/src/layout/AppHeader.tsx b/src/layout/AppHeader.tsx index fe8f96d..2b11f2e 100644 --- a/src/layout/AppHeader.tsx +++ b/src/layout/AppHeader.tsx @@ -3,6 +3,7 @@ import { useLocation } from 'react-router-dom'; const PAGE_META: Record = { '/': { title: 'Dashboard', subtitle: 'Overview of system status and active incidents.' }, + '/simulation': { title: 'Simulation Terminal', subtitle: 'Integrated manifest-level assessment engine.' }, '/cohorts': { title: 'Cohort Management', subtitle: 'Dynamic passenger segmentation for targeted recovery and recovery intelligence.' }, '/policy-engine': { title: 'Policy Engine Framework Registry', subtitle: 'Manage framework policies, conditions, and automated actions.' }, '/policy-engine/add': { title: 'Deploy New Policy', subtitle: 'Configure policy framework details, targeting rules, and action payloads.' },