From a2cb6a12d1d24df0b38d5783116f790f88979341 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 3 Sep 2026 09:15:27 +0200 Subject: [PATCH] fix(mobile): a stay chip in the timeline header opens the stay, not the day (#2210) The hotel chips next to the day pill shared its target, so tapping a stay opened the day sheet. A chip now carries its stay and opens it the way the stay card in the day sheet does: the accommodation editor for members who may edit days, otherwise the hotel's place, and the day sheet only when neither applies. Closing the editor returns to the timeline it was opened from instead of surfacing the day sheet, and the chip's accessible name says whether it is a check-in, a check-out or an ongoing stay. --- client/src/mobile/screens/trip/MTripShell.tsx | 3 +- .../screens/trip/plan/MPlanTimeline.tsx | 32 ++++++++++++++++--- .../screens/trip/plan/planTimelineModel.ts | 10 ++++-- .../trip/sheets/MAccommodationSheet.tsx | 14 +++++--- .../mobile/trip/MAccommodationSheet.test.tsx | 18 +++++++++++ .../unit/mobile/trip/MPlanTimeline.test.tsx | 30 +++++++++++++++-- .../mobile/trip/planTimelineModel.test.ts | 12 +++---- .../unit/mobile/trip/useMPlanTimeline.test.ts | 2 +- 8 files changed, 98 insertions(+), 23 deletions(-) diff --git a/client/src/mobile/screens/trip/MTripShell.tsx b/client/src/mobile/screens/trip/MTripShell.tsx index 2cdda4137..d940921ce 100644 --- a/client/src/mobile/screens/trip/MTripShell.tsx +++ b/client/src/mobile/screens/trip/MTripShell.tsx @@ -40,7 +40,8 @@ export type MTripCollabTab = 'chat' | 'notes' | 'polls' /** * Currently open bottom/floating sheet. Well-known ids (owned by the sheets * screen): 'day' (payload { dayId }), 'days', 'mehr', 'note' (payload - * { dayId, note? }), 'transport' (payload { reservationId }), 'bract' + * { dayId, note? }), 'accommodation' (payload { dayId?, accId?, from? }), + * 'transport' (payload { reservationId }), 'bract' * (payload { placeId, dayPicker? }), 'import', 'export', 'members', * 'tripedit', 'bags', 'task'. The place inspector keys off the planner's * place selection instead of a sheet id, and planner-backed editors (place diff --git a/client/src/mobile/screens/trip/plan/MPlanTimeline.tsx b/client/src/mobile/screens/trip/plan/MPlanTimeline.tsx index cb0c030e1..a3f1cb59e 100644 --- a/client/src/mobile/screens/trip/plan/MPlanTimeline.tsx +++ b/client/src/mobile/screens/trip/plan/MPlanTimeline.tsx @@ -10,7 +10,7 @@ import { fmtTransitDuration } from '../../../../components/Planner/transitDispla import { formatTime } from '../../../../utils/formatters' import { useMPlanTimeline, type MPlanTimelineController } from './useMPlanTimeline' import { cityPillsForDay, weatherIconFor } from './planTimelineModel' -import type { PlanRow } from './planTimelineModel' +import type { HotelChip, PlanRow } from './planTimelineModel' import { useMPlanDragReorder } from './useMPlanDragReorder' import { useTouchDragBridge } from '../../../../hooks/useTouchDragBridge' import { useIsTouch } from '../../../../hooks/useIsTouch' @@ -56,6 +56,21 @@ export default function MPlanTimeline({ planner, shell }: MPlanTimelineProps) { const daySchedule = usePluginDaySchedule(planner.tripId) const day = tl.day const dayId = day?.id + // A stay chip opens the stay, the same way the stay card in the day sheet + // does: the editor for members who may edit days, otherwise the hotel's + // place. A stay with neither still leads to the day sheet, so the chip + // never goes dead (#2210). + const openStay = (chip: HotelChip) => { + if (!day) return + if (canEdit) shell.openSheet('accommodation', { dayId: day.id, accId: chip.accId, from: 'timeline' }) + else if (chip.placeId != null) planner.handlePlaceClick(chip.placeId) + else shell.openSheet('day', { dayId: day.id }) + } + // The icon alone tells check-out from check-in; the accessible name says it. + const stayLabel = (chip: HotelChip) => { + const kind = chip.variant === 'checkin' ? t('day.checkIn') : chip.variant === 'checkout' ? t('day.checkOut') : t('mobileTrip.stay') + return `${kind} · ${chip.name}${chip.time ? ` · ${chip.time.slice(0, 5)}` : ''}` + } // Mirrors MDaySheet's own label so the pill and the sheet it opens agree. const dayLabel = day ? day.title || t('planner.dayN', { n: day.day_number || planner.days.indexOf(day) + 1 }) @@ -159,6 +174,8 @@ export default function MPlanTimeline({ planner, shell }: MPlanTimelineProps) { ? `${t('day.overview')} · ${t('day.weatherFor', { name: tl.weatherPlaceName })}` : undefined} onOpenDay={() => shell.openSheet('day', { dayId: day.id })} + onOpenStay={openStay} + stayLabel={stayLabel} /> )} @@ -396,16 +413,20 @@ function EditHeader({ tl, planner, shell }: { * (check-out / check-in / stay) and the weather chip. * * The day pill leads unconditionally. It used to be the accommodation chip that - * carried this, which left a day without a stay — and, with no weather either, - * the whole header — with no way into the day sheet at all (#2004). + * carried this, which left a day without a stay (and, with no weather either, + * the whole header) with no way into the day sheet at all (#2004). Since then + * the stay chips have a target of their own: tapping a hotel opens that stay, + * not the day (#2210). Only the weather chip still shares the day pill's target. */ -function TimelineHeader({ tl, dayLabel, openLabel, weatherLabel, onOpenDay }: { +function TimelineHeader({ tl, dayLabel, openLabel, weatherLabel, onOpenDay, onOpenStay, stayLabel }: { tl: MPlanTimelineController dayLabel: string openLabel: string /** Weather-chip label naming the forecast's anchor place (#2167); falls back to openLabel. */ weatherLabel?: string onOpenDay: () => void + onOpenStay: (chip: HotelChip) => void + stayLabel: (chip: HotelChip) => string }) { const WeatherIcon = weatherIconFor(tl.weather?.main) return ( @@ -428,7 +449,8 @@ function TimelineHeader({ tl, dayLabel, openLabel, weatherLabel, onOpenDay }: {