Merge pull request #334 from micro92/feat/accomodationPDF

Add Accomodation to PDF
This commit is contained in:
Maurice
2026-04-04 17:51:31 +02:00
committed by GitHub

View File

@@ -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 ? `<div class="info-row"><span class="info-spacer"></span><span class="info-text muted italic">${escHtml(place.notes)}</span></div>` : ''}
</div>
</div>`
}).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 `
<div class="day-accommodation">
<div class="day-accommodation-title accommodation-center-icon" >${accomoEmoji} ${escHtml(accomoAction)}</div>
${accomoTime ? `<div class="accommodation-center-icon">${accomoEmoji} <b>${accomoTime}</b></div>` : ''}
<div class="accommodation-center-icon">${ICON_ACC_ACCOMMODATION} ${escHtml(item.place_name)}</div>
${item.place_address ? `<div class="accommodation-center-icon">${ICON_ACC_LOCATION} ${escHtml(item.place_address)}</div>` : ''}
${item.notes ? `<div class="accommodation-center-icon">${ICON_ACC_NOTE} ${escHtml(item.notes)}</div>` : ''}
${isCheckIn && item.confirmation ? `<div class="accommodation-center-icon">${ICON_ACC_CONFIRMATION} ${escHtml(item.confirmation)}</div>` : ''}
</div>
`
}).join('');
const accommodationsHtml = accommodationDetails ?
`<div class="day-accommodations-overview">
<div class="day-accommodations ${accommodationsForDay.length === 1 ? 'single' : ''}">${accommodationDetails}</div>
</div>` : '';
return `
<div class="day-section${di > 0 ? ' page-break' : ''}">
@@ -233,8 +292,8 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
${day.date ? `<span class="day-date">${shortDate(day.date, loc)}</span>` : ''}
${cost ? `<span class="day-cost">${cost}</span>` : ''}
</div>
<div class="day-body">${itemsHtml}</div>
</div>`
<div class="day-body">${accommodationsHtml}${itemsHtml}</div>
</div>`
}).join('')
const html = `<!DOCTYPE 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;