diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index ce03819e9..2c8417e9a 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -912,3 +912,87 @@ describe('downloadTripPDF defaults', () => { expect(srcdoc()).toContain('Day 1') }) }) + +// #1292 — every day started a new page, so a plan of short days printed one +// sheet per handful of lines. The flowing layout is opt-in and remembered. +describe('page breaks between days (#1292)', () => { + const twoDays = { + ...minimalArgs, + days: [ + { id: 1, day_number: 1, title: 'First', date: '2025-06-01' }, + { id: 2, day_number: 2, title: 'Second', date: '2025-06-02' }, + ] as any[], + } + + beforeEach(() => { + localStorage.removeItem('trek_pdf_page_break_per_day') + }) + + const toggle = () => + document.querySelector('#pdf-daybreak-toggle input') + + it('FE-PDF-BREAK-001: days break onto their own page by default', async () => { + await downloadTripPDF(twoDays) + const html = getIframe()!.srcdoc + + expect(html).toContain('day-section day-break') + expect(html).not.toContain('class="pdf-flow"') + expect(toggle()!.checked).toBe(true) + }) + + it('FE-PDF-BREAK-002: the remembered flowing layout comes back on the next export', async () => { + localStorage.setItem('trek_pdf_page_break_per_day', '0') + await downloadTripPDF(twoDays) + const html = getIframe()!.srcdoc + + // The class stays on the day; the body is what decides whether it breaks. + expect(html).toContain('day-section day-break') + expect(html).toContain('') + expect(toggle()!.checked).toBe(false) + }) + + it('FE-PDF-BREAK-003: the first day never carries a break of its own', async () => { + await downloadTripPDF(twoDays) + const html = getIframe()!.srcdoc + + expect(html.indexOf('')).toBeGreaterThan(-1) + expect(html.match(/day-section day-break/g)).toHaveLength(1) + }) + + it('FE-PDF-BREAK-004: turning the breaks off is remembered and shown at once', async () => { + await downloadTripPDF(twoDays) + const box = toggle()! + box.checked = false + box.dispatchEvent(new Event('change')) + + expect(localStorage.getItem('trek_pdf_page_break_per_day')).toBe('0') + const body = getIframe()!.contentDocument?.body + if (body) expect(body.classList.contains('pdf-flow')).toBe(true) + }) + + it('FE-PDF-BREAK-005: turning them back on is remembered too', async () => { + localStorage.setItem('trek_pdf_page_break_per_day', '0') + await downloadTripPDF(twoDays) + const box = toggle()! + box.checked = true + box.dispatchEvent(new Event('change')) + + expect(localStorage.getItem('trek_pdf_page_break_per_day')).toBe('1') + const body = getIframe()!.contentDocument?.body + if (body) expect(body.classList.contains('pdf-flow')).toBe(false) + }) + + it('FE-PDF-BREAK-006: a storage the browser blocks costs the preference, not the export', async () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('blocked') }) + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('blocked') }) + + await downloadTripPDF(twoDays) + const html = getIframe()!.srcdoc + expect(html).toContain('day-section day-break') + expect(html).not.toContain('class="pdf-flow"') + + const box = toggle()! + box.checked = false + expect(() => box.dispatchEvent(new Event('change'))).not.toThrow() + }) +}) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index a39515fb6..3f29a400c 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -9,6 +9,29 @@ import { formatMoney, formatMoneySum, splitReservationDateTime, type MoneyEntry import { fetchExchangeRates } from '../../hooks/useExchangeRates' import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs' +/** + * Every day starts a new page by default. On a trip of short days that prints + * one sheet per handful of lines, so the preview offers the flowing layout the + * in-app view already uses, and remembers which one was picked (#1292). + */ +const PAGE_BREAK_KEY = 'trek_pdf_page_break_per_day' + +function pageBreakPerDay(): boolean { + try { + return localStorage.getItem(PAGE_BREAK_KEY) !== '0' + } catch { + return true + } +} + +function rememberPageBreakPerDay(on: boolean): void { + try { + localStorage.setItem(PAGE_BREAK_KEY, on ? '1' : '0') + } catch { + // A blocked localStorage costs the preference, not the export. + } +} + function renderLucideIcon(icon:LucideIcon, props = {}) { if (!_renderToStaticMarkup) return '' return _renderToStaticMarkup( @@ -150,6 +173,7 @@ interface downloadTripPDFProps { // relies on it being an object. export async function downloadTripPDF({ trip, days, places, assignments = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) { await ensureRenderer() + const breaksPerDay = pageBreakPerDay() const loc = _locale || undefined const tr = _t || (k => k) const sorted = [...(days || [])].sort((a, b) => a.day_number - b.day_number) @@ -403,7 +427,7 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca // every page an overflowing day spills onto (#1471). CSS `table-header-group` // on a
is NOT repeated by Chromium's print engine — only real thead is. return ` -
+
${escHtml(tr('dayplan.dayN', { n: day.day_number })).toUpperCase()} @@ -505,6 +529,11 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca /* ── Day ───────────────────────────────────────── */ /* .day-section is a real ; its day header repeats on overflow pages. */ .page-break { page-break-before: always; } + /* Days break by default; .pdf-flow on is the toggle in the preview (#1292). + 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; } + .pdf-flow .day-section + .day-section { margin-top: 18px; } .day-section { width: 100%; border-collapse: collapse; table-layout: fixed; } .day-header-bar { background: #0f172a; padding: 11px 28px; @@ -608,7 +637,7 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca } - +