fix(pdf): use the trip's actual currency instead of hardcoded EUR (#1519)

* fix(pdf): use the trip's actual currency instead of hardcoded EUR

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(i18n): drop hardcoded currency text from pdf.costLabel across locales

Remove redundant EUR/currency references from pdf.costLabel translations
across 21 locales now that the PDF export correctly renders amounts
with their actual trip currency via formatMoney().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(pdf): drop hardcoded euro-shaped icon from place price chip

svgEuro rendered a fixed € glyph next to the price chip regardless of
the trip's actual currency, undermining the currency fix. Swap for a
currency-neutral coin icon.

---------

Co-authored-by: Nguyen Trong Binh <nguytb15@VN1N07HO1CD1015.local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
trongbinhnguyen
2026-07-15 01:50:27 +07:00
committed by GitHub
parent bd6dd59a4b
commit 8267650b2d
23 changed files with 66 additions and 32 deletions
+38 -4
View File
@@ -286,13 +286,47 @@ describe('downloadTripPDF', () => {
expect(iframe!.srcdoc).toContain('CONF999')
})
it('FE-COMP-TRIPPDF-016: renders place description and price chip', async () => {
it('FE-COMP-TRIPPDF-016: renders place description and a currency-formatted price chip', async () => {
await downloadTripPDF(richArgs)
const iframe = getIframe()
expect(iframe!.srcdoc).toContain('Ancient amphitheater')
// Price chip: 15 EUR
expect(iframe!.srcdoc).toContain('15')
expect(iframe!.srcdoc).toContain('EUR')
// richArgs trip has no explicit currency, place has no currency override —
// formatMoney falls back to EUR, formatted via Intl (symbol, not literal "EUR" text).
expect(iframe!.srcdoc).toContain('15,00')
expect(iframe!.srcdoc).toContain('€')
})
it('FE-COMP-TRIPPDF-016b: formats price chip and totals in the trip currency, not EUR', async () => {
const usdArgs = {
...richArgs,
trip: { ...richArgs.trip, currency: 'USD' },
}
await downloadTripPDF(usdArgs)
const iframe = getIframe()
// Place price chip: place has no currency override, falls back to trip.currency (USD).
expect(iframe!.srcdoc).toContain('$15.00')
// No literal "EUR" text should leak into a USD trip's export.
expect(iframe!.srcdoc).not.toContain('EUR')
// Price chip icon must stay currency-neutral — not the euro-shaped glyph.
expect(iframe!.srcdoc).not.toContain('M14 5c-3.87 0-7 3.13-7 7s3.13 7 7 7c2.17 0 4.1-.99 5.4-2.55')
})
it('FE-COMP-TRIPPDF-016c: a place with its own currency overrides the trip currency for its price chip', async () => {
const mixedArgs = {
...richArgs,
trip: { ...richArgs.trip, currency: 'EUR' },
assignments: {
'10': [{
...assignmentForDay,
place: { ...placeWithDetails, currency: 'JPY', price: '1500' },
}],
} as any,
}
await downloadTripPDF(mixedArgs)
const iframe = getIframe()
// JPY is a zero-decimal currency (currencyDecimals) and uses its own symbol, not EUR.
// Note: Intl renders JPY with the fullwidth yen sign (U+FFE5 "¥"), not U+00A5 "¥".
expect(iframe!.srcdoc).toContain('¥1,500')
})
it('FE-COMP-TRIPPDF-017: renders trip description on cover', async () => {
+7 -7
View File
@@ -5,7 +5,7 @@ import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship
import { accommodationsApi, mapsApi, pluginsApi } from '../../api/client'
import type { Trip, Day, Place, Category, AssignmentsMap, DayNote } from '../../types'
import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder'
import { splitReservationDateTime } from '../../utils/formatters'
import { formatMoney, splitReservationDateTime } from '../../utils/formatters'
import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs'
function renderLucideIcon(icon:LucideIcon, props = {}) {
@@ -40,7 +40,7 @@ const svgPin = `<svg width="11" height="11" viewBox="0 0 24 24" fill="#94a3b8"
const svgClock = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>`
const svgClock2= `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>`
const svgCheck = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L19 7"/></svg>`
const svgEuro = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2" stroke-linecap="round"><path d="M14 5c-3.87 0-7 3.13-7 7s3.13 7 7 7c2.17 0 4.1-.99 5.4-2.55"/><path d="M5 11h8M5 13h8"/></svg>`
const svgMoney = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12h6" stroke-linecap="round"/></svg>`
function escHtml(str) {
if (!str) return ''
@@ -92,9 +92,9 @@ function longDateRange(days, locale) {
return `${f.toLocaleDateString(locale, { day: 'numeric', month: 'long', timeZone: 'UTC' })} – ${l.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}`
}
function dayCost(assignments, dayId, locale) {
function dayCost(assignments, dayId, locale, currency) {
const total = (assignments[String(dayId)] || []).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0)
return total > 0 ? `${total.toLocaleString(locale)} EUR` : null
return total > 0 ? formatMoney(total, currency, locale) : null
}
// Pre-fetch place photos for all assigned places.
@@ -204,7 +204,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
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 cost = dayCost(assignments, day.id, loc, trip.currency)
// Reservations for this day (hotel rendered via accommodations block; car middle-phase rendered in sidebar header only)
const dayReservations = pdfGetTransportForDay(day.id)
@@ -320,7 +320,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
const chips = [
place.place_time ? `<span class="chip">${svgClock}${escHtml(place.place_time)}</span>` : '',
place.price && parseFloat(place.price) > 0 ? `<span class="chip chip-green">${svgEuro}${Number(place.price).toLocaleString(loc)} EUR</span>` : '',
place.price && parseFloat(place.price) > 0 ? `<span class="chip chip-green">${svgMoney}${formatMoney(Number(place.price), place.currency || trip.currency, loc)}</span>` : '',
].filter(Boolean).join('')
return `
@@ -625,7 +625,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
<div class="cover-stat-lbl">${escHtml(tr('pdf.planned'))}</div>
</div>
${totalCost > 0 ? `<div>
<div class="cover-stat-num">${totalCost.toLocaleString(loc)}</div>
<div class="cover-stat-num">${formatMoney(totalCost, trip.currency, loc)}</div>
<div class="cover-stat-lbl">${escHtml(tr('pdf.costLabel'))}</div>
</div>` : ''}
</div>