mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 08:25:43 +00:00
Merge pull request #823 from Tria-plc/alpha
Include coach number to voucher
This commit is contained in:
@@ -18,12 +18,11 @@
|
|||||||
* parseEthiopianTime('2026-06-15') // Treats as midnight EAT
|
* parseEthiopianTime('2026-06-15') // Treats as midnight EAT
|
||||||
*/
|
*/
|
||||||
export function parseEthiopianTime(dateInput: string | Date): Date {
|
export function parseEthiopianTime(dateInput: string | Date): Date {
|
||||||
if (dateInput instanceof Date) {
|
if (dateInput instanceof Date) return dateInput;
|
||||||
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)
|
||||||
// Parse as local time (EAT) since TZ is set to Africa/Addis_Ababa
|
return new Date(dateInput + '+03:00');
|
||||||
return new Date(dateInput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ export class ReportsService {
|
|||||||
_count: { select: { seats: true } },
|
_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" } }],
|
orderBy: [{ seat: { coach: { number: "asc" } } }, { seat: { seatNumber: "asc" } }],
|
||||||
});
|
});
|
||||||
@@ -474,13 +474,25 @@ export class ReportsService {
|
|||||||
passportNumber: bs.passportNumber,
|
passportNumber: bs.passportNumber,
|
||||||
passportCountry: bs.passportCountry,
|
passportCountry: bs.passportCountry,
|
||||||
seatLabel: bs.seatLabelSnapshot,
|
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,
|
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,
|
origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? null) : null,
|
||||||
destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? null) : null,
|
destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? null) : null,
|
||||||
amountPaidMinor: bs.booking.totalMinor,
|
amountPaidMinor: bs.booking.totalMinor,
|
||||||
currency: bs.booking.currency ?? 'ETB',
|
currency: bs.booking.currency ?? 'ETB',
|
||||||
isGroupBooking: (bs.booking._count?.seats ?? 0) > 1,
|
isGroupBooking: (bs.booking._count?.seats ?? 0) > 1,
|
||||||
|
bookingStatus: bs.booking.status,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -52,13 +52,17 @@ interface PassengerRow {
|
|||||||
passportNumber: string | null;
|
passportNumber: string | null;
|
||||||
passportCountry: string | null;
|
passportCountry: string | null;
|
||||||
seatLabel: string | null;
|
seatLabel: string | null;
|
||||||
|
seatNumber: string | null;
|
||||||
|
seatClassName: string | null;
|
||||||
coachNumber: string | null;
|
coachNumber: string | null;
|
||||||
coachType: string | null;
|
coachType: string | null;
|
||||||
|
nationality: string | null;
|
||||||
origin: string | null;
|
origin: string | null;
|
||||||
destination: string | null;
|
destination: string | null;
|
||||||
amountPaidMinor: number;
|
amountPaidMinor: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
isGroupBooking: boolean;
|
isGroupBooking: boolean;
|
||||||
|
bookingStatus: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = "occupancy" | "list";
|
type Tab = "occupancy" | "list";
|
||||||
@@ -105,6 +109,7 @@ export default function PassengersReportPage() {
|
|||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
if (filterCoach && p.coachNumber !== filterCoach) return false;
|
if (filterCoach && p.coachNumber !== filterCoach) return false;
|
||||||
if (filterOrigin && p.origin !== filterOrigin) return false;
|
if (filterOrigin && p.origin !== filterOrigin) return false;
|
||||||
|
if (filterSeatClass && p.seatClassName !== filterSeatClass) return false;
|
||||||
if (listSearch.trim()) {
|
if (listSearch.trim()) {
|
||||||
const q = listSearch.toLowerCase();
|
const q = listSearch.toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -454,6 +459,30 @@ export default function PassengersReportPage() {
|
|||||||
value={listSearch}
|
value={listSearch}
|
||||||
onChange={(e) => setListSearch(e.target.value)}
|
onChange={(e) => setListSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<select
|
||||||
|
className="input w-40"
|
||||||
|
value={filterSeatClass}
|
||||||
|
onChange={(e) => setFilterSeatClass(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">All classes</option>
|
||||||
|
{seatClassOptions.map((c) => (
|
||||||
|
<option key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="input w-36"
|
||||||
|
value={filterOrigin}
|
||||||
|
onChange={(e) => setFilterOrigin(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">All origins</option>
|
||||||
|
{originOptions.map((o) => (
|
||||||
|
<option key={o} value={o}>
|
||||||
|
{o}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
{passengerList.length > 0 && (
|
{passengerList.length > 0 && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={Download}
|
icon={Download}
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ export default function SchedulesPage() {
|
|||||||
const payload: any = {
|
const payload: any = {
|
||||||
trainId: bulkForm.trainId,
|
trainId: bulkForm.trainId,
|
||||||
routeId: bulkForm.routeId,
|
routeId: bulkForm.routeId,
|
||||||
startDateTime: bulkForm.startDateTime,
|
startDateTime: new Date(bulkForm.startDateTime).toISOString(),
|
||||||
durationHours: parseInt(bulkForm.durationHours),
|
durationHours: parseInt(bulkForm.durationHours),
|
||||||
repeatEveryDays: parseInt(bulkForm.repeatEveryDays),
|
repeatEveryDays: parseInt(bulkForm.repeatEveryDays),
|
||||||
forNextDays: parseInt(bulkForm.forNextDays),
|
forNextDays: parseInt(bulkForm.forNextDays),
|
||||||
|
|||||||
@@ -263,21 +263,24 @@ export default function ConfirmationPage() {
|
|||||||
const ticketNumber = matchedTicket?.barcodePayload || "Not yet issued";
|
const ticketNumber = matchedTicket?.barcodePayload || "Not yet issued";
|
||||||
|
|
||||||
await generatePassengerVoucherPDF({
|
await generatePassengerVoucherPDF({
|
||||||
bookingRef: pnr,
|
bookingRef: pnr,
|
||||||
ticketNumber,
|
ticketNumber,
|
||||||
passengerName: p.name || `Passenger ${i + 1}`,
|
passengerName: p.name || `Passenger ${i + 1}`,
|
||||||
dateOfBirth: p.dateOfBirth,
|
dateOfBirth: p.dateOfBirth,
|
||||||
nationality: p.nationality,
|
nationality: p.nationality,
|
||||||
seatNumber: p.seatNumber,
|
seatNumber: p.seatNumber,
|
||||||
outboundSeatNumber: (p as any).outboundSeatNumber,
|
coachNumber: p.coachNumber,
|
||||||
inboundSeatNumber: (p as any).inboundSeatNumber,
|
outboundSeatNumber: p.outboundSeatNumber,
|
||||||
|
outboundCoachNumber: p.outboundCoachNumber,
|
||||||
|
inboundSeatNumber: p.inboundSeatNumber,
|
||||||
|
inboundCoachNumber: p.inboundCoachNumber,
|
||||||
status,
|
status,
|
||||||
outboundSchedule: outbound,
|
outboundSchedule: outbound,
|
||||||
inboundSchedule: inbound,
|
inboundSchedule: inbound,
|
||||||
isRoundTrip,
|
isRoundTrip,
|
||||||
fareMinor: hasSettledAmount ? settledAmountMinor! : getEtbFare(i),
|
fareMinor: hasSettledAmount ? settledAmountMinor! : getEtbFare(i),
|
||||||
currency: voucherCurrency,
|
currency: voucherCurrency,
|
||||||
fareIsMajorUnits: hasSettledAmount,
|
fareIsMajorUnits: hasSettledAmount,
|
||||||
createdAt,
|
createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ interface PassengerVoucherData {
|
|||||||
dateOfBirth?: string;
|
dateOfBirth?: string;
|
||||||
nationality?: string;
|
nationality?: string;
|
||||||
seatNumber?: string;
|
seatNumber?: string;
|
||||||
|
coachNumber?: string;
|
||||||
outboundSeatNumber?: string;
|
outboundSeatNumber?: string;
|
||||||
|
outboundCoachNumber?: string;
|
||||||
inboundSeatNumber?: string;
|
inboundSeatNumber?: string;
|
||||||
|
inboundCoachNumber?: string;
|
||||||
status: string;
|
status: string;
|
||||||
outboundSchedule: ScheduleInfo;
|
outboundSchedule: ScheduleInfo;
|
||||||
inboundSchedule?: ScheduleInfo;
|
inboundSchedule?: ScheduleInfo;
|
||||||
@@ -314,16 +317,21 @@ function drawPassengerDetails(doc: jsPDF, data: PassengerVoucherData, y: number,
|
|||||||
label(doc, 'Passenger details', margin, y);
|
label(doc, 'Passenger details', margin, y);
|
||||||
y += 7;
|
y += 7;
|
||||||
|
|
||||||
|
const formatSeat = (coach: string | undefined, seat: string | undefined): string => {
|
||||||
|
if (!seat) return '—';
|
||||||
|
return coach ? `Coach ${coach} · Seat ${seat}` : `Seat ${seat}`;
|
||||||
|
};
|
||||||
|
|
||||||
const rows: [string, string][] = [
|
const rows: [string, string][] = [
|
||||||
['Full name', data.passengerName || '—'],
|
['Full name', data.passengerName || '—'],
|
||||||
['Date of birth', data.dateOfBirth ? new Date(data.dateOfBirth).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '—'],
|
['Date of birth', data.dateOfBirth ? new Date(data.dateOfBirth).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '—'],
|
||||||
['Nationality', data.nationality || '—'],
|
['Nationality', data.nationality || '—'],
|
||||||
];
|
];
|
||||||
if (data.isRoundTrip) {
|
if (data.isRoundTrip) {
|
||||||
rows.push(['Outbound seat', data.outboundSeatNumber || '—']);
|
rows.push(['Outbound seat', formatSeat(data.outboundCoachNumber, data.outboundSeatNumber)]);
|
||||||
rows.push(['Return seat', data.inboundSeatNumber || '—']);
|
rows.push(['Return seat', formatSeat(data.inboundCoachNumber, data.inboundSeatNumber)]);
|
||||||
} else {
|
} else {
|
||||||
rows.push(['Seat', data.seatNumber || '—']);
|
rows.push(['Seat', formatSeat(data.coachNumber, data.seatNumber)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowH = 7;
|
const rowH = 7;
|
||||||
@@ -485,9 +493,10 @@ export const generateVoucherPDF = async (booking: VoucherData): Promise<void> =>
|
|||||||
// Group leg rows back into one entry per real passenger — without this, a round trip
|
// Group leg rows back into one entry per real passenger — without this, a round trip
|
||||||
// produced two half-passenger vouchers (one per leg, each showing only its own leg's
|
// produced two half-passenger vouchers (one per leg, each showing only its own leg's
|
||||||
// seat) instead of one voucher per passenger covering both legs.
|
// seat) instead of one voucher per passenger covering both legs.
|
||||||
|
type SeatInfo = VoucherData['passengers'][number]['seat'];
|
||||||
const grouped = new Map<
|
const grouped = new Map<
|
||||||
string,
|
string,
|
||||||
{ fullName: string; category: string; outboundSeat?: VoucherData['passengers'][number]['seat']; returnSeat?: VoucherData['passengers'][number]['seat'] }
|
{ fullName: string; category: string; outboundSeat?: SeatInfo; returnSeat?: SeatInfo }
|
||||||
>();
|
>();
|
||||||
booking.passengers.forEach((p) => {
|
booking.passengers.forEach((p) => {
|
||||||
const key = `${p.fullName}|${p.dateOfBirth}|${p.category}`;
|
const key = `${p.fullName}|${p.dateOfBirth}|${p.category}`;
|
||||||
@@ -514,20 +523,23 @@ export const generateVoucherPDF = async (booking: VoucherData): Promise<void> =>
|
|||||||
const ticketNumber = matchedTicket?.barcodePayload || 'Not yet issued';
|
const ticketNumber = matchedTicket?.barcodePayload || 'Not yet issued';
|
||||||
|
|
||||||
await generatePassengerVoucherPDF({
|
await generatePassengerVoucherPDF({
|
||||||
bookingRef: booking.bookingRef,
|
bookingRef: booking.bookingRef,
|
||||||
ticketNumber,
|
ticketNumber,
|
||||||
passengerName: p.fullName,
|
passengerName: p.fullName,
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
outboundSchedule: { ...booking.schedule, seatClass: p.outboundSeat?.seatClass },
|
outboundSchedule: { ...booking.schedule, seatClass: p.outboundSeat?.seatClass },
|
||||||
inboundSchedule: isRoundTrip ? { ...booking.returnSchedule!, seatClass: p.returnSeat?.seatClass } : undefined,
|
inboundSchedule: isRoundTrip ? { ...booking.returnSchedule!, seatClass: p.returnSeat?.seatClass } : undefined,
|
||||||
isRoundTrip,
|
isRoundTrip,
|
||||||
seatNumber: isRoundTrip ? undefined : p.outboundSeat?.number,
|
seatNumber: isRoundTrip ? undefined : p.outboundSeat?.number,
|
||||||
outboundSeatNumber: isRoundTrip ? p.outboundSeat?.number : undefined,
|
coachNumber: isRoundTrip ? undefined : p.outboundSeat?.coach,
|
||||||
inboundSeatNumber: isRoundTrip ? p.returnSeat?.number : undefined,
|
outboundSeatNumber: isRoundTrip ? p.outboundSeat?.number : undefined,
|
||||||
fareMinor: voucherFareMinor,
|
outboundCoachNumber: isRoundTrip ? p.outboundSeat?.coach : undefined,
|
||||||
currency: voucherCurrency,
|
inboundSeatNumber: isRoundTrip ? p.returnSeat?.number : undefined,
|
||||||
fareIsMajorUnits: useSettledAmount,
|
inboundCoachNumber: isRoundTrip ? p.returnSeat?.coach : undefined,
|
||||||
createdAt: booking.createdAt,
|
fareMinor: voucherFareMinor,
|
||||||
|
currency: voucherCurrency,
|
||||||
|
fareIsMajorUnits: useSettledAmount,
|
||||||
|
createdAt: booking.createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user