From a4e3e83d3f63a4f6fb942299d4debe34c124270b Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 10:48:31 +0200 Subject: [PATCH] refactor(planner): share the transport detail day and segment labels between desktop and phone The transport detail dialog and the phone transport sheet name the booking's day and each segment's own booking code through transportDetailModel, next to the files filter they already share. The rest of the two views reads the booking differently on purpose and stays as it is. --- .../DayPlanSidebarTransportDetailModal.tsx | 8 +++--- .../Planner/transportDetailModel.test.ts | 21 ++++++++++++++++ .../Planner/transportDetailModel.ts | 25 +++++++++++++++++++ .../screens/trip/sheets/MTransportSheet.tsx | 9 +++---- 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 client/src/components/Planner/transportDetailModel.test.ts create mode 100644 client/src/components/Planner/transportDetailModel.ts diff --git a/client/src/components/Planner/DayPlanSidebarTransportDetailModal.tsx b/client/src/components/Planner/DayPlanSidebarTransportDetailModal.tsx index ddd614c2b..6c580622b 100644 --- a/client/src/components/Planner/DayPlanSidebarTransportDetailModal.tsx +++ b/client/src/components/Planner/DayPlanSidebarTransportDetailModal.tsx @@ -12,6 +12,7 @@ import { parseReservationMetadata } from '../../utils/flightLegs' import { BookingDialogHeader } from './bookings/BookingDialogShell' import { BOX, Eyebrow, Field, fs, toneColor, toneOf } from './bookings/bookingParts' import { filesFor } from '../../utils/reservationFiles' +import { bookingDayLabel, segmentCodeLabel } from './transportDetailModel' import { TransitLegs, transitSummary } from './bookings/transitParts' import type { Reservation } from '../../types' @@ -185,8 +186,7 @@ function detailCells(res: Reservation, meta: Record, t: Transla if (legs.length > 1) { for (const leg of legs) { if (!leg?.confirmation_number) continue - const segment = [leg.from, leg.to].filter(Boolean).join(' → ') - add(segment || t('reservations.confirmationCode'), leg.confirmation_number, true) + add(segmentCodeLabel(leg, t), leg.confirmation_number, true) } } return cells @@ -196,9 +196,7 @@ function detailCells(res: Reservation, meta: Record, t: Transla function whenOf(res: Reservation, locale: string, timeFormat: string): { day: string; time: string } { const { date, time } = splitReservationDateTime(res.reservation_time) const { time: endTime } = splitReservationDateTime(res.reservation_end_time) - const day = date - ? new Date(date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) - : '' + const day = date ? bookingDayLabel(date, locale) : '' const start = time ? formatTime(time, locale, timeFormat) : '' const end = endTime ? formatTime(endTime, locale, timeFormat) : '' return { day, time: start && end ? `${start} → ${end}` : start } diff --git a/client/src/components/Planner/transportDetailModel.test.ts b/client/src/components/Planner/transportDetailModel.test.ts new file mode 100644 index 000000000..94728a6de --- /dev/null +++ b/client/src/components/Planner/transportDetailModel.test.ts @@ -0,0 +1,21 @@ +// FE-PLANNER-TRANSPORTDETAIL-001 to -002: the labels the desktop transport detail +// and the phone transport sheet both read off a booking. +import { describe, expect, it } from 'vitest'; + +import { bookingDayLabel, segmentCodeLabel } from './transportDetailModel'; + +const t = (key: string) => `t:${key}`; + +describe('transportDetailModel', () => { + it('FE-PLANNER-TRANSPORTDETAIL-001: the day reads as weekday, day and month, on the calendar date itself', () => { + expect(bookingDayLabel('2026-07-01', 'en-US')).toBe('Wed, Jul 1'); + expect(bookingDayLabel('2026-12-31', 'en-US')).toBe('Thu, Dec 31'); + }); + + it('FE-PLANNER-TRANSPORTDETAIL-002: a segment code is listed under its route, or the plain code label', () => { + expect(segmentCodeLabel({ from: 'MUC', to: 'FRA' }, t)).toBe('MUC → FRA'); + expect(segmentCodeLabel({ from: 'MUC', to: null }, t)).toBe('MUC'); + expect(segmentCodeLabel({ to: 'FRA' }, t)).toBe('FRA'); + expect(segmentCodeLabel({ from: null, to: '' }, t)).toBe('t:reservations.confirmationCode'); + }); +}); diff --git a/client/src/components/Planner/transportDetailModel.ts b/client/src/components/Planner/transportDetailModel.ts new file mode 100644 index 000000000..431962a0a --- /dev/null +++ b/client/src/components/Planner/transportDetailModel.ts @@ -0,0 +1,25 @@ +type Translate = (key: string, params?: Record) => string; + +/** + * What the desktop transport detail dialog and the phone transport sheet both read + * off a booking. The booking's files come from utils/reservationFiles. + */ + +/** The day a booking runs on, as the head of either detail names it, in the locale's order ("Wed, Jul 1" in en-US). */ +export function bookingDayLabel(date: string, locale: string): string { + return new Date(`${date}T00:00:00Z`).toLocaleDateString(locale, { + weekday: 'short', + day: 'numeric', + month: 'short', + timeZone: 'UTC', + }); +} + +/** + * The name a segment's own booking code is listed under (#1943): where it leaves + * from and where it goes, or the plain booking code label when the segment names + * neither. + */ +export function segmentCodeLabel(leg: { from?: string | null; to?: string | null }, t: Translate): string { + return [leg.from, leg.to].filter(Boolean).join(' → ') || t('reservations.confirmationCode'); +} diff --git a/client/src/mobile/screens/trip/sheets/MTransportSheet.tsx b/client/src/mobile/screens/trip/sheets/MTransportSheet.tsx index 22c31294e..e7a40ecac 100644 --- a/client/src/mobile/screens/trip/sheets/MTransportSheet.tsx +++ b/client/src/mobile/screens/trip/sheets/MTransportSheet.tsx @@ -10,6 +10,7 @@ import { getFlightLegs, getTrainLegs, usesStationRoute } from '../../../../utils import { openFile } from '../../../../utils/fileDownload' import { runsOnDay } from '../../../../utils/reservationRoutes' import { filesFor } from '../../../../utils/reservationFiles' +import { bookingDayLabel, segmentCodeLabel } from '../../../../components/Planner/transportDetailModel' import type { Reservation } from '../../../../types' import { Eyebrow, INNER_CLS, StatBox, TileHeader, displayTime } from './MTripSheetUi' @@ -88,11 +89,7 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) { if (meta.flight_number) subParts.push(meta.flight_number) if (meta.train_number) subParts.push(meta.train_number) if (from?.name && to?.name) subParts.push(`${from.name} → ${to.name}`) - else if (date) { - subParts.push(new Date(`${date}T00:00:00Z`).toLocaleDateString(locale, { - weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC', - })) - } + else if (date) subParts.push(bookingDayLabel(date, locale)) const seat = meta.seat const platform = meta.platform @@ -247,7 +244,7 @@ export default function MTransportSheet({ planner, shell }: MTripSheetsProps) { {legCodes.map((leg, i) => (
- {[leg.from, leg.to].filter(Boolean).join(' → ') || t('reservations.confirmationCode')} + {segmentCodeLabel(leg, t)}