From 264206b57a83bb15dd15d7e54f5886f15778110f Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sun, 19 Jul 2026 20:58:00 +0300 Subject: [PATCH] Passenger report updates --- .../src/modules/reports/reports.service.ts | 15 +- .../src/app/reports/passengers/page.tsx | 85 ++++++------ .../backoffice/src/app/schedules/page.tsx | 129 +++++++++++------- 3 files changed, 135 insertions(+), 94 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index 5fa590620..7cee8e552 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -311,7 +311,17 @@ export class ReportsService { booking: { status: { in: ['CONFIRMED', 'BOARDED'] } }, }, include: { - booking: { select: { bookingRef: true, status: true, originStationId: true, destinationStationId: true } }, + booking: { + select: { + bookingRef: true, + status: true, + originStationId: true, + destinationStationId: true, + totalMinor: true, + currency: true, + _count: { select: { seats: true } }, + }, + }, seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } }, }, orderBy: [{ seat: { coach: { number: 'asc' } } }], @@ -339,6 +349,9 @@ export class ReportsService { coachType: (bs.seat?.coach as any)?.coachType?.name ?? null, origin: bs.booking.originStationId ? (stationMap.get(bs.booking.originStationId) ?? null) : null, destination: bs.booking.destinationStationId ? (stationMap.get(bs.booking.destinationStationId) ?? null) : null, + amountPaidMinor: bs.booking.totalMinor, + currency: bs.booking.currency ?? 'ETB', + isGroupBooking: (bs.booking._count?.seats ?? 0) > 1, })); } 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 60d5cea09..2cbc07ca6 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 @@ -27,12 +27,14 @@ interface PassengerRow { idDocumentNumber: string | null; passportNumber: string | null; passportCountry: string | null; - nationality: string | null; seatLabel: string | null; coachNumber: string | null; coachType: string | null; origin: string | null; destination: string | null; + amountPaidMinor: number; + currency: string; + isGroupBooking: boolean; } type Tab = 'occupancy' | 'list'; @@ -65,20 +67,22 @@ export default function PassengersReportPage() { const coachOptions = [...new Set(passengerList.map(p => p.coachNumber).filter(Boolean))].sort() as string[]; const originOptions = [...new Set(passengerList.map(p => p.origin).filter(Boolean))].sort() as string[]; - const filteredList = passengerList.filter(p => { - if (filterCoach && p.coachNumber !== filterCoach) return false; - if (filterOrigin && p.origin !== filterOrigin) return false; - if (listSearch.trim()) { - const q = listSearch.toLowerCase(); - return ( - p.passengerName.toLowerCase().includes(q) || - p.bookingRef.toLowerCase().includes(q) || - (p.idDocumentNumber ?? '').toLowerCase().includes(q) || - (p.passportNumber ?? '').toLowerCase().includes(q) - ); - } - return true; - }); + const filteredList = passengerList + .filter(p => { + if (filterCoach && p.coachNumber !== filterCoach) return false; + if (filterOrigin && p.origin !== filterOrigin) return false; + if (listSearch.trim()) { + const q = listSearch.toLowerCase(); + return ( + p.passengerName.toLowerCase().includes(q) || + p.bookingRef.toLowerCase().includes(q) || + (p.idDocumentNumber ?? '').toLowerCase().includes(q) || + (p.passportNumber ?? '').toLowerCase().includes(q) + ); + } + return true; + }) + .sort((a, b) => a.bookingRef.localeCompare(b.bookingRef)); const downloadCsv = (csv: string, filename: string) => { const blob = new Blob([csv], { type: 'text/csv' }); @@ -96,12 +100,14 @@ export default function PassengersReportPage() { const doExportList = () => { if (!passengerList.length) return; - const headers = ['Booking Ref', 'Status', 'Name', 'Category', 'Nationality / Passport', 'Coach · Seat', 'Origin', 'Destination']; - const rows = passengerList.map(p => [ - p.bookingRef, p.bookingStatus, p.passengerName, p.passengerCategory, + const headers = ['Booking Ref', 'Name', 'Nationality', 'Coach · Seat', 'Trip', 'Amount Paid', 'Group Booking']; + const rows = [...passengerList].sort((a, b) => a.bookingRef.localeCompare(b.bookingRef)).map(p => [ + p.bookingRef, p.passengerName, p.passportNumber ? `${p.passportCountry ?? ''} · ${p.passportNumber}` : (p.idDocumentNumber ?? ''), p.coachNumber && p.seatLabel ? `${p.coachNumber} · ${p.seatLabel}` : (p.coachNumber ?? p.seatLabel ?? ''), - p.origin ?? '', p.destination ?? '', + p.origin && p.destination ? `${p.origin} → ${p.destination}` : (p.origin ?? p.destination ?? ''), + `${(p.amountPaidMinor / 100).toFixed(2)} ${p.currency}`, + p.isGroupBooking ? 'Yes' : 'No', ].map(v => `"${String(v).replace(/"/g, '""')}"`)); downloadCsv([headers.join(','), ...rows.map(r => r.join(','))].join('\n'), `passengers-${scheduleId}.csv`); }; @@ -314,27 +320,18 @@ export default function PassengersReportPage() { - - - + - - - - + + + {filteredList.map((p, i) => ( - - - - - - + + ))} {filteredList.length === 0 && ( - + )}
# NameCategoryNationality / PassportNationality Coach · SeatOriginDestinationBooking RefStatusTripAmount PaidBooking Ref
{i + 1} {p.passengerName} - - {p.passengerCategory} - - {p.passportNumber ? ( <> @@ -347,21 +344,27 @@ export default function PassengersReportPage() { {p.coachNumber && p.seatLabel - ? `${p.coachNumber} · ${p.seatLabel}` + ? <>{p.coachNumber} · {p.seatLabel}{p.coachType && ({p.coachType})} : (p.coachNumber ?? p.seatLabel ?? '—')} {p.origin ?? '—'}{p.destination ?? '—'}{p.bookingRef} - - {p.bookingStatus} - + + {p.origin && p.destination ? `${p.origin} → ${p.destination}` : (p.origin ?? p.destination ?? '—')} +
+ + {(p.amountPaidMinor / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {p.currency} + + {p.isGroupBooking && ( + Group + )} +
+
{p.bookingRef}
No passengers found
No passengers found
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 4ef9b97ed..d9a0e7ba8 100644 --- a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx @@ -3,6 +3,68 @@ import { useState, useEffect } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Plus, Loader2, Zap, Trash2, Edit, Search, X, GripVertical } from 'lucide-react'; + +// ── 12-hour datetime picker ────────────────────────────────────────────────── +interface DTPProps { label: string; value: string; onChange: (v: string) => void; required?: boolean; } + +/** value / onChange use "YYYY-MM-DDTHH:mm" (24-hr, local) — same as datetime-local */ +function DateTimePicker({ label, value, onChange, required }: DTPProps) { + const datePart = value.slice(0, 10); + const timePart = value.slice(11, 16); // HH:mm 24-hr + + const hour24 = timePart ? parseInt(timePart.slice(0, 2), 10) : 12; + const minute = timePart ? timePart.slice(3, 5) : '00'; + const period = hour24 >= 12 ? 'PM' : 'AM'; + const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12; + + const emit = (d: string, h12: number, m: string, p: string) => { + if (!d) return; + const h24 = p === 'AM' ? (h12 === 12 ? 0 : h12) : (h12 === 12 ? 12 : h12 + 12); + onChange(`${d}T${String(h24).padStart(2, '0')}:${m}`); + }; + + return ( +
+ +
+ emit(e.target.value, hour12, minute, period)} + /> + + + +
+
+ ); +} +// ──────────────────────────────────────────────────────────────────────────── import DataTable from '@/components/ui/DataTable'; import ActionButton from '@/components/ui/ActionButton'; import Modal from '@/components/ui/Modal'; @@ -327,18 +389,15 @@ export default function SchedulesPage() { const handleEditClick = (schedule: Schedule) => { setEditingSchedule(schedule); - // Convert UTC dates to local time for datetime-local input - // datetime-local expects local time (no timezone info) - const dep = new Date(schedule.departureAt); - const arr = new Date(schedule.arrivalAt); + // Format UTC ISO string as local YYYY-MM-DDTHH:mm for the picker + const toLocalDT = (iso: string) => { + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; + }; - // Convert to local time by adding the timezone offset - const depLocal = new Date(dep.getTime() + dep.getTimezoneOffset() * 60000); - const arrLocal = new Date(arr.getTime() + arr.getTimezoneOffset() * 60000); - - // Format for datetime-local input (YYYY-MM-DDTHH:mm) - const depStr = depLocal.toISOString().slice(0, 16); - const arrStr = arrLocal.toISOString().slice(0, 16); + const depStr = toLocalDT(schedule.departureAt); + const arrStr = toLocalDT(schedule.arrivalAt); setEditForm({ departureAt: depStr, @@ -696,15 +755,9 @@ export default function SchedulesPage() { -
-
- - setAddForm({ ...addForm, departureAt: e.target.value })} required /> -
-
- - setAddForm({ ...addForm, arrivalAt: e.target.value })} required /> -
+
+ setAddForm({ ...addForm, departureAt: v })} required /> + setAddForm({ ...addForm, arrivalAt: v })} required />
@@ -832,16 +885,7 @@ export default function SchedulesPage() {
-
- - setBulkForm({ ...bulkForm, startDateTime: e.target.value })} - className="input" - required - /> -
+ setBulkForm({ ...bulkForm, startDateTime: v })} required />
@@ -1019,28 +1063,9 @@ export default function SchedulesPage() {
)} -
-
- - setEditForm({ ...editForm, departureAt: e.target.value })} - className="input" - required - /> -
- -
- - setEditForm({ ...editForm, arrivalAt: e.target.value })} - className="input" - required - /> -
+
+ setEditForm({ ...editForm, departureAt: v })} required /> + setEditForm({ ...editForm, arrivalAt: v })} required />