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
This commit is contained in:
Maurice
2026-10-12 10:16:01 +02:00
parent 7cb96c3d74
commit 4ee5e3fa96
10 changed files with 151 additions and 17 deletions
@@ -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 });
@@ -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)
@@ -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(<WhenFields facts={facts({ label: 'Germany Departure Flight with a much longer custom title → Korea Arrival', date: 'Oct 21 → Oct 22', range: true })} />);
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(<WhenFields facts={facts({ label: 'Day 3', date: null, range: false }, null)} />);
expect(screen.getByText('Day 3')).toBeInTheDocument();
});
});
@@ -65,8 +65,13 @@ export function WhenFields({ facts }: { facts: BookingFacts }) {
<div className={`flex gap-2 ${facts.day?.range ? 'flex-col' : ''}`}>
{facts.day && (
<Field label={t('reservations.date')} className="flex-[1.4]">
{facts.day.label}
{facts.day.date && <span className="ms-1.5 font-medium text-content-faint">{facts.day.date}</span>}
{facts.day.date ? (
// The date never shrinks; a long day title is cut instead.
<span className="flex min-w-0 items-center justify-center gap-1.5">
<span className="min-w-0 truncate">{facts.day.label}</span>
<span className="flex-none font-medium text-content-faint">{facts.day.date}</span>
</span>
) : facts.day.label}
</Field>
)}
{facts.time && <Field label={t('reservations.time')} className="flex-1" tabular>{facts.time}</Field>}