diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 2a4c03a..91da01d 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -1,22 +1,33 @@ // Trip PDF via browser print window import { createElement } from 'react' 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 } from 'lucide-react' -import { mapsApi } from '../../api/client' +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 { accommodationsApi, mapsApi } from '../../api/client' import type { Trip, Day, Place, Category, AssignmentsMap, DayNotesMap } from '../../types' +function renderLucideIcon(icon:LucideIcon, props = {}) { + if (!_renderToStaticMarkup) return '' + return _renderToStaticMarkup( + createElement(icon, props) + ); +} + const NOTE_ICON_MAP = { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark } function noteIconSvg(iconId) { - if (!_renderToStaticMarkup) return '' const Icon = NOTE_ICON_MAP[iconId] || FileText - return _renderToStaticMarkup(createElement(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 } function transportIconSvg(type) { - if (!_renderToStaticMarkup) return '' const Icon = TRANSPORT_ICON_MAP[type] || Ticket - return _renderToStaticMarkup(createElement(Icon, { size: 14, strokeWidth: 1.8, color: '#3b82f6' })) + return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#3b82f6' }) +} + +const ACCOMMODATION_ICON_MAP = { accommodation: Hotel, checkin: LogIn, checkout: LogOut, location: MapPin, note: FileText, confirmation: KeyRound } +function accommodationIconSvg(type) { + const Icon = ACCOMMODATION_ICON_MAP[type] || BedDouble + return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#03398f', className: 'accommodation-icon' }) } // ── SVG inline icons (for chips) ───────────────────────────────────────────── @@ -115,6 +126,8 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const sorted = [...(days || [])].sort((a, b) => a.day_number - b.day_number) const range = longDateRange(sorted, loc) const coverImg = safeImg(trip?.cover_image) + //retrieve accommodations for the trip to display on the day sections and prefetch their photos if needed + const accommodations = await accommodationsApi.list(trip.id); // Pre-fetch place photos from Google const photoMap = await fetchPlacePhotos(assignments) @@ -223,7 +236,53 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${place.notes ? `
${escHtml(place.notes)}
` : ''} ` - }).join('') + }).join('') + + const accommodationsForDay = accommodations.accommodations?.filter(a => + days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id) + ).sort((a, b) => a.start_day_id - b.start_day_id); + + //Const icons for accommodation actions and details + const ICON_ACC_CHECKIN = accommodationIconSvg('checkin'); + const ICON_ACC_CHECKOUT = accommodationIconSvg('checkout'); + const ICON_ACC_LOCATION = accommodationIconSvg('location'); + const ICON_ACC_NOTE = accommodationIconSvg('note'); + const ICON_ACC_CONFIRMATION = accommodationIconSvg('confirmation'); + const ICON_ACC_ACCOMMODATION = accommodationIconSvg('accommodation'); + + const accommodationDetails = accommodationsForDay.map(item => { + + const isCheckIn = day.id === item.start_day_id; + const isCheckOut = day.id === item.end_day_id; + const accomoAction = isCheckIn ? tr('reservations.meta.checkIn') + : isCheckOut ? tr('reservations.meta.checkOut') + : tr('reservations.meta.linkAccommodation') + + const accomoEmoji = isCheckIn ? ICON_ACC_CHECKIN + : isCheckOut ? ICON_ACC_CHECKOUT + : ICON_ACC_ACCOMMODATION + + const accomoTime = isCheckIn ? item.check_in || 'N/A' + : isCheckOut ? item.check_out || 'N/A' + : '' + + return ` +
+
${accomoEmoji} ${escHtml(accomoAction)}
+ ${accomoTime ? `
${accomoEmoji} ${accomoTime}
` : ''} + +
${ICON_ACC_ACCOMMODATION} ${escHtml(item.place_name)}
+ ${item.place_address ? `
${ICON_ACC_LOCATION} ${escHtml(item.place_address)}
` : ''} + ${item.notes ? `
${ICON_ACC_NOTE} ${escHtml(item.notes)}
` : ''} + ${isCheckIn && item.confirmation ? `
${ICON_ACC_CONFIRMATION} ${escHtml(item.confirmation)}
` : ''} +
+ ` + }).join(''); + + const accommodationsHtml = accommodationDetails ? + `
+
${accommodationDetails}
+
` : ''; return `
@@ -233,8 +292,8 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${day.date ? `${shortDate(day.date, loc)}` : ''} ${cost ? `${cost}` : ''}
-
${itemsHtml}
- ` +
${accommodationsHtml}${itemsHtml}
+ ` }).join('') const html = ` @@ -317,6 +376,40 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor .day-cost { font-size: 9px; font-weight: 600; color: rgba(255,255,255,0.65); } .day-body { padding: 12px 28px 6px; } + /* accommodation info */ + .day-accommodations-overview { font-size: 12px; } + .day-accommodations { display: flex; flex-direction: row; justify-content: space-between; } + .day-accommodations.single { justify-content: center; } + + .day-accommodation { + width: 50%; + margin:10px; + padding:10px; + border:2px solid #e2e8f0; + border-radius: 12px; + justify-content: center; + display: flex; + flex-direction: column; + } + + .day-accommodation-title { + font-size: 18px; + font-weight: 600; + text-align: center; + margin-bottom: 4px; + align-self: center; + } + + .accommodation-center-icon { + display: flex; + align-items: center; + } + + .accommodation-icon { + margin-right: 4px; + } + + /* ── Place card ────────────────────────────────── */ .place-card { display: flex; align-items: stretch;