- {weather.type === 'climate' ? 'Ø ' : ''}{cTemp(weather.temp, isFahrenheit)}{unit}
+ {weather.type === 'climate' ? 'Ø ' : ''}{toDisplayTemp(weather.temp, isFahrenheit)}{unit}
{weather.temp_max != null && (
- {cTemp(weather.temp_min, isFahrenheit)}° / {cTemp(weather.temp_max, isFahrenheit)}°
+ {toDisplayTemp(weather.temp_min, isFahrenheit)}° / {toDisplayTemp(weather.temp_max, isFahrenheit)}°
)}
{weather.description &&
{weather.description}}
@@ -360,7 +353,7 @@ function DayWeather({ weather, loading, isFahrenheit, placeName }: { weather: We
50 ? 'bg-info-soft' : ''}`}>
{String(h.hour).padStart(2, '0')}
-
{cTemp(h.temp, isFahrenheit)}°
+
{toDisplayTemp(h.temp, isFahrenheit)}°
{h.precipitation_probability > 0 && (
{h.precipitation_probability}%
)}
@@ -383,11 +376,7 @@ const OPENS_BOOKING = 'w-full cursor-pointer text-start transition-shadow hover:
function DayReservations({ day, assignments, reservations, is12h, onOpen }: { day: Day; assignments: AssignmentsMap; reservations: Reservation[]; is12h: boolean; onOpen?: (r: Reservation) => void }) {
const { t } = useTranslation()
const dayAssignments = assignments[String(day.id)] || []
- const dayReservations = reservations.filter(r => {
- if (r.type === 'hotel') return false
- if (r.assignment_id && dayAssignments.some(a => a.id === r.assignment_id)) return true
- return r.day_id === day.id
- })
+ const dayReservations = dayBookings(day.id, dayAssignments, reservations)
if (dayReservations.length === 0) return null
return (
@@ -449,10 +438,7 @@ function AccommodationCard({ acc, day, linked, canEdit, onEdit, onRemove, onOpen
}
const isCheckInDay = acc.start_day_id === day.id
const isCheckOutDay = acc.end_day_id === day.id
- const dayLabel = isCheckInDay && isCheckOutDay ? t('day.checkIn') + ' & ' + t('day.checkOut')
- : isCheckInDay ? t('day.checkIn')
- : isCheckOutDay ? t('day.checkOut')
- : null
+ const dayLabel = stayDayLabel(acc, day.id, t)
const leaving = isCheckOutDay && !isCheckInDay
const tint = isCheckInDay ? tintOf('var(--success)') : isCheckOutDay ? tintOf('var(--danger)') : NEUTRAL_TINT
const cells: { label: string; value: ReactNode }[] = []
diff --git a/client/src/components/Planner/dayDetailModel.test.ts b/client/src/components/Planner/dayDetailModel.test.ts
new file mode 100644
index 000000000..acd07f3d0
--- /dev/null
+++ b/client/src/components/Planner/dayDetailModel.test.ts
@@ -0,0 +1,50 @@
+// FE-PLANNER-DAYDETAILMODEL-001 to -005: the day detail derivations the desktop day
+// panel and the phone day sheet share.
+import { describe, expect, it } from 'vitest';
+
+import { buildReservation } from '../../../tests/helpers/factories';
+import { dayBookings, stayDayLabel, toDisplayTemp } from './dayDetailModel';
+
+const t = (key: string) => `t:${key}`;
+
+describe('dayBookings', () => {
+ it('FE-PLANNER-DAYDETAILMODEL-001: the day keeps what hangs on its stops and what is booked on it, hotels aside', () => {
+ const onStop = buildReservation({ id: 1, type: 'restaurant', assignment_id: 11, day_id: 9 });
+ const onDay = buildReservation({ id: 2, type: 'event', assignment_id: null, day_id: 4 });
+ const hotel = buildReservation({ id: 3, type: 'hotel', assignment_id: 11, day_id: 4 });
+ const otherDay = buildReservation({ id: 4, type: 'tour', assignment_id: 99, day_id: 5 });
+ const result = dayBookings(4, [{ id: 11 }, { id: 12 }], [onStop, onDay, hotel, otherDay]);
+ expect(result.map((r) => r.id)).toEqual([1, 2]);
+ });
+
+ it('FE-PLANNER-DAYDETAILMODEL-002: a booking on a stop of another day falls back to its own day', () => {
+ const moved = buildReservation({ id: 5, type: 'event', assignment_id: 77, day_id: 4 });
+ expect(dayBookings(4, [{ id: 11 }], [moved]).map((r) => r.id)).toEqual([5]);
+ expect(dayBookings(6, [{ id: 11 }], [moved])).toEqual([]);
+ });
+});
+
+describe('toDisplayTemp', () => {
+ it('FE-PLANNER-DAYDETAILMODEL-003: rounds in the user unit, and a missing value stays unreadable', () => {
+ expect(toDisplayTemp(21.4, false)).toBe(21);
+ expect(toDisplayTemp(21.4, true)).toBe(71);
+ expect(toDisplayTemp(-0.6, false)).toBe(-1);
+ expect(toDisplayTemp(undefined, false)).toBeNaN();
+ expect(toDisplayTemp(undefined, true)).toBeNaN();
+ });
+});
+
+describe('stayDayLabel', () => {
+ const stay = { start_day_id: 2, end_day_id: 4 };
+
+ it('FE-PLANNER-DAYDETAILMODEL-004: names the check-in and the check-out day', () => {
+ expect(stayDayLabel(stay, 2, t)).toBe('t:day.checkIn');
+ expect(stayDayLabel(stay, 4, t)).toBe('t:day.checkOut');
+ expect(stayDayLabel({ start_day_id: 3, end_day_id: 3 }, 3, t)).toBe('t:day.checkIn & t:day.checkOut');
+ });
+
+ it('FE-PLANNER-DAYDETAILMODEL-005: a night in between, or no day at all, has no label', () => {
+ expect(stayDayLabel(stay, 3, t)).toBeNull();
+ expect(stayDayLabel(stay, undefined, t)).toBeNull();
+ });
+});
diff --git a/client/src/components/Planner/dayDetailModel.ts b/client/src/components/Planner/dayDetailModel.ts
new file mode 100644
index 000000000..f8536b3a3
--- /dev/null
+++ b/client/src/components/Planner/dayDetailModel.ts
@@ -0,0 +1,38 @@
+import type { Accommodation, Reservation } from '../../types';
+
+type Translate = (key: string, params?: Record) => string;
+
+/**
+ * The bookings of a day, hotels aside (they have their own block): the ones hanging
+ * on one of the day's stops and the ones booked on the day itself.
+ */
+export function dayBookings>(
+ dayId: number,
+ dayAssignments: { id: number }[],
+ reservations: R[]
+): R[] {
+ return reservations.filter((r) => {
+ if (r.type === 'hotel') return false;
+ if (r.assignment_id && dayAssignments.some((a) => a.id === r.assignment_id)) return true;
+ return r.day_id === dayId;
+ });
+}
+
+/** A forecast temperature (always in °C from the API) rounded in the user's unit. */
+export function toDisplayTemp(celsius: number | undefined, fahrenheit: boolean): number {
+ return Math.round(fahrenheit ? ((celsius ?? Number.NaN) * 9) / 5 + 32 : (celsius ?? Number.NaN));
+}
+
+/** What a stay means for the day: check-in, check-out, both, or nothing (a night in between). */
+export function stayDayLabel(
+ acc: Pick,
+ dayId: number | undefined,
+ t: Translate
+): string | null {
+ const checksIn = acc.start_day_id === dayId;
+ const checksOut = acc.end_day_id === dayId;
+ if (checksIn && checksOut) return `${t('day.checkIn')} & ${t('day.checkOut')}`;
+ if (checksIn) return t('day.checkIn');
+ if (checksOut) return t('day.checkOut');
+ return null;
+}
diff --git a/client/src/components/Planner/useDayDetail.test.ts b/client/src/components/Planner/useDayDetail.test.ts
new file mode 100644
index 000000000..90abecc9a
--- /dev/null
+++ b/client/src/components/Planner/useDayDetail.test.ts
@@ -0,0 +1,109 @@
+// FE-PLANNER-USEDAYDETAIL-001 to -007: the forecast and the in-place rename the
+// desktop day panel and the phone day sheet share.
+import { act, renderHook, waitFor } from '@testing-library/react';
+import type { WeatherResult } from '@trek/shared';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { weatherApi } from '../../api/client';
+import { useDayForecast, useDayRename } from './useDayDetail';
+
+const SUNNY = { temp: 21, main: 'Clear', description: 'clear sky' } as WeatherResult;
+
+/** A promise this test settles itself, to look at the hook while a request is out. */
+function deferred() {
+ let resolve: (value: T) => void = () => undefined;
+ const promise = new Promise((r) => (resolve = r));
+ return { promise, resolve };
+}
+
+afterEach(() => vi.restoreAllMocks());
+
+describe('useDayForecast', () => {
+ it('FE-PLANNER-USEDAYDETAIL-001: asks for nothing while not ready', () => {
+ const spy = vi.spyOn(weatherApi, 'getDetailed');
+ const { result } = renderHook(() => useDayForecast(false, '2026-05-01', 48.1, 11.5, 'en'));
+ expect(spy).not.toHaveBeenCalled();
+ expect(result.current).toEqual({ weather: null, loading: false });
+ });
+
+ it('FE-PLANNER-USEDAYDETAIL-002: loads the day at the place and stops loading with the answer', async () => {
+ const answer = deferred();
+ const spy = vi.spyOn(weatherApi, 'getDetailed').mockReturnValue(answer.promise);
+ const { result } = renderHook(() => useDayForecast(true, '2026-05-01', 48.1, 11.5, 'de'));
+ expect(spy).toHaveBeenCalledWith(48.1, 11.5, '2026-05-01', 'de');
+ expect(result.current.loading).toBe(true);
+
+ await act(async () => answer.resolve(SUNNY));
+ await waitFor(() => expect(result.current).toEqual({ weather: SUNNY, loading: false }));
+ });
+
+ it('FE-PLANNER-USEDAYDETAIL-003: an error answer or a failed request leaves no forecast', async () => {
+ vi.spyOn(weatherApi, 'getDetailed').mockResolvedValueOnce({ ...SUNNY, error: 'no data' } as WeatherResult);
+ const errorAnswer = renderHook(() => useDayForecast(true, '2026-05-01', 1, 2, 'en'));
+ await waitFor(() => expect(errorAnswer.result.current.loading).toBe(false));
+ expect(errorAnswer.result.current.weather).toBeNull();
+
+ vi.spyOn(weatherApi, 'getDetailed').mockRejectedValueOnce(new Error('offline'));
+ const failed = renderHook(() => useDayForecast(true, '2026-05-01', 1, 2, 'en'));
+ await waitFor(() => expect(failed.result.current.loading).toBe(false));
+ expect(failed.result.current.weather).toBeNull();
+ });
+
+ it('FE-PLANNER-USEDAYDETAIL-004: a late answer for the previous day is dropped', async () => {
+ const first = deferred();
+ const second = deferred();
+ vi.spyOn(weatherApi, 'getDetailed').mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
+ const { result, rerender } = renderHook(({ date }) => useDayForecast(true, date, 1, 2, 'en'), {
+ initialProps: { date: '2026-05-01' },
+ });
+ rerender({ date: '2026-05-02' });
+
+ await act(async () => second.resolve(SUNNY));
+ await waitFor(() => expect(result.current.weather).toEqual(SUNNY));
+ await act(async () => {
+ first.resolve({ ...SUNNY, temp: 5 });
+ await first.promise;
+ });
+ expect(result.current.weather).toEqual(SUNNY);
+ });
+
+ it('FE-PLANNER-USEDAYDETAIL-005: no longer ready clears the forecast', async () => {
+ vi.spyOn(weatherApi, 'getDetailed').mockResolvedValue(SUNNY);
+ const { result, rerender } = renderHook(({ ready }) => useDayForecast(ready, '2026-05-01', 1, 2, 'en'), {
+ initialProps: { ready: true },
+ });
+ await waitFor(() => expect(result.current.weather).toEqual(SUNNY));
+ rerender({ ready: false });
+ expect(result.current.weather).toBeNull();
+ });
+});
+
+describe('useDayRename', () => {
+ it('FE-PLANNER-USEDAYDETAIL-006: opens on the current title, takes the focus and commits it trimmed', () => {
+ const onCommit = vi.fn();
+ const { result } = renderHook(() => useDayRename(onCommit));
+ const input = document.createElement('input');
+ document.body.appendChild(input);
+ result.current.titleInputRef.current = input;
+
+ act(() => result.current.startRename('Arrival'));
+ expect(result.current.editingTitle).toBe(true);
+ expect(result.current.titleDraft).toBe('Arrival');
+ expect(document.activeElement).toBe(input);
+
+ act(() => result.current.setTitleDraft(' Old town '));
+ act(() => result.current.commitRename());
+ expect(result.current.editingTitle).toBe(false);
+ expect(onCommit).toHaveBeenCalledWith('Old town');
+ input.remove();
+ });
+
+ it('FE-PLANNER-USEDAYDETAIL-007: closing the field by hand commits nothing', () => {
+ const onCommit = vi.fn();
+ const { result } = renderHook(() => useDayRename(onCommit));
+ act(() => result.current.startRename(''));
+ act(() => result.current.setEditingTitle(false));
+ expect(result.current.editingTitle).toBe(false);
+ expect(onCommit).not.toHaveBeenCalled();
+ });
+});
diff --git a/client/src/components/Planner/useDayDetail.ts b/client/src/components/Planner/useDayDetail.ts
index 65f4a2606..31588d93d 100644
--- a/client/src/components/Planner/useDayDetail.ts
+++ b/client/src/components/Planner/useDayDetail.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
import type { WeatherResult } from '@trek/shared'
import { weatherApi, accommodationsApi } from '../../api/client'
import { isDayInAccommodationRange } from '../../utils/dayOrder'
@@ -23,14 +23,57 @@ export type HotelPickerMode = boolean | 'edit'
const EMPTY_HOTEL_FORM: HotelForm = { check_in: '', check_in_end: '', check_out: '', confirmation: '', place_id: null }
+/**
+ * A day's detailed forecast (Open-Meteo through the weather service, climate
+ * fallback), shared by the desktop day panel and the phone day sheet. `ready` says
+ * whether there is a day and a place to ask about; while it is false the forecast
+ * is cleared. An answer that arrives after the day or the place changed is dropped:
+ * the panel stays mounted across a day switch, and the previous day's late
+ * response would otherwise overwrite the new day's forecast.
+ */
+export function useDayForecast(ready: boolean, date: string | null | undefined, lat: number | null, lng: number | null, language: string) {
+ const [weather, setWeather] = useState(null)
+ const [loading, setLoading] = useState(false)
+ useEffect(() => {
+ if (!ready || !date || lat == null || lng == null) { setWeather(null); return }
+ let cancelled = false
+ setLoading(true)
+ weatherApi.getDetailed(lat, lng, date, language)
+ .then(data => { if (!cancelled) setWeather(data.error ? null : data) })
+ .catch(() => { if (!cancelled) setWeather(null) })
+ .finally(() => { if (!cancelled) setLoading(false) })
+ return () => { cancelled = true }
+ }, [ready, date, lat, lng, language])
+ return { weather, loading }
+}
+
+/**
+ * Renaming a day in place (#1065), shared by the desktop day panel and the phone
+ * day sheet: the field takes the focus when it opens, and committing closes it and
+ * hands the trimmed title on.
+ */
+export function useDayRename(onCommit: (title: string) => void) {
+ const [editingTitle, setEditingTitle] = useState(false)
+ const [titleDraft, setTitleDraft] = useState('')
+ const titleInputRef = useRef(null)
+ useEffect(() => { if (editingTitle) titleInputRef.current?.focus() }, [editingTitle])
+ const startRename = (title: string) => {
+ setTitleDraft(title)
+ setEditingTitle(true)
+ }
+ const commitRename = () => {
+ setEditingTitle(false)
+ onCommit(titleDraft.trim())
+ }
+ return { editingTitle, setEditingTitle, titleDraft, setTitleDraft, titleInputRef, startRename, commitRename }
+}
+
/** Day-detail data + accommodation logic: weather load, accommodations list,
* hotel picker form state and create/update/delete handlers.
*
* The write handlers let a failed request through to the caller, which is the
* one that can tell the user; the picker stays open with what was entered. */
export function useDayDetail(day: Day | null, days: Day[], tripId: number, lat: number | null, lng: number | null, language: string, onAccommodationChange?: () => void) {
- const [weather, setWeather] = useState(null)
- const [loading, setLoading] = useState(false)
const [accommodation, setAccommodation] = useState(null)
const [dayAccommodations, setDayAccommodations] = useState([])
const [accommodations, setAccommodations] = useState([])
@@ -50,16 +93,7 @@ export function useDayDetail(day: Day | null, days: Day[], tripId: number, lat:
// Both effects drop an answer that arrives after the day changed: the panel
// stays mounted across a day switch, and the previous day's late response
// would otherwise overwrite the new day's forecast and hotel.
- useEffect(() => {
- if (!day?.date || !lat || !lng) { setWeather(null); return }
- let cancelled = false
- setLoading(true)
- weatherApi.getDetailed(lat, lng, day.date, language)
- .then(data => { if (!cancelled) setWeather(data.error ? null : data) })
- .catch(() => { if (!cancelled) setWeather(null) })
- .finally(() => { if (!cancelled) setLoading(false) })
- return () => { cancelled = true }
- }, [day?.date, lat, lng, language])
+ const { weather, loading } = useDayForecast(!!(day?.date && lat && lng), day?.date, lat, lng, language)
useEffect(() => {
if (!tripId) return
diff --git a/client/src/mobile/screens/trip/plan/useMPlanTimeline.ts b/client/src/mobile/screens/trip/plan/useMPlanTimeline.ts
index 3c4164494..4530799b0 100644
--- a/client/src/mobile/screens/trip/plan/useMPlanTimeline.ts
+++ b/client/src/mobile/screens/trip/plan/useMPlanTimeline.ts
@@ -3,6 +3,7 @@ import { useTripStore } from '../../../../store/tripStore'
import { useRouteCalculation } from '../../../../hooks/useRouteCalculation'
import { reservationsApi, weatherApi } from '../../../../api/client'
import { useLegModeActions } from '../../../../components/Planner/useLegModeActions'
+import { toDisplayTemp } from '../../../../components/Planner/dayDetailModel'
import { getDayBookendHotels } from '../../../../utils/dayOrder'
import { dayWeatherAnchor } from '../../../../utils/dayWeather'
import { metadataWithLegPositions, planMergedOrder, withTransportPositions } from '../../../../utils/mergedOrder'
@@ -323,7 +324,7 @@ export function useMPlanTimeline(planner: TripPlanner) {
// Rounded display temperature in the user's unit (the API always answers in °C).
const weatherTemp = weather
- ? Math.round(settings.temperature_unit === 'fahrenheit' ? weather.temp * 9 / 5 + 32 : weather.temp)
+ ? toDisplayTemp(weather.temp, settings.temperature_unit === 'fahrenheit')
: null
// ── Per-segment travel mode (#1281) ──
diff --git a/client/src/mobile/screens/trip/sheets/MDaySheet.tsx b/client/src/mobile/screens/trip/sheets/MDaySheet.tsx
index a4c2aa448..a637e37bd 100644
--- a/client/src/mobile/screens/trip/sheets/MDaySheet.tsx
+++ b/client/src/mobile/screens/trip/sheets/MDaySheet.tsx
@@ -1,15 +1,15 @@
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useMemo } from 'react'
import { CalendarDays, Compass, Eraser, Hotel, MapPin, Pencil, Plus, RotateCcw, Route as RouteIcon, TramFront } from 'lucide-react'
-import type { WeatherResult } from '@trek/shared'
import MSheet from '../../../components/MSheet'
import type { MTripSheetsProps } from '../MTripShell'
import { useTranslation } from '../../../../i18n'
-import { weatherApi } from '../../../../api/client'
import { useSettingsStore } from '../../../../store/settingsStore'
import { routeModeIcon, useRouteModeOptions } from '../../../../components/Planner/routeModes'
import { dayHeadingParts } from '../../../../utils/dayLabel'
import { stayDayTimes } from '../../../../components/Planner/stayDayTimes'
import { useDayNotes } from '../../../../hooks/useDayNotes'
+import { useDayForecast, useDayRename } from '../../../../components/Planner/useDayDetail'
+import { dayBookings, stayDayLabel, toDisplayTemp } from '../../../../components/Planner/dayDetailModel'
import { RES_ICONS, getNoteIcon } from '../../../../components/Planner/DayPlanSidebar.constants'
import { getDayBookendHotels, isDayInAccommodationRange } from '../../../../utils/dayOrder'
import { getTransportForDay, hasCarrierEndpointOnDay } from '../../../../utils/dayMerge'
@@ -67,29 +67,15 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
() => (day ? getDayBookendHotels(day, planner.days, planner.tripAccommodations).morning : undefined),
)
- const [weather, setWeather] = useState(null)
- const [weatherLoading, setWeatherLoading] = useState(false)
- useEffect(() => {
- if (!open || !day?.date || lat == null || lng == null) { setWeather(null); return }
- let cancelled = false
- setWeatherLoading(true)
- weatherApi.getDetailed(lat, lng, day.date, planner.language)
- .then(data => { if (!cancelled) setWeather(data.error ? null : data) })
- .catch(() => { if (!cancelled) setWeather(null) })
- .finally(() => { if (!cancelled) setWeatherLoading(false) })
- return () => { cancelled = true }
- }, [open, day?.date, lat, lng, planner.language])
+ const { weather, loading: weatherLoading } = useDayForecast(
+ open && !!day?.date && lat != null && lng != null, day?.date, lat, lng, planner.language,
+ )
// Inline rename — the day sheet owns the pencil on mobile (#1065 parity).
- const [editingTitle, setEditingTitle] = useState(false)
- const [titleDraft, setTitleDraft] = useState('')
- const titleInputRef = useRef(null)
- useEffect(() => { if (editingTitle) titleInputRef.current?.focus() }, [editingTitle])
- useEffect(() => { if (!open) setEditingTitle(false) }, [open])
- const commitRename = () => {
- setEditingTitle(false)
- if (day) planner.handleUpdateDayTitle(day.id, titleDraft.trim())
- }
+ const { editingTitle, setEditingTitle, titleDraft, setTitleDraft, titleInputRef, startRename, commitRename } = useDayRename(title => {
+ if (day) planner.handleUpdateDayTitle(day.id, title)
+ })
+ useEffect(() => { if (!open) setEditingTitle(false) }, [open, setEditingTitle])
const notes = useDayNotes(planner.tripId)
const dayNotes: DayNote[] = day
@@ -98,11 +84,7 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
const dayReservations = useMemo(() => {
if (!day) return []
- return planner.reservations.filter(r => {
- if (r.type === 'hotel') return false
- if (r.assignment_id && dayAssignments.some(a => a.id === r.assignment_id)) return true
- return r.day_id === day.id
- })
+ return dayBookings(day.id, dayAssignments, planner.reservations)
}, [day, planner.reservations, dayAssignments])
const dayAccommodations = useMemo(() => {
@@ -218,7 +200,7 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
planner.handlePlaceClick(placeId)
}
- const cTemp = (c: number) => Math.round(isFahrenheit ? c * 9 / 5 + 32 : c)
+ const cTemp = (c: number) => toDisplayTemp(c, isFahrenheit)
const formattedDate = day?.date
? new Date(`${day.date.slice(0, 10)}T00:00:00Z`).toLocaleDateString(locale, {
weekday: 'long', day: 'numeric', month: 'long', timeZone: 'UTC',
@@ -230,11 +212,9 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
const WeatherIcon = weatherIconFor(weather?.main)
const stayBadge = (acc: (typeof dayAccommodations)[number]) => {
- const isIn = acc.start_day_id === day?.id
- const isOut = acc.end_day_id === day?.id
- if (isIn && isOut) return { label: `${t('day.checkIn')} & ${t('day.checkOut')}`, cls: 'border-[color:var(--m-st-confirmed)] text-[color:var(--m-st-confirmed)]' }
- if (isIn) return { label: t('day.checkIn'), cls: 'border-[color:var(--m-st-confirmed)] text-[color:var(--m-st-confirmed)]' }
- if (isOut) return { label: t('day.checkOut'), cls: 'border-[color:var(--m-st-danger)] text-[color:var(--m-st-danger)]' }
+ const label = stayDayLabel(acc, day?.id, t)
+ if (label && acc.start_day_id === day?.id) return { label, cls: 'border-[color:var(--m-st-confirmed)] text-[color:var(--m-st-confirmed)]' }
+ if (label) return { label, cls: 'border-[color:var(--m-st-danger)] text-[color:var(--m-st-danger)]' }
return { label: t('mobileTrip.stay'), cls: 'border-[color:var(--m-faint)] text-m-muted' }
}
@@ -261,7 +241,7 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
{canEditDays && (