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 3d6345af7..55f3e21bf 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -323,9 +323,12 @@ export class ReportsService { status: true, originStationId: true, destinationStationId: true, + totalMinor: true, + currency: true, + _count: { select: { seats: true } }, }, }, - seat: { include: { coach: { select: { number: true } } } }, + seat: { include: { coach: { select: { number: true, coachType: { select: { name: true } } } } } }, }, orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }], }); @@ -347,12 +350,19 @@ export class ReportsService { return seats.map(bs => ({ bookingRef: bs.booking.bookingRef, passengerName: bs.passengerName, - coachSeat: bs.seat?.coach?.number && bs.seatLabelSnapshot - ? `${bs.seat.coach.number}·${bs.seatLabelSnapshot}` - : (bs.seatLabelSnapshot ?? '—'), - origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? '—') : '—', - destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? '—') : '—', - departureAt: schedule?.departureAt ?? null, + passengerCategory: bs.passengerCategory, + idDocumentType: bs.idDocumentType, + idDocumentNumber: bs.idDocumentNumber, + passportNumber: bs.passportNumber, + passportCountry: bs.passportCountry, + seatLabel: bs.seatLabelSnapshot, + coachNumber: bs.seat?.coach?.number ?? null, + coachType: (bs.seat?.coach as any)?.coachType?.name ?? 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, })); } 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 949412950..4e2647877 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 @@ -21,10 +21,19 @@ interface PassengersReport { interface PassengerRow { bookingRef: string; passengerName: string; - coachSeat: string; - origin: string; - destination: string; - departureAt: string | null; + passengerCategory: string; + idDocumentType: string | null; + idDocumentNumber: string | null; + passportNumber: string | null; + passportCountry: 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'; @@ -54,12 +63,25 @@ export default function PassengersReportPage() { enabled: !!scheduleId, }); - const filteredList = listSearch.trim() - ? passengerList.filter(p => - p.passengerName.toLowerCase().includes(listSearch.toLowerCase()) || - p.bookingRef.toLowerCase().includes(listSearch.toLowerCase()), - ) - : passengerList; + 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; + }) + .sort((a, b) => a.bookingRef.localeCompare(b.bookingRef)); const downloadCsv = (csv: string, filename: string) => { const blob = new Blob([csv], { type: 'text/csv' }); @@ -77,10 +99,14 @@ export default function PassengersReportPage() { const doExportList = () => { if (!passengerList.length) return; - 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, + 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.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`); }; @@ -272,38 +298,58 @@ export default function PassengersReportPage() { Export CSV )} -
-
- - - - - - - - - - +
+
#NameCoach · SeatOriginDestinationDateBooking Ref
+ + + + + + + + + + + + {filteredList.map((p, i) => ( + + + + + + + - - - {filteredList.map((p, i) => ( - - - - - - - - - - ))} - {filteredList.length === 0 && ( - - )} - -
NameNationalityCoach · SeatTripAmount PaidBooking Ref
{p.passengerName} + {p.passportNumber ? ( + <> + {p.passportCountry ?? 'Intl'} + {p.passportNumber} + + ) : ( + {p.idDocumentNumber ?? '—'} + )} + + {p.coachNumber && p.seatLabel + ? <>{p.coachNumber} · {p.seatLabel}{p.coachType && ({p.coachType})} + : (p.coachNumber ?? p.seatLabel ?? '—')} + + {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}
{i + 1}{p.passengerName}{p.coachSeat}{p.origin}{p.destination}{p.departureAt ? formatDateTime(p.departureAt) : '—'}{p.bookingRef}
No passengers found
-
+ ))} + {filteredList.length === 0 && ( + 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 />