diff --git a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx index 0170a3b8e..3a9a65353 100644 --- a/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/reports/passengers/page.tsx @@ -1,264 +1,129 @@ -"use client"; +'use client'; -import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { Users, Armchair, TrendingUp, Train, Download } from "lucide-react"; -import { - BarChart, - Bar, - 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"; +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Users, Armchair, BarChart3, Train, Download } from 'lucide-react'; +import { apiClient } from '@/lib/api-client'; +import { formatDateTime } from '@/lib/utils'; +import ActionButton from '@/components/ui/ActionButton'; -const COLORS = [ - "#10b981", - "#3b82f6", - "#f59e0b", - "#8b5cf6", - "#ef4444", - "#06b6d4", -]; +interface ScheduleOption { id: string; label: string; } -function StatCard({ - label, - value, - sub, - icon: Icon, - color, -}: { - label: string; - value: string | number; - sub?: string; - icon: any; - color: string; -}) { - return ( -
-
-

- {label} -

-
- -
-
-

{value}

- {sub &&

{sub}

} -
- ); +interface PassengersReport { + schedule: { id: string; trainName: string; origin: string; destination: string; departureAt: string; arrivalAt: string; }; + summary: { totalSeats: number; totalPassengers: number; occupancyRate: number }; + byCoach: { coachNumber: string; coachType: string; totalSeats: number; booked: number; occupancyRate: number }[]; + byClass: { className: string; totalSeats: number; booked: number; occupancyRate: number }[]; + byOrigin: { stationName: string; passengers: number }[]; + byDestination: { stationName: string; passengers: number }[]; } interface PassengerRow { bookingRef: string; - bookingStatus: string; passengerName: string; - passengerCategory: string; - idDocumentType: string | null; - idDocumentNumber: string | null; - passportNumber: string | null; - passportCountry: string | null; - seatLabel: string | null; - coachNumber: string | null; - coachType: string | null; + coachSeat: string; + origin: string; + destination: string; + departureAt: string | null; } -type Tab = "occupancy" | "list"; +type Tab = 'occupancy' | 'list'; export default function PassengersReportPage() { - const [scheduleId, setScheduleId] = useState(""); - const [tab, setTab] = useState("occupancy"); - const [listSearch, setListSearch] = useState(""); + const [scheduleId, setScheduleId] = useState(''); + const [tab, setTab] = useState('occupancy'); + const [listSearch, setListSearch] = useState(''); - const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery< - ScheduleOption[] - >({ - queryKey: ["report-schedules"], - queryFn: () => apiClient.get("/reports/schedules"), + const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery({ + queryKey: ['report-schedules'], + queryFn: () => apiClient.get('/reports/schedules'), }); const schedules = schedulesRaw ?? []; const { data, isLoading, isError } = useQuery({ - queryKey: ["passengers-report", scheduleId], - queryFn: () => - apiClient.get(`/reports/passengers?scheduleId=${scheduleId}`), + queryKey: ['passengers-report', scheduleId], + queryFn: () => apiClient.get(`/reports/passengers?scheduleId=${scheduleId}`), enabled: !!scheduleId, }); - const { data: passengerList = [], isLoading: listLoading } = useQuery< - PassengerRow[] - >({ - queryKey: ["passengers-list", scheduleId], - queryFn: () => - apiClient.get(`/reports/passengers/list?scheduleId=${scheduleId}`), + const { data: passengerList = [], isLoading: listLoading } = useQuery({ + queryKey: ['passengers-list', scheduleId], + queryFn: () => apiClient.get(`/reports/passengers/list?scheduleId=${scheduleId}`), enabled: !!scheduleId, }); const filteredList = listSearch.trim() - ? passengerList.filter( - (p) => - p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) || - p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()) || - (p.idDocumentNumber ?? "") - .toLowerCase() - .includes(listSearch.toLowerCase()) || - (p.passportNumber ?? "") - .toLowerCase() - .includes(listSearch.toLowerCase()), + ? passengerList.filter(p => + p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) || + p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()), ) : passengerList; 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 a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); + const a = document.createElement('a'); + a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; const doExportOccupancy = () => { if (!data) return; - const rows = data.byCoach.map((c) => [ - c.coachNumber, - 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 rows = data.byCoach.map(c => [c.coachNumber, 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 = () => { if (!passengerList.length) return; - const headers = [ - "Booking Ref", - "Status", - "Name", - "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`, + const headers = ['#', 'Name', 'Coach·Seat', 'Origin', 'Destination', 'Date', 'Booking Ref']; + const rows = passengerList.map((p, i) => + [String(i + 1), p.passengerName, p.coachSeat, p.origin, p.destination, p.departureAt ? formatDateTime(p.departureAt) : '—', p.bookingRef] + .map(v => `"${String(v).replace(/"/g, '""')}"`) ); + downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`); }; return (
-

- Passengers Report -

-

- Select a schedule to view passenger occupancy breakdown -

+

Passengers Report

+

Occupancy and passenger breakdown for a schedule

{/* Schedule selector */}
-
-
+
+
- {data && tab === "occupancy" && ( - - Export CSV - - )} - {passengerList.length > 0 && tab === "list" && ( - - Export CSV - + {data && tab === 'occupancy' && ( + Export CSV )}
- {(isLoading || listLoading) && ( -

Loading…

- )} - {isError && ( -

Failed to load report.

- )} + {(isLoading || listLoading) &&

Loading…

} + {isError &&

Failed to load report.

}
- {isFetching && ( -
- Loading passengers data… -
- )} - - {report && ( + {data && ( <> - {/* Schedule Info */} + {/* Schedule info */}
-

{report.schedule.trainName}

+

{data.schedule.trainName}

- {report.schedule.origin} → {report.schedule.destination} · - Departure: {formatDateTime(report.schedule.departureAt)} + {data.schedule.origin} → {data.schedule.destination} · Departure: {formatDateTime(data.schedule.departureAt)}

@@ -266,75 +131,51 @@ export default function PassengersReportPage() { {/* Tabs */}
{/* Occupancy tab */} - {tab === "occupancy" && ( + {tab === 'occupancy' && (
-

- Total Seats -

-
- -
+

Total Seats

+
-

- {data.summary.totalSeats} -

+

{data.summary.totalSeats}

-

- Passengers -

-
- -
+

Passengers

+
-

- {data.summary.totalPassengers} -

+

{data.summary.totalPassengers}

-

- Occupancy Rate -

-
- -
+

Occupancy Rate

+
-

- {data.summary.occupancyRate}% -

+

{data.summary.occupancyRate}%

-
+
-

- By Coach -

+

By Coach

@@ -347,31 +188,18 @@ export default function PassengersReportPage() { - {data.byCoach.map((c) => ( + {data.byCoach.map(c => ( - - - - + + + + @@ -383,77 +211,46 @@ export default function PassengersReportPage() {
-

- By Class -

+

By Class

- {data.byClass.map((c) => ( + {data.byClass.map(c => (
{c.className} - - {c.booked}/{c.totalSeats} - + {c.booked}/{c.totalSeats}
-
+
- - {c.occupancyRate}% - + {c.occupancyRate}%
))}
-

- By Boarding Station -

+

By Boarding Station

- {data.byOrigin.map((o) => ( -
- - {o.stationName} - - - {o.passengers} - + {data.byOrigin.map(o => ( +
+ {o.stationName} + {o.passengers}
))} - {data.byOrigin.length === 0 && ( -

No data

- )} + {data.byOrigin.length === 0 &&

No data

}
-

- By Alighting Station -

+

By Alighting Station

- {data.byDestination.map((d) => ( -
- - {d.stationName} - - - {d.passengers} - + {data.byDestination.map(d => ( +
+ {d.stationName} + {d.passengers}
))} - {data.byDestination.length === 0 && ( -

No data

- )} + {data.byDestination.length === 0 &&

No data

}
@@ -461,101 +258,60 @@ export default function PassengersReportPage() { )} {/* Passenger List tab */} - {tab === "list" && ( -
- setListSearch(e.target.value)} - /> -
-
- {c.coachNumber} - - {c.coachType} - - {c.totalSeats} - - {c.booked} - {c.coachNumber}{c.coachType}{c.totalSeats}{c.booked}
-
+
- - {c.occupancyRate}% - + {c.occupancyRate}%
- - - - - - - - - - - - - - {filteredList.map((p, i) => ( - - - - - - - - - + {tab === 'list' && ( +
+
+ setListSearch(e.target.value)} + /> + {passengerList.length > 0 && ( + Export CSV + )} +
+
+
+
#NameCategoryID / PassportSeatCoachBooking RefStatus
- {i + 1} - - {p.passengerName} - - - {p.passengerCategory} - - - {p.idDocumentNumber ?? p.passportNumber ?? "—"} - {p.passportCountry && ( - - ({p.passportCountry}) - - )} - - {p.seatLabel ?? "—"} - - {p.coachNumber ?? "—"} - {p.coachType && ( - - ({p.coachType}) - - )} - - {p.bookingRef} - - - {p.bookingStatus} - -
+ + + + + + + + + - ))} - {filteredList.length === 0 && ( - - - - )} - -
#NameCoach · SeatOriginDestinationDateBooking Ref
- No passengers found -
+ + + {filteredList.map((p, i) => ( + + {i + 1} + {p.passengerName} + {p.coachSeat} + {p.origin} + {p.destination} + {p.departureAt ? formatDateTime(p.departureAt) : '—'} + {p.bookingRef} + + ))} + {filteredList.length === 0 && ( + No passengers found + )} + + +
)} )} - {!report && !isFetching && scheduleId && ( -
- No data found for this schedule. -
+ {!data && !isLoading && scheduleId && ( +
No data found for this schedule.
)} {!scheduleId && ( diff --git a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx index d48e10d51..2298ebf9f 100644 --- a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx @@ -184,7 +184,7 @@ export default function SchedulesPage() { 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({ search: '',