From b4f0a6dea34485f9de5952f403ec3cc9f3d67e70 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 1 Oct 2026 09:30:43 +0200 Subject: [PATCH] feat(pdf): transport notes in the trip PDF, with a switch in the preview The notes of a flight, train or rental print under its booking code, like the notes of a place. A Transport notes switch next to Page break per day leaves them out and is remembered; it only shows when a transport has a note. (#1571) --- client/src/components/PDF/TripPDF.test.ts | 50 +++++++++++++++ client/src/components/PDF/TripPDF.tsx | 78 +++++++++++++++++------ shared/src/i18n/ar/pdf.ts | 1 + shared/src/i18n/az/pdf.ts | 1 + shared/src/i18n/br/pdf.ts | 1 + shared/src/i18n/ca/pdf.ts | 1 + shared/src/i18n/cs/pdf.ts | 1 + shared/src/i18n/de/pdf.ts | 1 + shared/src/i18n/en/pdf.ts | 1 + shared/src/i18n/es/pdf.ts | 1 + shared/src/i18n/et/pdf.ts | 1 + shared/src/i18n/fr/pdf.ts | 1 + shared/src/i18n/gr/pdf.ts | 1 + shared/src/i18n/hu/pdf.ts | 1 + shared/src/i18n/id/pdf.ts | 1 + shared/src/i18n/it/pdf.ts | 1 + shared/src/i18n/ja/pdf.ts | 1 + shared/src/i18n/ko/pdf.ts | 1 + shared/src/i18n/nl/pdf.ts | 1 + shared/src/i18n/pl/pdf.ts | 1 + shared/src/i18n/ru/pdf.ts | 1 + shared/src/i18n/sk/pdf.ts | 1 + shared/src/i18n/sv/pdf.ts | 1 + shared/src/i18n/th/pdf.ts | 1 + shared/src/i18n/tr/pdf.ts | 1 + shared/src/i18n/uk/pdf.ts | 1 + shared/src/i18n/vi/pdf.ts | 1 + shared/src/i18n/zh-TW/pdf.ts | 1 + shared/src/i18n/zh/pdf.ts | 1 + 29 files changed, 135 insertions(+), 20 deletions(-) diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index 5c8a28e4d..1d2c4c5ce 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -1363,3 +1363,53 @@ describe('a printed day follows the plan (#1978)', () => { expect(seen).toEqual([...seen].sort((a, b) => a - b)) }) }) + +// ── Transport notes (#1571) ───────────────────────────────────────────────── + +describe('downloadTripPDF — transport notes', () => { + const withNote = { ...transportReservation, notes: 'Seat 14A\nCheck in online the day before' } + + beforeEach(() => { + localStorage.removeItem('trek_pdf_transport_notes') + }) + + const notesToggle = () => document.querySelector('#pdf-transport-notes-toggle') + + it('FE-PDF-TNOTES-001: prints the note of a transport, escaped, and offers the switch', async () => { + await downloadTripPDF({ ...richArgs, reservations: [{ ...withNote, notes: 'Gate B12' }] }) + const html = getIframe()!.srcdoc + + expect(html).toContain('
Gate <b>B12</b>
') + expect(html).not.toContain('pdf-no-transport-notes"') + expect(notesToggle()!.getAttribute('aria-checked')).toBe('true') + }) + + it('FE-PDF-TNOTES-002: without any transport note there is nothing to switch', async () => { + await downloadTripPDF(richArgs) + + expect(getIframe()!.srcdoc).not.toContain('class="note-time transport-note"') + expect(notesToggle()).toBeNull() + // The page break switch is still there on its own. + expect(document.querySelector('#pdf-daybreak-toggle')).not.toBeNull() + }) + + it('FE-PDF-TNOTES-003: turning the notes off hides them at once and is remembered', async () => { + await downloadTripPDF({ ...richArgs, reservations: [withNote] }) + notesToggle()!.click() + + expect(notesToggle()!.getAttribute('aria-checked')).toBe('false') + expect(localStorage.getItem('trek_pdf_transport_notes')).toBe('0') + const body = getIframe()!.contentDocument?.body + if (body) expect(body.classList.contains('pdf-no-transport-notes')).toBe(true) + }) + + it('FE-PDF-TNOTES-004: the remembered choice comes back, next to the flowing layout', async () => { + localStorage.setItem('trek_pdf_transport_notes', '0') + localStorage.setItem('trek_pdf_page_break_per_day', '0') + await downloadTripPDF({ ...richArgs, reservations: [withNote] }) + + expect(getIframe()!.srcdoc).toContain('') + expect(notesToggle()!.getAttribute('aria-checked')).toBe('false') + localStorage.removeItem('trek_pdf_page_break_per_day') + }) +}) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index c20db2893..51a38a3cd 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -29,17 +29,25 @@ import { onlyMyPlan } from './pdfScope' */ const PAGE_BREAK_KEY = 'trek_pdf_page_break_per_day' -function pageBreakPerDay(): boolean { +/** + * The notes of a flight, train or rental (#1571): printed by default, like the + * notes of a place, and a switch in the preview leaves them out for a reader who + * wants the plan without the fine print. + */ +const TRANSPORT_NOTES_KEY = 'trek_pdf_transport_notes' + +/** A preview switch that is on unless it was turned off; both default to on. */ +function pdfSwitchOn(key: string): boolean { try { - return localStorage.getItem(PAGE_BREAK_KEY) !== '0' + return localStorage.getItem(key) !== '0' } catch { return true } } -function rememberPageBreakPerDay(on: boolean): void { +function rememberPdfSwitch(key: string, on: boolean): void { try { - localStorage.setItem(PAGE_BREAK_KEY, on ? '1' : '0') + localStorage.setItem(key, on ? '1' : '0') } catch { // A blocked localStorage costs the preference, not the export. } @@ -231,7 +239,10 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor ? onlyMyPlan(allStored, allReservations, onlyUserId) : { assignments: allStored, reservations: allReservations } const assignments = planAssignments(stored, showServiceStops) - const breaksPerDay = pageBreakPerDay() + const breaksPerDay = pdfSwitchOn(PAGE_BREAK_KEY) + const showTransportNotes = pdfSwitchOn(TRANSPORT_NOTES_KEY) + // Set while the days render: the switch only shows when there is a note to hide. + let hasTransportNotes = false const loc = _locale || undefined const tr = _t || (k => k) // The store read is the fallback, not the source: a caller that forgets the @@ -516,6 +527,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor const endTime = phase === 'single' ? fmtTime(splitReservationDateTime(r.reservation_end_time).time) : '' const time = [startTime, endTime].filter(Boolean).join(' – ') const titleHtml = `${spanLabel ? escHtml(spanLabel) + ': ' : ''}${escHtml(r.title)}` + const transportNote = typeof r.notes === 'string' ? r.notes.trim() : '' + if (transportNote) hasTransportNotes = true return `
@@ -525,6 +538,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor ${subtitleLines.filter(Boolean).map(s => `
${escHtml(s)}
`).join('')} ${locationLine ? `
${escHtml(locationLine)}
` : ''} ${r.confirmation_number ? `
Code: ${escHtml(r.confirmation_number)}
` : ''} + ${transportNote ? `
${escHtml(transportNote)}
` : ''}
` } @@ -751,6 +765,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor Flowing days butt against each other without the page edge between them. */ .day-break { page-break-before: always; } .pdf-flow .day-break { page-break-before: auto; } + .transport-note { font-style: italic; white-space: pre-line; } + .pdf-no-transport-notes .transport-note { display: none; } .pdf-flow .day-section + .day-section { margin-top: 18px; } /* Hold a flowing day together. Without this the header bar can be placed at the foot of a sheet while its content moves to the next one, which then repeats @@ -868,7 +884,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor } - +