Merge pull request 'azeem' (#22) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_frontend/pulls/22 Reviewed-by: Syed Waseem khadri Rafai <waseem.khadri@maskatech.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import HomePage from './app/dashboard'
|
|||||||
import CohortManage from './app/cohartManage'
|
import CohortManage from './app/cohartManage'
|
||||||
import PolicyEngineList from './app/policyEngine/components/PolicyEngineList'
|
import PolicyEngineList from './app/policyEngine/components/PolicyEngineList'
|
||||||
import AddPolicyEngine from './app/policyEngine/components/AddPolicyEngine'
|
import AddPolicyEngine from './app/policyEngine/components/AddPolicyEngine'
|
||||||
|
import RecoveryIncidentsList from './app/recoveryIncidents/components/RecoveryIncidentsList'
|
||||||
|
import RecoveryIncidentTabs from './app/recoveryIncidents/tabs/index'
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
@@ -13,6 +15,8 @@ function AppRoutes() {
|
|||||||
<Route path="/cohorts" element={<CohortManage />} />
|
<Route path="/cohorts" element={<CohortManage />} />
|
||||||
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
||||||
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
||||||
|
<Route path="/recovery" element={<RecoveryIncidentsList />} />
|
||||||
|
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const ApiClient = axios.create({
|
|||||||
baseURL: API_BASE,
|
baseURL: API_BASE,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Id': 'demo-airline',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export default function AddPolicyEngine() {
|
|||||||
|
|
||||||
{/* ─── Body Content ──────────────────────────────────────────────── */}
|
{/* ─── Body Content ──────────────────────────────────────────────── */}
|
||||||
<div className="flex-1 overflow-y-auto px-8 py-6 pb-32">
|
<div className="flex-1 overflow-y-auto px-8 py-6 pb-32">
|
||||||
<div className="max-w-[1200px] mx-auto flex flex-col gap-6">
|
<div className="w-full flex flex-col gap-6">
|
||||||
|
|
||||||
{/* Policy Information Card */}
|
{/* Policy Information Card */}
|
||||||
<div className="bg-[#FAFAFA] rounded-[16px] p-6 border border-gray-100">
|
<div className="bg-[#FAFAFA] rounded-[16px] p-6 border border-gray-100">
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export interface IncidentStatus {
|
||||||
|
text: string;
|
||||||
|
variant: "success" | "error" | "warning" | "info" | "neutral" | "brand";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecoveryIncident {
|
||||||
|
id: string;
|
||||||
|
recoveryId: string;
|
||||||
|
date: string;
|
||||||
|
passengerName?: string;
|
||||||
|
pnr?: string;
|
||||||
|
flightNumber: string;
|
||||||
|
flightRoute: string;
|
||||||
|
category?: string;
|
||||||
|
statuses: IncidentStatus[];
|
||||||
|
value: string;
|
||||||
|
isGroupHeader?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { FileText, User, Airplane, WarningCircle, CaretRight } from '@phosphor-icons/react';
|
||||||
|
import {
|
||||||
|
CustomModal,
|
||||||
|
CustomInput,
|
||||||
|
CustomDropdown,
|
||||||
|
} from "../../../components/custom";
|
||||||
|
|
||||||
|
interface AddRecoveryIncidentsProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_TITLE_CLASS = "flex items-center gap-2 mb-4 text-[#4A5568] font-bold text-xs tracking-wider uppercase";
|
||||||
|
const SECTION_CONTAINER_CLASS = "bg-[#F9FAFB] rounded-[14px] p-5 border border-gray-100";
|
||||||
|
|
||||||
|
export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryIncidentsProps) {
|
||||||
|
return (
|
||||||
|
<CustomModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="New Recovery Incident"
|
||||||
|
description="Log a disruption case and assess against policy frameworks"
|
||||||
|
icon={<FileText className="text-[#1B9869]" />}
|
||||||
|
size="lg"
|
||||||
|
primaryAction={{
|
||||||
|
label: "Assess & Log Incident",
|
||||||
|
onClick: onClose,
|
||||||
|
icon: <CaretRight size={16} />
|
||||||
|
}}
|
||||||
|
secondaryAction={{
|
||||||
|
label: "Discard",
|
||||||
|
onClick: onClose,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{/* PASSENGER IDENTITY */}
|
||||||
|
<div className={SECTION_CONTAINER_CLASS}>
|
||||||
|
<div className={SECTION_TITLE_CLASS}>
|
||||||
|
<User size={16} />
|
||||||
|
<span>Passenger Identity</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<CustomInput
|
||||||
|
label="Full Name"
|
||||||
|
placeholder="e.g. John Doe"
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
label="PNR Reference"
|
||||||
|
placeholder="e.g. FT7687T9I"
|
||||||
|
/>
|
||||||
|
<CustomDropdown
|
||||||
|
label="Loyalty Tier"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "Gold", value: "gold" },
|
||||||
|
{ label: "Silver", value: "silver" },
|
||||||
|
{ label: "Bronze", value: "bronze" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FLIGHT CONTEXT */}
|
||||||
|
<div className={SECTION_CONTAINER_CLASS}>
|
||||||
|
<div className={SECTION_TITLE_CLASS}>
|
||||||
|
<Airplane size={16} />
|
||||||
|
<span>Flight Context</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<CustomDropdown
|
||||||
|
label="Flight Number"
|
||||||
|
placeholder="e.g. B7687YT"
|
||||||
|
options={[
|
||||||
|
{ label: "B7687YT", value: "B7687YT" },
|
||||||
|
{ label: "Q23SXD", value: "Q23SXD" },
|
||||||
|
{ label: "AZ404", value: "AZ404" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
type="date"
|
||||||
|
label="Date"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
/>
|
||||||
|
<CustomDropdown
|
||||||
|
label="Origin (IATA)"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "FRA", value: "FRA" },
|
||||||
|
{ label: "LHR", value: "LHR" },
|
||||||
|
{ label: "SFO", value: "SFO" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<CustomDropdown
|
||||||
|
label="Destination (IATA)"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "JFK", value: "JFK" },
|
||||||
|
{ label: "CDG", value: "CDG" },
|
||||||
|
{ label: "NRT", value: "NRT" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DISRUPTION & JURISDICTION */}
|
||||||
|
<div className={SECTION_CONTAINER_CLASS}>
|
||||||
|
<div className={SECTION_TITLE_CLASS}>
|
||||||
|
<WarningCircle size={16} />
|
||||||
|
<span>Disruption & Jurisdiction</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<CustomDropdown
|
||||||
|
label="Category"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "Flight Ops", value: "flight_ops" },
|
||||||
|
{ label: "Travel Exp", value: "travel_exp" },
|
||||||
|
{ label: "Weather", value: "weather" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<CustomDropdown
|
||||||
|
label="Scenario"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "Delay", value: "delay" },
|
||||||
|
{ label: "Cancellation", value: "cancellation" },
|
||||||
|
{ label: "Denied Boarding", value: "denied_boarding" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<CustomDropdown
|
||||||
|
label="Jurisdiction"
|
||||||
|
placeholder="Selected Option"
|
||||||
|
options={[
|
||||||
|
{ label: "EU261", value: "eu261" },
|
||||||
|
{ label: "US DOT", value: "us_dot" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
type="number"
|
||||||
|
label="Delay Duration (Mins)"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CustomModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
MagnifyingGlass,
|
||||||
|
CaretUp,
|
||||||
|
CaretDown,
|
||||||
|
DotsThreeVertical,
|
||||||
|
FadersHorizontal,
|
||||||
|
SquaresFour,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import {
|
||||||
|
CustomTable,
|
||||||
|
CustomInput,
|
||||||
|
CustomButton,
|
||||||
|
CustomStatus,
|
||||||
|
CustomCheckBox,
|
||||||
|
Skeleton,
|
||||||
|
} from "../../../components/custom";
|
||||||
|
import type { Column } from "../../../components/custom/CustomTable";
|
||||||
|
import type { RecoveryIncident } from "../RecoveryIncidentsTypes";
|
||||||
|
import AddRecoveryIncidents from "./AddRecoveryIncidents";
|
||||||
|
|
||||||
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
// ─── Mock Data ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MOCK_INCIDENTS: RecoveryIncident[] = [
|
||||||
|
// GROUP 1: Q23SXD (FRA -> JFK)
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
recoveryId: "juD4IpfxKVzIyU0bogQ2",
|
||||||
|
date: "4 Jun 2026, 4:09pm",
|
||||||
|
flightNumber: "Q23SXD",
|
||||||
|
flightRoute: "FRA → JFK",
|
||||||
|
statuses: [
|
||||||
|
{ text: "0 Pending", variant: "warning" },
|
||||||
|
{ text: "0 App", variant: "success" },
|
||||||
|
],
|
||||||
|
value: "$400",
|
||||||
|
isGroupHeader: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
recoveryId: "juD4IpfxKVzIyU0bogQ2",
|
||||||
|
date: "4 Jun 2026, 4:09pm",
|
||||||
|
passengerName: "John Doe",
|
||||||
|
pnr: "XZW34R",
|
||||||
|
flightNumber: "Q23SXD",
|
||||||
|
flightRoute: "FRA → JFK",
|
||||||
|
category: "Travel Exp",
|
||||||
|
statuses: [{ text: "Active", variant: "success" }],
|
||||||
|
value: "$200",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
recoveryId: "juD4IpfxKVzIyU0bogQ2",
|
||||||
|
date: "4 Jun 2026, 4:09pm",
|
||||||
|
passengerName: "Jane Smith",
|
||||||
|
pnr: "ABC12D",
|
||||||
|
flightNumber: "Q23SXD",
|
||||||
|
flightRoute: "FRA → JFK",
|
||||||
|
category: "Travel Exp",
|
||||||
|
statuses: [{ text: "Active", variant: "success" }],
|
||||||
|
value: "$200",
|
||||||
|
},
|
||||||
|
|
||||||
|
// GROUP 2: AZ404 (LHR -> CDG)
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
recoveryId: "aZ99LmNoPRtUyX1xyzF3",
|
||||||
|
date: "5 Jun 2026, 9:15am",
|
||||||
|
flightNumber: "AZ404",
|
||||||
|
flightRoute: "LHR → CDG",
|
||||||
|
statuses: [
|
||||||
|
{ text: "1 Pending", variant: "warning" },
|
||||||
|
{ text: "1 App", variant: "success" },
|
||||||
|
],
|
||||||
|
value: "$550",
|
||||||
|
isGroupHeader: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
recoveryId: "aZ99LmNoPRtUyX1xyzF3",
|
||||||
|
date: "5 Jun 2026, 9:15am",
|
||||||
|
passengerName: "Michael Chang",
|
||||||
|
pnr: "LMN89P",
|
||||||
|
flightNumber: "AZ404",
|
||||||
|
flightRoute: "LHR → CDG",
|
||||||
|
category: "Flight Ops",
|
||||||
|
statuses: [{ text: "Pending", variant: "warning" }],
|
||||||
|
value: "$300",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
recoveryId: "aZ99LmNoPRtUyX1xyzF3",
|
||||||
|
date: "5 Jun 2026, 9:15am",
|
||||||
|
passengerName: "Sarah Connor",
|
||||||
|
pnr: "QWE45T",
|
||||||
|
flightNumber: "AZ404",
|
||||||
|
flightRoute: "LHR → CDG",
|
||||||
|
category: "Flight Ops",
|
||||||
|
statuses: [{ text: "Active", variant: "success" }],
|
||||||
|
value: "$250",
|
||||||
|
},
|
||||||
|
|
||||||
|
// GROUP 3: UA990 (SFO -> NRT)
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
recoveryId: "uA77KhJxWVyTz2abcD4",
|
||||||
|
date: "6 Jun 2026, 2:30pm",
|
||||||
|
flightNumber: "UA990",
|
||||||
|
flightRoute: "SFO → NRT",
|
||||||
|
statuses: [
|
||||||
|
{ text: "0 Pending", variant: "warning" },
|
||||||
|
{ text: "0 App", variant: "success" },
|
||||||
|
],
|
||||||
|
value: "$800",
|
||||||
|
isGroupHeader: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
recoveryId: "uA77KhJxWVyTz2abcD4",
|
||||||
|
date: "6 Jun 2026, 2:30pm",
|
||||||
|
passengerName: "Emily Davis",
|
||||||
|
pnr: "RTY56U",
|
||||||
|
flightNumber: "UA990",
|
||||||
|
flightRoute: "SFO → NRT",
|
||||||
|
category: "Travel Exp",
|
||||||
|
statuses: [{ text: "Active", variant: "success" }],
|
||||||
|
value: "$400",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
recoveryId: "uA77KhJxWVyTz2abcD4",
|
||||||
|
date: "6 Jun 2026, 2:30pm",
|
||||||
|
passengerName: "David Miller",
|
||||||
|
pnr: "IOP78J",
|
||||||
|
flightNumber: "UA990",
|
||||||
|
flightRoute: "SFO → NRT",
|
||||||
|
category: "Travel Exp",
|
||||||
|
statuses: [{ text: "Active", variant: "success" }],
|
||||||
|
value: "$400",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function HeaderLabel({
|
||||||
|
text,
|
||||||
|
rightIcon,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-[13px] font-semibold text-[#6C766D] tracking-[0px]">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
{rightIcon && <span className="text-[#6C766D]">{rightIcon}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrimaryText({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="text-[13px] font-semibold text-[#0F172B] leading-[18px] tracking-[0px]">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SecondaryText({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="text-[12px] font-medium text-[#6C766D] leading-[16px] tracking-[0px] mt-0.5">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BadgeLabel({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 bg-gray-100 text-gray-500 rounded-full text-xs font-semibold">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Metric Card Component ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string;
|
||||||
|
value?: string;
|
||||||
|
trendText: string;
|
||||||
|
trendValue: string;
|
||||||
|
trendType: "positive" | "negative" | "neutral";
|
||||||
|
sparklineColor: "green" | "red";
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
trendText,
|
||||||
|
trendValue,
|
||||||
|
trendType,
|
||||||
|
sparklineColor,
|
||||||
|
}: MetricCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#F8F9FA] rounded-[16px] p-5 flex flex-col gap-4 flex-1 min-w-[220px]">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-[14px] font-semibold text-gray-700">{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{value && (
|
||||||
|
<span className="text-[28px] font-bold text-gray-900 leading-none">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<span
|
||||||
|
className={`text-[12px] font-bold ${
|
||||||
|
trendType === "positive"
|
||||||
|
? "text-[#1B9869]"
|
||||||
|
: trendType === "negative"
|
||||||
|
? "text-red-500"
|
||||||
|
: "text-gray-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{trendValue}
|
||||||
|
</span>
|
||||||
|
<span className="text-[12px] font-medium text-gray-500">
|
||||||
|
{trendText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simple SVG Sparkline placeholder based on color */}
|
||||||
|
<div className="w-[60px] h-[30px] flex items-center justify-end">
|
||||||
|
{sparklineColor === "green" ? (
|
||||||
|
<svg
|
||||||
|
width="60"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 60 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 20C10 20 12 12 20 12C28 12 32 18 40 18C48 18 52 4 58 4"
|
||||||
|
stroke="#1B9869"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="60"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 60 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 4C10 4 12 12 20 12C28 12 32 6 40 6C48 6 52 20 58 20"
|
||||||
|
stroke="#EF4444"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function RecoveryIncidentsList() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [incidents, setIncidents] = useState<RecoveryIncident[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [totalItems, setTotalItems] = useState(0);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [isGrouped, setIsGrouped] = useState(true);
|
||||||
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// ─── Fetch data ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const fetchIncidents = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Simulate API call with timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
const filteredData = MOCK_INCIDENTS.filter((p) => {
|
||||||
|
const matchesSearch = p.recoveryId.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
p.flightNumber.toLowerCase().includes(search.toLowerCase());
|
||||||
|
const matchesGroup = isGrouped ? true : !p.isGroupHeader;
|
||||||
|
return matchesSearch && matchesGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = filteredData.length;
|
||||||
|
const pages = Math.ceil(total / PAGE_SIZE);
|
||||||
|
const start = (page - 1) * PAGE_SIZE;
|
||||||
|
const paginatedData = filteredData.slice(start, start + PAGE_SIZE);
|
||||||
|
|
||||||
|
setIncidents(paginatedData);
|
||||||
|
setTotalItems(total);
|
||||||
|
setTotalPages(pages || 1);
|
||||||
|
setLoading(false);
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[search, isGrouped],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIncidents(currentPage);
|
||||||
|
}, [currentPage, search, isGrouped, fetchIncidents]);
|
||||||
|
|
||||||
|
// ─── Handlers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (val: string) => {
|
||||||
|
setSearch(val);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selectedIds.size === incidents.length && incidents.length > 0) {
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set(incidents.map((i) => i.id)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectOne = (id: string) => {
|
||||||
|
const newSelected = new Set(selectedIds);
|
||||||
|
if (newSelected.has(id)) {
|
||||||
|
newSelected.delete(id);
|
||||||
|
} else {
|
||||||
|
newSelected.add(id);
|
||||||
|
}
|
||||||
|
setSelectedIds(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (id: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCollapsedGroups((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
|
||||||
|
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
|
||||||
|
|
||||||
|
const displayData = useMemo(() => {
|
||||||
|
if (!isGrouped) return incidents;
|
||||||
|
return incidents.filter(
|
||||||
|
(p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryId)
|
||||||
|
);
|
||||||
|
}, [incidents, isGrouped, collapsedGroups]);
|
||||||
|
|
||||||
|
// ─── Table columns ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const columns: Column<RecoveryIncident>[] = [
|
||||||
|
{
|
||||||
|
header: (
|
||||||
|
<CustomCheckBox
|
||||||
|
checked={
|
||||||
|
incidents.length > 0 && selectedIds.size === incidents.length
|
||||||
|
}
|
||||||
|
onChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
className: "w-[40px] pr-0",
|
||||||
|
accessor: (row) => row.isGroupHeader ? null : (
|
||||||
|
<CustomCheckBox
|
||||||
|
checked={selectedIds.has(row.id)}
|
||||||
|
onChange={() => toggleSelectOne(row.id)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <HeaderLabel text="Recovery ID" />,
|
||||||
|
accessor: (row) => (
|
||||||
|
<div>
|
||||||
|
<PrimaryText text={row.recoveryId} />
|
||||||
|
<SecondaryText text={row.date} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: (
|
||||||
|
<HeaderLabel
|
||||||
|
text="Passenger / PNR"
|
||||||
|
rightIcon={<FadersHorizontal size={12} className="rotate-90" />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
accessor: (row) =>
|
||||||
|
row.passengerName ? (
|
||||||
|
<div>
|
||||||
|
<PrimaryText text={row.passengerName} />
|
||||||
|
<SecondaryText text={row.pnr || ""} />
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: (
|
||||||
|
<HeaderLabel
|
||||||
|
text="Flight"
|
||||||
|
rightIcon={<FadersHorizontal size={12} className="rotate-90" />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
accessor: (row) => (
|
||||||
|
<div>
|
||||||
|
<PrimaryText text={row.flightNumber} />
|
||||||
|
<SecondaryText text={row.flightRoute} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <HeaderLabel text="Category" />,
|
||||||
|
accessor: (row) =>
|
||||||
|
row.category ? <BadgeLabel text={row.category} /> : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <HeaderLabel text="Status" />,
|
||||||
|
accessor: (row) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{row.statuses.map((status, idx) => (
|
||||||
|
<CustomStatus
|
||||||
|
key={idx}
|
||||||
|
status={status.text}
|
||||||
|
variant={status.variant}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: (
|
||||||
|
<HeaderLabel
|
||||||
|
text="Value"
|
||||||
|
rightIcon={<FadersHorizontal size={12} className="rotate-90" />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
accessor: (row) => <PrimaryText text={row.value} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <HeaderLabel text="Action" />,
|
||||||
|
className: "text-right",
|
||||||
|
accessor: (row) => (
|
||||||
|
<div className="flex justify-end pr-2">
|
||||||
|
{row.isGroupHeader ? (
|
||||||
|
<div
|
||||||
|
className="cursor-pointer text-gray-500 hover:text-gray-900 transition-colors p-1"
|
||||||
|
onClick={(e) => toggleGroup(row.recoveryId, e)}
|
||||||
|
>
|
||||||
|
{collapsedGroups.has(row.recoveryId) ? (
|
||||||
|
<CaretDown size={20} />
|
||||||
|
) : (
|
||||||
|
<CaretUp size={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="cursor-pointer text-gray-500 hover:text-gray-900 transition-colors p-1">
|
||||||
|
<DotsThreeVertical size={20} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Loading skeleton ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col bg-white rounded-[20px] shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||||
|
<Skeleton width={320} height={36} />
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton width={148} height={36} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 flex flex-col gap-4">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} height={52} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Render ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-6">
|
||||||
|
{/* Metrics Row */}
|
||||||
|
<div className="flex gap-4 w-full">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Recoveries"
|
||||||
|
value="1,284"
|
||||||
|
trendValue="40%"
|
||||||
|
trendText="since last week"
|
||||||
|
trendType="positive"
|
||||||
|
sparklineColor="green"
|
||||||
|
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Pending Approval"
|
||||||
|
trendValue="High Priority"
|
||||||
|
trendText="since last week"
|
||||||
|
trendType="positive"
|
||||||
|
value="274"
|
||||||
|
sparklineColor="green"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Refund Value"
|
||||||
|
value="$412k"
|
||||||
|
trendValue="MTD"
|
||||||
|
trendText="since last week"
|
||||||
|
trendType="negative"
|
||||||
|
sparklineColor="red"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Customer Satisfaction"
|
||||||
|
value="94%"
|
||||||
|
trendValue="+2.1%"
|
||||||
|
trendText="since last week"
|
||||||
|
trendType="positive"
|
||||||
|
sparklineColor="green"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<CustomTable<RecoveryIncident>
|
||||||
|
columns={columns}
|
||||||
|
data={displayData}
|
||||||
|
leftHeaderActions={
|
||||||
|
<div className="w-[320px]">
|
||||||
|
<CustomInput
|
||||||
|
placeholder="Search framework registry..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
leftIcon={<MagnifyingGlass size={16} />}
|
||||||
|
className="!bg-[#F3F6F5] !rounded-[10px] !h-[40px] !border !border-[#E5E7EB]"
|
||||||
|
containerClassName="!gap-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightHeaderActions={
|
||||||
|
<>
|
||||||
|
<CustomButton
|
||||||
|
variant="outlined"
|
||||||
|
size="md"
|
||||||
|
leftIcon={<FadersHorizontal size={16} />}
|
||||||
|
className="!rounded-[10px] !gap-[8px] !h-[40px] !border-primary !text-primary hover:!bg-primary/5"
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
variant="outlined"
|
||||||
|
size="md"
|
||||||
|
leftIcon={<SquaresFour size={16} />}
|
||||||
|
className="!rounded-[10px] !gap-[8px] !h-[40px] !border-primary !text-primary hover:!bg-primary/5"
|
||||||
|
onClick={() => setIsGrouped(!isGrouped)}
|
||||||
|
>
|
||||||
|
{isGrouped ? "Ungroup" : "Group by Flight"}
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
leftIcon={<Plus size={16} />}
|
||||||
|
className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
New Incident
|
||||||
|
</CustomButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={totalItems}
|
||||||
|
startIndex={startIndex}
|
||||||
|
endIndex={endIndex}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
itemName="Policies"
|
||||||
|
onRowClick={(row) => !row.isGroupHeader && navigate(`/recovery/${row.id}`)}
|
||||||
|
rowClassName={(row) => row.isGroupHeader ? "bg-white" : "bg-[#F9FAFB] border-transparent cursor-pointer hover:bg-[#F3F4F6]"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<AddRecoveryIncidents
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { ClipboardTextIcon, CheckCircleIcon } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
export default function AuditTrailTab() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ClipboardTextIcon size={20} weight="bold" className="text-[#143d30]" />
|
||||||
|
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider uppercase">Lifecycle Timeline & Audit Trail</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col mt-4 pl-2">
|
||||||
|
{/* Step 1: Flight Disruption Recorded */}
|
||||||
|
<div className="relative pl-10 pb-8">
|
||||||
|
<div className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-[#1B9869]"></div>
|
||||||
|
<div className="absolute left-[-4px] top-0.5 bg-white">
|
||||||
|
<CheckCircleIcon size={24} weight="fill" className="text-[#1B9869]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Flight Disruption Recorded</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Denied Boarding identified for flight Q23SXD.</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">Assessment Point</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2: Simulation Engine Executed */}
|
||||||
|
<div className="relative pl-10 pb-8">
|
||||||
|
<div className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-gray-200"></div>
|
||||||
|
<div className="absolute left-0 top-1 w-4 h-4 rounded-full bg-[#1B9869] ring-4 ring-[#E5F0EB]"></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Simulation Engine Executed</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Automated eligibility assessment performed against active frameworks.</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">T-10m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 3: Policy Evaluated */}
|
||||||
|
<div className="relative pl-10 pb-8">
|
||||||
|
<div className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-gray-200"></div>
|
||||||
|
<div className="absolute left-[-2px] top-1 w-5 h-5 rounded-full border-2 border-gray-200 bg-white"></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Policy Evaluated</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Pending final approval from Case Officer.</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">T-8m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 4: Status: Under Review */}
|
||||||
|
<div className="relative pl-10 pb-8">
|
||||||
|
<div className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-gray-200"></div>
|
||||||
|
<div className="absolute left-[-2px] top-1 w-5 h-5 rounded-full border-2 border-gray-200 bg-white"></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Status: Under Review</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Tuesday, 28 May 2024</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">Current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 5: Policy Engine Rerun */}
|
||||||
|
<div className="relative pl-10 pb-8">
|
||||||
|
<div className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-gray-200"></div>
|
||||||
|
<div className="absolute left-[-2px] top-1 w-5 h-5 rounded-full border-2 border-gray-200 bg-white"></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Policy Engine Rerun</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Manual re-assessment triggered. Applied: Standard Policy.</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">Recent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 6: Recovery Resolution */}
|
||||||
|
<div className="relative pl-10">
|
||||||
|
<div className="absolute left-[-2px] top-1 w-5 h-5 rounded-full border-2 border-gray-200 bg-white"></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-1">Recovery Resolution</h4>
|
||||||
|
<p className="text-[13px] text-gray-500">Refund and compensation settlement will initiate upon final approval.</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider">Pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { IdentificationCardIcon, AirplaneTiltIcon, WarningCircleIcon } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
export default function CaseDetailsTab() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* PASSENGER INFORMATION */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IdentificationCardIcon size={20} weight="bold" className="text-gray-700" />
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 tracking-wider">PASSENGER INFORMATION</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">FULL NAME</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">JOHN WICK</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">PNR / REFERENCE</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">XZW34R</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">LOYALTY TIER</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">None</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">PASSENGER TYPE</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Adult</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FLIGHT JOURNEY */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AirplaneTiltIcon size={20} weight="bold" className="text-gray-700" />
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 tracking-wider">FLIGHT JOURNEY</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-y-6 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">FLIGHT NUMBER</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Q23SXD</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SECTOR</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">FRA → JFK</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">CABIN</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">Economy</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">DELAY (ARRIVAL)</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">--</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">ORIGINAL CABIN</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Economy</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">ACTUAL CABIN</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Economy</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DISRUPTION DETAILS */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WarningCircleIcon size={20} weight="bold" className="text-gray-700" />
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 tracking-wider">DISRUPTION DETAILS</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">DISRUPTION CATEGORY</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Travel Exp</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SCENARIO</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Denied Boarding</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SUB-TYPE</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">None</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">ROOT CAUSE ANALYSIS</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">
|
||||||
|
Operational issues resulting in service disruption. Analysis pending manual confirmation.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { CreditCardIcon, GiftIcon, HandHeartIcon } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
export default function RecoveryPlanTab() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* FINANCIAL REFUND */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CreditCardIcon size={20} weight="bold" className="text-[#143d30]" />
|
||||||
|
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">FINANCIAL REFUND</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">REFUND AMOUNT</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">EUR 0</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">REFUND STATUS</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Pending Approval</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">REFUND METHOD</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Original Payment Method</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COMPENSATION & PERKS */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GiftIcon size={20} weight="bold" className="text-[#143d30]" />
|
||||||
|
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">COMPENSATION & PERKS</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">CASH COMPENSATION</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">EUR 0</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">VOUCHER ALTERNATIVE</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Available (120%)</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">LOYALTY MILES</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">5,000 Points (Bonus)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PASSENGER CARE */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HandHeartIcon size={20} weight="bold" className="text-[#143d30]" />
|
||||||
|
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">PASSENGER CARE</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">MEAL VOUCHERS</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-[#1B9869]">2 x $15.00 Issued</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">HOTEL ACCOMMODATION</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">1 Night (Pending)</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">GROUND TRANSPORT</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">Airport to City Center</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { SparkleIcon, UserIcon, ArrowRightIcon } from "@phosphor-icons/react";
|
||||||
|
import { CustomButton } from "../../../components/custom";
|
||||||
|
|
||||||
|
export default function SummaryTab() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* AI STRATEGIC ASSESSMENT */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm relative overflow-hidden">
|
||||||
|
{/* Subtle decorative glow */}
|
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-[#1B9869]/5 rounded-full blur-2xl -mr-16 -mt-16 pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<span className="text-[#1B9869]">
|
||||||
|
<SparkleIcon size={20} weight="fill" />
|
||||||
|
</span>
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-800 tracking-wider">
|
||||||
|
AI STRATEGIC ASSESSMENT
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[15px] text-gray-700 italic leading-relaxed">
|
||||||
|
"Analysis of JOHN WICK's history and the flight disruption suggest
|
||||||
|
this is a high-retention opportunity. Automated settlement is
|
||||||
|
recommended to maintain NPS within the Platinum segment."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row of 3 Cards */}
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{/* INCIDENT ROOT */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-5">
|
||||||
|
<h3 className="text-[13px] font-bold text-[#1B9869] tracking-wider">
|
||||||
|
INCIDENT ROOT
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-[#1B9869] tracking-wider mb-1">
|
||||||
|
Recovery Source
|
||||||
|
</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">
|
||||||
|
Simulation Engine
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-[#1B9869] tracking-wider mb-1">
|
||||||
|
Policy Applied
|
||||||
|
</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">
|
||||||
|
Standard Policy
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="block text-[11px] font-bold text-[#1B9869] tracking-wider mb-1">
|
||||||
|
Jurisdiction
|
||||||
|
</span>
|
||||||
|
<span className="block text-[15px] font-semibold text-gray-900">
|
||||||
|
EU261
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ASSIGNMENT */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-5">
|
||||||
|
<h3 className="text-[13px] font-bold text-[#1B9869] tracking-wider">
|
||||||
|
ASSIGNMENT
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 bg-white/40 backdrop-blur-[2px] z-10 flex items-center justify-center">
|
||||||
|
<h4 className="text-xl font-bold text-gray-900 italic">
|
||||||
|
"Coming soon"
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-30 pointer-events-none">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-400">
|
||||||
|
<UserIcon size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900">Emma Watson</div>
|
||||||
|
<div className="text-xs text-gray-500">Case Officer</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomButton
|
||||||
|
variant="outlined"
|
||||||
|
leftIcon={<ArrowRightIcon size={16} />}
|
||||||
|
className="w-full !border-gray-200 !text-gray-600 !font-semibold hover:!bg-gray-50"
|
||||||
|
>
|
||||||
|
Change Assignee
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RECOVERY SCORE */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-5">
|
||||||
|
<h3 className="text-[13px] font-bold text-[#1B9869] tracking-wider">
|
||||||
|
RECOVERY SCORE
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-1 mt-2 mb-2">
|
||||||
|
<span className="text-[48px] font-bold text-gray-900 leading-none">
|
||||||
|
75
|
||||||
|
</span>
|
||||||
|
<span className="text-xl text-gray-400 font-semibold">/ 100</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 leading-relaxed">
|
||||||
|
Manual review recommended. Aligns with standard EU261 recovery
|
||||||
|
logic.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default function RecoveryIncidentTabs() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [activeTab, setActiveTab] = useState('Summary');
|
||||||
|
|
||||||
|
const tabItems = [
|
||||||
|
{
|
||||||
|
id: 'Summary',
|
||||||
|
label: 'Summary',
|
||||||
|
content: <SummaryTab />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Case Details',
|
||||||
|
label: 'Case Details',
|
||||||
|
content: <CaseDetailsTab />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Recovery Plan',
|
||||||
|
label: 'Recovery Plan',
|
||||||
|
content: <RecoveryPlanTab />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Audit Trail',
|
||||||
|
label: 'Audit Trail',
|
||||||
|
content: <AuditTrailTab />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col h-full relative">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between pb-6 border-b border-gray-100">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CustomBackButton />
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">{id || 'kD6zO86KybbpgHhuRcun'}</h1>
|
||||||
|
<CustomStatus status="UNDER REVIEW" variant="warning" className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide" />
|
||||||
|
<CustomStatus status="HIGH PRIORITY" variant="error" className="uppercase !text-[11px] !px-2.5 !py-1 !bg-red-100 !text-red-800 !tracking-wide" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-500 ml-8">
|
||||||
|
<div className="flex items-center gap-1.5"><User size={16} /> JOHN WICK</div>
|
||||||
|
<span>•</span>
|
||||||
|
<div className="flex items-center gap-1.5"><AirplaneTiltIcon size={16} /> Q23SXD (FRA → JFK)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomButton
|
||||||
|
leftIcon={<ArrowsClockwiseIcon size={18} weight="bold" />}
|
||||||
|
className="!bg-[#1B9869] hover:!bg-[#14704E] !text-white !font-semibold !rounded-lg !px-5 !py-2.5"
|
||||||
|
>
|
||||||
|
RE-RUN ENGINE
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs Row */}
|
||||||
|
<div className="flex items-center justify-between py-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<CustomTabs
|
||||||
|
tabs={tabItems}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
contentClassName="!hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CustomButton variant="outlined" className="!border-[#1B9869] !text-[#1B9869] hover:!bg-green-50 !font-semibold !rounded-lg">
|
||||||
|
Share with Finance
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton variant="outlined" className="!border-[#1B9869] !text-[#1B9869] hover:!bg-green-50 !font-semibold !rounded-lg">
|
||||||
|
Export Report (PDF)
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex gap-6 items-start pb-24">
|
||||||
|
{/* Left Column - Tab Content */}
|
||||||
|
<div className="flex-1 bg-[#F8F9FA] rounded-[24px] p-3 border border-gray-100">
|
||||||
|
<CustomTabs
|
||||||
|
tabs={tabItems}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
tabListClassName="!hidden"
|
||||||
|
contentClassName="!mt-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Sidebar */}
|
||||||
|
<div className="w-[360px] flex-shrink-0 bg-[#F8F9FA] rounded-[16px] border border-gray-100 relative">
|
||||||
|
|
||||||
|
{/* Blur Overlay */}
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/20 backdrop-blur-[3px] rounded-[16px]">
|
||||||
|
<h4 className="text-[18px] font-bold text-[#143d30] italic">"Coming soon"</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar Content */}
|
||||||
|
<div className="p-6 select-none pointer-events-none">
|
||||||
|
<div className="flex items-center gap-2 mb-6">
|
||||||
|
<span className="text-[#1B9869]"><SparkleIcon size={20} height="fill" /></span>
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider">AI RECOMMENDATION</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-end mb-2">
|
||||||
|
<span className="text-xs font-bold text-gray-400 tracking-wider">SATISFACTION PREDICT</span>
|
||||||
|
<span className="text-sm font-bold text-[#1B9869]">84%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
||||||
|
<div className="h-full bg-[#1B9869] w-[84%] rounded-full opacity-60"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex justify-between items-end mb-2">
|
||||||
|
<span className="text-xs font-bold text-gray-400 tracking-wider">ESCALATION RISK</span>
|
||||||
|
<span className="text-sm font-bold text-blue-500">12%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
||||||
|
<div className="h-full bg-blue-500 w-[12%] rounded-full opacity-60"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-6 border-t border-gray-200">
|
||||||
|
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider mb-4">NEXT RECOMMENDED ACTION</h3>
|
||||||
|
<div className="bg-white rounded-xl p-5 mb-4 border border-gray-100 shadow-sm">
|
||||||
|
<p className="text-sm text-gray-500 text-center">Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomButton
|
||||||
|
disabled
|
||||||
|
rightIcon={<ArrowLeftIcon size={16} weight="bold" className="rotate-180" />}
|
||||||
|
className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
|
||||||
|
>
|
||||||
|
EXECUTE RECOMMENDATION
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Sticky Action Bar */}
|
||||||
|
<div className="sticky bottom-[-24px] -mx-8 px-8 py-4 bg-white/90 backdrop-blur-md border-t border-gray-100 flex justify-between items-center z-10 mt-auto shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
|
||||||
|
<div></div> {/* Spacer */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<CustomButton
|
||||||
|
variant="text"
|
||||||
|
leftIcon={<X size={18} weight="bold" />}
|
||||||
|
className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100"
|
||||||
|
>
|
||||||
|
REJECT RECOVERY
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
variant="text"
|
||||||
|
leftIcon={<ClockCounterClockwiseIcon size={18} weight="bold" />}
|
||||||
|
className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A]"
|
||||||
|
>
|
||||||
|
MARK FOR REVIEW
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
variant="primary"
|
||||||
|
leftIcon={<Checks size={18} weight="bold" />}
|
||||||
|
className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E]"
|
||||||
|
>
|
||||||
|
APPROVE RECOVERY
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ interface CustomTableProps<T> {
|
|||||||
|
|
||||||
// Table Props
|
// Table Props
|
||||||
onRowClick?: (row: T) => void;
|
onRowClick?: (row: T) => void;
|
||||||
|
rowClassName?: (row: T) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CustomTable<T>({
|
export function CustomTable<T>({
|
||||||
@@ -50,6 +51,7 @@ export function CustomTable<T>({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
itemName = "items",
|
itemName = "items",
|
||||||
onRowClick,
|
onRowClick,
|
||||||
|
rowClassName,
|
||||||
}: CustomTableProps<T>) {
|
}: CustomTableProps<T>) {
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
@@ -117,7 +119,7 @@ export function CustomTable<T>({
|
|||||||
<tr
|
<tr
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
onClick={() => onRowClick?.(row)}
|
onClick={() => onRowClick?.(row)}
|
||||||
className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors ${onRowClick ? "cursor-pointer" : ""}`}
|
className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors ${onRowClick ? "cursor-pointer" : ""} ${rowClassName ? rowClassName(row) : ""}`}
|
||||||
>
|
>
|
||||||
{columns.map((col, colIndex) => (
|
{columns.map((col, colIndex) => (
|
||||||
<td key={colIndex} className={`py-5 px-6 ${col.className || ''}`}>
|
<td key={colIndex} className={`py-5 px-6 ${col.className || ''}`}>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const CustomTabs: React.FC<CustomTabsProps> = ({
|
|||||||
className={`
|
className={`
|
||||||
relative inline-flex items-center justify-center gap-2 px-5 py-2 text-sm font-medium rounded-xl transition-all duration-200
|
relative inline-flex items-center justify-center gap-2 px-5 py-2 text-sm font-medium rounded-xl transition-all duration-200
|
||||||
${isActive
|
${isActive
|
||||||
? "bg-[#0B3B6A] text-white shadow-sm"
|
? "bg-gradient-to-b from-primary to-primary-dark text-white shadow-md shadow-primary/20 font-semibold text-white shadow-sm"
|
||||||
: "text-slate-500 hover:text-slate-800 hover:bg-slate-50"
|
: "text-slate-500 hover:text-slate-800 hover:bg-slate-50"
|
||||||
}
|
}
|
||||||
${tab.disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}
|
${tab.disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer"}
|
||||||
@@ -105,7 +105,7 @@ const CustomTabs: React.FC<CustomTabsProps> = ({
|
|||||||
className={`rounded-md px-1.5 py-0.5 text-[11px] font-bold leading-none flex items-center justify-center min-w-[22px] h-[22px] ${
|
className={`rounded-md px-1.5 py-0.5 text-[11px] font-bold leading-none flex items-center justify-center min-w-[22px] h-[22px] ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-white/20 text-white"
|
? "bg-white/20 text-white"
|
||||||
: "bg-[#0B3B6A] text-white"
|
: "bg-[#1B9869] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.badge}
|
{tab.badge}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
|
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
|
||||||
'/': { title: 'Dashboard', subtitle: 'Overview of system status and active incidents.' },
|
'/': { title: 'Dashboard', subtitle: 'Overview of system status and active incidents.' },
|
||||||
'/cohorts': { title: 'Cohort Management', subtitle: 'Dynamic passenger segmentation for targeted recovery and recovery intelligence.' },
|
'/cohorts': { title: 'Cohort Management', subtitle: 'Dynamic passenger segmentation for targeted recovery and recovery intelligence.' },
|
||||||
|
'/recovery': { title: 'Recovery Incidents', subtitle: 'Operational workspace for managing passenger disruption cases.' },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AppHeaderProps {
|
interface AppHeaderProps {
|
||||||
|
|||||||
+111
-61
@@ -1,27 +1,27 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
ShieldCheckIcon,
|
|
||||||
GearIcon,
|
|
||||||
ClockCounterClockwiseIcon,
|
|
||||||
CaretDoubleRightIcon,
|
|
||||||
SquaresFourIcon,
|
SquaresFourIcon,
|
||||||
FadersIcon,
|
FadersIcon,
|
||||||
ArrowsClockwiseIcon,
|
ArrowsClockwiseIcon,
|
||||||
UsersFourIcon,
|
UsersFourIcon,
|
||||||
CaretDoubleLeftIcon,
|
CaretDoubleLeftIcon,
|
||||||
|
QuestionIcon,
|
||||||
SignOutIcon,
|
SignOutIcon,
|
||||||
QuestionIcon
|
GearIcon,
|
||||||
} from '@phosphor-icons/react';
|
ClockCounterClockwiseIcon,
|
||||||
|
CaretDoubleRightIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import { ShieldCheckIcon } from "lucide-react";
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ label: 'Dashboard', path: '/', icon: SquaresFourIcon },
|
{ label: "Dashboard", path: "/", icon: SquaresFourIcon },
|
||||||
{ label: 'Simulation Engine', path: '/simulation', icon: FadersIcon },
|
{ label: "Simulation Engine", path: "/simulation", icon: FadersIcon },
|
||||||
{ label: 'Recovery Incidents', path: '/recovery', icon: ArrowsClockwiseIcon , dot: true },
|
{ label: "Recovery Incidents", path: "/recovery", icon: ArrowsClockwiseIcon },
|
||||||
{ label: 'Cohort Management', path: '/cohorts', icon: UsersFourIcon },
|
{ label: "Cohort Management", path: "/cohorts", icon: UsersFourIcon },
|
||||||
{ label: 'Policy Engine', path: '/policy-engine', icon: ShieldCheckIcon },
|
{ label: "Policy Engine", path: "/policy-engine", icon: ShieldCheckIcon },
|
||||||
{ label: 'Configuration', path: '/config', icon: GearIcon },
|
{ label: "Configuration", path: "/config", icon: GearIcon },
|
||||||
{ label: 'Audit Logs', path: '/audit', icon: ClockCounterClockwiseIcon },
|
{ label: "Audit Logs", path: "/audit", icon: ClockCounterClockwiseIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AppSidebarProps {
|
interface AppSidebarProps {
|
||||||
@@ -37,47 +37,70 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
|
|||||||
<>
|
<>
|
||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-y-0 left-0 transform ${isOpen ? 'translate-x-0' : '-translate-x-full'} lg:relative lg:translate-x-0 z-50 ${isCollapsed ? 'w-[88px]' : 'w-[260px]'} h-screen flex flex-col bg-[#F4F7F6] font-sans transition-all duration-300 ease-in-out`}
|
className={`fixed inset-y-0 left-0 transform ${isOpen ? "translate-x-0" : "-translate-x-full"} lg:relative lg:translate-x-0 z-50 ${isCollapsed ? "w-[88px]" : "w-[260px]"} h-screen flex flex-col bg-[#F4F7F6] font-sans transition-all duration-300 ease-in-out`}
|
||||||
>
|
>
|
||||||
{/* Logo Area */}
|
{/* Logo Area */}
|
||||||
<div className={`pt-6 pb-4 flex items-center ${isCollapsed ? 'px-0 justify-center flex-col gap-4' : 'px-5 justify-between'}`}>
|
<div
|
||||||
|
className={`pt-6 pb-4 flex items-center ${isCollapsed ? "px-0 justify-center flex-col gap-4" : "px-5 justify-between"}`}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-[42px] h-[42px] bg-[#4B4B4B] rounded-[14px] flex flex-col items-center justify-center text-white shadow-sm shrink-0">
|
<div className="w-[42px] h-[42px] bg-[#4B4B4B] rounded-[14px] flex flex-col items-center justify-center text-white shadow-sm shrink-0">
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="mb-0.5">
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mb-0.5"
|
||||||
|
>
|
||||||
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.2-1.1.7l-1.2 3.3c-.2.5.1 1.1.6 1.2l6.9 1.7-2.9 2.9-3.6-.9c-.5-.1-.9.2-1.1.7l-1.3 3.5c-.2.5.1 1.1.6 1.2l12.4 3.1c.5.1.9-.2 1.1-.7l.8-2.3c.1-.5-.2-1.1-.7-1.2z" />
|
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.2-1.1.7l-1.2 3.3c-.2.5.1 1.1.6 1.2l6.9 1.7-2.9 2.9-3.6-.9c-.5-.1-.9.2-1.1.7l-1.3 3.5c-.2.5.1 1.1.6 1.2l12.4 3.1c.5.1.9-.2 1.1-.7l.8-2.3c.1-.5-.2-1.1-.7-1.2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div className="w-[18px] h-[2px] bg-white rounded-full"></div>
|
<div className="w-[18px] h-[2px] bg-white rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<span className="text-[17px] font-extrabold text-[#111827] tracking-tight whitespace-nowrap">Aero Resolve</span>
|
<span className="text-[17px] font-extrabold text-[#111827] tracking-tight whitespace-nowrap">
|
||||||
|
Aero Resolve
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 transition-colors lg:hidden">
|
<button
|
||||||
<CaretDoubleLeftIcon size={20} weight="bold" />
|
onClick={onClose}
|
||||||
</button>
|
className="text-slate-400 hover:text-slate-600 transition-colors lg:hidden"
|
||||||
<button
|
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
||||||
className="text-slate-400 hover:text-slate-600 transition-colors hidden lg:block"
|
|
||||||
>
|
>
|
||||||
{isCollapsed ? <CaretDoubleRightIcon size={20} weight="bold" /> : <CaretDoubleLeftIcon size={20} weight="bold" />}
|
<CaretDoubleLeftIcon size={20} weight="bold" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
className={`hidden lg:block text-slate-400 hover:text-slate-600 transition-colors ${isCollapsed ? "" : "ml-auto"}`}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<CaretDoubleRightIcon size={20} weight="bold" />
|
||||||
|
) : (
|
||||||
|
<CaretDoubleLeftIcon size={20} weight="bold" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className={`flex-1 py-3 space-y-1 overflow-y-auto overflow-x-hidden ${isCollapsed ? 'px-3' : 'px-4'}`}>
|
<nav
|
||||||
|
className={`flex-1 py-3 space-y-1 overflow-y-auto overflow-x-hidden ${isCollapsed ? "px-3" : "px-4"}`}
|
||||||
|
>
|
||||||
{NAV_ITEMS.map((item) => {
|
{NAV_ITEMS.map((item) => {
|
||||||
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
|
const isActive =
|
||||||
|
location.pathname === item.path ||
|
||||||
|
(item.path !== "/" && location.pathname.startsWith(item.path));
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
@@ -86,51 +109,78 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.innerWidth < 1024) onClose();
|
if (window.innerWidth < 1024) onClose();
|
||||||
}}
|
}}
|
||||||
className={`group flex items-center ${isCollapsed ? 'justify-center px-0 w-12 mx-auto' : 'gap-3 px-3.5'} py-[10px] rounded-[12px] text-[13px] transition-all duration-200 relative ${
|
className={`group flex items-center ${isCollapsed ? "justify-center px-0 w-12 mx-auto" : "gap-3 px-3.5"} py-[10px] rounded-[12px] text-[13px] transition-all duration-200 relative ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-gradient-to-b from-primary to-primary-dark text-white shadow-md shadow-primary/20 font-semibold'
|
? "bg-gradient-to-b from-primary to-primary-dark text-white shadow-md shadow-primary/20 font-semibold"
|
||||||
: 'text-[#475569] font-medium hover:bg-slate-200/40 hover:text-slate-900'
|
: "text-[#475569] font-medium hover:bg-slate-200/40 hover:text-slate-900"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
size={18}
|
size={18}
|
||||||
className={`${isActive ? 'text-white' : 'text-slate-600 group-hover:text-slate-800'} shrink-0`}
|
className={`${isActive ? "text-white" : "text-slate-600 group-hover:text-slate-800"} shrink-0`}
|
||||||
strokeWidth={isActive ? 2 : 1.5}
|
strokeWidth={isActive ? 2 : 1.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<span className="whitespace-nowrap">{item.label}</span>
|
<span className="whitespace-nowrap">{item.label}</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.dot && (
|
|
||||||
<div className={`${isCollapsed ? 'absolute top-2 right-2' : 'ml-auto'} w-1.5 h-1.5 rounded-full ${isActive ? 'bg-white' : 'bg-primary'}`} />
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Bottom Section Card */}
|
{/* Bottom Section Card */}
|
||||||
<div className={`mb-2 mt-2 bg-gradient-to-br from-[#FAFCFB] to-[#E3EFE9] border border-white rounded-[24px] shadow-[0_4px_12px_-4px_rgba(0,0,0,0.05)] relative overflow-hidden transition-all duration-300 ${isCollapsed ? 'mx-2 p-2 flex flex-col items-center gap-3' : 'mx-4 p-3'}`}>
|
<div
|
||||||
|
className={`mb-2 mt-2 bg-gradient-to-br from-[#FAFCFB] to-[#E3EFE9] border border-white rounded-[24px] shadow-[0_4px_12px_-4px_rgba(0,0,0,0.05)] relative overflow-hidden transition-all duration-300 ${isCollapsed ? "mx-2 p-2 flex flex-col items-center gap-3" : "mx-4 p-3"}`}
|
||||||
|
>
|
||||||
{/* Soft decorative glow */}
|
{/* Soft decorative glow */}
|
||||||
{!isCollapsed && <div className="absolute -top-10 -right-10 w-32 h-32 bg-white/60 rounded-full blur-2xl pointer-events-none" />}
|
{!isCollapsed && (
|
||||||
|
<div className="absolute -top-10 -right-10 w-32 h-32 bg-white/60 rounded-full blur-2xl pointer-events-none" />
|
||||||
<div className={`relative z-10 flex flex-col ${isCollapsed ? 'gap-2 w-full' : 'gap-0.5'}`}>
|
)}
|
||||||
<button title={isCollapsed ? "Sign Out" : undefined} className={`flex items-center ${isCollapsed ? 'justify-center px-0 h-10 w-full' : 'gap-3 px-3 py-2'} text-[13px] font-semibold text-[#E02424] hover:bg-red-50/50 rounded-[10px] transition-colors`}>
|
|
||||||
<SignOutIcon size={17} className="text-[#E02424] shrink-0" weight="bold" />
|
<div
|
||||||
{!isCollapsed && <span className="whitespace-nowrap">Sign Out</span>}
|
className={`relative z-10 flex flex-col ${isCollapsed ? "gap-2 w-full" : "gap-0.5"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title={isCollapsed ? "Sign Out" : undefined}
|
||||||
|
className={`flex items-center ${isCollapsed ? "justify-center px-0 h-10 w-full" : "gap-3 px-3 py-2"} text-[13px] font-semibold text-[#E02424] hover:bg-red-50/50 rounded-[10px] transition-colors`}
|
||||||
|
>
|
||||||
|
<SignOutIcon
|
||||||
|
size={17}
|
||||||
|
className="text-[#E02424] shrink-0"
|
||||||
|
weight="bold"
|
||||||
|
/>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="whitespace-nowrap">Sign Out</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button title={isCollapsed ? "Help Center" : undefined} className={`flex items-center ${isCollapsed ? 'justify-center px-0 h-10 w-full' : 'gap-3 px-3 py-2'} text-[13px] font-medium text-slate-700 hover:bg-white/40 rounded-[10px] transition-colors`}>
|
<button
|
||||||
<QuestionIcon size={17} className="text-slate-700 shrink-0" weight="regular" />
|
title={isCollapsed ? "Help Center" : undefined}
|
||||||
{!isCollapsed && <span className="whitespace-nowrap">Help center</span>}
|
className={`flex items-center ${isCollapsed ? "justify-center px-0 h-10 w-full" : "gap-3 px-3 py-2"} text-[13px] font-medium text-slate-700 hover:bg-white/40 rounded-[10px] transition-colors`}
|
||||||
|
>
|
||||||
|
<QuestionIcon
|
||||||
|
size={17}
|
||||||
|
className="text-slate-700 shrink-0"
|
||||||
|
weight="regular"
|
||||||
|
/>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="whitespace-nowrap">Help center</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div title={isCollapsed ? "Admin Demo" : undefined} className={`p-2 bg-white rounded-[16px] shadow-[0_2px_8px_-4px_rgba(0,0,0,0.08)] border border-white flex items-center cursor-pointer hover:shadow-md transition-shadow ${isCollapsed ? 'justify-center mt-1 w-full h-12' : 'gap-2.5 mt-2'}`}>
|
<div
|
||||||
|
title={isCollapsed ? "Admin Demo" : undefined}
|
||||||
|
className={`p-2 bg-white rounded-[16px] shadow-[0_2px_8px_-4px_rgba(0,0,0,0.08)] border border-white flex items-center cursor-pointer hover:shadow-md transition-shadow ${isCollapsed ? "justify-center mt-1 w-full h-12" : "gap-2.5 mt-2"}`}
|
||||||
|
>
|
||||||
<div className="w-8 h-8 rounded-full bg-[#C2D1E0] flex-shrink-0" />
|
<div className="w-8 h-8 rounded-full bg-[#C2D1E0] flex-shrink-0" />
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="flex flex-col justify-center overflow-hidden">
|
<div className="flex flex-col justify-center overflow-hidden">
|
||||||
<span className="text-[13px] font-bold text-[#111827] leading-tight truncate">Admin Demo</span>
|
<span className="text-[13px] font-bold text-[#111827] leading-tight truncate">
|
||||||
<span className="text-[10px] text-slate-500 font-medium mt-0.5 leading-tight truncate">System Administrator</span>
|
Admin Demo
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-500 font-medium mt-0.5 leading-tight truncate">
|
||||||
|
System Administrator
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user