feat: implement dashboard UI components, data types, and API integration
This commit is contained in:
+26
-20
@@ -1,28 +1,34 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||
import Layout from './layout/AppLayout'
|
||||
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'
|
||||
import AuditLogsList from './app/auditLogs/components/AuditLogsList'
|
||||
import ConfigurationPage from './app/configuration'
|
||||
import CustomAppLoader from './components/custom/CustomAppLoader'
|
||||
|
||||
const HomePage = lazy(() => import('./app/dashboard'))
|
||||
const CohortManage = lazy(() => import('./app/cohartManage'))
|
||||
const PolicyEngineList = lazy(() => import('./app/policyEngine/components/PolicyEngineList'))
|
||||
const AddPolicyEngine = lazy(() => import('./app/policyEngine/components/AddPolicyEngine'))
|
||||
const RecoveryIncidentsList = lazy(() => import('./app/recoveryIncidents/components/RecoveryIncidentsList'))
|
||||
const RecoveryIncidentTabs = lazy(() => import('./app/recoveryIncidents/tabs/index'))
|
||||
const AuditLogsList = lazy(() => import('./app/auditLogs/components/AuditLogsList'))
|
||||
const ConfigurationPage = lazy(() => import('./app/configuration'))
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/cohorts" element={<CohortManage />} />
|
||||
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
||||
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
||||
<Route path="/policy-engine/edit/:id" element={<AddPolicyEngine />} />
|
||||
<Route path="/action-builder" element={<Navigate to="/config?tab=action-builder" replace />} />
|
||||
<Route path="/config" element={<ConfigurationPage />} />
|
||||
<Route path="/recovery" element={<RecoveryIncidentsList />} />
|
||||
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
|
||||
<Route path="/audit-logs" element={<AuditLogsList />} />
|
||||
</Routes>
|
||||
<Suspense fallback={<CustomAppLoader />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/cohorts" element={<CohortManage />} />
|
||||
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
||||
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
||||
<Route path="/policy-engine/edit/:id" element={<AddPolicyEngine />} />
|
||||
<Route path="/action-builder" element={<Navigate to="/config?tab=action-builder" replace />} />
|
||||
<Route path="/config" element={<ConfigurationPage />} />
|
||||
<Route path="/recovery" element={<RecoveryIncidentsList />} />
|
||||
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
|
||||
<Route path="/audit-logs" element={<AuditLogsList />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiClient } from '../api/ApiClient';
|
||||
import type {
|
||||
DashboardMetric,
|
||||
ExposureChartItem,
|
||||
DisruptionMixItem,
|
||||
RecentIncident,
|
||||
} from './DashboardTypes';
|
||||
|
||||
export function getDashboardMetrics(): Promise<DashboardMetric[]> {
|
||||
return ApiClient.get<any, DashboardMetric[]>('/dashboard/metrics');
|
||||
}
|
||||
|
||||
export function getExposureData(): Promise<ExposureChartItem[]> {
|
||||
return ApiClient.get<any, ExposureChartItem[]>('/dashboard/exposure');
|
||||
}
|
||||
|
||||
export function getDisruptionMixData(): Promise<DisruptionMixItem[]> {
|
||||
return ApiClient.get<any, DisruptionMixItem[]>('/dashboard/disruption-mix');
|
||||
}
|
||||
|
||||
export function getRecentIncidents(): Promise<RecentIncident[]> {
|
||||
return ApiClient.get<any, RecentIncident[]>('/dashboard/recent-incidents');
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface DashboardMetric {
|
||||
id: string;
|
||||
title: string;
|
||||
value: string;
|
||||
trend: string;
|
||||
isPositive?: boolean;
|
||||
sparklineData: number[];
|
||||
}
|
||||
|
||||
export interface ExposureChartItem {
|
||||
category: string;
|
||||
amount: number;
|
||||
displayAmount: string;
|
||||
color: string;
|
||||
views?: string;
|
||||
date?: string;
|
||||
hasTooltip?: boolean;
|
||||
}
|
||||
|
||||
export interface DisruptionMixItem {
|
||||
label: string;
|
||||
percentage: number;
|
||||
value: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface RecentIncident {
|
||||
id: string;
|
||||
recoveryId: string;
|
||||
passengerName: string;
|
||||
pnr: string;
|
||||
flightNumber: string;
|
||||
category: string;
|
||||
status: string;
|
||||
value: string;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState } from "react";
|
||||
import type { DisruptionMixItem } from "../DashboardTypes";
|
||||
|
||||
interface DisruptionMixChartProps {
|
||||
data: DisruptionMixItem[];
|
||||
}
|
||||
|
||||
export const DisruptionMixChart: React.FC<DisruptionMixChartProps> = ({ data }) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
// Calculate SVG donut stroke offsets
|
||||
const size = 180;
|
||||
const strokeWidth = 24;
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
let accumulatedPercent = 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between h-full">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||
Disruption Mix
|
||||
</h2>
|
||||
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||
Distribution of passenger recovery cases.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Donut Chart Visual */}
|
||||
<div className="flex flex-col items-center justify-center my-6 relative">
|
||||
<div className="relative w-[180px] h-[180px] flex items-center justify-center">
|
||||
<svg width={size} height={size} className="transform -rotate-90">
|
||||
{data.map((item, index) => {
|
||||
const strokeDasharray = `${(item.percentage / 100) * circumference} ${circumference}`;
|
||||
const strokeDashoffset = -((accumulatedPercent / 100) * circumference);
|
||||
accumulatedPercent += item.percentage;
|
||||
|
||||
const isHovered = hoveredIndex === index;
|
||||
|
||||
return (
|
||||
<circle
|
||||
key={item.label}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="transparent"
|
||||
stroke={item.color}
|
||||
strokeWidth={isHovered ? strokeWidth + 4 : strokeWidth}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className="transition-all duration-300 cursor-pointer"
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Donut Center Info */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
||||
<span className="text-[22px] font-bold text-gray-900 leading-none">
|
||||
{hoveredIndex !== null ? `${data[hoveredIndex].percentage}%` : "100%"}
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-gray-400 mt-1">
|
||||
{hoveredIndex !== null ? data[hoveredIndex].label : "Total Cases"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className={`flex items-center justify-between p-2 rounded-lg transition-colors cursor-pointer ${
|
||||
hoveredIndex === index ? "bg-gray-50" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span className="text-[12px] font-medium text-gray-700">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[12px] font-bold text-gray-900">
|
||||
{item.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisruptionMixChart;
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useState } from "react";
|
||||
import type { ExposureChartItem } from "../DashboardTypes";
|
||||
|
||||
interface ExposureBarChartProps {
|
||||
data: ExposureChartItem[];
|
||||
}
|
||||
|
||||
export const ExposureBarChart: React.FC<ExposureBarChartProps> = ({ data }) => {
|
||||
const [activeBar, setActiveBar] = useState<string | null>("Baggage");
|
||||
|
||||
const maxAmount = 60;
|
||||
const yAxisTicks = ["$50", "$50", "$50", "$30", "$10"];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between h-full">
|
||||
{/* Chart Title Header */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||
Refund & Compensation Exposure
|
||||
</h2>
|
||||
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||
Real-time assessment log of refund and compensation cases.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart Body */}
|
||||
<div className="relative w-full h-[280px] flex flex-col justify-end pt-8 pb-2">
|
||||
{/* Horizontal Background Grid Lines */}
|
||||
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none pb-8 pt-8 pl-12 pr-4">
|
||||
{yAxisTicks.map((_, idx) => (
|
||||
<div key={idx} className="w-full border-b border-gray-100/80 h-0" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Chart Content Area */}
|
||||
<div className="flex w-full h-full relative z-10">
|
||||
{/* Y-Axis Labels */}
|
||||
<div className="flex flex-col justify-between pr-4 pb-8 text-[11px] font-medium text-gray-400 text-right w-12 select-none">
|
||||
{yAxisTicks.map((tick, idx) => (
|
||||
<span key={idx} className="leading-none">
|
||||
{tick}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bars Grid */}
|
||||
<div className="flex-1 grid grid-cols-4 items-end gap-6 px-4 pb-8 h-full">
|
||||
{data.map((item) => {
|
||||
const heightPercent = Math.min(100, Math.max(10, (item.amount / maxAmount) * 100));
|
||||
const isSelected = activeBar === item.category || (item.hasTooltip && !activeBar);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.category}
|
||||
className="relative flex flex-col items-center justify-end h-full group cursor-pointer"
|
||||
onMouseEnter={() => setActiveBar(item.category)}
|
||||
onMouseLeave={() => setActiveBar("Baggage")}
|
||||
>
|
||||
{/* Tooltip Bubble (Visible for selected bar e.g. Baggage) */}
|
||||
{isSelected && (item.hasTooltip || activeBar === item.category) && (
|
||||
<div className="absolute -top-14 z-20 flex flex-col items-center animate-fadeIn">
|
||||
<div className="bg-white border border-gray-100 shadow-md rounded-[8px] px-3 py-1.5 text-center whitespace-nowrap min-w-[120px]">
|
||||
<p className="text-[11px] font-bold text-gray-800 leading-tight">
|
||||
{item.views || `${item.amount} cases`}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-400 font-medium leading-tight mt-0.5">
|
||||
{item.date || "Monday, April 22nd"}
|
||||
</p>
|
||||
</div>
|
||||
{/* Tooltip Caret */}
|
||||
<div className="w-2.5 h-2.5 bg-white border-r border-b border-gray-100 transform rotate-45 -mt-1.5 shadow-xs" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bar Element */}
|
||||
<div
|
||||
className="w-full max-w-[110px] rounded-t-[4px] transition-all duration-300 relative group-hover:brightness-105"
|
||||
style={{
|
||||
height: `${heightPercent}%`,
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* X-Axis Category Labels */}
|
||||
<div className="grid grid-cols-4 pl-12 pr-4 text-center">
|
||||
{data.map((item) => (
|
||||
<span
|
||||
key={item.category}
|
||||
className={`text-[12px] font-medium transition-colors ${
|
||||
activeBar === item.category ? "text-gray-900 font-semibold" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item.category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExposureBarChart;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
import type { RecentIncident } from "../DashboardTypes";
|
||||
import { CustomTable, CustomStatus } from "../../../components/custom";
|
||||
import type { Column } from "../../../components/custom/CustomTable";
|
||||
|
||||
interface RecentIncidentsTableProps {
|
||||
incidents: RecentIncident[];
|
||||
}
|
||||
|
||||
export const RecentIncidentsTable: React.FC<RecentIncidentsTableProps> = ({ incidents }) => {
|
||||
const columns: Column<RecentIncident>[] = [
|
||||
{
|
||||
header: "Recovery ID",
|
||||
accessor: (row) => (
|
||||
<span className="text-[13px] font-bold text-gray-900 font-mono">
|
||||
{row.recoveryId}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Passenger / PNR",
|
||||
accessor: (row) => (
|
||||
<span className="text-[13px] font-bold text-gray-900">
|
||||
{row.passengerName}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Flight",
|
||||
accessor: (row) => (
|
||||
<span className="text-[13px] font-bold text-gray-900">
|
||||
{row.flightNumber}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Category",
|
||||
accessor: (row) => (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-semibold bg-[#EFEFEF] text-gray-600">
|
||||
{row.category}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
accessor: (row) => <CustomStatus status={row.status} />,
|
||||
},
|
||||
{
|
||||
header: "Value",
|
||||
accessor: (row) => (
|
||||
<span className="text-[13px] font-bold text-gray-900">
|
||||
{row.value}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col gap-5">
|
||||
{/* Table Header Section */}
|
||||
<div>
|
||||
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||
Recent Recovery Incidents
|
||||
</h2>
|
||||
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||
Real-time assessment log of refund and compensation cases.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom Table Component */}
|
||||
<CustomTable<RecentIncident>
|
||||
columns={columns}
|
||||
data={incidents}
|
||||
itemName="incidents"
|
||||
totalItems={incidents.length}
|
||||
startIndex={incidents.length > 0 ? 1 : 0}
|
||||
endIndex={incidents.length}
|
||||
totalPages={1}
|
||||
currentPage={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentIncidentsTable;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import type { DashboardMetric } from "../DashboardTypes";
|
||||
|
||||
export const StatCard: React.FC<{ metric: DashboardMetric }> = ({ metric }) => {
|
||||
return (
|
||||
<div className="bg-white rounded-[14px] p-4 px-5 border border-gray-100/80 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between flex-1 min-w-[170px] hover:shadow-md transition-all duration-200">
|
||||
<div>
|
||||
<span className="text-[13px] font-medium text-gray-600 block">
|
||||
{metric.title}
|
||||
</span>
|
||||
<span className="text-[26px] font-bold text-gray-900 leading-tight tracking-tight mt-1.5 block">
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-3 pt-1">
|
||||
<span className="text-[12px] font-bold text-[#1B9869] bg-[#E4FAE7]/60 px-2 py-0.5 rounded-full inline-flex items-center">
|
||||
{metric.trend}
|
||||
</span>
|
||||
|
||||
{/* Sparkline curve */}
|
||||
<div className="w-[60px] h-[26px] flex items-center justify-end">
|
||||
<svg
|
||||
width="60"
|
||||
height="26"
|
||||
viewBox="0 0 60 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="overflow-visible"
|
||||
>
|
||||
<path
|
||||
d="M 2 20 Q 12 22, 20 15 T 40 18 T 58 6"
|
||||
stroke="#1B9869"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatCard;
|
||||
@@ -1,10 +1,4 @@
|
||||
function HomePage() {
|
||||
return (
|
||||
<section className="page home-page">
|
||||
<h1>Home Page</h1>
|
||||
<p>Welcome to the home page. Use the navigation links to switch pages.</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
export { StatCard } from "./StatCard";
|
||||
export { ExposureBarChart } from "./ExposureBarChart";
|
||||
export { DisruptionMixChart } from "./DisruptionMixChart";
|
||||
export { RecentIncidentsTable } from "./RecentIncidentsTable";
|
||||
|
||||
+138
-372
@@ -1,383 +1,149 @@
|
||||
import { useState } from "react";
|
||||
import CustomButton from "../../components/custom/CustomButton";
|
||||
import CustomSearchableDropdown from "../../components/custom/CustomSearchableDropdown";
|
||||
import CustomDropdown from "../../components/custom/CustomDropdown";
|
||||
import CustomMultiSelect from "../../components/custom/CustomMultiSelect";
|
||||
import CustomModal from "../../components/custom/CustomModal";
|
||||
import CustomInput from "../../components/custom/CustomInput";
|
||||
import CustomTextArea from "../../components/custom/CustomTextArea";
|
||||
import CustomStatus from "../../components/custom/CustomStatus";
|
||||
import CustomCheckBox from "../../components/custom/CustomCheckBox";
|
||||
import CustomRadio from "../../components/custom/CustomRadio";
|
||||
import CustomSwitch from "../../components/custom/CustomSwitch";
|
||||
import CustomTable, { type Column } from "../../components/custom/CustomTable";
|
||||
import CustomActionMenu, { CustomActionItem } from "../../components/custom/CustomActionMenu";
|
||||
import { FileTextIcon, CaretRightIcon, CheckIcon, XIcon, CopyIcon, PencilSimpleIcon, TrashIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Skeleton } from "../../components/custom";
|
||||
import {
|
||||
StatCard,
|
||||
ExposureBarChart,
|
||||
DisruptionMixChart,
|
||||
RecentIncidentsTable,
|
||||
} from "./components";
|
||||
import type {
|
||||
DashboardMetric,
|
||||
ExposureChartItem,
|
||||
DisruptionMixItem,
|
||||
RecentIncident,
|
||||
} from "./DashboardTypes";
|
||||
import {
|
||||
getDashboardMetrics,
|
||||
getExposureData,
|
||||
getDisruptionMixData,
|
||||
getRecentIncidents,
|
||||
} from "./DashboardApi";
|
||||
|
||||
function HomePage() {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dropdownValue, setDropdownValue] = useState("active");
|
||||
const [multiSelectValue, setMultiSelectValue] = useState<(string | number)[]>(["active"]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(true);
|
||||
const [radioValue, setRadioValue] = useState("option1");
|
||||
const [isSwitchOn, setIsSwitchOn] = useState(true);
|
||||
export default function HomePage() {
|
||||
const [metrics, setMetrics] = useState<DashboardMetric[]>([]);
|
||||
const [exposureData, setExposureData] = useState<ExposureChartItem[]>([]);
|
||||
const [disruptionMix, setDisruptionMix] = useState<DisruptionMixItem[]>([]);
|
||||
const [recentIncidents, setRecentIncidents] = useState<RecentIncident[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
name: "Strategic Accounts",
|
||||
description: "Key corporate account travelers.",
|
||||
status: "active",
|
||||
lastModified: "4 Jun 2026, 4:09pm",
|
||||
lastModifiedBy: "John Doe",
|
||||
},
|
||||
{
|
||||
name: "Families with Infants",
|
||||
description: "Passengers traveling with children < 2yrs.",
|
||||
status: "active",
|
||||
lastModified: "4 Jun 2026, 4:09pm",
|
||||
lastModifiedBy: "John Doe",
|
||||
},
|
||||
{
|
||||
name: "Inactive Users",
|
||||
description: "Users who haven't booked in 12 months.",
|
||||
status: "inactive",
|
||||
lastModified: "3 Jun 2026, 2:15pm",
|
||||
lastModifiedBy: "Jane Smith",
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [metricsRes, exposureRes, mixRes, incidentsRes] = await Promise.all([
|
||||
getDashboardMetrics(),
|
||||
getExposureData(),
|
||||
getDisruptionMixData(),
|
||||
getRecentIncidents(),
|
||||
]);
|
||||
|
||||
const tableColumns: Column<typeof tableData[0]>[] = [
|
||||
{
|
||||
header: "Cohort Name",
|
||||
accessor: (row) => <span className="font-bold text-gray-900">{row.name}</span>,
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessor: "description",
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
accessor: (row) => <CustomStatus status={row.status} />,
|
||||
},
|
||||
{
|
||||
header: "Last Modified",
|
||||
accessor: "lastModified",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
header: "Last Modified By",
|
||||
accessor: "lastModifiedBy",
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
accessor: () => (
|
||||
<div className="flex justify-center w-full">
|
||||
<CustomActionMenu>
|
||||
<CustomActionItem variant="success" icon={<CheckIcon size={16} />}>Activate</CustomActionItem>
|
||||
<CustomActionItem icon={<XIcon size={16} />}>Deactivate</CustomActionItem>
|
||||
<CustomActionItem icon={<CopyIcon size={16} />}>Duplicate</CustomActionItem>
|
||||
<CustomActionItem icon={<PencilSimpleIcon size={16} />}>Edit</CustomActionItem>
|
||||
<CustomActionItem variant="danger" icon={<TrashIcon size={16} />}>Delete</CustomActionItem>
|
||||
</CustomActionMenu>
|
||||
setMetrics(metricsRes);
|
||||
setExposureData(exposureRes);
|
||||
setDisruptionMix(mixRes);
|
||||
setRecentIncidents(incidentsRes);
|
||||
} catch (error) {
|
||||
console.error("Error loading dashboard data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
{/* Top Stat Cards Skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="bg-white rounded-[14px] p-5 border border-gray-100/80 flex flex-col justify-between h-[100px]">
|
||||
<div className="space-y-2">
|
||||
<Skeleton width="60%" height={16} />
|
||||
<Skeleton width="40%" height={28} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<Skeleton width={45} height={18} />
|
||||
<Skeleton width={50} height={20} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
className: "text-center w-24",
|
||||
},
|
||||
];
|
||||
|
||||
const options = [
|
||||
{ label: "Draft", value: "draft" },
|
||||
{ label: "Active", value: "active" },
|
||||
{ label: "Inactive", value: "inactive" },
|
||||
];
|
||||
{/* Middle Section Skeleton */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||
<div className="lg:col-span-8 bg-white rounded-[16px] border border-gray-100 p-6 flex flex-col justify-between h-[360px]">
|
||||
<div className="space-y-2">
|
||||
<Skeleton width="40%" height={22} />
|
||||
<Skeleton width="60%" height={14} />
|
||||
</div>
|
||||
<div className="flex items-end gap-6 h-[240px] pt-8">
|
||||
<Skeleton className="flex-1" height="60%" />
|
||||
<Skeleton className="flex-1" height="90%" />
|
||||
<Skeleton className="flex-1" height="40%" />
|
||||
<Skeleton className="flex-1" height="85%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 bg-white rounded-[16px] border border-gray-100 p-6 flex flex-col justify-between h-[360px]">
|
||||
<div className="space-y-2">
|
||||
<Skeleton width="50%" height={22} />
|
||||
<Skeleton width="70%" height={14} />
|
||||
</div>
|
||||
<div className="flex items-center justify-center my-4">
|
||||
<Skeleton variant="circular" width={160} height={160} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} height={24} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Table Skeleton */}
|
||||
<div className="bg-white rounded-[16px] border border-gray-100 p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton width="30%" height={22} />
|
||||
<Skeleton width="50%" height={14} />
|
||||
</div>
|
||||
<div className="space-y-3 pt-2">
|
||||
<Skeleton height={40} />
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} height={48} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto flex flex-col gap-8">
|
||||
<div className="p-4 md:p-6 lg:p-4 space-y-6">
|
||||
{/* 1. Top Stat Cards Row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
{metrics.map((metric) => (
|
||||
<StatCard key={metric.id} metric={metric} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 2. Middle Section: Refund Exposure + Disruption Mix */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||
{/* Refund & Compensation Exposure (approx 7 cols) */}
|
||||
<div className="lg:col-span-8 flex flex-col">
|
||||
<ExposureBarChart data={exposureData} />
|
||||
</div>
|
||||
|
||||
{/* Disruption Mix (approx 4 cols) */}
|
||||
<div className="lg:col-span-4 flex flex-col">
|
||||
<DisruptionMixChart data={disruptionMix} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Bottom Section: Recent Recovery Incidents */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2 text-gray-900">Dashboard (UI Test Page)</h1>
|
||||
<p className="text-gray-500">Test all button variants and the custom components here.</p>
|
||||
<RecentIncidentsTable incidents={recentIncidents} />
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Table Component</h2>
|
||||
|
||||
<CustomTable
|
||||
columns={tableColumns}
|
||||
data={tableData}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
searchPlaceholder="Search cohorts..."
|
||||
currentPage={currentPage}
|
||||
totalPages={3}
|
||||
totalItems={12}
|
||||
startIndex={1}
|
||||
endIndex={3}
|
||||
onPageChange={setCurrentPage}
|
||||
itemName="cohorts"
|
||||
rightHeaderActions={
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-40">
|
||||
<CustomDropdown
|
||||
options={options}
|
||||
value="active"
|
||||
placeholder="Choose Status"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<CustomButton variant="primary" size="md" leftIcon={<PlusIcon size={16} />}>
|
||||
Create Cohort
|
||||
</CustomButton>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Modal Components</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<CustomButton variant="primary" onClick={() => setIsModalOpen(true)}>
|
||||
Open Test Modal
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Status Pills & Toggles</h2>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<span className="text-sm font-semibold w-24">Status Pills:</span>
|
||||
<CustomStatus status="active" />
|
||||
<CustomStatus status="inactive" />
|
||||
<CustomStatus status="pending" />
|
||||
<CustomStatus status="shipped" />
|
||||
<CustomStatus status="neutral" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<span className="text-sm font-semibold w-24">Checkboxes:</span>
|
||||
<CustomCheckBox checked={isChecked} onChange={() => setIsChecked(!isChecked)} label="Checked State" />
|
||||
<CustomCheckBox checked={false} onChange={() => {}} label="Unchecked State" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<span className="text-sm font-semibold w-24">Radios:</span>
|
||||
<CustomRadio checked={radioValue === "option1"} onChange={() => setRadioValue("option1")} label="Option 1" />
|
||||
<CustomRadio checked={radioValue === "option2"} onChange={() => setRadioValue("option2")} label="Option 2" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<span className="text-sm font-semibold w-24">Switches:</span>
|
||||
<CustomSwitch checked={isSwitchOn} onChange={() => setIsSwitchOn(!isSwitchOn)} label="Toggle Feature" />
|
||||
<CustomSwitch checked={false} onChange={() => {}} label="Off Toggle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Button Variants</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<CustomButton variant="primary">Primary</CustomButton>
|
||||
<CustomButton variant="secondary">Secondary</CustomButton>
|
||||
<CustomButton variant="outlined">Outlined</CustomButton>
|
||||
<CustomButton variant="text">Text Button</CustomButton>
|
||||
<CustomButton variant="link">Link Button</CustomButton>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center mt-2">
|
||||
<CustomButton variant="primary" size="sm">Small</CustomButton>
|
||||
<CustomButton variant="primary" size="md">Medium</CustomButton>
|
||||
<CustomButton variant="primary" size="lg">Large</CustomButton>
|
||||
<CustomButton variant="primary" loading>Loading</CustomButton>
|
||||
<CustomButton variant="primary" disabled>Disabled</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Input & TextArea Variants</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-6 items-end">
|
||||
<div className="w-48">
|
||||
<CustomInput
|
||||
label="Small (sm)"
|
||||
size="sm"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<CustomInput
|
||||
label="Medium (md)"
|
||||
size="md"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-64">
|
||||
<CustomInput
|
||||
label="Large (lg)"
|
||||
size="lg"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-6 items-end mt-4">
|
||||
<div className="w-48">
|
||||
<CustomTextArea
|
||||
label="Small (sm)"
|
||||
size="sm"
|
||||
placeholder="Enter text..."
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<CustomTextArea
|
||||
label="Medium (md)"
|
||||
size="md"
|
||||
placeholder="Enter text..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-64">
|
||||
<CustomTextArea
|
||||
label="Large (lg)"
|
||||
size="lg"
|
||||
placeholder="Enter text..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Standard Dropdown Variants</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-6 items-end">
|
||||
<div className="w-48">
|
||||
<CustomDropdown
|
||||
label="Small (sm)"
|
||||
size="sm"
|
||||
options={options}
|
||||
value={dropdownValue}
|
||||
onChange={setDropdownValue}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<CustomDropdown
|
||||
label="Medium (md)"
|
||||
size="md"
|
||||
options={options}
|
||||
value={dropdownValue}
|
||||
onChange={setDropdownValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Searchable Dropdown Variants</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-6 items-end">
|
||||
<div className="w-48">
|
||||
<CustomSearchableDropdown
|
||||
label="Small (sm)"
|
||||
size="sm"
|
||||
options={options}
|
||||
value={dropdownValue}
|
||||
onChange={setDropdownValue}
|
||||
placeholder="Small size..."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<CustomSearchableDropdown
|
||||
label="Medium (md)"
|
||||
size="md"
|
||||
options={options}
|
||||
value={dropdownValue}
|
||||
onChange={setDropdownValue}
|
||||
placeholder="Medium size..."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-64">
|
||||
<CustomSearchableDropdown
|
||||
label="Large (lg)"
|
||||
size="lg"
|
||||
options={options}
|
||||
value={dropdownValue}
|
||||
onChange={setDropdownValue}
|
||||
placeholder="Large size..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6 mb-32">
|
||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Multi-Select Dropdown</h2>
|
||||
|
||||
<div className="w-[400px]">
|
||||
<CustomMultiSelect
|
||||
label="Select Multiple Statuses"
|
||||
options={options}
|
||||
value={multiSelectValue}
|
||||
onChange={setMultiSelectValue}
|
||||
placeholder="Select statuses..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title="New Dynamic Cohorts"
|
||||
description="Configure targeting criteria for high-precision recovery."
|
||||
icon={<FileTextIcon size={24} />}
|
||||
primaryAction={{
|
||||
label: "Next Step",
|
||||
onClick: () => setIsModalOpen(false),
|
||||
icon: <CaretRightIcon size={18} />
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Cancel",
|
||||
onClick: () => setIsModalOpen(false)
|
||||
}}
|
||||
size="md"
|
||||
>
|
||||
<div className="flex flex-col gap-6 h-64">
|
||||
<div className="flex justify-between border-b border-gray-100 pb-2">
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-[#1B9869] tracking-wider uppercase">1 - Information</span>
|
||||
<h3 className="text-sm font-bold text-[#1B9869]">Cohort Identity</h3>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase">2 - Targeting</span>
|
||||
<h3 className="text-sm font-bold text-gray-400">Targeting Criteria</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="w-full">
|
||||
<label className="text-xs font-bold text-gray-700 mb-1 block">Cohort Name</label>
|
||||
<input type="text" placeholder="Enter" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#1B9869]" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<CustomDropdown
|
||||
label="Status"
|
||||
options={[{label: "Selected Option", value: "selected"}]}
|
||||
value="selected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<label className="text-xs font-bold text-gray-700 mb-1 block">Description</label>
|
||||
<textarea placeholder="Enter" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm h-24 focus:outline-none focus:border-[#1B9869]"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</CustomModal>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { UsersIcon, BriefcaseIcon, BuildingsIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
AirplaneTiltIcon,
|
||||
ShieldCheckIcon,
|
||||
ArrowsClockwiseIcon,
|
||||
UsersFourIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
const CustomAppLoader = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// HRM related icons
|
||||
// AeroResolve domain icons
|
||||
const icons = [
|
||||
{ component: UsersIcon, key: "users" },
|
||||
{ component: BriefcaseIcon, key: "briefcase" },
|
||||
{ component: BuildingsIcon, key: "building" },
|
||||
{ component: AirplaneTiltIcon, key: "flight", label: "Flight Operations" },
|
||||
{ component: ArrowsClockwiseIcon, key: "recovery", label: "Recovery Incidents" },
|
||||
{ component: ShieldCheckIcon, key: "policy", label: "Policy Engine" },
|
||||
{ component: UsersFourIcon, key: "cohorts", label: "Passenger Cohorts" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % icons.length);
|
||||
}, 800); // Slower, smoother transition
|
||||
}, 700);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [icons.length]);
|
||||
@@ -22,28 +28,34 @@ const CustomAppLoader = () => {
|
||||
const ActiveIcon = icons[currentIndex].component;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-6 h-screen w-full ">
|
||||
<div className="flex flex-col items-center justify-center space-y-6 min-h-[400px] h-screen w-full bg-[#F8FAFC] font-sans">
|
||||
<div className="relative flex items-center justify-center">
|
||||
{/* Outer Glowing Pulsing Aura */}
|
||||
<div className="absolute w-24 h-24 bg-[#1B9869]/10 rounded-full animate-ping pointer-events-none" />
|
||||
|
||||
{/* Outer Spinning Ring */}
|
||||
<div className="w-20 h-20 border-4 border-gray-200 border-t-[#0B3B6A] rounded-full animate-spin"></div>
|
||||
<div className="w-20 h-20 border-4 border-gray-200/80 border-t-[#1B9869] border-r-[#1B9869]/40 rounded-full animate-spin" />
|
||||
|
||||
{/* Icon Container */}
|
||||
<div className="absolute flex items-center justify-center">
|
||||
<div className="absolute flex items-center justify-center w-12 h-12 bg-white rounded-full shadow-sm border border-gray-100">
|
||||
<ActiveIcon
|
||||
key={icons[currentIndex].key}
|
||||
size={32}
|
||||
className="text-[#0B3B6A] transition-all duration-500 opacity-100"
|
||||
strokeWidth={1.5}
|
||||
size={28}
|
||||
className="text-[#1B9869] transition-all duration-300 transform scale-100"
|
||||
weight="bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<h3 className="text-[#0B3B6A] font-semibold text-lg tracking-wide">
|
||||
HRM System
|
||||
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||
<h3 className="text-gray-900 font-bold text-xl tracking-tight flex items-center gap-2">
|
||||
Aero<span className="text-[#1B9869]">Resolve</span>
|
||||
</h3>
|
||||
<span className="text-gray-500 text-sm animate-pulse">
|
||||
Loading resources...
|
||||
<span className="text-gray-500 text-xs font-semibold uppercase tracking-wider">
|
||||
Passenger Recovery & Resolution Platform
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-[#1B9869] animate-pulse mt-2">
|
||||
Loading {icons[currentIndex].label}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user