mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
Fix voucher alignment
This commit is contained in:
@@ -218,15 +218,18 @@ function drawTicketHero(doc: jsPDF, bookingRef: string, ticketNumber: string, st
|
||||
doc.addImage(qrDataUrl, 'PNG', qrCardX + qrPad, cardY + qrPad, qrSize, qrSize);
|
||||
}
|
||||
|
||||
return y + cardH + 10;
|
||||
return y + cardH + 8;
|
||||
}
|
||||
|
||||
// ─── journey card ──────────────────────────────────────────────────────────
|
||||
|
||||
function drawJourneyLeg(doc: jsPDF, schedule: ScheduleInfo, legLabel: string | null, y: number, margin: number, pageWidth: number): number {
|
||||
const cardW = pageWidth - margin * 2;
|
||||
const routeH = 30;
|
||||
const trainRowH = 9;
|
||||
// Tall enough to clear the time+date block below (topY + 22, see below) with a margin
|
||||
// before the train sub-row's hairline — previously 30, which the date line (topY + 25)
|
||||
// overran by several mm, printing "Wed, Jul 15" directly on top of the TRAIN row.
|
||||
const routeH = 35;
|
||||
const trainRowH = 8;
|
||||
const cardH = routeH + trainRowH;
|
||||
|
||||
doc.setDrawColor(...HAIRLINE);
|
||||
@@ -241,37 +244,37 @@ function drawJourneyLeg(doc: jsPDF, schedule: ScheduleInfo, legLabel: string | n
|
||||
}
|
||||
|
||||
const padX = 8;
|
||||
const topY = y + (legLabel ? 12 : 8);
|
||||
const topY = y + (legLabel ? 11 : 7);
|
||||
|
||||
// Origin block
|
||||
label(doc, 'From', margin + padX, topY);
|
||||
doc.setTextColor(...INK); doc.setFontSize(16); doc.setFont('helvetica', 'bold');
|
||||
doc.text(schedule.origin.code, margin + padX, topY + 8);
|
||||
doc.text(schedule.origin.code, margin + padX, topY + 7);
|
||||
doc.setTextColor(...BODY); doc.setFontSize(8.5); doc.setFont('helvetica', 'normal');
|
||||
doc.text(schedule.origin.city || schedule.origin.name, margin + padX, topY + 13);
|
||||
doc.text(schedule.origin.city || schedule.origin.name, margin + padX, topY + 11.5);
|
||||
|
||||
const dep = new Date(schedule.departureAt);
|
||||
doc.setTextColor(...BRAND); doc.setFontSize(11.5); doc.setFont('helvetica', 'bold');
|
||||
doc.text(dep.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), margin + padX, topY + 20.5);
|
||||
doc.text(dep.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), margin + padX, topY + 18);
|
||||
doc.setTextColor(...MUTED); doc.setFontSize(7); doc.setFont('helvetica', 'normal');
|
||||
doc.text(dep.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), margin + padX, topY + 25);
|
||||
doc.text(dep.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), margin + padX, topY + 22);
|
||||
|
||||
// Destination block (right-aligned)
|
||||
const dx = pageWidth - margin - padX;
|
||||
label(doc, 'To', dx, topY, { align: 'right' });
|
||||
doc.setTextColor(...INK); doc.setFontSize(16); doc.setFont('helvetica', 'bold');
|
||||
doc.text(schedule.destination.code, dx, topY + 8, { align: 'right' });
|
||||
doc.text(schedule.destination.code, dx, topY + 7, { align: 'right' });
|
||||
doc.setTextColor(...BODY); doc.setFontSize(8.5); doc.setFont('helvetica', 'normal');
|
||||
doc.text(schedule.destination.city || schedule.destination.name, dx, topY + 13, { align: 'right' });
|
||||
doc.text(schedule.destination.city || schedule.destination.name, dx, topY + 11.5, { align: 'right' });
|
||||
|
||||
const arr = new Date(schedule.arrivalAt);
|
||||
doc.setTextColor(...BRAND); doc.setFontSize(11.5); doc.setFont('helvetica', 'bold');
|
||||
doc.text(arr.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), dx, topY + 20.5, { align: 'right' });
|
||||
doc.text(arr.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), dx, topY + 18, { align: 'right' });
|
||||
doc.setTextColor(...MUTED); doc.setFontSize(7); doc.setFont('helvetica', 'normal');
|
||||
doc.text(arr.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), dx, topY + 25, { align: 'right' });
|
||||
doc.text(arr.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), dx, topY + 22, { align: 'right' });
|
||||
|
||||
// Dashed route line with endpoint markers, connecting the two blocks
|
||||
const lineY = topY + 8.5;
|
||||
const lineY = topY + 7.5;
|
||||
const lineX1 = margin + padX + 24;
|
||||
const lineX2 = dx - 24;
|
||||
doc.setDrawColor(...HAIRLINE);
|
||||
@@ -283,19 +286,20 @@ function drawJourneyLeg(doc: jsPDF, schedule: ScheduleInfo, legLabel: string | n
|
||||
doc.circle(lineX1, lineY, 0.9, 'F');
|
||||
doc.circle(lineX2, lineY, 0.9, 'F');
|
||||
|
||||
// Train info sub-row
|
||||
// Train info sub-row — starts at routeH, comfortably below the date line above (topY +
|
||||
// 22, i.e. y + 33 at most) so it never overlaps the journey block's text.
|
||||
const rowY = y + routeH;
|
||||
hairline(doc, margin, rowY, margin + cardW);
|
||||
doc.setFontSize(8); doc.setTextColor(...MUTED); doc.setFont('helvetica', 'normal');
|
||||
doc.text('TRAIN', margin + padX, rowY + 6, { charSpace: 0.3 });
|
||||
doc.text('TRAIN', margin + padX, rowY + 5.5, { charSpace: 0.3 });
|
||||
doc.setTextColor(...INK); doc.setFont('helvetica', 'bold');
|
||||
doc.text(schedule.trainNumber + (schedule.trainName ? ` · ${schedule.trainName}` : ''), margin + padX + 15, rowY + 6);
|
||||
doc.text(schedule.trainNumber + (schedule.trainName ? ` · ${schedule.trainName}` : ''), margin + padX + 15, rowY + 5.5);
|
||||
if (schedule.seatClass) {
|
||||
doc.setFont('helvetica', 'normal'); doc.setTextColor(...BODY);
|
||||
doc.text(schedule.seatClass, pageWidth - margin - padX, rowY + 6, { align: 'right' });
|
||||
doc.text(schedule.seatClass, pageWidth - margin - padX, rowY + 5.5, { align: 'right' });
|
||||
}
|
||||
|
||||
return y + cardH + 8;
|
||||
return y + cardH + 6;
|
||||
}
|
||||
|
||||
// ─── passenger details ─────────────────────────────────────────────────────
|
||||
@@ -316,7 +320,7 @@ function drawPassengerDetails(doc: jsPDF, data: PassengerVoucherData, y: number,
|
||||
rows.push(['Seat', data.seatNumber || '—']);
|
||||
}
|
||||
|
||||
const rowH = 8;
|
||||
const rowH = 7;
|
||||
rows.forEach(([k, v], i) => {
|
||||
const rowY = y + i * rowH;
|
||||
doc.setFontSize(8.5); doc.setTextColor(...MUTED); doc.setFont('helvetica', 'normal');
|
||||
@@ -345,7 +349,7 @@ function drawFareSummary(doc: jsPDF, fareMinor: number, currency: string, y: num
|
||||
const displayAmount = fareIsMajorUnits ? fareMinor : fareMinor / 100;
|
||||
doc.text(`${currency} ${displayAmount.toFixed(2)}`, pageWidth - margin - padX, y + 13, { align: 'right' });
|
||||
|
||||
return y + cardH + 8;
|
||||
return y + cardH + 6;
|
||||
}
|
||||
|
||||
// ─── instructions ──────────────────────────────────────────────────────────
|
||||
@@ -366,15 +370,18 @@ function drawInstructions(doc: jsPDF, y: number, margin: number, pageWidth: numb
|
||||
doc.text('Present this voucher (printed or on your phone) at the terminal for boarding.', margin + padX, y + 11);
|
||||
doc.text('Please arrive at least 30 minutes before scheduled departure.', margin + padX, y + 15);
|
||||
|
||||
return y + cardH + 6;
|
||||
return y + cardH + 4;
|
||||
}
|
||||
|
||||
// ─── footer ────────────────────────────────────────────────────────────────
|
||||
|
||||
function drawFooter(doc: jsPDF, createdAt: string): void {
|
||||
function drawFooter(doc: jsPDF, createdAt: string, contentY: number): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const footerY = pageHeight - 20;
|
||||
// Pinned near the bottom for short (one-way) content, same as before — but grows past
|
||||
// that floor instead of staying fixed when a round trip's extra journey card pushes
|
||||
// content lower, which previously made the footer overlap the instructions card.
|
||||
const footerY = Math.max(contentY + 4, pageHeight - 20);
|
||||
|
||||
hairline(doc, PAGE_MARGIN, footerY, pageWidth - PAGE_MARGIN);
|
||||
doc.setFontSize(7.5); doc.setTextColor(...MUTED); doc.setFont('helvetica', 'normal');
|
||||
@@ -395,7 +402,7 @@ async function drawPassengerVoucherPage(doc: jsPDF, data: PassengerVoucherData):
|
||||
y = drawTicketHero(doc, data.bookingRef, data.ticketNumber, data.status, qrDataUrl, y, margin, pageW);
|
||||
|
||||
label(doc, 'Journey details', margin, y);
|
||||
y += 7;
|
||||
y += 6;
|
||||
y = drawJourneyLeg(doc, data.outboundSchedule, data.isRoundTrip ? 'Outbound' : null, y, margin, pageW);
|
||||
|
||||
if (data.isRoundTrip && data.inboundSchedule) {
|
||||
@@ -404,8 +411,8 @@ async function drawPassengerVoucherPage(doc: jsPDF, data: PassengerVoucherData):
|
||||
|
||||
y = drawPassengerDetails(doc, data, y, margin, pageW);
|
||||
y = drawFareSummary(doc, data.fareMinor, data.currency, y, margin, pageW, data.fareIsMajorUnits);
|
||||
drawInstructions(doc, y, margin, pageW);
|
||||
drawFooter(doc, data.createdAt);
|
||||
y = drawInstructions(doc, y, margin, pageW);
|
||||
drawFooter(doc, data.createdAt, y);
|
||||
}
|
||||
|
||||
/** Generates and downloads one PDF voucher for a single passenger. */
|
||||
|
||||
Reference in New Issue
Block a user