From dc4ccb2304a7714c1b2e00266a10799179aa346c Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 29 Jul 2026 19:38:26 +0200 Subject: [PATCH 1/6] chore(i18n): add the PDF page break label --- shared/src/i18n/ar/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/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/sv/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 + 23 files changed, 23 insertions(+) diff --git a/shared/src/i18n/ar/pdf.ts b/shared/src/i18n/ar/pdf.ts index 86a7a9423..0eba805b3 100644 --- a/shared/src/i18n/ar/pdf.ts +++ b/shared/src/i18n/ar/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'التكلفة', 'pdf.preview': 'معاينة PDF', 'pdf.saveAsPdf': 'حفظ كـ PDF', + 'pdf.pageBreakPerDay': 'فاصل صفحة لكل يوم', }; export default pdf; diff --git a/shared/src/i18n/br/pdf.ts b/shared/src/i18n/br/pdf.ts index 7ca63ccb4..b272b6e2e 100644 --- a/shared/src/i18n/br/pdf.ts +++ b/shared/src/i18n/br/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Custo', 'pdf.preview': 'Pré-visualização do PDF', 'pdf.saveAsPdf': 'Salvar como PDF', + 'pdf.pageBreakPerDay': 'Quebra de página por dia', }; export default pdf; diff --git a/shared/src/i18n/ca/pdf.ts b/shared/src/i18n/ca/pdf.ts index cb48be3ac..667c30e77 100644 --- a/shared/src/i18n/ca/pdf.ts +++ b/shared/src/i18n/ca/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Cost EUR', 'pdf.preview': 'Vista prèvia del PDF', 'pdf.saveAsPdf': 'Desa com a PDF', + 'pdf.pageBreakPerDay': 'Salt de pàgina per dia', }; export default pdf; diff --git a/shared/src/i18n/cs/pdf.ts b/shared/src/i18n/cs/pdf.ts index cce6e43e2..bd610b9f3 100644 --- a/shared/src/i18n/cs/pdf.ts +++ b/shared/src/i18n/cs/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Náklady', 'pdf.preview': 'Náhled PDF', 'pdf.saveAsPdf': 'Uložit jako PDF', + 'pdf.pageBreakPerDay': 'Zalomení stránky pro každý den', }; export default pdf; diff --git a/shared/src/i18n/de/pdf.ts b/shared/src/i18n/de/pdf.ts index a259c54ad..2fb75fa61 100644 --- a/shared/src/i18n/de/pdf.ts +++ b/shared/src/i18n/de/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Kosten', 'pdf.preview': 'PDF Vorschau', 'pdf.saveAsPdf': 'Als PDF speichern', + 'pdf.pageBreakPerDay': 'Seitenumbruch pro Tag', }; export default pdf; diff --git a/shared/src/i18n/en/pdf.ts b/shared/src/i18n/en/pdf.ts index 1dcedfea0..c6f092b62 100644 --- a/shared/src/i18n/en/pdf.ts +++ b/shared/src/i18n/en/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Cost', 'pdf.preview': 'PDF Preview', 'pdf.saveAsPdf': 'Save as PDF', + 'pdf.pageBreakPerDay': 'Page break per day', }; export default pdf; diff --git a/shared/src/i18n/es/pdf.ts b/shared/src/i18n/es/pdf.ts index e3eaafb7f..af12208e3 100644 --- a/shared/src/i18n/es/pdf.ts +++ b/shared/src/i18n/es/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Coste', 'pdf.preview': 'Vista previa PDF', 'pdf.saveAsPdf': 'Guardar como PDF', + 'pdf.pageBreakPerDay': 'Salto de página por día', }; export default pdf; diff --git a/shared/src/i18n/fr/pdf.ts b/shared/src/i18n/fr/pdf.ts index b717f9822..1ddc9cee1 100644 --- a/shared/src/i18n/fr/pdf.ts +++ b/shared/src/i18n/fr/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Coût', 'pdf.preview': 'Aperçu PDF', 'pdf.saveAsPdf': 'Enregistrer en PDF', + 'pdf.pageBreakPerDay': 'Saut de page par jour', }; export default pdf; diff --git a/shared/src/i18n/gr/pdf.ts b/shared/src/i18n/gr/pdf.ts index 0b616f89e..2ece28623 100644 --- a/shared/src/i18n/gr/pdf.ts +++ b/shared/src/i18n/gr/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Κόστος', 'pdf.preview': 'Προεπισκόπηση PDF', 'pdf.saveAsPdf': 'Αποθήκευση ως PDF', + 'pdf.pageBreakPerDay': 'Αλλαγή σελίδας ανά ημέρα', }; export default pdf; diff --git a/shared/src/i18n/hu/pdf.ts b/shared/src/i18n/hu/pdf.ts index f19283df7..460941102 100644 --- a/shared/src/i18n/hu/pdf.ts +++ b/shared/src/i18n/hu/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Költség', 'pdf.preview': 'PDF előnézet', 'pdf.saveAsPdf': 'Mentés PDF-ként', + 'pdf.pageBreakPerDay': 'Oldaltörés naponta', }; export default pdf; diff --git a/shared/src/i18n/id/pdf.ts b/shared/src/i18n/id/pdf.ts index f63e35b5f..d0f7f3391 100644 --- a/shared/src/i18n/id/pdf.ts +++ b/shared/src/i18n/id/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Biaya', 'pdf.preview': 'Pratinjau PDF', 'pdf.saveAsPdf': 'Simpan sebagai PDF', + 'pdf.pageBreakPerDay': 'Pemisah halaman per hari', }; export default pdf; diff --git a/shared/src/i18n/it/pdf.ts b/shared/src/i18n/it/pdf.ts index e070d6b3a..1219600d9 100644 --- a/shared/src/i18n/it/pdf.ts +++ b/shared/src/i18n/it/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Costo', 'pdf.preview': 'Anteprima PDF', 'pdf.saveAsPdf': 'Salva come PDF', + 'pdf.pageBreakPerDay': 'Interruzione di pagina per giorno', }; export default pdf; diff --git a/shared/src/i18n/ja/pdf.ts b/shared/src/i18n/ja/pdf.ts index 77b45b604..43c64a6c2 100644 --- a/shared/src/i18n/ja/pdf.ts +++ b/shared/src/i18n/ja/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': '費用', 'pdf.preview': 'PDFプレビュー', 'pdf.saveAsPdf': 'PDFとして保存', + 'pdf.pageBreakPerDay': '日ごとに改ページ', }; export default pdf; diff --git a/shared/src/i18n/ko/pdf.ts b/shared/src/i18n/ko/pdf.ts index 58950e70f..3241d7bc9 100644 --- a/shared/src/i18n/ko/pdf.ts +++ b/shared/src/i18n/ko/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': '비용', 'pdf.preview': 'PDF 미리보기', 'pdf.saveAsPdf': 'PDF로 저장', + 'pdf.pageBreakPerDay': '날짜별 페이지 나누기', }; export default pdf; diff --git a/shared/src/i18n/nl/pdf.ts b/shared/src/i18n/nl/pdf.ts index 751200f91..c447ad9c3 100644 --- a/shared/src/i18n/nl/pdf.ts +++ b/shared/src/i18n/nl/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Kosten', 'pdf.preview': 'PDF-voorbeeld', 'pdf.saveAsPdf': 'Opslaan als PDF', + 'pdf.pageBreakPerDay': 'Pagina-einde per dag', }; export default pdf; diff --git a/shared/src/i18n/pl/pdf.ts b/shared/src/i18n/pl/pdf.ts index 45863f772..793842215 100644 --- a/shared/src/i18n/pl/pdf.ts +++ b/shared/src/i18n/pl/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Koszt', 'pdf.preview': 'Podgląd PDF', 'pdf.saveAsPdf': 'Zapisz jako PDF', + 'pdf.pageBreakPerDay': 'Podział strony dla każdego dnia', }; export default pdf; diff --git a/shared/src/i18n/ru/pdf.ts b/shared/src/i18n/ru/pdf.ts index 38e42597b..c3455396b 100644 --- a/shared/src/i18n/ru/pdf.ts +++ b/shared/src/i18n/ru/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Стоимость', 'pdf.preview': 'Предпросмотр PDF', 'pdf.saveAsPdf': 'Сохранить как PDF', + 'pdf.pageBreakPerDay': 'Разрыв страницы для каждого дня', }; export default pdf; diff --git a/shared/src/i18n/sv/pdf.ts b/shared/src/i18n/sv/pdf.ts index 18e57da00..2a523c439 100644 --- a/shared/src/i18n/sv/pdf.ts +++ b/shared/src/i18n/sv/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Kostnad', 'pdf.preview': 'Förhandsgranskning av PDF', 'pdf.saveAsPdf': 'Spara som PDF', + 'pdf.pageBreakPerDay': 'Sidbrytning per dag', }; export default pdf; diff --git a/shared/src/i18n/tr/pdf.ts b/shared/src/i18n/tr/pdf.ts index d0c67970b..13865a40e 100644 --- a/shared/src/i18n/tr/pdf.ts +++ b/shared/src/i18n/tr/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Maliyet', 'pdf.preview': 'PDF Önizleme', 'pdf.saveAsPdf': 'PDF olarak Kaydet', + 'pdf.pageBreakPerDay': 'Her gün için sayfa sonu', }; export default pdf; diff --git a/shared/src/i18n/uk/pdf.ts b/shared/src/i18n/uk/pdf.ts index 415d46a00..7e301e6ff 100644 --- a/shared/src/i18n/uk/pdf.ts +++ b/shared/src/i18n/uk/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Вартість', 'pdf.preview': 'Попередній перегляд PDF', 'pdf.saveAsPdf': 'Зберегти як PDF', + 'pdf.pageBreakPerDay': 'Розрив сторінки для кожного дня', }; export default pdf; diff --git a/shared/src/i18n/vi/pdf.ts b/shared/src/i18n/vi/pdf.ts index d8bfe1618..e513e5cf6 100644 --- a/shared/src/i18n/vi/pdf.ts +++ b/shared/src/i18n/vi/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': 'Chi phí', 'pdf.preview': 'PDF Xem trước', 'pdf.saveAsPdf': 'Lưu dưới dạng PDF', + 'pdf.pageBreakPerDay': 'Ngắt trang cho mỗi ngày', }; export default pdf; diff --git a/shared/src/i18n/zh-TW/pdf.ts b/shared/src/i18n/zh-TW/pdf.ts index 8c0e4bcd7..7b61fc94c 100644 --- a/shared/src/i18n/zh-TW/pdf.ts +++ b/shared/src/i18n/zh-TW/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': '費用', 'pdf.preview': 'PDF 預覽', 'pdf.saveAsPdf': '儲存為 PDF', + 'pdf.pageBreakPerDay': '每天分頁', }; export default pdf; diff --git a/shared/src/i18n/zh/pdf.ts b/shared/src/i18n/zh/pdf.ts index b7634db07..09610dbec 100644 --- a/shared/src/i18n/zh/pdf.ts +++ b/shared/src/i18n/zh/pdf.ts @@ -6,5 +6,6 @@ const pdf: TranslationStrings = { 'pdf.costLabel': '费用', 'pdf.preview': 'PDF 预览', 'pdf.saveAsPdf': '保存为 PDF', + 'pdf.pageBreakPerDay': '每天分页', }; export default pdf; From 03be438e0b996b12428e7f4438d34966dbc5230a Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 29 Jul 2026 19:38:27 +0200 Subject: [PATCH 2/6] feat(pdf): make the page break between days optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every day started a new page, so a plan of short days printed one sheet per handful of lines and looked nothing like the view it was exported from. The preview now carries the choice, next to Save as PDF, and remembers it. The two layouts differ by a single class on , so ticking the box re-lays the open preview out immediately instead of rebuilding the document and re-fetching every photo. Breaking per day stays the default — this only adds the other option. --- client/src/components/PDF/TripPDF.test.ts | 84 +++++++++++++++++++++++ client/src/components/PDF/TripPDF.tsx | 49 +++++++++++-- 2 files changed, 129 insertions(+), 4 deletions(-) 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 } - +