mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
feat(pdf): add legs to pdf export
This commit is contained in:
@@ -84,6 +84,22 @@ const transportReservation = {
|
|||||||
metadata: JSON.stringify({ airline: 'Air Italia', flight_number: 'AI123', departure_airport: 'CDG', arrival_airport: 'FCO' }),
|
metadata: JSON.stringify({ airline: 'Air Italia', flight_number: 'AI123', departure_airport: 'CDG', arrival_airport: 'FCO' }),
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
|
const multiLegFlight = {
|
||||||
|
id: 401,
|
||||||
|
title: 'Flight to Tokyo',
|
||||||
|
type: 'flight',
|
||||||
|
day_id: 10,
|
||||||
|
reservation_time: '2025-06-01T08:00:00',
|
||||||
|
confirmation_number: 'XYZ789',
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
legs: [
|
||||||
|
{ from: 'FRA', to: 'BER', airline: 'Lufthansa', flight_number: 'LH1' },
|
||||||
|
{ from: 'BER', to: 'HND', airline: 'Lufthansa', flight_number: 'LH2' },
|
||||||
|
],
|
||||||
|
departure_airport: 'FRA', arrival_airport: 'HND', airline: 'Lufthansa', flight_number: 'LH1',
|
||||||
|
}),
|
||||||
|
} as any
|
||||||
|
|
||||||
const richArgs = {
|
const richArgs = {
|
||||||
trip: { id: 10, title: 'Italy Trip', description: 'Summer adventure', cover_image: '/uploads/cover.jpg' } as any,
|
trip: { id: 10, title: 'Italy Trip', description: 'Summer adventure', cover_image: '/uploads/cover.jpg' } as any,
|
||||||
days: [dayWithPlaces],
|
days: [dayWithPlaces],
|
||||||
@@ -196,6 +212,16 @@ describe('downloadTripPDF', () => {
|
|||||||
const iframe = getIframe()
|
const iframe = getIframe()
|
||||||
expect(iframe!.srcdoc).toContain('Flight to Rome')
|
expect(iframe!.srcdoc).toContain('Flight to Rome')
|
||||||
expect(iframe!.srcdoc).toContain('ABC123')
|
expect(iframe!.srcdoc).toContain('ABC123')
|
||||||
|
// Single-leg flight keeps its full-route subtitle.
|
||||||
|
expect(iframe!.srcdoc).toContain('Air Italia · AI123 · CDG → FCO')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('FE-COMP-TRIPPDF-013b: renders every flight number for a multi-leg flight', async () => {
|
||||||
|
await downloadTripPDF({ ...richArgs, reservations: [multiLegFlight] })
|
||||||
|
const iframe = getIframe()
|
||||||
|
// One subtitle line per leg, each with its own flight number and segment route.
|
||||||
|
expect(iframe!.srcdoc).toContain('Lufthansa · LH1 · FRA → BER')
|
||||||
|
expect(iframe!.srcdoc).toContain('Lufthansa · LH2 · BER → HND')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('FE-COMP-TRIPPDF-014: renders cover image when trip has cover_image', async () => {
|
it('FE-COMP-TRIPPDF-014: renders cover image when trip has cover_image', async () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { accommodationsApi, mapsApi } from '../../api/client'
|
|||||||
import type { Trip, Day, Place, Category, AssignmentsMap, DayNote } from '../../types'
|
import type { Trip, Day, Place, Category, AssignmentsMap, DayNote } from '../../types'
|
||||||
import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder'
|
import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder'
|
||||||
import { splitReservationDateTime } from '../../utils/formatters'
|
import { splitReservationDateTime } from '../../utils/formatters'
|
||||||
|
import { getFlightLegs } from '../../utils/flightLegs'
|
||||||
|
|
||||||
function renderLucideIcon(icon:LucideIcon, props = {}) {
|
function renderLucideIcon(icon:LucideIcon, props = {}) {
|
||||||
if (!_renderToStaticMarkup) return ''
|
if (!_renderToStaticMarkup) return ''
|
||||||
@@ -215,17 +216,30 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
|
|||||||
const icon = reservationIconSvg(r.type)
|
const icon = reservationIconSvg(r.type)
|
||||||
const color = RESERVATION_COLOR_MAP[r.type] || '#3b82f6'
|
const color = RESERVATION_COLOR_MAP[r.type] || '#3b82f6'
|
||||||
let subtitle = ''
|
let subtitle = ''
|
||||||
|
// Flights render one subtitle line per leg (see below); everything else is a single line.
|
||||||
|
let subtitleLines: string[] = []
|
||||||
if (r.type === 'flight') {
|
if (r.type === 'flight') {
|
||||||
// Full route over all waypoints (FRA → BER → HND), falling back to the
|
const legs = getFlightLegs(r)
|
||||||
// flat metadata pair for legacy single-leg flights without endpoints.
|
if (legs.length > 1) {
|
||||||
const stops = (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)).map(e => e.code || e.name)
|
// Multi-leg: one line per leg so every flight number + segment route is shown.
|
||||||
const route = stops.length >= 2 ? stops.join(' → ') : (meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : '')
|
subtitleLines = legs.map(l =>
|
||||||
subtitle = [meta.airline, meta.flight_number, route].filter(Boolean).join(' · ')
|
[l.airline, l.flight_number,
|
||||||
|
(l.from || l.to) ? [l.from, l.to].filter(Boolean).join(' → ') : '']
|
||||||
|
.filter(Boolean).join(' · '))
|
||||||
|
.filter(Boolean)
|
||||||
|
} else {
|
||||||
|
// Single-leg: full route over all waypoints (FRA → BER → HND), falling back to the
|
||||||
|
// flat metadata pair for legacy single-leg flights without endpoints.
|
||||||
|
const stops = (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0)).map(e => e.code || e.name)
|
||||||
|
const route = stops.length >= 2 ? stops.join(' → ') : (meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : '')
|
||||||
|
subtitle = [meta.airline, meta.flight_number, route].filter(Boolean).join(' · ')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (r.type === 'train') subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Seat ${meta.seat}` : ''].filter(Boolean).join(' · ')
|
else if (r.type === 'train') subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Seat ${meta.seat}` : ''].filter(Boolean).join(' · ')
|
||||||
else if (r.type === 'restaurant') subtitle = [meta.party_size ? `${meta.party_size} guests` : ''].filter(Boolean).join(' · ')
|
else if (r.type === 'restaurant') subtitle = [meta.party_size ? `${meta.party_size} guests` : ''].filter(Boolean).join(' · ')
|
||||||
else if (r.type === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ')
|
else if (r.type === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ')
|
||||||
else if (r.type === 'tour') subtitle = [meta.operator].filter(Boolean).join(' · ')
|
else if (r.type === 'tour') subtitle = [meta.operator].filter(Boolean).join(' · ')
|
||||||
|
if (subtitleLines.length === 0 && subtitle) subtitleLines = [subtitle]
|
||||||
const locationLine = r.location || meta.location || ''
|
const locationLine = r.location || meta.location || ''
|
||||||
const phase = pdfGetSpanPhase(r, day.id)
|
const phase = pdfGetSpanPhase(r, day.id)
|
||||||
const spanLabel = pdfGetSpanLabel(r, phase)
|
const spanLabel = pdfGetSpanLabel(r, phase)
|
||||||
@@ -238,7 +252,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
|
|||||||
<span class="note-icon">${icon}</span>
|
<span class="note-icon">${icon}</span>
|
||||||
<div class="note-body">
|
<div class="note-body">
|
||||||
<div class="note-text" style="font-weight: 600;">${titleHtml}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div>
|
<div class="note-text" style="font-weight: 600;">${titleHtml}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div>
|
||||||
${subtitle ? `<div class="note-time">${escHtml(subtitle)}</div>` : ''}
|
${subtitleLines.filter(Boolean).map(s => `<div class="note-time">${escHtml(s)}</div>`).join('')}
|
||||||
${locationLine ? `<div class="note-time">${escHtml(locationLine)}</div>` : ''}
|
${locationLine ? `<div class="note-time">${escHtml(locationLine)}</div>` : ''}
|
||||||
${r.confirmation_number ? `<div class="note-time" style="font-size:9px;">Code: ${escHtml(r.confirmation_number)}</div>` : ''}
|
${r.confirmation_number ? `<div class="note-time" style="font-size:9px;">Code: ${escHtml(r.confirmation_number)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user