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 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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
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 || ''}`}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user