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:
Syed Waseem khadri Rafai
2026-07-22 10:56:33 +00:00
16 changed files with 1488 additions and 65 deletions
+4
View File
@@ -4,6 +4,8 @@ import HomePage from './app/dashboard'
import CohortManage from './app/cohartManage'
import PolicyEngineList from './app/policyEngine/components/PolicyEngineList'
import AddPolicyEngine from './app/policyEngine/components/AddPolicyEngine'
import RecoveryIncidentsList from './app/recoveryIncidents/components/RecoveryIncidentsList'
import RecoveryIncidentTabs from './app/recoveryIncidents/tabs/index'
function AppRoutes() {
return (
@@ -13,6 +15,8 @@ function AppRoutes() {
<Route path="/cohorts" element={<CohortManage />} />
<Route path="/policy-engine" element={<PolicyEngineList />} />
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
<Route path="/recovery" element={<RecoveryIncidentsList />} />
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
</Routes>
</Layout>
)
+1
View File
@@ -7,6 +7,7 @@ export const ApiClient = axios.create({
baseURL: API_BASE,
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': 'demo-airline',
},
});
@@ -331,7 +331,7 @@ export default function AddPolicyEngine() {
{/* ─── Body Content ──────────────────────────────────────────────── */}
<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 */}
<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>
);
}
+179
View File
@@ -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>
);
}
+3 -1
View File
@@ -32,6 +32,7 @@ interface CustomTableProps<T> {
// Table Props
onRowClick?: (row: T) => void;
rowClassName?: (row: T) => string;
}
export function CustomTable<T>({
@@ -50,6 +51,7 @@ export function CustomTable<T>({
onPageChange,
itemName = "items",
onRowClick,
rowClassName,
}: CustomTableProps<T>) {
const handlePageChange = (newPage: number) => {
@@ -117,7 +119,7 @@ export function CustomTable<T>({
<tr
key={rowIndex}
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) => (
<td key={colIndex} className={`py-5 px-6 ${col.className || ''}`}>
+2 -2
View File
@@ -90,7 +90,7 @@ const CustomTabs: React.FC<CustomTabsProps> = ({
className={`
relative inline-flex items-center justify-center gap-2 px-5 py-2 text-sm font-medium rounded-xl transition-all duration-200
${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"
}
${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] ${
isActive
? "bg-white/20 text-white"
: "bg-[#0B3B6A] text-white"
: "bg-[#1B9869] text-white"
}`}
>
{tab.badge}
+1
View File
@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
'/': { title: 'Dashboard', subtitle: 'Overview of system status and active incidents.' },
'/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 {
+96 -46
View File
@@ -1,27 +1,27 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import {
ShieldCheckIcon,
GearIcon,
ClockCounterClockwiseIcon,
CaretDoubleRightIcon,
SquaresFourIcon,
FadersIcon,
ArrowsClockwiseIcon,
UsersFourIcon,
CaretDoubleLeftIcon,
QuestionIcon,
SignOutIcon,
QuestionIcon
} from '@phosphor-icons/react';
GearIcon,
ClockCounterClockwiseIcon,
CaretDoubleRightIcon,
} from "@phosphor-icons/react";
import { ShieldCheckIcon } from "lucide-react";
const NAV_ITEMS = [
{ label: 'Dashboard', path: '/', icon: SquaresFourIcon },
{ label: 'Simulation Engine', path: '/simulation', icon: FadersIcon },
{ label: 'Recovery Incidents', path: '/recovery', icon: ArrowsClockwiseIcon , dot: true },
{ label: 'Cohort Management', path: '/cohorts', icon: UsersFourIcon },
{ label: 'Policy Engine', path: '/policy-engine', icon: ShieldCheckIcon },
{ label: 'Configuration', path: '/config', icon: GearIcon },
{ label: 'Audit Logs', path: '/audit', icon: ClockCounterClockwiseIcon },
{ label: "Dashboard", path: "/", icon: SquaresFourIcon },
{ label: "Simulation Engine", path: "/simulation", icon: FadersIcon },
{ label: "Recovery Incidents", path: "/recovery", icon: ArrowsClockwiseIcon },
{ label: "Cohort Management", path: "/cohorts", icon: UsersFourIcon },
{ label: "Policy Engine", path: "/policy-engine", icon: ShieldCheckIcon },
{ label: "Configuration", path: "/config", icon: GearIcon },
{ label: "Audit Logs", path: "/audit", icon: ClockCounterClockwiseIcon },
];
interface AppSidebarProps {
@@ -44,38 +44,61 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
)}
<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 */}
<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="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" />
</svg>
<div className="w-[18px] h-[2px] bg-white rounded-full"></div>
</div>
{!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>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 transition-colors lg:hidden">
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 transition-colors lg:hidden"
>
<CaretDoubleLeftIcon size={20} weight="bold" />
</button>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="text-slate-400 hover:text-slate-600 transition-colors hidden lg:block"
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" />}
{isCollapsed ? (
<CaretDoubleRightIcon size={20} weight="bold" />
) : (
<CaretDoubleLeftIcon size={20} weight="bold" />
)}
</button>
</div>
{/* 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) => {
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;
return (
@@ -86,51 +109,78 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
onClick={() => {
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
? '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'
? "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"
}`}
>
<Icon
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}
/>
{!isCollapsed && (
<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>
);
})}
</nav>
{/* 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 */}
{!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" />
{!isCollapsed && <span className="whitespace-nowrap">Sign Out</span>}
<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"
/>
{!isCollapsed && (
<span className="whitespace-nowrap">Sign Out</span>
)}
</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`}>
<QuestionIcon size={17} className="text-slate-700 shrink-0" weight="regular" />
{!isCollapsed && <span className="whitespace-nowrap">Help center</span>}
<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`}
>
<QuestionIcon
size={17}
className="text-slate-700 shrink-0"
weight="regular"
/>
{!isCollapsed && (
<span className="whitespace-nowrap">Help center</span>
)}
</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" />
{!isCollapsed && (
<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-[10px] text-slate-500 font-medium mt-0.5 leading-tight truncate">System Administrator</span>
<span className="text-[13px] font-bold text-[#111827] leading-tight truncate">
Admin Demo
</span>
<span className="text-[10px] text-slate-500 font-medium mt-0.5 leading-tight truncate">
System Administrator
</span>
</div>
)}
</div>