feat: implement recovery incident detail view with tabbed navigation and case information display
This commit is contained in:
@@ -1,24 +1,22 @@
|
||||
import { ApiClient } from '../api/ApiClient';
|
||||
import type { RecoveryIncident } from './RecoveryIncidentsTypes';
|
||||
|
||||
export const RecoveryIncidentsApi = {
|
||||
getAll: async (): Promise<RecoveryIncident[]> => {
|
||||
return ApiClient.get('/recovery-incidents');
|
||||
},
|
||||
export function getRecoveryIncidents(): Promise<RecoveryIncident[]> {
|
||||
return ApiClient.get<any, RecoveryIncident[]>('/recovery-incidents');
|
||||
}
|
||||
|
||||
getById: async (id: string): Promise<RecoveryIncident> => {
|
||||
return ApiClient.get(`/recovery-incidents/${id}`);
|
||||
},
|
||||
export function getRecoveryIncident(id: string): Promise<RecoveryIncident> {
|
||||
return ApiClient.get<any, RecoveryIncident>(`/recovery-incidents/${id}`);
|
||||
}
|
||||
|
||||
create: async (data: Omit<RecoveryIncident, 'id'>): Promise<RecoveryIncident> => {
|
||||
return ApiClient.post('/recovery-incidents', data);
|
||||
},
|
||||
export function createRecoveryIncident(data: Omit<RecoveryIncident, 'id'>): Promise<RecoveryIncident> {
|
||||
return ApiClient.post<any, RecoveryIncident>('/recovery-incidents', data);
|
||||
}
|
||||
|
||||
update: async (id: string, data: Partial<RecoveryIncident>): Promise<RecoveryIncident> => {
|
||||
return ApiClient.patch(`/recovery-incidents/${id}`, data);
|
||||
},
|
||||
export function updateRecoveryIncident(id: string, data: Partial<RecoveryIncident>): Promise<RecoveryIncident> {
|
||||
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}`, data);
|
||||
}
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
return ApiClient.delete(`/recovery-incidents/${id}`);
|
||||
},
|
||||
};
|
||||
export function deleteRecoveryIncident(id: string): Promise<void> {
|
||||
return ApiClient.delete<any, void>(`/recovery-incidents/${id}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface IncidentStatus {
|
||||
|
||||
export interface RecoveryIncident {
|
||||
id: string;
|
||||
recoveryId: string;
|
||||
recoveryCode: string;
|
||||
date: string;
|
||||
passengerName?: string;
|
||||
pnr?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
CustomInput,
|
||||
CustomDropdown,
|
||||
} from "../../../components/custom";
|
||||
import { RecoveryIncidentsApi } from '../RecoveryIncidentsApi';
|
||||
import { createRecoveryIncident } from '../RecoveryIncidentsApi';
|
||||
|
||||
interface AddRecoveryIncidentsProps {
|
||||
isOpen: boolean;
|
||||
@@ -40,7 +40,7 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
recoveryId: "REC-" + Math.floor(Math.random() * 10000),
|
||||
recoveryCode: "REC-" + Math.floor(Math.random() * 10000),
|
||||
passengerName: formData.passengerName || "Unknown",
|
||||
pnr: formData.pnr || "N/A",
|
||||
flightNumber: formData.flightNumber || "TBD",
|
||||
@@ -52,7 +52,7 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
|
||||
};
|
||||
|
||||
console.log("Submitting payload:", payload);
|
||||
await RecoveryIncidentsApi.create(payload);
|
||||
await createRecoveryIncident(payload);
|
||||
onClose(); // Will trigger refresh in parent list
|
||||
} catch (error: any) {
|
||||
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 { RecoveryIncident } from "../RecoveryIncidentsTypes";
|
||||
import AddRecoveryIncidents from "./AddRecoveryIncidents";
|
||||
import { RecoveryIncidentsApi } from "../RecoveryIncidentsApi";
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
import { getRecoveryIncidents } from "../RecoveryIncidentsApi";
|
||||
|
||||
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,
|
||||
@@ -308,10 +183,10 @@ export default function RecoveryIncidentsList() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await RecoveryIncidentsApi.getAll();
|
||||
const data = await getRecoveryIncidents();
|
||||
|
||||
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());
|
||||
const matchesGroup = isGrouped ? true : !p.isGroupHeader;
|
||||
return matchesSearch && matchesGroup;
|
||||
@@ -387,7 +262,7 @@ export default function RecoveryIncidentsList() {
|
||||
const displayData = useMemo(() => {
|
||||
if (!isGrouped) return incidents;
|
||||
return incidents.filter(
|
||||
(p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryId)
|
||||
(p) => p.isGroupHeader || !collapsedGroups.has(p.recoveryCode)
|
||||
);
|
||||
}, [incidents, isGrouped, collapsedGroups]);
|
||||
|
||||
@@ -416,7 +291,7 @@ export default function RecoveryIncidentsList() {
|
||||
header: <HeaderLabel text="Recovery ID" />,
|
||||
accessor: (row) => (
|
||||
<div>
|
||||
<PrimaryText text={row.recoveryId} />
|
||||
<PrimaryText text={row.recoveryCode} />
|
||||
<SecondaryText text={row.date} />
|
||||
</div>
|
||||
),
|
||||
@@ -486,9 +361,9 @@ export default function RecoveryIncidentsList() {
|
||||
{row.isGroupHeader ? (
|
||||
<div
|
||||
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} />
|
||||
) : (
|
||||
<CaretUp size={20} />
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* PASSENGER INFORMATION */}
|
||||
@@ -13,11 +20,11 @@ export default function CaseDetailsTab() {
|
||||
<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>
|
||||
<span className="block text-[15px] font-semibold text-gray-900">{incident.passengerName || 'Unknown'}</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>
|
||||
<span className="block text-[15px] font-semibold text-gray-900">{incident.pnr || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">SUB-TYPE</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } 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';
|
||||
@@ -7,10 +7,30 @@ import CaseDetailsTab from './CaseDetailsTab';
|
||||
import RecoveryPlanTab from './RecoveryPlanTab';
|
||||
import AuditTrailTab from './AuditTrailTab';
|
||||
import { SparkleIcon } from 'lucide-react';
|
||||
import { getRecoveryIncident } from '../RecoveryIncidentsApi';
|
||||
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
|
||||
|
||||
export default function RecoveryIncidentTabs() {
|
||||
const { id } = useParams();
|
||||
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 = [
|
||||
{
|
||||
@@ -21,7 +41,7 @@ export default function RecoveryIncidentTabs() {
|
||||
{
|
||||
id: 'Case Details',
|
||||
label: 'Case Details',
|
||||
content: <CaseDetailsTab />
|
||||
content: <CaseDetailsTab incident={incident} />
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div className="w-full flex flex-col h-full relative">
|
||||
{/* Header */}
|
||||
@@ -42,14 +66,14 @@ export default function RecoveryIncidentTabs() {
|
||||
<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>
|
||||
<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="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>
|
||||
<div className="flex items-center gap-1.5"><User size={16} /> {incident?.passengerName || 'Unknown'}</div>
|
||||
<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>
|
||||
<CustomButton
|
||||
|
||||
Reference in New Issue
Block a user