From 1e44b25a0c6d1c2e99ef75de07685f69a6ff7830 Mon Sep 17 00:00:00 2001 From: micro92 Date: Thu, 2 Apr 2026 20:59:02 -0400 Subject: [PATCH 1/3] Add Accomodation to PDF --- client/src/components/PDF/TripPDF.tsx | 71 +++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index c83cc4b7..57412d22 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -2,7 +2,7 @@ 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 { accommodationsApi, mapsApi } from '../../api/client' import type { Trip, Day, Place, Category, AssignmentsMap, DayNotesMap } from '../../types' const NOTE_ICON_MAP = { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark } @@ -115,6 +115,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 accomodations for the trip to display on the day sections and prefetch their photos if needed + const accomodations = await accommodationsApi.list(trip.id); // Pre-fetch place photos from Google const photoMap = await fetchPlacePhotos(assignments) @@ -223,7 +225,45 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${place.notes ? `
${escHtml(place.notes)}
` : ''} ` - }).join('') + }).join('') + + const accomodationsForDay = accomodations.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 accomodationDetails = accomodationsForDay.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 ? '🛎️' + : isCheckOut ? '🧳' + : '' + + const accomoTime = isCheckIn ? item.check_in || 'N/A' + : isCheckOut ? item.check_out || 'N/A' + : '' + + return ` +
+
${escHtml(accomoAction)}
+ ${accomoTime ? `
${accomoEmoji} ${accomoTime}
` : ''} + +
🏨 ${escHtml(item.place_name)}
+ ${item.place_address ? `
📌 ${escHtml(item.place_address)}
` : ''} + ${item.notes ? `
📝 ${escHtml(item.notes)}
` : ''} + ${isCheckIn && item.confirmation ? `
🔑 ${escHtml(item.confirmation)}
` : ''} +
+ ` + }).join(''); + + const accomodationsHtml = accomodationDetails ? + `
+
${accomodationDetails}
+
` : ''; return `
@@ -233,8 +273,8 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${day.date ? `${shortDate(day.date, loc)}` : ''} ${cost ? `${cost}` : ''}
-
${itemsHtml}
- ` +
${accomodationsHtml}${itemsHtml}
+ ` }).join('') const html = ` @@ -317,6 +357,29 @@ 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; } + /* Accomodation info */ + .day-accomodations-overview { font-size: 12px; } + .day-accomodations { display: flex; flex-direction: row; justify-content: space-between; } + .day-accomodations.single { justify-content: center; } + + .day-accomodation { + width: 50%; + margin:10px; + padding:10px; + border:2px solid #e2e8f0; + border-radius: 12px; + justify-content: center; + display: flex; + flex-direction: column; + } + + .day-accomodation-action { + font-size: 18px; + font-weight: 600; + text-align: center; + margin-bottom: 4px; + } + /* ── Place card ────────────────────────────────── */ .place-card { display: flex; align-items: stretch; From a9c392e26e3dd7bc0b7a1e75e20138789ae9b3c7 Mon Sep 17 00:00:00 2001 From: micro92 Date: Fri, 3 Apr 2026 11:26:28 -0400 Subject: [PATCH 2/3] Replace Emoji By Lucide Icon --- client/src/components/PDF/TripPDF.tsx | 68 +++++++++++++++++++-------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 57412d22..64b5df73 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 { 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) ───────────────────────────────────────────── @@ -231,17 +242,25 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor 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 accomodationDetails = accomodationsForDay.map(item => { + //Const icons for accomodation 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 accomodationDetails = accomodationsForDay.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 accomoAction = isCheckIn ? tr('reservations.meta.checkIn') + : isCheckOut ? tr('reservations.meta.checkOut') + : tr('reservations.meta.linkAccommodation') - const accomoEmoji = isCheckIn ? '🛎️' - : isCheckOut ? '🧳' - : '' + 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' @@ -249,13 +268,13 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor return `
-
${escHtml(accomoAction)}
- ${accomoTime ? `
${accomoEmoji} ${accomoTime}
` : ''} +
${accomoEmoji} ${escHtml(accomoAction)}
+ ${accomoTime ? `
${accomoEmoji} ${accomoTime}
` : ''} -
🏨 ${escHtml(item.place_name)}
- ${item.place_address ? `
📌 ${escHtml(item.place_address)}
` : ''} - ${item.notes ? `
📝 ${escHtml(item.notes)}
` : ''} - ${isCheckIn && item.confirmation ? `
🔑 ${escHtml(item.confirmation)}
` : ''} +
${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(''); @@ -373,13 +392,24 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor flex-direction: column; } - .day-accomodation-action { + .day-accomodation-title { font-size: 18px; font-weight: 600; text-align: center; margin-bottom: 4px; + align-self: center; } + .accomodation-center-icon { + display: flex; + align-items: center; + } + + .accommodation-icon { + margin-right: 4px; + } + + /* ── Place card ────────────────────────────────── */ .place-card { display: flex; align-items: stretch; From f4f768a1b33d4c6b33593451ce353c1fcb541750 Mon Sep 17 00:00:00 2001 From: micro92 Date: Fri, 3 Apr 2026 11:27:17 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix=20accomodation=20-=C2=AD=C2=AD>=20accom?= =?UTF-8?q?modation=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/PDF/TripPDF.tsx | 48 +++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 64b5df73..edb76498 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -126,8 +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 accomodations for the trip to display on the day sections and prefetch their photos if needed - const accomodations = await accommodationsApi.list(trip.id); + //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) @@ -238,11 +238,11 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ` }).join('') - const accomodationsForDay = accomodations.accommodations?.filter(a => + 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 accomodation actions and details + //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'); @@ -250,8 +250,8 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const ICON_ACC_CONFIRMATION = accommodationIconSvg('confirmation'); const ICON_ACC_ACCOMMODATION = accommodationIconSvg('accommodation'); - const accomodationDetails = accomodationsForDay.map(item => { - + 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') @@ -267,21 +267,21 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor : '' return ` -
-
${accomoEmoji} ${escHtml(accomoAction)}
- ${accomoTime ? `
${accomoEmoji} ${accomoTime}
` : ''} +
+
${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)}
` : ''} +
${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 accomodationsHtml = accomodationDetails ? - `
-
${accomodationDetails}
+ const accommodationsHtml = accommodationDetails ? + `
+
${accommodationDetails}
` : ''; return ` @@ -292,7 +292,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${day.date ? `${shortDate(day.date, loc)}` : ''} ${cost ? `${cost}` : ''}
-
${accomodationsHtml}${itemsHtml}
+
${accommodationsHtml}${itemsHtml}
` }).join('') @@ -376,12 +376,12 @@ 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; } - /* Accomodation info */ - .day-accomodations-overview { font-size: 12px; } - .day-accomodations { display: flex; flex-direction: row; justify-content: space-between; } - .day-accomodations.single { justify-content: center; } + /* 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-accomodation { + .day-accommodation { width: 50%; margin:10px; padding:10px; @@ -392,7 +392,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor flex-direction: column; } - .day-accomodation-title { + .day-accommodation-title { font-size: 18px; font-weight: 600; text-align: center; @@ -400,7 +400,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor align-self: center; } - .accomodation-center-icon { + .accommodation-center-icon { display: flex; align-items: center; }