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 fa164dbca..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'; @@ -33,6 +42,8 @@ export default function PassengersReportPage() { const [scheduleId, setScheduleId] = useState(''); const [tab, setTab] = useState('occupancy'); const [listSearch, setListSearch] = useState(''); + const [filterCoach, setFilterCoach] = useState(''); + const [filterOrigin, setFilterOrigin] = useState(''); const { data: schedulesRaw, isLoading: loadingSchedules } = useQuery({ queryKey: ['report-schedules'], @@ -52,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' }); @@ -75,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`); }; @@ -98,7 +126,7 @@ export default function PassengersReportPage() { 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 />