Build issue resolution

This commit is contained in:
Stephanos A
2026-07-19 09:08:30 +03:00
parent 6115d7d782
commit 20718d5b98
2 changed files with 148 additions and 392 deletions

View File

@@ -1,264 +1,129 @@
"use client"; 'use client';
import { useState } from "react"; import { useState } from 'react';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from '@tanstack/react-query';
import { Users, Armchair, TrendingUp, Train, Download } from "lucide-react"; import { Users, Armchair, BarChart3, Train, Download } from 'lucide-react';
import { import { apiClient } from '@/lib/api-client';
BarChart, import { formatDateTime } from '@/lib/utils';
Bar, import ActionButton from '@/components/ui/ActionButton';
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import { apiClient } from "@/lib/api-client";
import { formatDateTime } from "@/lib/utils";
import ActionButton from "@/components/ui/ActionButton";
const COLORS = [ interface ScheduleOption { id: string; label: string; }
"#10b981",
"#3b82f6",
"#f59e0b",
"#8b5cf6",
"#ef4444",
"#06b6d4",
];
function StatCard({ interface PassengersReport {
label, schedule: { id: string; trainName: string; origin: string; destination: string; departureAt: string; arrivalAt: string; };
value, summary: { totalSeats: number; totalPassengers: number; occupancyRate: number };
sub, byCoach: { coachNumber: string; coachType: string; totalSeats: number; booked: number; occupancyRate: number }[];
icon: Icon, byClass: { className: string; totalSeats: number; booked: number; occupancyRate: number }[];
color, byOrigin: { stationName: string; passengers: number }[];
}: { byDestination: { stationName: string; passengers: number }[];
label: string;
value: string | number;
sub?: string;
icon: any;
color: string;
}) {
return (
<div className="card flex flex-col gap-1">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{label}
</p>
<div className={`rounded-lg p-1.5 ${color}`}>
<Icon className="h-4 w-4" />
</div>
</div>
<p className="text-2xl font-bold tabular-nums mt-1">{value}</p>
{sub && <p className="text-xs text-muted-foreground">{sub}</p>}
</div>
);
} }
interface PassengerRow { interface PassengerRow {
bookingRef: string; bookingRef: string;
bookingStatus: string;
passengerName: string; passengerName: string;
passengerCategory: string; coachSeat: string;
idDocumentType: string | null; origin: string;
idDocumentNumber: string | null; destination: string;
passportNumber: string | null; departureAt: string | null;
passportCountry: string | null;
seatLabel: string | null;
coachNumber: string | null;
coachType: string | null;
} }
type Tab = "occupancy" | "list"; type Tab = 'occupancy' | 'list';
export default function PassengersReportPage() { export default function PassengersReportPage() {
const [scheduleId, setScheduleId] = useState(""); const [scheduleId, setScheduleId] = useState('');
const [tab, setTab] = useState<Tab>("occupancy"); const [tab, setTab] = useState<Tab>('occupancy');
const [listSearch, setListSearch] = useState(""); const [listSearch, setListSearch] = useState('');
const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery< const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery<ScheduleOption[]>({
ScheduleOption[] queryKey: ['report-schedules'],
>({ queryFn: () => apiClient.get('/reports/schedules'),
queryKey: ["report-schedules"],
queryFn: () => apiClient.get("/reports/schedules"),
}); });
const schedules = schedulesRaw ?? []; const schedules = schedulesRaw ?? [];
const { data, isLoading, isError } = useQuery<PassengersReport>({ const { data, isLoading, isError } = useQuery<PassengersReport>({
queryKey: ["passengers-report", scheduleId], queryKey: ['passengers-report', scheduleId],
queryFn: () => queryFn: () => apiClient.get(`/reports/passengers?scheduleId=${scheduleId}`),
apiClient.get(`/reports/passengers?scheduleId=${scheduleId}`),
enabled: !!scheduleId, enabled: !!scheduleId,
}); });
const { data: passengerList = [], isLoading: listLoading } = useQuery< const { data: passengerList = [], isLoading: listLoading } = useQuery<PassengerRow[]>({
PassengerRow[] queryKey: ['passengers-list', scheduleId],
>({ queryFn: () => apiClient.get(`/reports/passengers/list?scheduleId=${scheduleId}`),
queryKey: ["passengers-list", scheduleId],
queryFn: () =>
apiClient.get(`/reports/passengers/list?scheduleId=${scheduleId}`),
enabled: !!scheduleId, enabled: !!scheduleId,
}); });
const filteredList = listSearch.trim() const filteredList = listSearch.trim()
? passengerList.filter( ? passengerList.filter(p =>
(p) => p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) ||
p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) || p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()),
p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()) ||
(p.idDocumentNumber ?? "")
.toLowerCase()
.includes(listSearch.toLowerCase()) ||
(p.passportNumber ?? "")
.toLowerCase()
.includes(listSearch.toLowerCase()),
) )
: passengerList; : passengerList;
const downloadCsv = (csv: string, filename: string) => { const downloadCsv = (csv: string, filename: string) => {
const blob = new Blob([csv], { type: "text/csv" }); const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement('a');
a.href = url; a.href = url; a.download = filename; a.click();
a.download = filename;
a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const doExportOccupancy = () => { const doExportOccupancy = () => {
if (!data) return; if (!data) return;
const rows = data.byCoach.map((c) => [ const rows = data.byCoach.map(c => [c.coachNumber, c.coachType, String(c.totalSeats), String(c.booked), `${c.occupancyRate}%`]);
c.coachNumber, downloadCsv([['Coach', 'Type', 'Total Seats', 'Booked', 'Occupancy'].join(','), ...rows.map(r => r.join(','))].join('\n'), `occupancy-${scheduleId}.csv`);
c.coachType,
String(c.totalSeats),
String(c.booked),
`${c.occupancyRate}%`,
]);
downloadCsv(
[
["Coach", "Type", "Total Seats", "Booked", "Occupancy"].join(","),
...rows.map((r) => r.join(",")),
].join("\n"),
`occupancy-${scheduleId}.csv`,
);
}; };
const doExportList = () => { const doExportList = () => {
if (!passengerList.length) return; if (!passengerList.length) return;
const headers = [ const headers = ['#', 'Name', 'Coach·Seat', 'Origin', 'Destination', 'Date', 'Booking Ref'];
"Booking Ref", const rows = passengerList.map((p, i) =>
"Status", [String(i + 1), p.passengerName, p.coachSeat, p.origin, p.destination, p.departureAt ? formatDateTime(p.departureAt) : '—', p.bookingRef]
"Name", .map(v => `"${String(v).replace(/"/g, '""')}"`)
"Category",
"ID Type",
"ID Number",
"Passport",
"Country",
"Seat",
"Coach",
"Class",
];
const rows = passengerList.map((p) =>
[
p.bookingRef,
p.bookingStatus,
p.passengerName,
p.passengerCategory,
p.idDocumentType ?? "",
p.idDocumentNumber ?? "",
p.passportNumber ?? "",
p.passportCountry ?? "",
p.seatLabel ?? "",
p.coachNumber ?? "",
p.coachType ?? "",
].map((v) => `"${String(v).replace(/"/g, '""')}"`),
);
downloadCsv(
[headers.join(","), ...rows.map((r) => r.join(","))].join("\n"),
`passengers-${scheduleId}.csv`,
); );
downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`);
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold text-foreground"> <h1 className="text-3xl font-bold text-foreground">Passengers Report</h1>
Passengers Report <p className="text-muted-foreground mt-1">Occupancy and passenger breakdown for a schedule</p>
</h1>
<p className="text-muted-foreground mt-1">
Select a schedule to view passenger occupancy breakdown
</p>
</div> </div>
{/* Schedule selector */} {/* Schedule selector */}
<div className="card"> <div className="card">
<div className="flex flex-wrap items-end gap-4"> <div className="flex items-end gap-4 flex-wrap">
<div className="flex-1 min-w-64"> <div className="flex-1 min-w-72">
<label className="label">Schedule</label> <label className="label">Schedule</label>
<select <select
className="input" className="input"
value={scheduleId} value={scheduleId}
onChange={(e) => { onChange={e => { setScheduleId(e.target.value); setTab('occupancy'); setListSearch(''); }}
setScheduleId(e.target.value);
setTab("occupancy");
setListSearch("");
}}
disabled={loadingSchedules} disabled={loadingSchedules}
> >
<option value=""> <option value="">{loadingSchedules ? 'Loading schedules…' : 'Select a schedule…'}</option>
{loadingSchedules ? "Loading schedules…" : "Select a schedule…"} {schedules.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
</option>
{schedules.map((s) => (
<option key={s.id} value={s.id}>
{s.label}
</option>
))}
</select> </select>
</div> </div>
{data && tab === "occupancy" && ( {data && tab === 'occupancy' && (
<ActionButton <ActionButton icon={Download} variant="secondary" onClick={doExportOccupancy}>Export CSV</ActionButton>
icon={Download}
variant="secondary"
onClick={doExportOccupancy}
>
Export CSV
</ActionButton>
)}
{passengerList.length > 0 && tab === "list" && (
<ActionButton
icon={Download}
variant="secondary"
onClick={doExportList}
>
Export CSV
</ActionButton>
)} )}
</div> </div>
{(isLoading || listLoading) && ( {(isLoading || listLoading) && <p className="text-xs text-muted-foreground mt-2">Loading</p>}
<p className="text-xs text-muted-foreground mt-2">Loading</p> {isError && <p className="text-xs text-red-500 mt-2">Failed to load report.</p>}
)}
{isError && (
<p className="text-xs text-red-500 mt-2">Failed to load report.</p>
)}
</div> </div>
{isFetching && ( {data && (
<div className="card py-12 text-center text-muted-foreground">
Loading passengers data
</div>
)}
{report && (
<> <>
{/* Schedule Info */} {/* Schedule info */}
<div className="card flex items-center gap-4"> <div className="card flex items-center gap-4">
<div className="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 p-2.5"> <div className="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 p-2.5">
<Train className="h-5 w-5 text-emerald-600 dark:text-emerald-400" /> <Train className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
</div> </div>
<div> <div>
<p className="font-semibold">{report.schedule.trainName}</p> <p className="font-semibold">{data.schedule.trainName}</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{report.schedule.origin} {report.schedule.destination} · {data.schedule.origin} {data.schedule.destination} · Departure: {formatDateTime(data.schedule.departureAt)}
Departure: {formatDateTime(report.schedule.departureAt)}
</p> </p>
</div> </div>
</div> </div>
@@ -266,75 +131,51 @@ export default function PassengersReportPage() {
{/* Tabs */} {/* Tabs */}
<div className="border-b border-border flex"> <div className="border-b border-border flex">
<button <button
onClick={() => setTab("occupancy")} onClick={() => setTab('occupancy')}
className={`px-5 py-2.5 text-sm font-medium border-b-2 transition-colors ${tab === "occupancy" ? "border-emerald-500 text-emerald-600 dark:text-emerald-400" : "border-transparent text-muted-foreground hover:text-foreground"}`} className={`px-5 py-2.5 text-sm font-medium border-b-2 transition-colors ${tab === 'occupancy' ? 'border-emerald-500 text-emerald-600 dark:text-emerald-400' : 'border-transparent text-muted-foreground hover:text-foreground'}`}
> >
Occupancy Occupancy
</button> </button>
<button <button
onClick={() => setTab("list")} onClick={() => setTab('list')}
className={`px-5 py-2.5 text-sm font-medium border-b-2 transition-colors ${tab === "list" ? "border-emerald-500 text-emerald-600 dark:text-emerald-400" : "border-transparent text-muted-foreground hover:text-foreground"}`} className={`px-5 py-2.5 text-sm font-medium border-b-2 transition-colors ${tab === 'list' ? 'border-emerald-500 text-emerald-600 dark:text-emerald-400' : 'border-transparent text-muted-foreground hover:text-foreground'}`}
> >
Passenger List Passenger List{passengerList.length > 0 ? ` (${passengerList.length})` : ''}
{passengerList.length > 0 ? ` (${passengerList.length})` : ""}
</button> </button>
</div> </div>
{/* Occupancy tab */} {/* Occupancy tab */}
{tab === "occupancy" && ( {tab === 'occupancy' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="card flex flex-col gap-1"> <div className="card flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"> <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Total Seats</p>
Total Seats <div className="rounded-lg bg-blue-100 dark:bg-blue-900/30 p-1.5"><Armchair className="h-4 w-4 text-blue-600 dark:text-blue-400" /></div>
</p>
<div className="rounded-lg bg-blue-100 dark:bg-blue-900/30 p-1.5">
<Armchair className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
</div> </div>
<p className="text-2xl font-bold tabular-nums mt-1"> <p className="text-2xl font-bold tabular-nums mt-1">{data.summary.totalSeats}</p>
{data.summary.totalSeats}
</p>
</div> </div>
<div className="card flex flex-col gap-1"> <div className="card flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"> <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Passengers</p>
Passengers <div className="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 p-1.5"><Users className="h-4 w-4 text-emerald-600 dark:text-emerald-400" /></div>
</p>
<div className="rounded-lg bg-emerald-100 dark:bg-emerald-900/30 p-1.5">
<Users className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
</div>
</div> </div>
<p className="text-2xl font-bold tabular-nums mt-1"> <p className="text-2xl font-bold tabular-nums mt-1">{data.summary.totalPassengers}</p>
{data.summary.totalPassengers}
</p>
</div> </div>
<div className="card flex flex-col gap-1"> <div className="card flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"> <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Occupancy Rate</p>
Occupancy Rate <div className="rounded-lg bg-purple-100 dark:bg-purple-900/30 p-1.5"><BarChart3 className="h-4 w-4 text-purple-600 dark:text-purple-400" /></div>
</p>
<div className="rounded-lg bg-purple-100 dark:bg-purple-900/30 p-1.5">
<BarChart3 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
</div>
</div> </div>
<p className="text-2xl font-bold tabular-nums mt-1"> <p className="text-2xl font-bold tabular-nums mt-1">{data.summary.occupancyRate}%</p>
{data.summary.occupancyRate}%
</p>
<div className="w-full bg-muted rounded-full h-1.5 mt-1"> <div className="w-full bg-muted rounded-full h-1.5 mt-1">
<div <div className="bg-purple-500 h-1.5 rounded-full" style={{ width: `${data.summary.occupancyRate}%` }} />
className="bg-purple-500 h-1.5 rounded-full"
style={{ width: `${data.summary.occupancyRate}%` }}
/>
</div> </div>
</div> </div>
</div> </div>
<div className="card"> <div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"> <h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Coach</h3>
By Coach
</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
@@ -347,31 +188,18 @@ export default function PassengersReportPage() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{data.byCoach.map((c) => ( {data.byCoach.map(c => (
<tr key={c.coachNumber} className="hover:bg-muted/30"> <tr key={c.coachNumber} className="hover:bg-muted/30">
<td className="py-2 pr-4 font-semibold"> <td className="py-2 pr-4 font-semibold">{c.coachNumber}</td>
{c.coachNumber} <td className="py-2 pr-4 text-muted-foreground">{c.coachType}</td>
</td> <td className="py-2 pr-4 text-right tabular-nums">{c.totalSeats}</td>
<td className="py-2 pr-4 text-muted-foreground"> <td className="py-2 pr-4 text-right tabular-nums">{c.booked}</td>
{c.coachType}
</td>
<td className="py-2 pr-4 text-right tabular-nums">
{c.totalSeats}
</td>
<td className="py-2 pr-4 text-right tabular-nums">
{c.booked}
</td>
<td className="py-2"> <td className="py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 bg-muted rounded-full h-1.5"> <div className="flex-1 bg-muted rounded-full h-1.5">
<div <div className="bg-emerald-500 h-1.5 rounded-full" style={{ width: `${c.occupancyRate}%` }} />
className="bg-emerald-500 h-1.5 rounded-full"
style={{ width: `${c.occupancyRate}%` }}
/>
</div> </div>
<span className="tabular-nums text-xs w-10 text-right"> <span className="tabular-nums text-xs w-10 text-right">{c.occupancyRate}%</span>
{c.occupancyRate}%
</span>
</div> </div>
</td> </td>
</tr> </tr>
@@ -383,77 +211,46 @@ export default function PassengersReportPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="card"> <div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"> <h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Class</h3>
By Class
</h3>
<div className="space-y-3"> <div className="space-y-3">
{data.byClass.map((c) => ( {data.byClass.map(c => (
<div key={c.className}> <div key={c.className}>
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1">
<span className="font-medium">{c.className}</span> <span className="font-medium">{c.className}</span>
<span className="tabular-nums text-muted-foreground"> <span className="tabular-nums text-muted-foreground">{c.booked}/{c.totalSeats}</span>
{c.booked}/{c.totalSeats}
</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 bg-muted rounded-full h-1.5"> <div className="flex-1 bg-muted rounded-full h-1.5">
<div <div className="bg-blue-500 h-1.5 rounded-full" style={{ width: `${c.occupancyRate}%` }} />
className="bg-blue-500 h-1.5 rounded-full"
style={{ width: `${c.occupancyRate}%` }}
/>
</div> </div>
<span className="text-xs tabular-nums w-10 text-right"> <span className="text-xs tabular-nums w-10 text-right">{c.occupancyRate}%</span>
{c.occupancyRate}%
</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="card"> <div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"> <h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Boarding Station</h3>
By Boarding Station
</h3>
<div className="space-y-2"> <div className="space-y-2">
{data.byOrigin.map((o) => ( {data.byOrigin.map(o => (
<div <div key={o.stationName} className="flex justify-between text-sm">
key={o.stationName} <span className="text-muted-foreground truncate">{o.stationName}</span>
className="flex justify-between text-sm" <span className="font-semibold tabular-nums ml-2">{o.passengers}</span>
>
<span className="text-muted-foreground truncate">
{o.stationName}
</span>
<span className="font-semibold tabular-nums ml-2">
{o.passengers}
</span>
</div> </div>
))} ))}
{data.byOrigin.length === 0 && ( {data.byOrigin.length === 0 && <p className="text-xs text-muted-foreground">No data</p>}
<p className="text-xs text-muted-foreground">No data</p>
)}
</div> </div>
</div> </div>
<div className="card"> <div className="card">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4"> <h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">By Alighting Station</h3>
By Alighting Station
</h3>
<div className="space-y-2"> <div className="space-y-2">
{data.byDestination.map((d) => ( {data.byDestination.map(d => (
<div <div key={d.stationName} className="flex justify-between text-sm">
key={d.stationName} <span className="text-muted-foreground truncate">{d.stationName}</span>
className="flex justify-between text-sm" <span className="font-semibold tabular-nums ml-2">{d.passengers}</span>
>
<span className="text-muted-foreground truncate">
{d.stationName}
</span>
<span className="font-semibold tabular-nums ml-2">
{d.passengers}
</span>
</div> </div>
))} ))}
{data.byDestination.length === 0 && ( {data.byDestination.length === 0 && <p className="text-xs text-muted-foreground">No data</p>}
<p className="text-xs text-muted-foreground">No data</p>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -461,101 +258,60 @@ export default function PassengersReportPage() {
)} )}
{/* Passenger List tab */} {/* Passenger List tab */}
{tab === "list" && ( {tab === 'list' && (
<div className="card space-y-4"> <div className="space-y-4">
<input <div className="flex items-center gap-3 flex-wrap">
type="text" <input
className="input max-w-sm" type="text"
placeholder="Search by name, booking ref or ID…" className="input max-w-sm flex-1"
value={listSearch} placeholder="Search by name or booking ref…"
onChange={(e) => setListSearch(e.target.value)} value={listSearch}
/> onChange={e => setListSearch(e.target.value)}
<div className="overflow-x-auto"> />
<table className="w-full text-sm"> {passengerList.length > 0 && (
<thead> <ActionButton icon={Download} variant="secondary" onClick={doExportList}>Export CSV</ActionButton>
<tr className="border-b border-border text-left text-xs text-muted-foreground uppercase tracking-wider"> )}
<th className="pb-2 pr-4">#</th> </div>
<th className="pb-2 pr-4">Name</th> <div className="card p-0">
<th className="pb-2 pr-4">Category</th> <div className="overflow-x-auto">
<th className="pb-2 pr-4">ID / Passport</th> <table className="w-full text-sm">
<th className="pb-2 pr-4">Seat</th> <thead>
<th className="pb-2 pr-4">Coach</th> <tr className="border-b border-border text-left text-xs text-muted-foreground uppercase tracking-wider">
<th className="pb-2 pr-4">Booking Ref</th> <th className="px-4 py-3">#</th>
<th className="pb-2">Status</th> <th className="px-4 py-3">Name</th>
</tr> <th className="px-4 py-3">Coach · Seat</th>
</thead> <th className="px-4 py-3">Origin</th>
<tbody className="divide-y divide-border"> <th className="px-4 py-3">Destination</th>
{filteredList.map((p, i) => ( <th className="px-4 py-3">Date</th>
<tr <th className="px-4 py-3">Booking Ref</th>
key={`${p.bookingRef}-${i}`}
className="hover:bg-muted/30"
>
<td className="py-2 pr-4 text-muted-foreground tabular-nums">
{i + 1}
</td>
<td className="py-2 pr-4 font-medium">
{p.passengerName}
</td>
<td className="py-2 pr-4">
<span
className={`text-xs font-semibold px-1.5 py-0.5 rounded ${p.passengerCategory === "CHILD" ? "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`}
>
{p.passengerCategory}
</span>
</td>
<td className="py-2 pr-4 text-muted-foreground text-xs">
{p.idDocumentNumber ?? p.passportNumber ?? "—"}
{p.passportCountry && (
<span className="ml-1 text-muted-foreground/60">
({p.passportCountry})
</span>
)}
</td>
<td className="py-2 pr-4 font-mono text-xs">
{p.seatLabel ?? "—"}
</td>
<td className="py-2 pr-4 text-muted-foreground">
{p.coachNumber ?? "—"}
{p.coachType && (
<span className="ml-1 text-xs text-muted-foreground/60">
({p.coachType})
</span>
)}
</td>
<td className="py-2 pr-4 font-mono text-xs">
{p.bookingRef}
</td>
<td className="py-2">
<span
className={`text-xs font-semibold px-1.5 py-0.5 rounded ${p.bookingStatus === "BOARDED" ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400" : "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"}`}
>
{p.bookingStatus}
</span>
</td>
</tr> </tr>
))} </thead>
{filteredList.length === 0 && ( <tbody className="divide-y divide-border">
<tr> {filteredList.map((p, i) => (
<td <tr key={`${p.bookingRef}-${i}`} className="hover:bg-muted/30">
colSpan={8} <td className="px-4 py-3 text-muted-foreground tabular-nums">{i + 1}</td>
className="py-8 text-center text-sm text-muted-foreground" <td className="px-4 py-3 font-medium">{p.passengerName}</td>
> <td className="px-4 py-3 font-mono text-xs">{p.coachSeat}</td>
No passengers found <td className="px-4 py-3 text-muted-foreground">{p.origin}</td>
</td> <td className="px-4 py-3 text-muted-foreground">{p.destination}</td>
</tr> <td className="px-4 py-3 text-xs text-muted-foreground whitespace-nowrap">{p.departureAt ? formatDateTime(p.departureAt) : '—'}</td>
)} <td className="px-4 py-3 font-mono text-xs">{p.bookingRef}</td>
</tbody> </tr>
</table> ))}
{filteredList.length === 0 && (
<tr><td colSpan={7} className="py-8 text-center text-sm text-muted-foreground">No passengers found</td></tr>
)}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
)} )}
</> </>
)} )}
{!report && !isFetching && scheduleId && ( {!data && !isLoading && scheduleId && (
<div className="card py-12 text-center text-muted-foreground"> <div className="card py-12 text-center text-muted-foreground">No data found for this schedule.</div>
No data found for this schedule.
</div>
)} )}
{!scheduleId && ( {!scheduleId && (

View File

@@ -184,7 +184,7 @@ export default function SchedulesPage() {
plannedDepartureAt: eatDateStr && depTimePart ? `${eatDateStr}T${depTimePart}` : '', plannedDepartureAt: eatDateStr && depTimePart ? `${eatDateStr}T${depTimePart}` : '',
}; };
})); }));
}, [editRouteDetail, editingSchedule?.id, editForm.departureAt]); }, [editRouteDetail, editingSchedule?.id, editForm.departureAt]); // eslint-disable-line react-hooks/exhaustive-deps
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
search: '', search: '',