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
@@ -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 (
<div className="mt-2 overflow-hidden rounded-2xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)]">
@@ -146,6 +146,8 @@ function BookingCard({ res, planner, canEdit, compact }: {
<div className="px-3 pb-3 pt-[9px]">
<CardWhenAndCode
dayValue={dayValue}
dayDate={dayDate}
dayRange={dayRange}
timeValue={timeValue}
code={res.confirmation_number}
card={card}
@@ -1,4 +1,4 @@
// FE-COMP-MRESCARD-001 to FE-COMP-MRESCARD-007
// FE-COMP-MRESCARD-001 to FE-COMP-MRESCARD-008
import { fireEvent, render, screen } from '@testing-library/react';
import { Hotel, Plane } from 'lucide-react';
import { describe, expect, it, vi } from 'vitest';
@@ -119,4 +119,26 @@ describe('MReservationCardParts', () => {
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(
<CardWhenAndCode
dayValue="Germany Departure Flight – Korea Arrival"
dayDate="Oct 21 – Oct 22"
dayRange
timeValue="15:35 – 09:55"
code={null}
card={cardState()}
onOpen={vi.fn()}
t={t}
/>
);
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');
});
});
@@ -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 (
<>
<button type="button" onClick={onOpen} className="block w-full text-start">
<div className="flex gap-2">
<div className={`flex gap-2 ${dayRange ? 'flex-col' : ''}`}>
<Field label={t('reservations.date')} className="flex-[1.4]">
{dayValue}
{dayDate ? (
<span className="flex min-w-0 items-center justify-center gap-1.5">
<span className="min-w-0 truncate">{dayValue}</span>
<span className="flex-none font-medium text-m-faint">{dayDate}</span>
</span>
) : dayValue}
</Field>
<Field label={t('reservations.time')} className="flex-1" tabular>
{timeValue}
@@ -130,7 +130,7 @@ function TransportCard({ res, planner, shell, canEdit, compact }: {
planner.setShowTransportModal(true)
}
const { dayValue, timeValue } = cardWhen(res, startDay, endDay, t, locale, timeFormat)
const { dayValue, dayDate, dayRange, timeValue } = cardWhen(res, startDay, endDay, t, locale, timeFormat)
return (
<div className="mt-2 overflow-hidden rounded-2xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)]">
@@ -158,6 +158,8 @@ function TransportCard({ res, planner, shell, canEdit, compact }: {
<div className="px-3 pb-3 pt-[9px]">
<CardWhenAndCode
dayValue={dayValue}
dayDate={dayDate}
dayRange={dayRange}
timeValue={timeValue}
code={res.confirmation_number}
card={card}
@@ -1,5 +1,6 @@
import { formatTime, splitReservationDateTime } from '../../../../utils/formatters'
import type { Day, Reservation, TranslationFn } from '../../../../types'
import { linkedDayDate } from '../../../../components/Planner/bookings/bookingFacts'
/**
* Transport view-model — the real-data counterpart to the demo's `trsSecs`
@@ -45,8 +46,8 @@ export function parseTransportMeta(res: Reservation): TransportMeta {
/**
* The date and time cells of a phone booking or transport card: the day (or day
* range) the booking sits on, else its own date, and its time or time range. A
* dash stands in for whichever is missing.
* range) the booking sits on with its calendar date beside it, else its own
* date, and its time or time range. A dash stands in for whichever is missing.
*/
export function cardWhen(
res: Reservation,
@@ -55,7 +56,7 @@ export function cardWhen(
t: TranslationFn,
locale: string,
timeFormat: string,
): { dayValue: string; timeValue: string } {
): { dayValue: string; dayDate: string | null; dayRange: boolean; timeValue: string } {
const startDt = splitReservationDateTime(res.reservation_time)
const endDt = splitReservationDateTime(res.reservation_end_time)
const fmtDate = (date: string) =>
@@ -64,10 +65,14 @@ export function cardWhen(
const timeValue = startDt.time
? `${formatTime(startDt.time, locale, timeFormat)}${endDt.time ? ` – ${formatTime(endDt.time, locale, timeFormat)}` : ''}`
: '—'
const rangeEnd = startDay && endDay && endDay.id !== startDay.id ? endDay : undefined
const dayValue = startDay
? `${dayLabel(startDay)}${endDay && endDay.id !== startDay.id ? ` – ${dayLabel(endDay)}` : ''}`
? `${dayLabel(startDay)}${rangeEnd ? ` – ${dayLabel(rangeEnd)}` : ''}`
: startDt.date
? fmtDate(startDt.date)
: '—'
return { dayValue, timeValue }
// The same date the desktop card prints beside the day, so a long day title
// can never hide when the booking actually is.
const dayDate = startDay ? linkedDayDate(startDay, endDay, { start: startDt.date, end: endDt.date }, locale, ' – ') : null
return { dayValue, dayDate, dayRange: !!rangeEnd, timeValue }
}