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 ( <>