mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Merge pull request #637 from mauriceboe/fix/595-pdf-non-transport-reservations
fix(pdf): render restaurant/event/tour/other reservations in trip PDF
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// Trip PDF via browser print window
|
// Trip PDF via browser print window
|
||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||||
import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, LogIn, LogOut, KeyRound, BedDouble, LucideIcon } from 'lucide-react'
|
import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, LogIn, LogOut, KeyRound, BedDouble, Utensils, Users, LucideIcon } from 'lucide-react'
|
||||||
import { accommodationsApi, mapsApi } from '../../api/client'
|
import { accommodationsApi, mapsApi } from '../../api/client'
|
||||||
import type { Trip, Day, Place, Category, AssignmentsMap, DayNotesMap } from '../../types'
|
import type { Trip, Day, Place, Category, AssignmentsMap, DayNotesMap } from '../../types'
|
||||||
|
|
||||||
@@ -18,10 +18,12 @@ function noteIconSvg(iconId) {
|
|||||||
return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#94a3b8' })
|
return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#94a3b8' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSPORT_ICON_MAP = { flight: Plane, train: Train, bus: Bus, car: Car, cruise: Ship }
|
const RESERVATION_ICON_MAP = { flight: Plane, train: Train, bus: Bus, car: Car, cruise: Ship, restaurant: Utensils, event: Ticket, tour: Users, other: FileText }
|
||||||
function transportIconSvg(type) {
|
const RESERVATION_COLOR_MAP = { flight: '#3b82f6', train: '#06b6d4', bus: '#6b7280', car: '#6b7280', cruise: '#0ea5e9', restaurant: '#ef4444', event: '#f59e0b', tour: '#10b981', other: '#6b7280' }
|
||||||
const Icon = TRANSPORT_ICON_MAP[type] || Ticket
|
function reservationIconSvg(type) {
|
||||||
return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#3b82f6' })
|
const Icon = RESERVATION_ICON_MAP[type] || Ticket
|
||||||
|
const color = RESERVATION_COLOR_MAP[type] || '#3b82f6'
|
||||||
|
return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCOMMODATION_ICON_MAP = { accommodation: Hotel, checkin: LogIn, checkout: LogOut, location: MapPin, note: FileText, confirmation: KeyRound }
|
const ACCOMMODATION_ICON_MAP = { accommodation: Hotel, checkin: LogIn, checkout: LogOut, location: MapPin, note: FileText, confirmation: KeyRound }
|
||||||
@@ -144,19 +146,18 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
|
|||||||
const notes = (dayNotes || []).filter(n => n.day_id === day.id)
|
const notes = (dayNotes || []).filter(n => n.day_id === day.id)
|
||||||
const cost = dayCost(assignments, day.id, loc)
|
const cost = dayCost(assignments, day.id, loc)
|
||||||
|
|
||||||
// Transport bookings for this day
|
// Reservations for this day (hotel rendered via accommodations block)
|
||||||
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'cruise'])
|
const dayReservations = (reservations || []).filter(r => {
|
||||||
const dayTransport = (reservations || []).filter(r => {
|
if (!r.reservation_time || r.type === 'hotel') return false
|
||||||
if (!r.reservation_time || !TRANSPORT_TYPES.has(r.type)) return false
|
|
||||||
return day.date && r.reservation_time.split('T')[0] === day.date
|
return day.date && r.reservation_time.split('T')[0] === day.date
|
||||||
})
|
})
|
||||||
|
|
||||||
const merged = []
|
const merged = []
|
||||||
assigned.forEach(a => merged.push({ type: 'place', k: a.order_index ?? a.sort_order ?? 0, data: a }))
|
assigned.forEach(a => merged.push({ type: 'place', k: a.order_index ?? a.sort_order ?? 0, data: a }))
|
||||||
notes.forEach(n => merged.push({ type: 'note', k: n.sort_order ?? 0, data: n }))
|
notes.forEach(n => merged.push({ type: 'note', k: n.sort_order ?? 0, data: n }))
|
||||||
dayTransport.forEach(r => {
|
dayReservations.forEach(r => {
|
||||||
const pos = r.day_plan_position ?? (merged.length > 0 ? Math.max(...merged.map(m => m.k)) + 0.5 : 0.5)
|
const pos = r.day_plan_position ?? (merged.length > 0 ? Math.max(...merged.map(m => m.k)) + 0.5 : 0.5)
|
||||||
merged.push({ type: 'transport', k: pos, data: r })
|
merged.push({ type: 'reservation', k: pos, data: r })
|
||||||
})
|
})
|
||||||
merged.sort((a, b) => a.k - b.k)
|
merged.sort((a, b) => a.k - b.k)
|
||||||
|
|
||||||
@@ -164,21 +165,27 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
|
|||||||
const itemsHtml = merged.length === 0
|
const itemsHtml = merged.length === 0
|
||||||
? `<div class="empty-day">${escHtml(tr('dayplan.emptyDay'))}</div>`
|
? `<div class="empty-day">${escHtml(tr('dayplan.emptyDay'))}</div>`
|
||||||
: merged.map(item => {
|
: merged.map(item => {
|
||||||
if (item.type === 'transport') {
|
if (item.type === 'reservation') {
|
||||||
const r = item.data
|
const r = item.data
|
||||||
const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {})
|
const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {})
|
||||||
const icon = transportIconSvg(r.type)
|
const icon = reservationIconSvg(r.type)
|
||||||
|
const color = RESERVATION_COLOR_MAP[r.type] || '#3b82f6'
|
||||||
let subtitle = ''
|
let subtitle = ''
|
||||||
if (r.type === 'flight') subtitle = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : ''].filter(Boolean).join(' · ')
|
if (r.type === 'flight') subtitle = [meta.airline, meta.flight_number, meta.departure_airport && meta.arrival_airport ? `${meta.departure_airport} → ${meta.arrival_airport}` : ''].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 === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ')
|
||||||
|
else if (r.type === 'tour') subtitle = [meta.operator].filter(Boolean).join(' · ')
|
||||||
|
const locationLine = r.location || meta.location || ''
|
||||||
const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : ''
|
const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : ''
|
||||||
return `
|
return `
|
||||||
<div class="note-card" style="border-left: 3px solid #3b82f6;">
|
<div class="note-card" style="border-left: 3px solid ${color};">
|
||||||
<div class="note-line" style="background: #3b82f6;"></div>
|
<div class="note-line" style="background: ${color};"></div>
|
||||||
<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;">${escHtml(r.title)}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div>
|
<div class="note-text" style="font-weight: 600;">${escHtml(r.title)}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div>
|
||||||
${subtitle ? `<div class="note-time">${escHtml(subtitle)}</div>` : ''}
|
${subtitle ? `<div class="note-time">${escHtml(subtitle)}</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>
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
Reference in New Issue
Block a user