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 (
<>
-
+
- {dayValue}
+ {dayDate ? (
+
+ {dayValue}
+ {dayDate}
+
+ ) : dayValue}
{timeValue}
diff --git a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx
index ad93bde64..55be19857 100644
--- a/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx
+++ b/client/src/mobile/screens/trip/tabs/MTransportsTab.tsx
@@ -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 (
@@ -158,6 +158,8 @@ function TransportCard({ res, planner, shell, canEdit, compact }: {
@@ -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 }
}
diff --git a/client/tests/unit/mobile/trip/transportsModel.test.ts b/client/tests/unit/mobile/trip/transportsModel.test.ts
index 5af33495b..5b49051d0 100644
--- a/client/tests/unit/mobile/trip/transportsModel.test.ts
+++ b/client/tests/unit/mobile/trip/transportsModel.test.ts
@@ -162,18 +162,24 @@ describe('transportsModel — cardWhen', () => {
const r = res({ reservation_time: '2026-07-01T09:05', reservation_end_time: '2026-07-03T18:30' });
expect(cardWhen(r, day1, day3, t, 'en-US', '24h')).toEqual({
dayValue: 'dayplan.dayN:1 – Coast',
+ dayDate: 'Jul 1 – Jul 3',
+ dayRange: true,
timeValue: '09:05 – 18:30',
});
- expect(cardWhen(r, day1, day1, t, 'en-US', '24h').dayValue).toBe('dayplan.dayN:1');
+ expect(cardWhen(r, day1, day1, t, 'en-US', '24h')).toMatchObject({ dayValue: 'dayplan.dayN:1', dayDate: 'Wed, Jul 1', dayRange: false });
});
it('FE-MOB-TRPM-016: without a day the booking date stands in, then a dash', () => {
expect(cardWhen(res({ reservation_time: '2026-07-04' }), undefined, undefined, t, 'en-US', '24h')).toEqual({
dayValue: 'Jul 4',
+ dayDate: null,
+ dayRange: false,
timeValue: '—',
});
expect(cardWhen(res({ reservation_time: null }), undefined, undefined, t, 'en-US', '24h')).toEqual({
dayValue: '—',
+ dayDate: null,
+ dayRange: false,
timeValue: '—',
});
});
@@ -182,4 +188,17 @@ describe('transportsModel — cardWhen', () => {
const r = res({ reservation_time: '2026-07-01T14:00', reservation_end_time: null });
expect(cardWhen(r, day1, undefined, t, 'en-US', '12h').timeValue).toBe('2:00 PM');
});
+
+ it('FE-MOB-TRPM-018: a day without a date shows the booking date beside its title (#2590)', () => {
+ const departure = buildDay({ id: 200, day_number: 1, title: 'Germany Departure Flight', date: null });
+ const arrival = buildDay({ id: 201, day_number: 2, title: 'Korea Arrival', date: null });
+ const r = res({ reservation_time: '2026-10-21T15:35', reservation_end_time: '2026-10-22T09:55' });
+ expect(cardWhen(r, departure, arrival, t, 'en-US', '24h')).toEqual({
+ dayValue: 'Germany Departure Flight – Korea Arrival',
+ dayDate: 'Oct 21 – Oct 22',
+ dayRange: true,
+ timeValue: '15:35 – 09:55',
+ });
+ expect(cardWhen(res({ reservation_time: null }), departure, undefined, t, 'en-US', '24h').dayDate).toBeNull();
+ });
});