mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user