feat: implement recovery incident detail view with tabbed navigation and case information display

This commit is contained in:
azeeee05
2026-08-04 19:21:11 +05:30
parent 3baff47c80
commit ba2535d4c7
6 changed files with 69 additions and 165 deletions
@@ -1,24 +1,22 @@
import { ApiClient } from '../api/ApiClient'; import { ApiClient } from '../api/ApiClient';
import type { RecoveryIncident } from './RecoveryIncidentsTypes'; import type { RecoveryIncident } from './RecoveryIncidentsTypes';
export const RecoveryIncidentsApi = { export function getRecoveryIncidents(): Promise<RecoveryIncident[]> {
getAll: async (): Promise<RecoveryIncident[]> => { return ApiClient.get<any, RecoveryIncident[]>('/recovery-incidents');
return ApiClient.get('/recovery-incidents'); }
},
getById: async (id: string): Promise<RecoveryIncident> => { export function getRecoveryIncident(id: string): Promise<RecoveryIncident> {
return ApiClient.get(`/recovery-incidents/${id}`); return ApiClient.get<any, RecoveryIncident>(`/recovery-incidents/${id}`);
}, }
create: async (data: Omit<RecoveryIncident, 'id'>): Promise<RecoveryIncident> => { export function createRecoveryIncident(data: Omit<RecoveryIncident, 'id'>): Promise<RecoveryIncident> {
return ApiClient.post('/recovery-incidents', data); return ApiClient.post<any, RecoveryIncident>('/recovery-incidents', data);
}, }
update: async (id: string, data: Partial<RecoveryIncident>): Promise<RecoveryIncident> => { export function updateRecoveryIncident(id: string, data: Partial<RecoveryIncident>): Promise<RecoveryIncident> {
return ApiClient.patch(`/recovery-incidents/${id}`, data); return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}`, data);
}, }
delete: async (id: string): Promise<void> => { export function deleteRecoveryIncident(id: string): Promise<void> {
return ApiClient.delete(`/recovery-incidents/${id}`); return ApiClient.delete<any, void>(`/recovery-incidents/${id}`);
}, }
};
@@ -5,7 +5,7 @@ export interface IncidentStatus {
export interface RecoveryIncident { export interface RecoveryIncident {
id: string; id: string;
recoveryId: string; recoveryCode: string;
date: string; date: string;
passengerName?: string; passengerName?: string;
pnr?: string; pnr?: string;
@@ -5,7 +5,7 @@ import {
CustomInput, CustomInput,
CustomDropdown, CustomDropdown,
} from "../../../components/custom"; } from "../../../components/custom";
import { RecoveryIncidentsApi } from '../RecoveryIncidentsApi'; import { createRecoveryIncident } from '../RecoveryIncidentsApi';
interface AddRecoveryIncidentsProps { interface AddRecoveryIncidentsProps {
isOpen: boolean; isOpen: boolean;
@@ -40,7 +40,7 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
setLoading(true); setLoading(true);
try { try {
const payload = { const payload = {
recoveryId: "REC-" + Math.floor(Math.random() * 10000), recoveryCode: "REC-" + Math.floor(Math.random() * 10000),
passengerName: formData.passengerName || "Unknown", passengerName: formData.passengerName || "Unknown",
pnr: formData.pnr || "N/A", pnr: formData.pnr || "N/A",
flightNumber: formData.flightNumber || "TBD", flightNumber: formData.flightNumber || "TBD",
@@ -52,7 +52,7 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
}; };
console.log("Submitting payload:", payload); console.log("Submitting payload:", payload);
await RecoveryIncidentsApi.create(payload); await createRecoveryIncident(payload);
onClose(); // Will trigger refresh in parent list onClose(); // Will trigger refresh in parent list
} catch (error: any) { } catch (error: any) {
console.error("Error creating incident:", error.response?.data || error.message || error); console.error("Error creating incident:", error.response?.data || error.message || error);
@@ -20,135 +20,10 @@ import {
import type { Column } from "../../../components/custom/CustomTable"; import type { Column } from "../../../components/custom/CustomTable";
import type { RecoveryIncident } from "../RecoveryIncidentsTypes"; import type { RecoveryIncident } from "../RecoveryIncidentsTypes";
import AddRecoveryIncidents from "./AddRecoveryIncidents"; import AddRecoveryIncidents from "./AddRecoveryIncidents";
import { RecoveryIncidentsApi } from "../RecoveryIncidentsApi"; import { getRecoveryIncidents } from "../RecoveryIncidentsApi";
// ─── Constants ───────────────────────────────────────────────────────────────
const PAGE_SIZE = 10; 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({ function HeaderLabel({
text, text,
rightIcon, rightIcon,
@@ -308,10 +183,10 @@ export default function RecoveryIncidentsList() {
setLoading(true); setLoading(true);
try { try {
const data = await RecoveryIncidentsApi.getAll(); const data = await getRecoveryIncidents();
const filteredData = data.filter((p) => { const filteredData = data.filter((p) => {
const matchesSearch = p.recoveryId.toLowerCase().includes(search.toLowerCase()) || const matchesSearch = p.recoveryCode.toLowerCase().includes(search.toLowerCase()) ||
p.flightNumber.toLowerCase().includes(search.toLowerCase()); p.flightNumber.toLowerCase().includes(search.toLowerCase());
const matchesGroup = isGrouped ? true : !p.isGroupHeader; const matchesGroup = isGrouped ? true : !p.isGroupHeader;
return matchesSearch && matchesGroup; return matchesSearch && matchesGroup;
@@ -387,7 +262,7 @@ export default function RecoveryIncidentsList() {
const displayData = useMemo(() => { const displayData = useMemo(() => {
if (!isGrouped) return incidents; if (!isGrouped) return incidents;
return incidents.filter( return incidents.filter(
(p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryId) (p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryCode)
); );
}, [incidents, isGrouped, collapsedGroups]); }, [incidents, isGrouped, collapsedGroups]);
@@ -416,7 +291,7 @@ export default function RecoveryIncidentsList() {
header: <HeaderLabel text="Recovery ID" />, header: <HeaderLabel text="Recovery ID" />,
accessor: (row) => ( accessor: (row) => (
<div> <div>
<PrimaryText text={row.recoveryId} /> <PrimaryText text={row.recoveryCode} />
<SecondaryText text={row.date} /> <SecondaryText text={row.date} />
</div> </div>
), ),
@@ -486,9 +361,9 @@ export default function RecoveryIncidentsList() {
{row.isGroupHeader ? ( {row.isGroupHeader ? (
<div <div
className="cursor-pointer text-gray-500 hover:text-gray-900 transition-colors p-1" className="cursor-pointer text-gray-500 hover:text-gray-900 transition-colors p-1"
onClick={(e) => toggleGroup(row.recoveryId, e)} onClick={(e) => toggleGroup(row.recoveryCode, e)}
> >
{collapsedGroups.has(row.recoveryId) ? ( {collapsedGroups.has(row.recoveryCode) ? (
<CaretDown size={20} /> <CaretDown size={20} />
) : ( ) : (
<CaretUp size={20} /> <CaretUp size={20} />
@@ -1,6 +1,13 @@
import { IdentificationCardIcon, AirplaneTiltIcon, WarningCircleIcon } from '@phosphor-icons/react'; import { IdentificationCardIcon, AirplaneTiltIcon, WarningCircleIcon } from '@phosphor-icons/react';
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
interface CaseDetailsTabProps {
incident?: RecoveryIncident | null;
}
export default function CaseDetailsTab({ incident }: CaseDetailsTabProps) {
if (!incident) return null;
export default function CaseDetailsTab() {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* PASSENGER INFORMATION */} {/* PASSENGER INFORMATION */}
@@ -13,11 +20,11 @@ export default function CaseDetailsTab() {
<div className="grid grid-cols-4 gap-6"> <div className="grid grid-cols-4 gap-6">
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">FULL NAME</span> <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> <span className="block text-[15px] font-semibold text-gray-900">{incident.passengerName || 'Unknown'}</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">PNR / REFERENCE</span> <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> <span className="block text-[15px] font-semibold text-gray-900">{incident.pnr || 'N/A'}</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">LOYALTY TIER</span> <span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">LOYALTY TIER</span>
@@ -40,11 +47,11 @@ export default function CaseDetailsTab() {
<div className="grid grid-cols-4 gap-y-6 gap-x-6"> <div className="grid grid-cols-4 gap-y-6 gap-x-6">
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">FLIGHT NUMBER</span> <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> <span className="block text-[15px] font-semibold text-gray-900">{incident.flightNumber || 'N/A'}</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SECTOR</span> <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> <span className="block text-[15px] font-semibold text-gray-900">{incident.flightRoute || 'N/A'}</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">CABIN</span> <span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">CABIN</span>
@@ -75,11 +82,11 @@ export default function CaseDetailsTab() {
<div className="grid grid-cols-3 gap-6"> <div className="grid grid-cols-3 gap-6">
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">DISRUPTION CATEGORY</span> <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> <span className="block text-[15px] font-semibold text-gray-900">{incident.category || 'N/A'}</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SCENARIO</span> <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> <span className="block text-[15px] font-semibold text-gray-900">N/A</span>
</div> </div>
<div> <div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SUB-TYPE</span> <span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SUB-TYPE</span>
+29 -5
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import {User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react'; import {User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom'; import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom';
@@ -7,10 +7,30 @@ import CaseDetailsTab from './CaseDetailsTab';
import RecoveryPlanTab from './RecoveryPlanTab'; import RecoveryPlanTab from './RecoveryPlanTab';
import AuditTrailTab from './AuditTrailTab'; import AuditTrailTab from './AuditTrailTab';
import { SparkleIcon } from 'lucide-react'; import { SparkleIcon } from 'lucide-react';
import { getRecoveryIncident } from '../RecoveryIncidentsApi';
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
export default function RecoveryIncidentTabs() { export default function RecoveryIncidentTabs() {
const { id } = useParams(); const { id } = useParams();
const [activeTab, setActiveTab] = useState('Summary'); const [activeTab, setActiveTab] = useState('Summary');
const [incident, setIncident] = useState<RecoveryIncident | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (id) {
setLoading(true);
getRecoveryIncident(id)
.then(data => {
setIncident(data);
})
.catch(err => {
console.error("Failed to load incident:", err);
})
.finally(() => {
setLoading(false);
});
}
}, [id]);
const tabItems = [ const tabItems = [
{ {
@@ -21,7 +41,7 @@ export default function RecoveryIncidentTabs() {
{ {
id: 'Case Details', id: 'Case Details',
label: 'Case Details', label: 'Case Details',
content: <CaseDetailsTab /> content: <CaseDetailsTab incident={incident} />
}, },
{ {
id: 'Recovery Plan', id: 'Recovery Plan',
@@ -35,6 +55,10 @@ export default function RecoveryIncidentTabs() {
} }
]; ];
if (loading) {
return <div className="p-8 text-center text-gray-500">Loading incident...</div>;
}
return ( return (
<div className="w-full flex flex-col h-full relative"> <div className="w-full flex flex-col h-full relative">
{/* Header */} {/* Header */}
@@ -42,14 +66,14 @@ export default function RecoveryIncidentTabs() {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<CustomBackButton /> <CustomBackButton />
<h1 className="text-xl font-bold text-gray-900">{id || 'kD6zO86KybbpgHhuRcun'}</h1> <h1 className="text-xl font-bold text-gray-900">{incident?.recoveryCode || id}</h1>
<CustomStatus status="UNDER REVIEW" variant="warning" className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide" /> <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" /> <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>
<div className="flex items-center gap-3 text-sm text-gray-500 ml-8"> <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> <div className="flex items-center gap-1.5"><User size={16} /> {incident?.passengerName || 'Unknown'}</div>
<span></span> <span></span>
<div className="flex items-center gap-1.5"><AirplaneTiltIcon size={16} /> Q23SXD (FRA JFK)</div> <div className="flex items-center gap-1.5"><AirplaneTiltIcon size={16} /> {incident?.flightNumber} ({incident?.flightRoute})</div>
</div> </div>
</div> </div>
<CustomButton <CustomButton