mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +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>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'خطة السفر',
|
||||
'pdf.planned': 'مخطط',
|
||||
'pdf.costLabel': 'التكلفة EUR',
|
||||
'pdf.costLabel': 'التكلفة',
|
||||
'pdf.preview': 'معاينة PDF',
|
||||
'pdf.saveAsPdf': 'حفظ كـ PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Plano de viagem',
|
||||
'pdf.planned': 'Planejado',
|
||||
'pdf.costLabel': 'Custo (EUR)',
|
||||
'pdf.costLabel': 'Custo',
|
||||
'pdf.preview': 'Pré-visualização do PDF',
|
||||
'pdf.saveAsPdf': 'Salvar como PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Cestovní plán',
|
||||
'pdf.planned': 'Naplánováno',
|
||||
'pdf.costLabel': 'Náklady EUR',
|
||||
'pdf.costLabel': 'Náklady',
|
||||
'pdf.preview': 'Náhled PDF',
|
||||
'pdf.saveAsPdf': 'Uložit jako PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Reiseplan',
|
||||
'pdf.planned': 'Eingeplant',
|
||||
'pdf.costLabel': 'Kosten EUR',
|
||||
'pdf.costLabel': 'Kosten',
|
||||
'pdf.preview': 'PDF Vorschau',
|
||||
'pdf.saveAsPdf': 'Als PDF speichern',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Travel Plan',
|
||||
'pdf.planned': 'Planned',
|
||||
'pdf.costLabel': 'Cost EUR',
|
||||
'pdf.costLabel': 'Cost',
|
||||
'pdf.preview': 'PDF Preview',
|
||||
'pdf.saveAsPdf': 'Save as PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Plan de viaje',
|
||||
'pdf.planned': 'Planificado',
|
||||
'pdf.costLabel': 'Coste EUR',
|
||||
'pdf.costLabel': 'Coste',
|
||||
'pdf.preview': 'Vista previa PDF',
|
||||
'pdf.saveAsPdf': 'Guardar como PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Plan de voyage',
|
||||
'pdf.planned': 'Planifié',
|
||||
'pdf.costLabel': 'Coût EUR',
|
||||
'pdf.costLabel': 'Coût',
|
||||
'pdf.preview': 'Aperçu PDF',
|
||||
'pdf.saveAsPdf': 'Enregistrer en PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Ταξιδιωτικό Πρόγραμμα',
|
||||
'pdf.planned': 'Προγραμματισμένο',
|
||||
'pdf.costLabel': 'Κόστος EUR',
|
||||
'pdf.costLabel': 'Κόστος',
|
||||
'pdf.preview': 'Προεπισκόπηση PDF',
|
||||
'pdf.saveAsPdf': 'Αποθήκευση ως PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Rencana Perjalanan',
|
||||
'pdf.planned': 'Direncanakan',
|
||||
'pdf.costLabel': 'Biaya EUR',
|
||||
'pdf.costLabel': 'Biaya',
|
||||
'pdf.preview': 'Pratinjau PDF',
|
||||
'pdf.saveAsPdf': 'Simpan sebagai PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Programma di viaggio',
|
||||
'pdf.planned': 'Programmato',
|
||||
'pdf.costLabel': 'Costo EUR',
|
||||
'pdf.costLabel': 'Costo',
|
||||
'pdf.preview': 'Anteprima PDF',
|
||||
'pdf.saveAsPdf': 'Salva come PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': '旅行計画',
|
||||
'pdf.planned': '予定',
|
||||
'pdf.costLabel': '費用(EUR)',
|
||||
'pdf.costLabel': '費用',
|
||||
'pdf.preview': 'PDFプレビュー',
|
||||
'pdf.saveAsPdf': 'PDFとして保存',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': '여행 계획',
|
||||
'pdf.planned': '계획됨',
|
||||
'pdf.costLabel': '비용 (원)',
|
||||
'pdf.costLabel': '비용',
|
||||
'pdf.preview': 'PDF 미리보기',
|
||||
'pdf.saveAsPdf': 'PDF로 저장',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Reisplan',
|
||||
'pdf.planned': 'Gepland',
|
||||
'pdf.costLabel': 'Kosten EUR',
|
||||
'pdf.costLabel': 'Kosten',
|
||||
'pdf.preview': 'PDF-voorbeeld',
|
||||
'pdf.saveAsPdf': 'Opslaan als PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Plan podróży',
|
||||
'pdf.planned': 'Zaplanowane',
|
||||
'pdf.costLabel': 'Koszt w EUR',
|
||||
'pdf.costLabel': 'Koszt',
|
||||
'pdf.preview': 'Podgląd PDF',
|
||||
'pdf.saveAsPdf': 'Zapisz jako PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'План поездки',
|
||||
'pdf.planned': 'Запланировано',
|
||||
'pdf.costLabel': 'Стоимость EUR',
|
||||
'pdf.costLabel': 'Стоимость',
|
||||
'pdf.preview': 'Предпросмотр PDF',
|
||||
'pdf.saveAsPdf': 'Сохранить как PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Resplan',
|
||||
'pdf.planned': 'Planerat',
|
||||
'pdf.costLabel': 'Kostnad EUR',
|
||||
'pdf.costLabel': 'Kostnad',
|
||||
'pdf.preview': 'Förhandsgranskning av PDF',
|
||||
'pdf.saveAsPdf': 'Spara som PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Seyahat Planı',
|
||||
'pdf.planned': 'Planlandı',
|
||||
'pdf.costLabel': 'Maliyet EUR',
|
||||
'pdf.costLabel': 'Maliyet',
|
||||
'pdf.preview': 'PDF Önizleme',
|
||||
'pdf.saveAsPdf': 'PDF olarak Kaydet',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'План поїздки',
|
||||
'pdf.planned': 'Заплановано',
|
||||
'pdf.costLabel': 'Вартість EUR',
|
||||
'pdf.costLabel': 'Вартість',
|
||||
'pdf.preview': 'Попередній перегляд PDF',
|
||||
'pdf.saveAsPdf': 'Зберегти як PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'Kế hoạch du lịch',
|
||||
'pdf.planned': 'Đã lên kế hoạch',
|
||||
'pdf.costLabel': 'Chi phí EUR',
|
||||
'pdf.costLabel': 'Chi phí',
|
||||
'pdf.preview': 'PDF Xem trước',
|
||||
'pdf.saveAsPdf': 'Lưu dưới dạng PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': '旅行計劃',
|
||||
'pdf.planned': '已規劃',
|
||||
'pdf.costLabel': '費用 EUR',
|
||||
'pdf.costLabel': '費用',
|
||||
'pdf.preview': 'PDF 預覽',
|
||||
'pdf.saveAsPdf': '儲存為 PDF',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types';
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': '旅行计划',
|
||||
'pdf.planned': '已规划',
|
||||
'pdf.costLabel': '费用 EUR',
|
||||
'pdf.costLabel': '费用',
|
||||
'pdf.preview': 'PDF 预览',
|
||||
'pdf.saveAsPdf': '保存为 PDF',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user