diff --git a/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx b/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx index 0c73c86..0d4c871 100644 --- a/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx +++ b/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx @@ -58,6 +58,18 @@ interface FieldDefinitionFormModalProps { onSubmit: (e: React.FormEvent) => void; } +const VALIDATION_RELEVANT_TYPES: FieldType[] = [ + 'textbox', + 'textarea', + 'email', + 'phone', + 'url', + 'number', + 'decimal', + 'percentage', + 'currency', +]; + export function FieldDefinitionFormModal({ isOpen, editingField, @@ -94,6 +106,12 @@ export function FieldDefinitionFormModal({ })), ]; + const isNumericType = ['number', 'decimal', 'percentage', 'currency'].includes(formData.fieldType); + const minLabel = isNumericType ? 'Min Value' : 'Min Length'; + const maxLabel = isNumericType ? 'Max Value' : 'Max Length'; + const minPlaceholder = isNumericType ? 'e.g. 0' : 'e.g. 3'; + const maxPlaceholder = isNumericType ? 'e.g. 1000' : 'e.g. 250'; + return ( { const newType = val as FieldType; const isLookupType = newType === 'dropdown' || newType === 'multi_select'; + const isValRelevant = VALIDATION_RELEVANT_TYPES.includes(newType); setFormData((prev) => ({ ...prev, fieldType: newType, lookupSource: isLookupType ? prev.lookupSource : undefined, - validationJson: isLookupType ? undefined : prev.validationJson, + validationJson: isValRelevant ? prev.validationJson : undefined, })); if (setError && !isLookupType) { setError(null); @@ -168,12 +187,12 @@ export function FieldDefinitionFormModal({
- {(formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select') && ( + {(formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select' || formData.fieldType === 'currency') && ( - {/* Validation JSON Rules */} - {formData.fieldType !== 'dropdown' && formData.fieldType !== 'multi_select' && ( + {/* Validation JSON Rules - Only shown for relevant field types */} + {VALIDATION_RELEVANT_TYPES.includes(formData.fieldType) && (
Validation Rules (validation_json)
@@ -253,11 +272,11 @@ export function FieldDefinitionFormModal({ }, })) } - placeholder="e.g. 312" + placeholder={minPlaceholder} /> @@ -269,7 +288,7 @@ export function FieldDefinitionFormModal({ }, })) } - placeholder="e.g. 245" + placeholder={maxPlaceholder} /> (); + + visibleFields.forEach((field) => { + const secName = field.section?.trim() || ''; + + const fields = groupedSectionsMap.get(secName) || []; + fields.push(field); + + groupedSectionsMap.set(secName, fields); + }); + return ( -
- {visibleFields.map((field) => { - const widthClass = WIDTH_GRID_MAP[field.width || 'full'] || 'col-span-12'; - const fieldVal = action.fieldValues?.[field.fieldCode]; - const lookupOpts = fieldLookupOptionsMap[field.lookupSource || ''] || []; - const helpText = field.helpText || (field as any).help_text; - const valRules = field.validationJson || (field as any).validation_json; +
+ {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; - return ( -
- {field.fieldType === 'textarea' ? ( - handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)} - placeholder={field.placeholder || 'Enter details...'} - /> - ) : field.fieldType === 'currency' ? ( -
- -
-
- { + 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, { - ...(fieldVal || {}), - amount: e.target.value, - }) - } - placeholder={field.placeholder || 'Amount'} + value={effectiveVal !== undefined && effectiveVal !== null ? String(effectiveVal) : ''} + onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)} + placeholder={field.placeholder || 'Enter details...'} /> -
- - handleFieldValueChange(rule.id, action.id, field.fieldCode, { - ...(fieldVal || {}), - 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)} - /> - - {fieldVal ? 'Enabled' : 'Disabled'} - -
-
- ) : ( - { + 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' - ? 'datetime-local' - : 'text' - } - value={fieldVal ?? ''} - onChange={(e) => 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 - } - /> - )} + ? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16) + : undefined + } + /> + )} - {helpText && ( -

- {helpText} -

- )} + {helpText && ( +

+ {helpText} +

+ )} +
+ ); + })}
- ); - })} +
+ ))}
); })()} diff --git a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx index 60531aa..e7f70b7 100644 --- a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx +++ b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { EyeIcon, PencilSimpleIcon, @@ -100,12 +100,20 @@ function getStatusVariant( export default function RecoveryIncidentsList() { const navigate = useNavigate(); + const location = useLocation(); const [incidents, setIncidents] = useState([]); const [metrics, setMetrics] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); + useEffect(() => { + if (location.state?.successMsg) { + setSuccessMsg(location.state.successMsg); + window.history.replaceState({}, document.title); + } + }, [location]); + // Pagination const [currentPage, setCurrentPage] = useState(1); diff --git a/src/app/recoveryIncidents/tabs/index.tsx b/src/app/recoveryIncidents/tabs/index.tsx index e30e1bc..bd6e5ab 100644 --- a/src/app/recoveryIncidents/tabs/index.tsx +++ b/src/app/recoveryIncidents/tabs/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react'; -import { CustomButton, CustomTabs, CustomBackButton, CustomStatus, Skeleton } from '../../../components/custom'; +import { CustomButton, CustomTabs, CustomBackButton, CustomStatus, Skeleton, CustomAlertBanner } from '../../../components/custom'; import SummaryTab from './SummaryTab'; import CaseDetailsTab from './CaseDetailsTab'; import RecoveryPlanTab from './RecoveryPlanTab'; @@ -27,6 +27,8 @@ export default function RecoveryIncidentTabs() { const [incident, setIncident] = useState(null); const [loading, setLoading] = useState(true); const [updating, setUpdating] = useState(false); + const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); const fetchIncident = async () => { if (!id) return; @@ -48,12 +50,19 @@ export default function RecoveryIncidentTabs() { const handleStatusChange = async (newStatus: string) => { if (!id || updating) return; setUpdating(true); + setError(null); try { const updated = await updateIncidentStatus(id, newStatus); setIncident(updated); - navigate('/recovery'); + const code = updated?.recoveryCode || incident?.recoveryCode || id; + navigate('/recovery', { + state: { + successMsg: `Incident ${code} status updated to ${newStatus} successfully.` + } + }); } catch (err) { console.error("Failed to update incident status:", err); + setError("Failed to update incident status. Please try again."); } finally { setUpdating(false); } @@ -62,11 +71,15 @@ export default function RecoveryIncidentTabs() { const handleReRunEngine = async () => { if (!id || updating) return; setUpdating(true); + setError(null); + setSuccessMsg(null); try { const updated = await reRunPolicyEngine(id); setIncident(updated); + setSuccessMsg("Policy engine re-evaluated successfully."); } catch (err) { console.error("Failed to re-run policy engine:", err); + setError("Failed to re-run policy engine. Please try again."); } finally { setUpdating(false); } @@ -154,6 +167,21 @@ export default function RecoveryIncidentTabs() { return (
+ {error && ( + setError(null)} + /> + )} + {successMsg && ( + setSuccessMsg(null)} + /> + )} + {/* Header */}
diff --git a/src/app/simulation/SimulationTypes.ts b/src/app/simulation/SimulationTypes.ts index eb4ece4..c09249e 100644 --- a/src/app/simulation/SimulationTypes.ts +++ b/src/app/simulation/SimulationTypes.ts @@ -1,30 +1,31 @@ -export interface SimulatedPassenger { +export interface SimulatedPassengerInput { 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; + bookingRef?: string; + ancillaryItem?: string; + [key: string]: any; +} + +export interface SimulatedPassenger extends SimulatedPassengerInput { + eligibility: 'ELIGIBLE' | 'INELIGIBLE' | 'REVIEW'; + refund: string; + comp: string; + perks: string[]; + statusBadges?: 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; + [key: string]: any; } diff --git a/src/app/simulation/components/SimulationTerminal.tsx b/src/app/simulation/components/SimulationTerminal.tsx index 0c50520..a14e07f 100644 --- a/src/app/simulation/components/SimulationTerminal.tsx +++ b/src/app/simulation/components/SimulationTerminal.tsx @@ -13,114 +13,76 @@ import { CustomDropdown, CustomButton, CustomTabs, + CustomSwitch, } from '../../../components/custom'; -import type { TabItem } from '../../../components/custom/CustomTabs'; import type { Option } from '../../../components/custom/CustomDropdown'; +import type { TabItem } from '../../../components/custom/CustomTabs'; import type { SimulatedPassenger, SimulationFormState } from '../SimulationTypes'; import { createRecoveryIncident } from '../../recoveryIncidents/RecoveryIncidentsApi'; import { getCategoryValues } from '../../configuration/masterData/MasterDataApi'; import { evaluateBatchSimulation } from '../SimulationApi'; +import { + CATEGORY_SIMULATION_CONFIGS, + type CategorySimulationConfig, + type FormFieldConfig, +} from '../simulationConfig'; -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: '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: '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' }, +const DEFAULT_JURISDICTIONS: Option[] = [ + { label: 'Global Policy', value: 'Global Policy' }, + { label: 'European Union (EU261)', value: 'European Union' }, + { label: 'United States (DOT)', value: 'United States' }, + { label: 'United Kingdom (UK261)', value: 'United Kingdom' }, + { label: 'India (DGCA)', value: 'India' }, + { label: 'United Arab Emirates', value: 'United Arab Emirates' }, + { label: 'Asia Pacific', value: 'Asia Pacific' }, ]; export default function SimulationTerminal() { const navigate = useNavigate(); - // Form State + // Initial Form State (no default selections) 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: '', + ...CATEGORY_SIMULATION_CONFIGS.Flight.defaultFormState, }); - const [jurisdictionOptions, setJurisdictionOptions] = useState([]); + const [jurisdictionOptions, setJurisdictionOptions] = useState(DEFAULT_JURISDICTIONS); const [passengers, setPassengers] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [isSimulating, setIsSimulating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [savedSuccessCount, setSavedSuccessCount] = useState(null); + // Category Tabs with Icons + const categoryTabs: TabItem[] = useMemo(() => [ + { + id: 'Flight', + label: 'FLIGHT', + content: null, + }, + { + id: 'Travel', + label: 'TRAVEL', + content: null, + }, + { + id: 'Ancillary', + label: 'ANCILLARY', + content: null, + }, + ], []); + + // Active Category Config + const activeCategory = (formData.category as 'Flight' | 'Travel' | 'Ancillary') || 'Flight'; + const categoryConfig: CategorySimulationConfig = + CATEGORY_SIMULATION_CONFIGS[activeCategory] || CATEGORY_SIMULATION_CONFIGS.Flight; + + // Dynamically derived sub-type options based on selected scenario + const subTypeOptions: Option[] = useMemo(() => { + return categoryConfig.scenarioSubTypes[formData.scenario] || []; + }, [categoryConfig, formData.scenario]); + // Load Master Data Jurisdictions useEffect(() => { let isMounted = true; @@ -130,16 +92,25 @@ export default function SimulationTerminal() { if (Array.isArray(items) && items.length > 0) { const activeOptions: Option[] = items .filter((m) => m.isActive !== false) - .map((m) => ({ - label: String(m.label || m.name || m.value || ''), - value: String(m.label || m.value || m.name || ''), - })); + .map((m) => { + let label = String(m.label || m.name || m.value || ''); + if (label.toLowerCase() === 'global' || label.toLowerCase() === 'global policy') { + label = 'Global Policy'; + } + return { + label, + value: label, + }; + }); if (activeOptions.length > 0) { - setJurisdictionOptions(activeOptions); - setFormData((prev) => ({ - ...prev, - jurisdiction: prev.jurisdiction || String(activeOptions[0].value), - })); + const merged = [...activeOptions]; + if (!merged.some((o) => o.value === 'Global Policy')) { + merged.unshift({ label: 'Global Policy', value: 'Global Policy' }); + } + const unique = merged.filter( + (opt, idx, arr) => arr.findIndex((x) => x.value === opt.value) === idx + ); + setJurisdictionOptions(unique); } } }) @@ -156,18 +127,45 @@ export default function SimulationTerminal() { const [currentPage, setCurrentPage] = useState(1); const pageSize = 4; - const handleInputChange = (field: keyof SimulationFormState, value: string) => { + const handleInputChange = (field: string, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); }; - // Run Simulation Handler with real Cohort & Policy Engine evaluation + // Handler for changing Scenario (do not set any default sub-type) + const handleScenarioChange = (newScenario: string) => { + setFormData((prev) => ({ + ...prev, + scenario: newScenario, + scenarioSubType: '', + })); + }; + + // Switch Strategic Category Tab (do not set any default scenario or fields) + const handleCategoryChange = (newCategory: 'Flight' | 'Travel' | 'Ancillary') => { + const newConfig = CATEGORY_SIMULATION_CONFIGS[newCategory] || CATEGORY_SIMULATION_CONFIGS.Flight; + + setFormData((prev) => ({ + ...prev, + category: newCategory, + jurisdiction: prev.jurisdiction || '', + ...newConfig.defaultFormState, + scenario: '', + scenarioSubType: '', + })); + setPassengers([]); + setSearchQuery(''); + setCurrentPage(1); + }; + + // Run Simulation Handler with Pure Policy-Engine Evaluation const handleRunSimulation = async () => { setIsSimulating(true); try { - const delay = parseInt(formData.arrDelay, 10) || 240; + const passengerPool = categoryConfig.passengerPool; + const delay = parseInt(formData.arrDelay || formData.deprivationHours || '240', 10) || 240; - // Build payload for each simulated passenger in the pool - const evaluationPayloads = SIMULATION_PASSENGER_POOL.map((p) => ({ + // Build payload for each simulated passenger in the category pool + const evaluationPayloads = passengerPool.map((p) => ({ recoveryCode: `SIM-${p.id}`, passengerName: p.name, pnr: p.pnr, @@ -177,37 +175,46 @@ export default function SimulationTerminal() { cabinClass: p.cabinClass, originalCabin: p.originalCabin, actualCabin: p.actualCabin, - flightNumber: formData.flightNumber || 'LH450', - flightRoute: `${formData.origin || 'FRA'} → ${formData.destination || 'JFK'}`, - origin: formData.origin || 'FRA', + flightNumber: formData.flightNumber || formData.bookingRef || formData.receiptId || 'SIM-101', + flightRoute: + formData.origin && formData.destination + ? `${formData.origin} → ${formData.destination}` + : formData.location || 'Terminal Hub', + origin: formData.origin || formData.location || 'FRA', destination: formData.destination || 'JFK', date: new Date().toISOString(), category: formData.category, scenario: formData.scenarioSubType || formData.scenario, - jurisdiction: formData.jurisdiction, + scenarioType: formData.scenario, + scenarioSubType: formData.scenarioSubType, + jurisdiction: formData.jurisdiction || 'Global Policy', delayDuration: delay, + ancillaryType: formData.scenario || p.ancillaryItem, + ancillaryPurchased: formData.scenario || p.ancillaryItem, + ancillaryDelivered: formData.delivered === true || formData.delivered === 'true', + serviceValue: Number(formData.serviceCost || 25), + serviceCost: Number(formData.serviceCost || 25), })); - // Call the live Policy & Cohort Engine backend + // Call Policy & Cohort Engine backend const results = await evaluateBatchSimulation(evaluationPayloads); - const simulatedPassengers: SimulatedPassenger[] = SIMULATION_PASSENGER_POOL.map((p, index) => { + const simulatedPassengers: SimulatedPassenger[] = passengerPool.map((p, index): SimulatedPassenger => { const res = Array.isArray(results) ? results[index] : null; let computedComp = '--'; let computedRefund = '--'; - const badges: string[] = []; + const perksList: 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 + // Refund Action resolved by Policy Engine const refundAction = rawActions.find( (a: any) => @@ -231,7 +238,7 @@ export default function SimulationTerminal() { computedRefund = refundAmt ? `${refundAmt} ${refundCurr}`.trim() : 'Applied'; } - // Find compensation action if any + // Compensation Action resolved by Policy Engine const compAction = rawActions.find( (a: any) => @@ -255,29 +262,41 @@ export default function SimulationTerminal() { computedComp = amt ? `${amt} ${curr}`.trim() : 'Applied'; } - // Extract ONLY applied action types for STATUS badges (strictly from policy engine, no hardcoded fallbacks) + // Perks and actions resolved by Policy Engine rawActions.forEach((act: any) => { - const actionName = act.actionName || act.actionTypeCode || act.category; - if (actionName && !badges.includes(actionName)) { - badges.push(actionName); + let actionName = act.actionName || act.normalizedAction?.title; + if (!actionName && act.actionTypeCode) { + actionName = act.actionTypeCode + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (c: string) => c.toUpperCase()); + } + if (actionName && !perksList.includes(actionName)) { + perksList.push(actionName); } }); - if (badges.length === 0) { + if (perksList.length === 0) { normalizedActions.forEach((act: any) => { const actionTitle = act.title || act.actionType || act.category; - if (actionTitle && !badges.includes(actionTitle)) { - badges.push(actionTitle); + if (actionTitle && !perksList.includes(actionTitle)) { + perksList.push(actionTitle); } }); } + + if (isMatched && perksList.length === 0) { + if (computedComp !== '--') perksList.push(`Compensation (${computedComp})`); + if (computedRefund !== '--') perksList.push(`Refund (${computedRefund})`); + } } return { ...p, refund: computedRefund, comp: computedComp, - statusBadges: badges, + perks: eligibility === 'ELIGIBLE' ? perksList : [], + statusBadges: perksList, eligibility, }; }); @@ -286,10 +305,11 @@ export default function SimulationTerminal() { setCurrentPage(1); } catch (err) { console.error('Failed to run live policy evaluation:', err); - const simulatedPassengers: SimulatedPassenger[] = SIMULATION_PASSENGER_POOL.map((p) => ({ + const simulatedPassengers: SimulatedPassenger[] = categoryConfig.passengerPool.map((p): SimulatedPassenger => ({ ...p, refund: '--', comp: '--', + perks: [], statusBadges: [], eligibility: 'INELIGIBLE', })); @@ -315,17 +335,20 @@ export default function SimulationTerminal() { cabinClass: p.cabinClass, originalCabin: p.originalCabin, actualCabin: p.actualCabin, - flightNumber: formData.flightNumber || 'LH450', - flightRoute: `${formData.origin || 'FRA'} → ${formData.destination || 'JFK'}`, - origin: formData.origin || 'FRA', + flightNumber: formData.flightNumber || formData.bookingRef || formData.receiptId || 'REC-101', + flightRoute: + formData.origin && formData.destination + ? `${formData.origin} → ${formData.destination}` + : formData.location || 'Terminal Hub', + origin: formData.origin || formData.location || '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, + category: formData.category === 'Flight' ? 'Flight Ops' : formData.category, + scenario: formData.scenarioSubType || formData.scenario, + jurisdiction: formData.jurisdiction || 'Global Policy', + delayDuration: parseInt(formData.arrDelay || formData.deprivationHours || '240', 10) || 240, status: 'Pending', - value: p.comp, + value: p.comp !== '--' ? p.comp : p.refund !== '--' ? p.refund : `${formData.serviceCost || 25} USD`, isPerksClaimed: false, recoverySource: 'Simulation Engine', }; @@ -350,7 +373,9 @@ export default function SimulationTerminal() { (p) => p.name.toLowerCase().includes(q) || p.pnr.toLowerCase().includes(q) || - p.tier.toLowerCase().includes(q) + p.tier.toLowerCase().includes(q) || + (p.bookingRef && p.bookingRef.toLowerCase().includes(q)) || + (p.ancillaryItem && p.ancillaryItem.toLowerCase().includes(q)) ); }, [passengers, searchQuery]); @@ -366,118 +391,118 @@ export default function SimulationTerminal() {
{/* Left Column: Assessment Setup */}
-

- Assessment Setup -

+
+

+ Assessment Setup +

+

+ Configure parameters for {categoryConfig.label}. +

+
- {/* 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" - /> + {/* Strategic Category Tabs */} +
+ + handleCategoryChange(tabId as 'Flight' | 'Travel' | 'Ancillary')} + 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" - /> + {/* Jurisdiction Dropdown */} +
- {/* 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" + handleInputChange('jurisdiction', val)} + placeholder="Select Jurisdiction" + searchable={false} size="sm" />
- {/* Origin & Dest */} -
- handleInputChange('origin', e.target.value)} - placeholder="FRA" - size="sm" - /> - handleInputChange('destination', e.target.value)} - placeholder="JFK" + {/* Scenario Dropdown */} +
+ + handleScenarioChange(val)} + placeholder="Select Scenario" + searchable={false} size="sm" />
- {/* Flight Distance (KM) */} - handleInputChange('flightDistance', val)} - searchable={false} - size="sm" - /> + {/* Dynamic Scenario Sub-Type Dropdown */} +
- {/* Arr Delay (Mins) & Dep Delay */} -
- handleInputChange('arrDelay', e.target.value)} - placeholder="240" - size="sm" - /> - handleInputChange('depDelay', e.target.value)} - placeholder="180" + handleInputChange('scenarioSubType', val)} + placeholder="Select Scenario Sub-Type" + searchable={false} size="sm" />
- {/* Jurisdiction */} - handleInputChange('jurisdiction', val)} - searchable={false} - size="sm" - /> + {/* Dynamic Category Fields */} +
+ {categoryConfig.fields.map((field: FormFieldConfig) => { + const isFullWidth = field.gridSpan === 2; + return ( +
+ {field.type === 'switch' ? ( +
+ + {field.label} + + handleInputChange(field.id, e.target.checked)} + /> +
+ ) : field.type === 'select' ? ( + handleInputChange(field.id, val)} + placeholder={field.placeholder || `Select ${field.label}`} + searchable={false} + size="sm" + /> + ) : ( + handleInputChange(field.id, e.target.value)} + placeholder={field.placeholder} + size="sm" + /> + )} +
+ ); + })} +
{/* Action Button using CustomButton */} @@ -502,32 +527,30 @@ export default function SimulationTerminal() { Assessment Manifest

- Real-time recovery outcomes. + Real-time recovery outcomes for {formData.category} simulation.

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

- No Simulation Manifest Generated + No {formData.category} Simulation Manifest Generated

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

- -
) : ( <> - {/* Action Bar (Search & Save as Recovery) */} + {/* Action Bar */}
{ setSearchQuery(e.target.value); @@ -552,25 +575,24 @@ export default function SimulationTerminal() {
- {/* Manifest Table */} + {/* Dynamic Manifest Table */}
- - + ))} + - - @@ -578,20 +600,88 @@ export default function SimulationTerminal() { {paginatedPassengers.length > 0 ? ( paginatedPassengers.map((passenger) => ( - {/* Passenger */} - + {/* Dynamic Columns based on Category */} + {categoryConfig.manifestColumns.map((col) => { + if (col.key === 'name') { + return ( + + ); + } - {/* Eligibility */} - + ); + } + + if (col.key === 'cabin') { + return ( + + ); + } + + if (col.key === 'pnr') { + return ( + + ); + } + + if (col.key === 'bookingRef') { + return ( + + ); + } + + if (col.key === 'ancillaryItem') { + return ( + + ); + } + + if (col.key === 'refund') { + return ( + + ); + } + + if (col.key === 'comp') { + return ( + + ); + } + + return ( + + ); + })} + + {/* Eligibility Status */} + - {/* Refund */} - - - {/* Comp */} - - - {/* Status Badges */} + {/* Applied Perks */} - @@ -695,8 +778,8 @@ export default function SimulationTerminal() { Manifest Saved to Recovery Database

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

diff --git a/src/app/simulation/simulationConfig.ts b/src/app/simulation/simulationConfig.ts new file mode 100644 index 0000000..b9cdc97 --- /dev/null +++ b/src/app/simulation/simulationConfig.ts @@ -0,0 +1,557 @@ +import type { Option } from '../../components/custom/CustomDropdown'; +import type { TabItem } from '../../components/custom/CustomTabs'; +import type { SimulatedPassengerInput } from './SimulationTypes'; + +export interface FormFieldConfig { + id: string; + label: string; + type: 'text' | 'number' | 'select' | 'switch'; + options?: Option[]; + placeholder?: string; + defaultValue: any; + gridSpan?: 1 | 2; // 1 = half width (grid-cols-2), 2 = full width (grid-cols-1) +} + +export interface ManifestColumnConfig { + key: string; + label: string; +} + +export interface CategorySimulationConfig { + id: 'Flight' | 'Travel' | 'Ancillary'; + label: string; + description: string; + scenarios: Option[]; + scenarioSubTypes: Record; + fields: FormFieldConfig[]; + passengerPool: SimulatedPassengerInput[]; + manifestColumns: ManifestColumnConfig[]; + defaultFormState: { + scenario: string; + scenarioSubType: string; + [key: string]: any; + }; +} + +export const CATEGORY_TABS: TabItem[] = [ + { id: 'Flight', label: 'FLIGHT', content: null }, + { id: 'Travel', label: 'TRAVEL', content: null }, + { id: 'Ancillary', label: 'ANCILLARY', content: null }, +]; + +export const CATEGORY_SIMULATION_CONFIGS: Record< + 'Flight' | 'Travel' | 'Ancillary', + CategorySimulationConfig +> = { + Flight: { + id: 'Flight', + label: 'Flight Operations Disruption', + description: 'Simulate flight delays, cancellations, denied boarding, and missed connections.', + scenarios: [ + { 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' }, + ], + scenarioSubTypes: { + 'Flight Delay Disruption': [ + { label: 'Short Delay (< 2 Hours)', value: 'Short Delay' }, + { label: 'Long Delay (3-4 Hours)', value: 'Long Delay' }, + { label: 'Extreme Delay (> 4 Hours)', value: 'Extreme Delay' }, + { label: 'Overnight Delay', value: 'Overnight Delay' }, + ], + 'Flight Cancellation': [ + { label: 'Same-Day Cancellation', value: 'Same-Day Cancellation' }, + { label: 'Short Notice Cancellation (< 14 Days)', value: 'Short Notice Cancellation' }, + { label: 'Advance Cancellation (> 14 Days)', value: 'Advance Cancellation' }, + { label: 'Weather Extraordinary Cancellation', value: 'Weather Cancellation' }, + ], + 'Denied Boarding': [ + { label: 'Involuntary Denied Boarding', value: 'Involuntary Denied Boarding' }, + { label: 'Voluntary Denied Boarding', value: 'Voluntary Denied Boarding' }, + { label: 'Involuntary Cabin Downgrade', value: 'Cabin Downgrade' }, + { label: 'Oversale Flight Capacity', value: 'Oversale' }, + ], + 'Missed Connection': [ + { label: 'Short Connection Miss (< 2 Hours)', value: 'Short Connection Miss' }, + { label: 'Long Connection Miss (> 3 Hours)', value: 'Long Connection Miss' }, + { label: 'Overnight Misconnection', value: 'Overnight Misconnection' }, + { label: 'Final Leg Connection Miss', value: 'Final Connection Miss' }, + ], + }, + fields: [ + { + id: 'flightNumber', + label: 'Flight No', + type: 'text', + placeholder: 'LH450', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'airline', + label: 'Airline', + type: 'text', + placeholder: 'Lufthansa', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'origin', + label: 'Origin', + type: 'text', + placeholder: 'FRA', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'destination', + label: 'Dest', + type: 'text', + placeholder: 'JFK', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'flightDistance', + label: 'Flight Distance (KM)', + type: 'select', + options: [ + { label: '1500', value: '1500' }, + { label: '3500', value: '3500' }, + { label: '6200', value: '6200' }, + { label: '8500', value: '8500' }, + { label: '11200', value: '11200' }, + ], + defaultValue: '', + gridSpan: 2, + }, + { + id: 'arrDelay', + label: 'Arr Delay (Mins)', + type: 'number', + placeholder: '240', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'depDelay', + label: 'Dep Delay (Mins)', + type: 'number', + placeholder: '180', + defaultValue: '', + gridSpan: 1, + }, + ], + defaultFormState: { + scenario: '', + scenarioSubType: '', + flightNumber: '', + airline: '', + origin: '', + destination: '', + flightDistance: '', + arrDelay: '', + depDelay: '', + }, + passengerPool: [ + { + 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', + }, + ], + manifestColumns: [ + { key: 'name', label: 'Passenger' }, + { key: 'tier', label: 'Tier & Type' }, + { key: 'cabin', label: 'Cabin Class' }, + { key: 'pnr', label: 'PNR' }, + { key: 'comp', label: 'Compensation' }, + { key: 'refund', label: 'Refund' }, + ], + }, + Travel: { + id: 'Travel', + label: 'Travel & Accommodation Disruption', + description: 'Simulate hotel overbookings, missed ground transfers, and package itinerary disruptions.', + scenarios: [ + { label: 'Hotel Overbooking / Relocation', value: 'Hotel Overbooking' }, + { label: 'Missed Ground Transit / Transfer', value: 'Missed Ground Transit' }, + { label: 'Package Itinerary Interruption', value: 'Itinerary Interruption' }, + { label: 'VIP Accommodation Downgrade', value: 'Accommodation Downgrade' }, + ], + scenarioSubTypes: { + 'Hotel Overbooking': [ + { label: 'Relocation to Lower Star Hotel', value: 'Relocation to Lower Star' }, + { label: 'Relocation Outside Airport Zone', value: 'Relocation Outside Airport Zone' }, + { label: 'Same Star Rating Relocation', value: 'Same Star Relocation' }, + { label: 'Unscheduled Hotel Night Incurred', value: 'Unscheduled Hotel Night' }, + ], + 'Missed Ground Transit': [ + { label: 'Ground Transfer Delay (> 4 Hours)', value: 'Ground Transfer Delay' }, + { label: 'Chauffeur Transit Breakdown', value: 'Chauffeur Transit Breakdown' }, + { label: 'Shuttle Service Cancellation', value: 'Shuttle Service Cancellation' }, + { label: 'Missed Connecting Rail/Bus Transit', value: 'Missed Rail/Bus Transit' }, + ], + 'Itinerary Interruption': [ + { label: 'Package Tour Cancellation', value: 'Package Tour Cancellation' }, + { label: 'Excursion Schedule Disruption', value: 'Excursion Disruption' }, + { label: 'Prepaid Activity Ticket Loss', value: 'Activity Ticket Loss' }, + { label: 'Cruiseline Departure Missed', value: 'Cruiseline Missed Departure' }, + ], + 'Accommodation Downgrade': [ + { label: 'Room Category Downgrade', value: 'Room Category Downgrade' }, + { label: 'Promised Amenities Non-Availability', value: 'Amenities Non-Availability' }, + { label: 'Shared Facility Relocation', value: 'Shared Facility Downgrade' }, + { label: 'Executive Lounge Access Loss', value: 'Executive Lounge Loss' }, + ], + }, + fields: [ + { + id: 'bookingRef', + label: 'Booking Ref', + type: 'text', + placeholder: 'TRV-8829', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'travelProvider', + label: 'Provider / Partner', + type: 'text', + placeholder: 'Marriott Bonvoy', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'location', + label: 'Location / City', + type: 'text', + placeholder: 'Frankfurt Airport City', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'accommodationClass', + label: 'Accommodation Rating', + type: 'select', + options: [ + { label: '5-Star Luxury', value: '5-Star Luxury' }, + { label: '4-Star Executive', value: '4-Star Executive' }, + { label: '3-Star Standard', value: '3-Star Standard' }, + { label: 'Transit Lodge', value: 'Transit Lodge' }, + ], + defaultValue: '', + gridSpan: 1, + }, + { + id: 'expenseIncurred', + label: 'Expense Incurred ($)', + type: 'number', + placeholder: '350', + defaultValue: '', + gridSpan: 1, + }, + { + id: 'disruptionDuration', + label: 'Duration (Nights/Hrs)', + type: 'text', + placeholder: '1 Night', + defaultValue: '', + gridSpan: 1, + }, + ], + defaultFormState: { + scenario: '', + scenarioSubType: '', + bookingRef: '', + travelProvider: '', + location: '', + accommodationClass: '', + expenseIncurred: '', + disruptionDuration: '', + }, + passengerPool: [ + { + id: 'p-1', + name: 'Alexander Wright', + pnr: 'TRV-PNR1', + tier: 'Platinum', + passengerType: 'VIP Traveler', + nationality: 'British', + bookingRef: 'TRV-8829', + }, + { + id: 'p-2', + name: 'Sarah Jenkins', + pnr: 'TRV-PNR2', + tier: 'Platinum', + passengerType: 'Corporate Traveler', + nationality: 'British', + bookingRef: 'TRV-8830', + }, + { + id: 'p-3', + name: 'The Miller Family', + pnr: 'TRV-PNR3', + tier: 'Gold', + passengerType: 'Family Group', + nationality: 'German', + bookingRef: 'TRV-8831', + }, + { + id: 'p-4', + name: 'Marcus Chen', + pnr: 'TRV-PNR4', + tier: 'Silver', + passengerType: 'Solo Traveler', + nationality: 'American', + bookingRef: 'TRV-8832', + }, + ], + manifestColumns: [ + { key: 'name', label: 'Traveler' }, + { key: 'tier', label: 'Tier & Type' }, + { key: 'bookingRef', label: 'Booking Ref' }, + { key: 'comp', label: 'Compensation' }, + { key: 'refund', label: 'Refund' }, + ], + }, + Ancillary: { + id: 'Ancillary', + label: 'Ancillary Services & Perks Disruption', + description: 'Simulate ancillary failures, non-delivery, seat defects, lounge denials, and baggage issues.', + scenarios: [ + { label: 'Preferred Seat', value: 'Preferred Seat' }, + { label: 'Wi-Fi', value: 'Wi-Fi' }, + { label: 'Lounge Access', value: 'Lounge Access' }, + { label: 'Priority Boarding', value: 'Priority Boarding' }, + { label: 'Fast Track Security', value: 'Fast Track Security' }, + { label: 'Special Meal', value: 'Special Meal' }, + { label: 'Paid Meal', value: 'Paid Meal' }, + { label: 'Extra Baggage', value: 'Extra Baggage' }, + { label: 'Sports Equipment', value: 'Sports Equipment' }, + { label: 'Musical Instrument', value: 'Musical Instrument' }, + { label: 'Upgrade Purchase', value: 'Upgrade Purchase' }, + { label: 'In-flight Entertainment', value: 'In-flight Entertainment' }, + { label: 'Airport Transfer', value: 'Airport Transfer' }, + { label: 'Chauffeur Service', value: 'Chauffeur Service' }, + { label: 'Power Outlet', value: 'Power Outlet' }, + { label: 'Extra Legroom', value: 'Extra Legroom' }, + { label: 'Carbon Offset', value: 'Carbon Offset' }, + ], + scenarioSubTypes: { + 'Wi-Fi': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Fee Paid Service Non-delivery', value: 'Fee Paid Service Non-delivery' }, + { label: 'System Outage / Inoperative', value: 'System Outage' }, + { label: 'Slow Speed / Unusable', value: 'Slow Speed' }, + { label: 'Partial Flight Unavailable', value: 'Partial Flight Unavailable' }, + ], + 'Preferred Seat': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Seat Hardware Defect', value: 'Seat Hardware Defect' }, + { label: 'Involuntary Seat Reassignment', value: 'Involuntary Seat Reassignment' }, + { label: 'Non-Reclining Exit Row Seat', value: 'Non-Reclining Exit Row Seat' }, + { label: 'Extra Legroom Feature Defect', value: 'Extra Legroom Feature Defect' }, + ], + 'Lounge Access': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Overcapacity Entry Refusal', value: 'Overcapacity Entry Refusal' }, + { label: 'Operating Hours Premature Closure', value: 'Operating Hours Premature Closure' }, + { label: 'Partner Airline Lounge Access Denial', value: 'Partner Lounge Denial' }, + { label: 'Pass Registration System Error', value: 'Pass Registration System Error' }, + ], + 'Priority Boarding': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Gate Priority Not Honored', value: 'Gate Priority Not Honored' }, + { label: 'Boarding Zone Error', value: 'Boarding Zone Error' }, + { label: 'Late Jet Bridge Call', value: 'Late Jet Bridge Call' }, + ], + 'Fast Track Security': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Fast Track Lane Closed', value: 'Fast Track Lane Closed' }, + { label: 'Security Voucher Rejected', value: 'Security Voucher Rejected' }, + { label: 'Terminal Access Error', value: 'Terminal Access Error' }, + ], + 'Special Meal': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Meal Not Loaded', value: 'Meal Not Loaded' }, + { label: 'Incorrect Dietary Meal', value: 'Incorrect Dietary Meal' }, + { label: 'Contaminated / Spoiled', value: 'Contaminated Meal' }, + ], + 'Paid Meal': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Paid Meal Out of Stock', value: 'Paid Meal Out of Stock' }, + { label: 'Incorrect Meal Delivered', value: 'Incorrect Meal Delivered' }, + { label: 'Quality Substandard', value: 'Quality Substandard' }, + ], + 'Extra Baggage': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Baggage Left Behind', value: 'Baggage Left Behind' }, + { label: 'Overcharge Dispute', value: 'Overcharge Dispute' }, + { label: 'Priority Baggage Delayed', value: 'Priority Baggage Delayed' }, + ], + 'Sports Equipment': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Equipment Refused at Check-in', value: 'Equipment Refused at Check-in' }, + { label: 'Equipment Damaged', value: 'Equipment Damaged' }, + { label: 'Delayed Delivery', value: 'Delayed Delivery' }, + ], + 'Musical Instrument': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Cabin Stowage Refused', value: 'Cabin Stowage Refused' }, + { label: 'Involuntary Gate Check', value: 'Involuntary Gate Check' }, + { label: 'Transit Damage', value: 'Transit Damage' }, + ], + 'Upgrade Purchase': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Involuntary Cabin Downgrade', value: 'Involuntary Cabin Downgrade' }, + { label: 'Seat Feature Inoperative', value: 'Seat Feature Inoperative' }, + { label: 'Overbooked Premium Cabin', value: 'Overbooked Premium Cabin' }, + ], + 'In-flight Entertainment': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'In-Flight Screen Hardware Fault', value: 'Screen Hardware Fault' }, + { label: 'Audio / Headset Jack Fault', value: 'Audio Headset Fault' }, + { label: 'Content Library Inaccessible', value: 'Content Library Inaccessible' }, + ], + 'Airport Transfer': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Driver No-Show', value: 'Driver No-Show' }, + { label: 'Transfer Vehicle Delay (> 2 Hours)', value: 'Transfer Vehicle Delay' }, + { label: 'Vehicle Class Downgrade', value: 'Vehicle Class Downgrade' }, + ], + 'Chauffeur Service': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Chauffeur Missed Pickup', value: 'Chauffeur Missed Pickup' }, + { label: 'Vehicle Breakdown', value: 'Vehicle Breakdown' }, + { label: 'Unscheduled Cancellation', value: 'Unscheduled Cancellation' }, + ], + 'Power Outlet': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'In-Seat Power Outage', value: 'Power Outage' }, + { label: 'Low Voltage / Non-Functional', value: 'Low Voltage' }, + { label: 'Physical Port Broken', value: 'Physical Port Broken' }, + ], + 'Extra Legroom': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Involuntary Seat Reassignment', value: 'Involuntary Seat Reassignment' }, + { label: 'Obstructed Legroom', value: 'Obstructed Legroom' }, + { label: 'Aircraft Swap Downgrade', value: 'Aircraft Swap Downgrade' }, + ], + 'Carbon Offset': [ + { label: 'Purchased but Unavailable', value: 'Purchased but Unavailable' }, + { label: 'Offset Certificate Failed', value: 'Offset Certificate Failed' }, + { label: 'Billing Mismatch', value: 'Billing Mismatch' }, + ], + }, + fields: [ + { + id: 'serviceCost', + label: 'SERVICE COST', + type: 'number', + placeholder: '25', + defaultValue: '', + gridSpan: 2, + }, + { + id: 'delivered', + label: 'DELIVERED', + type: 'switch', + defaultValue: false, + gridSpan: 2, + }, + ], + defaultFormState: { + scenario: '', + scenarioSubType: '', + serviceCost: '', + delivered: false, + }, + passengerPool: [ + { + id: 'p-1', + name: 'Alexander Wright', + pnr: 'ANC-PNR1', + tier: 'Platinum', + passengerType: 'VIP Guest', + nationality: 'British', + ancillaryItem: 'Wi-Fi', + }, + { + id: 'p-2', + name: 'Sarah Jenkins', + pnr: 'ANC-PNR2', + tier: 'Platinum', + passengerType: 'Frequent Flyer', + nationality: 'British', + ancillaryItem: 'Wi-Fi', + }, + { + id: 'p-3', + name: 'The Miller Family', + pnr: 'ANC-PNR3', + tier: 'Gold', + passengerType: 'Family Group', + nationality: 'German', + ancillaryItem: 'Wi-Fi', + }, + { + id: 'p-4', + name: 'Marcus Chen', + pnr: 'ANC-PNR4', + tier: 'Silver', + passengerType: 'Solo Traveler', + nationality: 'American', + ancillaryItem: 'Wi-Fi', + }, + ], + manifestColumns: [ + { key: 'name', label: 'Passenger' }, + { key: 'tier', label: 'Tier & Type' }, + { key: 'ancillaryItem', label: 'Service / Item' }, + { key: 'comp', label: 'Compensation' }, + { key: 'refund', label: 'Refund' }, + ], + }, +}; diff --git a/src/components/custom/CustomInput.tsx b/src/components/custom/CustomInput.tsx index 8f8eebc..53d2f7a 100644 --- a/src/components/custom/CustomInput.tsx +++ b/src/components/custom/CustomInput.tsx @@ -75,33 +75,19 @@ const CustomInput = forwardRef( const validateInput = (val: string) => { const fieldLabel = label ? `"${label}"` : "This field"; - // 1. Max length / value check + // 1. Max length check (character count) if (effectiveMax !== undefined && effectiveMax > 0) { - if (type === "number") { - if (val !== "" && Number(val) > effectiveMax) { - setValidationError(`${fieldLabel} cannot be greater than ${effectiveMax}`); - return; - } - } else { - if (val.length >= effectiveMax) { - setValidationError(`${fieldLabel} cannot exceed ${effectiveMax} characters`); - return; - } + if (val.length > effectiveMax) { + setValidationError(`${fieldLabel} cannot exceed ${effectiveMax} characters`); + return; } } - // 2. Min length / value check + // 2. Min length check (character count) if (effectiveMin !== undefined && effectiveMin > 0) { - if (type === "number") { - if (val !== "" && Number(val) < effectiveMin) { - setValidationError(`${fieldLabel} must be at least ${effectiveMin}`); - return; - } - } else { - if (val.length > 0 && val.length < effectiveMin) { - setValidationError(`${fieldLabel} must be at least ${effectiveMin} characters`); - return; - } + if (val.length > 0 && val.length < effectiveMin) { + setValidationError(`${fieldLabel} must be at least ${effectiveMin} characters`); + return; } } diff --git a/src/components/custom/CustomTextArea.tsx b/src/components/custom/CustomTextArea.tsx index 20ef9d1..bfcdb31 100644 --- a/src/components/custom/CustomTextArea.tsx +++ b/src/components/custom/CustomTextArea.tsx @@ -64,7 +64,7 @@ const CustomTextArea = forwardRef( const fieldLabel = label ? `"${label}"` : "This field"; if (effectiveMax !== undefined && effectiveMax > 0) { - if (val.length >= effectiveMax) { + if (val.length > effectiveMax) { setValidationError(`${fieldLabel} cannot exceed ${effectiveMax} characters`); return; }
- PASSENGER - + {categoryConfig.manifestColumns.map((col) => ( + + {col.label} + ELIGIBILITY - REFUND - - COMP - - STATUS + PERKS
-
- - {passenger.name} - - - {passenger.pnr} • {passenger.tier} - -
-
+
+ + {passenger.name} + + + {passenger.nationality || 'Verified Customer'} + +
+
+ if (col.key === 'tier') { + return ( + + {passenger.tier} ({passenger.passengerType || 'Adult'}) + + {passenger.cabinClass || 'Economy'} + + {passenger.pnr} + + {passenger.bookingRef || '--'} + + {passenger.ancillaryItem || '--'} + + {passenger.refund} + + {passenger.comp} + + {passenger[col.key] || '--'} + - {passenger.refund} - - {passenger.comp} - - {passenger.statusBadges && passenger.statusBadges.length > 0 ? ( -
- {passenger.statusBadges.map((badge, idx) => ( + {passenger.perks && passenger.perks.length > 0 ? ( +
+ {passenger.perks.map((perk, idx) => ( - {badge} + {perk} ))}
@@ -633,7 +713,10 @@ export default function SimulationTerminal() { )) ) : (
+ No matching passengers found in this manifest simulation.