From 8e84fcebd504cbc5f1c867e4b7a338dd301e4e72 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sun, 19 Jul 2026 22:52:23 +0300 Subject: [PATCH 1/2] Passenger list report filter updates --- .../src/modules/reports/reports.service.ts | 16 ++++++- .../src/app/reports/passengers/page.tsx | 43 ++++++++++++------- 2 files changed, 42 insertions(+), 17 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 55f3e21bf..b10d6963f 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -328,7 +328,7 @@ export class ReportsService { _count: { select: { seats: true } }, }, }, - seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } }, + seat: { select: { seatNumber: true, bedPosition: true, coach: { select: { number: true, coachType: { select: { name: true, seatClasses: { select: { name: true, bedPosition: true } } } } } } } }, }, orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }], }); @@ -356,13 +356,25 @@ export class ReportsService { passportNumber: bs.passportNumber, passportCountry: bs.passportCountry, seatLabel: bs.seatLabelSnapshot, + seatNumber: bs.seat?.seatNumber ?? null, + seatClassName: (() => { + const classes = bs.seat?.coach?.coachType?.seatClasses ?? []; + const matched = bs.seat?.bedPosition + ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === bs.seat!.bedPosition!.toLowerCase()) + : null; + return (matched ?? classes[0])?.name ?? bs.seat?.coach?.coachType?.name ?? null; + })(), coachNumber: bs.seat?.coach?.number ?? null, - coachType: (bs.seat?.coach as any)?.coachType?.name ?? null, + coachType: bs.seat?.coach?.coachType?.name ?? null, + nationality: bs.passportCountry + ? (bs.passportCountry === 'Djibouti' ? 'Djiboutian' : bs.passportCountry) + : bs.idDocumentType === 'NATIONAL_ID' ? 'Ethiopian' : null, origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? null) : null, destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? null) : null, amountPaidMinor: bs.booking.totalMinor, currency: bs.booking.currency ?? 'ETB', isGroupBooking: (bs.booking._count?.seats ?? 0) > 1, + bookingStatus: bs.booking.status, })); } 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 4e2647877..baa561728 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,13 +27,17 @@ interface PassengerRow { passportNumber: string | null; passportCountry: string | null; seatLabel: string | null; + seatNumber: string | null; + seatClassName: string | null; coachNumber: string | null; coachType: string | null; + nationality: string | null; origin: string | null; destination: string | null; amountPaidMinor: number; currency: string; isGroupBooking: boolean; + bookingStatus: string; } type Tab = 'occupancy' | 'list'; @@ -44,6 +48,7 @@ export default function PassengersReportPage() { const [listSearch, setListSearch] = useState(''); const [filterCoach, setFilterCoach] = useState(''); const [filterOrigin, setFilterOrigin] = useState(''); + const [filterSeatClass, setFilterSeatClass] = useState(''); const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery({ queryKey: ['report-schedules'], @@ -65,11 +70,13 @@ 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 seatClassOptions = [...new Set(passengerList.map(p => p.seatClassName).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 (filterSeatClass && p.seatClassName !== filterSeatClass) return false; if (listSearch.trim()) { const q = listSearch.toLowerCase(); return ( @@ -126,7 +133,7 @@ export default function PassengersReportPage() { setFilterSeatClass(e.target.value)}> + + {seatClassOptions.map(c => )} + + {passengerList.length > 0 && ( Export CSV )} @@ -314,20 +329,11 @@ export default function PassengersReportPage() { {filteredList.map((p, i) => ( {p.passengerName} - - {p.passportNumber ? ( - <> - {p.passportCountry ?? 'Intl'} - {p.passportNumber} - - ) : ( - {p.idDocumentNumber ?? '—'} - )} + + {[p.nationality, p.passportNumber].filter(Boolean).join(' · ') || '—'} - - {p.coachNumber && p.seatLabel - ? <>{p.coachNumber} · {p.seatLabel}{p.coachType && ({p.coachType})} - : (p.coachNumber ?? p.seatLabel ?? '—')} + + {[p.coachNumber, p.seatNumber ?? p.seatLabel, p.seatClassName].filter(Boolean).join(' · ') || '—'} {p.origin && p.destination ? `${p.origin} → ${p.destination}` : (p.origin ?? p.destination ?? '—')} @@ -342,7 +348,14 @@ export default function PassengersReportPage() { )} - {p.bookingRef} + +
+ {p.bookingRef} + {p.bookingStatus !== 'CONFIRMED' && p.bookingStatus !== 'BOARDED' && ( + {p.bookingStatus.replace('_', ' ')} + )} +
+ ))} {filteredList.length === 0 && ( From 57390647d877b1b63381c4eeb688b036cd54a229 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Sun, 19 Jul 2026 23:57:16 +0300 Subject: [PATCH 2/2] Search schedule update --- .../src/common/utils/timezone.utils.ts | 11 +++++------ .../src/modules/search/search.service.ts | 19 ++++++++++--------- .../backoffice/src/app/schedules/page.tsx | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/edr-passenger-api/src/common/utils/timezone.utils.ts b/apps/edr-passenger-api/src/common/utils/timezone.utils.ts index d21a8a7dc..325a14d1f 100644 --- a/apps/edr-passenger-api/src/common/utils/timezone.utils.ts +++ b/apps/edr-passenger-api/src/common/utils/timezone.utils.ts @@ -18,12 +18,11 @@ * parseEthiopianTime('2026-06-15') // Treats as midnight EAT */ export function parseEthiopianTime(dateInput: string | Date): Date { - if (dateInput instanceof Date) { - return dateInput; - } - - // Parse as local time (EAT) since TZ is set to Africa/Addis_Ababa - return new Date(dateInput); + if (dateInput instanceof Date) return dateInput; + // Already has timezone info (Z or +HH:MM) — parse directly as UTC + if (/Z$|[+-]\d{2}:\d{2}$/.test(dateInput)) return new Date(dateInput); + // Bare local string (e.g. "2026-07-23T21:00") — treat explicitly as EAT (UTC+3) + return new Date(dateInput + '+03:00'); } /** diff --git a/apps/edr-passenger-api/src/modules/search/search.service.ts b/apps/edr-passenger-api/src/modules/search/search.service.ts index 340d9fac9..91988ba55 100644 --- a/apps/edr-passenger-api/src/modules/search/search.service.ts +++ b/apps/edr-passenger-api/src/modules/search/search.service.ts @@ -153,8 +153,8 @@ export class SearchService { nationality?: string, ) { const [y, m, d] = dateStr.split('-').map(Number); - const requestedDate = new Date(y, m - 1, d, 0, 0, 0, 0); - const requestedNextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + const requestedDate = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}T00:00:00+03:00`); + const requestedNextDay = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d + 1).padStart(2,'0')}T00:00:00+03:00`); const now = new Date(); const totalPassengers = adultCount + (childCount ?? 0); const NEEDED = 3; @@ -213,14 +213,15 @@ export class SearchService { nationality?: string, ) { const [y, m, d] = dateStr.split('-').map(Number); - const date = new Date(y, m - 1, d, 0, 0, 0, 0); - const nextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + const date = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}T00:00:00+03:00`); + const nextDay = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d + 1).padStart(2,'0')}T00:00:00+03:00`); const now = new Date(); const totalPassengers = adultCount + (childCount ?? 0); const cutoffHours = await this.getCutoffHours(); const cutoffThreshold = new Date(now.getTime() + cutoffHours * 60 * 60 * 1000); - const isToday = now.getFullYear() === y && now.getMonth() === m - 1 && now.getDate() === d; + const nowInEAT = new Date(now.toLocaleString('en-US', { timeZone: 'Africa/Addis_Ababa' })); + const isToday = nowInEAT.getFullYear() === y && nowInEAT.getMonth() === m - 1 && nowInEAT.getDate() === d; const earliest = isToday ? cutoffThreshold : date; const schedules = await this.prisma.trainSchedule.findMany({ @@ -255,8 +256,8 @@ export class SearchService { nationality?: string, ) { const [y, m, d] = dateStr.split('-').map(Number); - const dayStart = new Date(y, m - 1, d, 0, 0, 0, 0); - const dayEnd = new Date(y, m - 1, d + 1, 0, 0, 0, 0); + const dayStart = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}T00:00:00+03:00`); + const dayEnd = new Date(`${String(y)}-${String(m).padStart(2,'0')}-${String(d + 1).padStart(2,'0')}T00:00:00+03:00`); const leg2WindowEnd = new Date(dayEnd.getTime() + this.MAX_CONNECTION_MINUTES * 60_000); const totalPassengers = adultCount + (childCount ?? 0); @@ -442,8 +443,8 @@ export class SearchService { } const coachTypes = this.buildCoachTypeDetails(schedule, faresByClass, nationality, availabilityByClass); - const legDepartureAt = originStop.plannedDepartureAt ?? schedule.departureAt; - const legArrivalAt = destStop.plannedArrivalAt ?? schedule.arrivalAt; + const legDepartureAt = schedule.departureAt; + const legArrivalAt = schedule.arrivalAt; const displayCurrency = faresByClass[0]?.displayCurrency ?? resolveCurrencyFromNationality(nationality); 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 d9a0e7ba8..b02b7ec76 100644 --- a/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/schedules/page.tsx @@ -300,7 +300,7 @@ export default function SchedulesPage() { const payload: any = { trainId: bulkForm.trainId, routeId: bulkForm.routeId, - startDateTime: bulkForm.startDateTime, + startDateTime: new Date(bulkForm.startDateTime).toISOString(), durationHours: parseInt(bulkForm.durationHours), repeatEveryDays: parseInt(bulkForm.repeatEveryDays), forNextDays: parseInt(bulkForm.forNextDays), @@ -389,11 +389,11 @@ export default function SchedulesPage() { const handleEditClick = (schedule: Schedule) => { setEditingSchedule(schedule); - // Format UTC ISO string as local YYYY-MM-DDTHH:mm for the picker + // Format UTC ISO string as EAT (Africa/Addis_Ababa) YYYY-MM-DDTHH:mm for the picker const toLocalDT = (iso: string) => { - const d = new Date(iso); + const eat = new Date(new Date(iso).toLocaleString('en-US', { timeZone: 'Africa/Addis_Ababa' })); 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())}`; + return `${eat.getFullYear()}-${pad(eat.getMonth() + 1)}-${pad(eat.getDate())}T${pad(eat.getHours())}:${pad(eat.getMinutes())}`; }; const depStr = toLocalDT(schedule.departureAt);