// Trip PDF via browser print window import { createElement } from 'react' import { getCategoryIcon } from '../shared/categoryIcons' import { mapsApi } from '../../api/client' // ── SVG inline icons (for chips) ───────────────────────────────────────────── const svgPin = `` const svgClock = `` const svgClock2= `` const svgCheck = `` const svgEuro = `` function escHtml(str) { if (!str) return '' return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } function absUrl(url) { if (!url) return null if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) return url return window.location.origin + (url.startsWith('/') ? '' : '/') + url } function safeImg(url) { if (!url) return null if (url.startsWith('https://') || url.startsWith('http://')) return url return /\.(jpe?g|png|webp|bmp|tiff?)(\?.*)?$/i.test(url) ? absUrl(url) : null } // Generate SVG string from Lucide icon name (for category thumbnails) let _renderToStaticMarkup = null async function ensureRenderer() { if (!_renderToStaticMarkup) { const mod = await import('react-dom/server') _renderToStaticMarkup = mod.renderToStaticMarkup } } function categoryIconSvg(iconName, color = '#6366f1', size = 24) { if (!_renderToStaticMarkup) return '' const Icon = getCategoryIcon(iconName) return _renderToStaticMarkup( createElement(Icon, { size, strokeWidth: 1.8, color: 'rgba(255,255,255,0.92)' }) ) } function shortDate(d, locale) { if (!d) return '' return new Date(d + 'T00:00:00').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) } function longDateRange(days, locale) { const dd = [...days].filter(d => d.date).sort((a, b) => a.day_number - b.day_number) if (!dd.length) return null const f = new Date(dd[0].date + 'T00:00:00') const l = new Date(dd[dd.length - 1].date + 'T00:00:00') return `${f.toLocaleDateString(locale, { day: 'numeric', month: 'long' })} – ${l.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' })}` } function dayCost(assignments, dayId, locale) { const total = (assignments[String(dayId)] || []).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0) return total > 0 ? `${total.toLocaleString(locale)} EUR` : null } // Pre-fetch Google Place photos for all assigned places async function fetchPlacePhotos(assignments) { const photoMap = {} // placeId → photoUrl const allPlaces = Object.values(assignments).flatMap(a => a.map(x => x.place)).filter(Boolean) const unique = [...new Map(allPlaces.map(p => [p.id, p])).values()] const toFetch = unique.filter(p => !p.image_url && p.google_place_id) await Promise.allSettled( toFetch.map(async (place) => { try { const data = await mapsApi.placePhoto(place.google_place_id) if (data.photoUrl) photoMap[place.id] = data.photoUrl } catch {} }) ) return photoMap } export async function downloadTripPDF({ trip, days, places, assignments, categories, dayNotes, t: _t, locale: _locale }) { await ensureRenderer() const loc = _locale || 'de-DE' const tr = _t || (k => k) const sorted = [...(days || [])].sort((a, b) => a.day_number - b.day_number) const range = longDateRange(sorted, loc) const coverImg = safeImg(trip?.cover_image) // Pre-fetch place photos from Google const photoMap = await fetchPlacePhotos(assignments) const totalAssigned = new Set( Object.values(assignments || {}).flatMap(a => a.map(x => x.place?.id)).filter(Boolean) ).size const totalCost = Object.values(assignments || {}) .flatMap(a => a).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0) // Build day HTML const daysHtml = sorted.map((day, di) => { const assigned = assignments[String(day.id)] || [] const notes = (dayNotes || []).filter(n => n.day_id === day.id) const cost = dayCost(assignments, day.id, loc) const merged = [] assigned.forEach(a => merged.push({ type: 'place', k: a.sort_order ?? 0, data: a })) notes.forEach(n => merged.push({ type: 'note', k: n.sort_order ?? 0, data: n })) merged.sort((a, b) => a.k - b.k) let pi = 0 const itemsHtml = merged.length === 0 ? `