fix(client): the audit round over the 4.3.1 release

Ride minutes and late marks follow an edited timetable instead of the routing
that fetched them; a landing day's join band reads as the ride it is rather
than a nine hour drive of no length; the refuel offer is there on the road
out of a terminal or a pick-up desk; the phone's stage map draws the ride;
the share page numbers its stops the way the planner does; the PDF lists a
day the way the day plan lists it, from both shells; a poster-less clip draws
its play badge everywhere, not only in the editor; the hero title has room
for its descenders and the list card keeps its badge clear; the release
notice keeps its close control in reach when it stacks; a link chip's actions
are thumb-sized and its delete asks first; the Google retry line and the
Google-only switch appear only where Google answers.
This commit is contained in:
Maurice
2026-09-22 12:10:21 +02:00
parent cb85ba915c
commit 85b5756e89
78 changed files with 2476 additions and 486 deletions
+34 -1
View File
@@ -31,6 +31,12 @@ function getIframe(): HTMLIFrameElement | null {
return document.querySelector('#pdf-preview-overlay iframe')
}
/** The cover's "Planned" figure. The stat is read off its label, since days and places print the same markup. */
function plannedStat(html: string): number | null {
const match = /<div class="cover-stat-num">(\d+)<\/div>\s*<div class="cover-stat-lbl">pdf\.planned<\/div>/.exec(html)
return match ? Number(match[1]) : null
}
// ── Setup ─────────────────────────────────────────────────────────────────────
beforeEach(() => {
@@ -257,7 +263,7 @@ describe('downloadTripPDF', () => {
// The desktop toolbar hands the export the store's assignments, hidden stop and
// all, and that stop heads its check-in day. The day plan hides it and shows the
// booking as the accommodation block, and the print lists what the plan lists.
const hotelPlace = { id: 101, name: 'Hotel Hafen Hamburg', address: 'Seewartenstr. 9', place_time: null } as any
const hotelPlace = { id: 101, name: 'Hotel Hafen Hamburg', address: 'Seewartenstr. 9', place_time: null, price: '120' } as any
const hotelStop = { id: 201, day_id: 10, place_id: 101, order_index: 0, accommodation_id: 30, place: hotelPlace }
const flight = { ...transportReservation, title: 'Morning flight', reservation_time: '2025-06-01T08:00:00' }
await downloadTripPDF({
@@ -269,6 +275,33 @@ describe('downloadTripPDF', () => {
expect(html).not.toContain('Seewartenstr. 9')
expect(html.indexOf('Morning flight')).toBeLessThan(html.indexOf('Colosseum'))
expect(html.indexOf('Morning flight')).toBeGreaterThan(-1)
// The cover and the day header read the same list: the hidden stop is not a planned
// stop and its hotel's price is not part of the day, whichever shell asked.
expect(plannedStat(html)).toBe(1)
const money = html.replace(/[\u00a0\u202f]/g, ' ')
expect(money).toContain('15,00 €')
expect(money).not.toContain('135,00 €')
})
it('FE-COMP-TRIPPDF-013g: a service stop the plan keeps to the road trip view is not printed either, and comes back with the switch', async () => {
// "Show in Days too" off keeps petrol stations and rest areas out of the day lists.
// The print follows the same switch, so the desktop, which hands over the store's
// assignments, and the phone print the same day.
const pump = { id: 102, name: 'Aral Autohof', address: 'Autobahnkreuz 1', stop_type: 'fuel', place_time: null } as any
const pumpStop = { id: 202, day_id: 10, place_id: 102, order_index: 1, place: pump }
const assignments = { '10': [assignmentForDay, pumpStop] } as any
await downloadTripPDF({ ...richArgs, assignments, showServiceStops: false })
const hidden = getIframe()!.srcdoc
expect(hidden).not.toContain('Aral Autohof')
expect(hidden).toContain('Colosseum')
expect(plannedStat(hidden)).toBe(1)
// On, and by default, the stop prints where the plan lists it.
getOverlay()?.remove()
await downloadTripPDF({ ...richArgs, assignments })
const shown = getIframe()!.srcdoc
expect(shown).toContain('Aral Autohof')
expect(plannedStat(shown)).toBe(2)
})
it('FE-COMP-TRIPPDF-013c: a flight that lands the same day shows both times (#1310)', async () => {
+31 -10
View File
@@ -16,6 +16,7 @@ import { renderTripMapImage } from './tripMapImage'
import { formatDistance } from '../../utils/units'
import { fetchExchangeRates } from '../../hooks/useExchangeRates'
import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs'
import { isServiceStopType } from '../Roadtrip/roadtripModel'
/**
* Every day starts a new page by default. On a trip of short days that prints
@@ -199,11 +200,34 @@ interface downloadTripPDFProps {
timeFormat?: string
/** 'metric' | 'imperial'. Same reasoning as `timeFormat` — read as a prop, not a hook. */
distanceUnit?: string
/**
* The road trip setting "Show in Days too" (`roadtrip_service_stops_in_days`), read as a
* prop for the same reason. Off, the day plan keeps petrol stations and rest areas to
* the road trip view, and so does the print.
*/
showServiceStops?: boolean
}
// `assignments` is normalised here once — every read below (and fetchPlacePhotos)
// relies on it being an object.
export async function downloadTripPDF({ trip, days, places, assignments = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale, timeFormat: _timeFormat, distanceUnit: _distanceUnit }: downloadTripPDFProps) {
/**
* The stops the day plan lists, out of everything the store holds for the trip.
*
* The stop a booked night wrote onto its check-in day is out whatever the switch says:
* the day already shows that booking as its accommodation block, and the row would be
* the same hotel a second time. The service stops go with the setting. One predicate
* for the whole document, so the day lists, the route map, the cover's planned count
* and every cost total agree with the plan and with each other, whichever shell asked.
*/
function planAssignments(assignments: AssignmentsMap, showServiceStops: boolean): AssignmentsMap {
return Object.fromEntries(Object.entries(assignments).map(([dayId, list]) => [
dayId,
(list || []).filter(a => a.accommodation_id == null && (showServiceStops || !isServiceStopType(a.place?.stop_type))),
]))
}
// `assignments` is normalised here once, to the plan's own list; every read below
// (and fetchPlacePhotos) relies on it being an object.
export async function downloadTripPDF({ trip, days, places, assignments: stored = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale, timeFormat: _timeFormat, distanceUnit: _distanceUnit, showServiceStops = true }: downloadTripPDFProps) {
const assignments = planAssignments(stored, showServiceStops)
const breaksPerDay = pageBreakPerDay()
const loc = _locale || undefined
const tr = _t || (k => k)
@@ -335,14 +359,11 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca
}
// Build day HTML
const daysHtml = sorted.map((day, di) => {
// Without the stop a booked night wrote onto its check-in day: the day plan hides
// it (the day already carries the booking as its accommodation block) and the
// export lists what the plan lists. The desktop toolbar hands the export the
// store's own assignments, filter and all, and the stop sits at the head of its
// day, so the hotel printed above a morning flight (#2434).
// What the plan lists (planAssignments): the stop a booked night wrote sat at the
// head of its day, so the hotel printed above a morning flight (#2434).
const assigned = (assignments[String(day.id)] || [])
.filter((a: any) => a.accommodation_id == null)
.sort((a: any, b: any) => (a.order_index ?? 0) - (b.order_index ?? 0))
.slice()
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
const notes = (dayNotes || []).filter(n => n.day_id === day.id).slice()
.sort((a: any, b: any) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
const cost = dayCost(assignments, day.id, loc, tripCur, fxRates)