From 4ee5e3fa96d8fd03ea284652cd1f629e5310fe8a Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 12 Oct 2026 10:16:01 +0200 Subject: [PATCH] fix(bookings): booking cards always show the calendar date of their linked day Phone booking and transport cards printed only the title of the linked day, so a long title cut off in the field left no date at all. They now print the date beside the title like the desktop card, as a span that never shrinks while the title is truncated, and a day range takes the full width with the time under it. The desktop card gets the same non-shrinking date. A day without a date of its own (a trip planned without dates) now borrows the booking's start or end date, on the desktop cards, the list, the detail dialog, the shared page and the phone alike. Refs #2590 --- .../Planner/bookings/bookingFacts.test.ts | 23 ++++++++++++++- .../Planner/bookings/bookingFacts.ts | 22 ++++++++++++-- .../Planner/bookings/bookingParts.test.tsx | 29 +++++++++++++++++++ .../Planner/bookings/bookingParts.tsx | 9 ++++-- .../mobile/screens/trip/tabs/MBookingsTab.tsx | 4 ++- .../trip/tabs/MReservationCardParts.test.tsx | 24 ++++++++++++++- .../trip/tabs/MReservationCardParts.tsx | 17 +++++++++-- .../screens/trip/tabs/MTransportsTab.tsx | 4 ++- .../screens/trip/tabs/transportsModel.ts | 15 ++++++---- .../unit/mobile/trip/transportsModel.test.ts | 21 +++++++++++++- 10 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 client/src/components/Planner/bookings/bookingParts.test.tsx diff --git a/client/src/components/Planner/bookings/bookingFacts.test.ts b/client/src/components/Planner/bookings/bookingFacts.test.ts index 1eef945f6..7593e82a4 100644 --- a/client/src/components/Planner/bookings/bookingFacts.test.ts +++ b/client/src/components/Planner/bookings/bookingFacts.test.ts @@ -1,4 +1,4 @@ -// FE-PLANNER-BKFACTS-001 to FE-PLANNER-BKFACTS-020 +// FE-PLANNER-BKFACTS-001 to FE-PLANNER-BKFACTS-022 import { describe, it, expect } from 'vitest'; import { buildDay, buildReservation } from '../../../../tests/helpers/factories'; import type { ReservationEndpoint } from '../../../types'; @@ -43,6 +43,27 @@ describe('bookingFacts: day', () => { expect(f.day).toEqual({ label: 'dayplan.dayN#3', date: null, range: false }); }); + it('FE-PLANNER-BKFACTS-021: a day without a date borrows the booking date (#2590)', () => { + const d4 = buildDay({ id: 704, day_number: 4, date: null, title: 'Korea Arrival' }); + const one = bookingFacts(buildReservation({ day_id: 703, reservation_time: '2025-06-02T15:35' }), ctx({ days: [d1, d2, d3, d4] })); + expect(one.day).toEqual({ label: 'dayplan.dayN#3', date: 'Mon, Jun 2', range: false }); + const range = bookingFacts( + buildReservation({ day_id: 703, end_day_id: 704, reservation_time: '2025-06-02T15:35', reservation_end_time: '2025-06-03T09:55' }), + ctx({ days: [d1, d2, d3, d4] }), + ); + expect(range.day).toEqual({ label: 'dayplan.dayN#3 → Korea Arrival', date: 'Jun 2 → Jun 3', range: true }); + }); + + it('FE-PLANNER-BKFACTS-022: a dated day keeps its own date over the booking date, and one date is not doubled', () => { + expect(bookingFacts(buildReservation({ day_id: 701, reservation_time: '2025-06-05T10:00' }), ctx()).day?.date).toBe('Sun, Jun 1'); + const d4 = buildDay({ id: 704, day_number: 4, date: null, title: null }); + const f = bookingFacts( + buildReservation({ day_id: 703, end_day_id: 704, reservation_time: '2025-06-02T08:00', reservation_end_time: '2025-06-02T22:00' }), + ctx({ days: [d1, d2, d3, d4] }), + ); + expect(f.day?.date).toBe('Mon, Jun 2'); + }); + it('FE-PLANNER-BKFACTS-005: without a day the booking dates stand in, a range when they differ', () => { expect(bookingFacts(buildReservation({ reservation_time: '2025-06-02T10:00' }), ctx()).day) .toEqual({ label: 'Mon, Jun 2', date: null, range: false }); diff --git a/client/src/components/Planner/bookings/bookingFacts.ts b/client/src/components/Planner/bookings/bookingFacts.ts index 6a414ac31..c58828e14 100644 --- a/client/src/components/Planner/bookings/bookingFacts.ts +++ b/client/src/components/Planner/bookings/bookingFacts.ts @@ -41,6 +41,25 @@ export function formatDay(date: string, locale: string, weekday = true): string return fmtDate(date, locale, weekday) } +/** + * The calendar date beside a linked day or day range, with a weekday when it is + * a single date. A day without a date of its own (a trip planned without dates) + * borrows the booking's own start or end date, so a date is never missing when + * the booking knows one. + */ +export function linkedDayDate( + start: Day, + end: Day | undefined, + booking: { start: string | null; end: string | null }, + locale: string, + separator = ' → ', +): string | null { + const first = start.date || booking.start + const last = end && end.id !== start.id ? end.date || booking.end : null + const dates = [first, last !== first ? last : null].filter(Boolean) as string[] + return dates.length ? dates.map(d => fmtDate(d, locale, dates.length === 1)).join(separator) : null +} + /** Everything a card, a row and the detail pane print about one booking, formatted once. */ export function bookingFacts(r: Reservation, c: FactsContext): BookingFacts { const { t, locale, timeFormat } = c @@ -55,8 +74,7 @@ export function bookingFacts(r: Reservation, c: FactsContext): BookingFacts { if (span.start) { const range = !!span.end && span.end.id !== span.start.id const label = range ? `${dayName(span.start)} → ${dayName(span.end!)}` : dayName(span.start) - const dates = [span.start.date, range ? span.end!.date : null].filter(Boolean) as string[] - day = { label, date: dates.length ? dates.map(d => fmtDate(d, locale, dates.length === 1)).join(' → ') : null, range } + day = { label, date: linkedDayDate(span.start, span.end, { start: startDt.date, end: endDt.date }, locale), range } } else if (startDt.date) { const range = !!endDt.date && endDt.date !== startDt.date const label = range ? `${fmtDate(startDt.date, locale)} → ${fmtDate(endDt.date!, locale)}` : fmtDate(startDt.date, locale) diff --git a/client/src/components/Planner/bookings/bookingParts.test.tsx b/client/src/components/Planner/bookings/bookingParts.test.tsx new file mode 100644 index 000000000..97fec48d3 --- /dev/null +++ b/client/src/components/Planner/bookings/bookingParts.test.tsx @@ -0,0 +1,29 @@ +// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-002 +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { WhenFields } from './bookingParts'; +import type { BookingFacts } from './bookingFacts'; + +function facts(day: BookingFacts['day'], time: string | null = '15:35 → 09:55'): BookingFacts { + return { + day, time, startTime: null, endTime: null, startDate: null, endDate: null, + cells: [], endpoints: [], legCodes: [], place: null, accommodation: null, linked: null, url: null, isHotel: false, + }; +} + +describe('WhenFields', () => { + it('FE-PLANNER-BKPARTS-001: a long day title is cut, the date beside it never shrinks (#2590)', () => { + render(); + + const title = screen.getByText('Germany Departure Flight with a much longer custom title → Korea Arrival'); + const date = screen.getByText('Oct 21 → Oct 22'); + expect(title).toHaveClass('truncate'); + expect(date).toHaveClass('flex-none'); + expect(title.contains(date)).toBe(false); + }); + + it('FE-PLANNER-BKPARTS-002: a day without a date prints its label alone', () => { + render(); + expect(screen.getByText('Day 3')).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Planner/bookings/bookingParts.tsx b/client/src/components/Planner/bookings/bookingParts.tsx index c37a386ea..6cf7776c6 100644 --- a/client/src/components/Planner/bookings/bookingParts.tsx +++ b/client/src/components/Planner/bookings/bookingParts.tsx @@ -65,8 +65,13 @@ export function WhenFields({ facts }: { facts: BookingFacts }) {
{facts.day && ( - {facts.day.label} - {facts.day.date && {facts.day.date}} + {facts.day.date ? ( + // The date never shrinks; a long day title is cut instead. + + {facts.day.label} + {facts.day.date} + + ) : facts.day.label} )} {facts.time && {facts.time}} diff --git a/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx b/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx index 26c5728d7..700a70c00 100644 --- a/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MBookingsTab.tsx @@ -129,7 +129,7 @@ function BookingCard({ res, planner, canEdit, compact }: { planner.setShowReservationModal(true) } - const { dayValue, timeValue } = cardWhen(res, startDay, endDay, t, locale, timeFormat) + const { dayValue, dayDate, dayRange, timeValue } = cardWhen(res, startDay, endDay, t, locale, timeFormat) return (
@@ -146,6 +146,8 @@ function BookingCard({ res, planner, canEdit, compact }: {
{ fireEvent.click(screen.getByRole('button', { name: 'common.delete' })); expect(card.confirmDelete).toHaveBeenCalledOnce(); }); + + it('FE-COMP-MRESCARD-008: the date sits beside the day title and never shrinks, a range stacks the time under it (#2590)', () => { + const { container } = render( + + ); + + const title = screen.getByText('Germany Departure Flight – Korea Arrival'); + const date = screen.getByText('Oct 21 – Oct 22'); + expect(title).toHaveClass('truncate'); + expect(date).toHaveClass('flex-none'); + expect(title.contains(date)).toBe(false); + expect(container.querySelector('button > div')).toHaveClass('flex-col'); + }); }); diff --git a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx index 75b033c71..73c0068ca 100644 --- a/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx +++ b/client/src/mobile/screens/trip/tabs/MReservationCardParts.tsx @@ -73,10 +73,14 @@ export function CardActions({ onEdit, onDelete, t }: { onEdit: () => void; onDel /** * Date and time row, followed by the booking code. The code sits outside the - * row button so its reveal can be its own control. + * row button so its reveal can be its own control. The calendar date never + * shrinks; a long day title is cut instead, and a day range takes the full + * width with the time under it. */ export function CardWhenAndCode({ dayValue, + dayDate = null, + dayRange = false, timeValue, code, card, @@ -84,6 +88,8 @@ export function CardWhenAndCode({ t, }: { dayValue: string; + dayDate?: string | null; + dayRange?: boolean; timeValue: string; code: string | null | undefined; card: CardState; @@ -93,9 +99,14 @@ export function CardWhenAndCode({ return ( <>