(draft?.overnight?.defaultEndDayId ?? null)
const [checkIn, setCheckIn] = useState(draft?.editing?.checkIn || draft?.arrivalTime || '')
- const [checkOut, setCheckOut] = useState(draft?.editing?.checkOut ?? '')
const [saving, setSaving] = useState(false)
if (!draft) return null
@@ -107,9 +103,13 @@ export default function RoadtripStopPopup({
try {
if (asNight) {
await onSaveNight?.({
- endDayId: endDayId ?? overnight!.defaultEndDayId,
+ // The night runs to the day the rail suggests. Choosing another one is a
+ // booking decision, and the booking lives under Days.
+ endDayId: overnight!.defaultEndDayId,
checkIn,
- checkOut,
+ // Passed back exactly as it was loaded. This surface no longer asks for a
+ // check-out, and sending a blank would quietly erase one entered under Days.
+ checkOut: draft.editing?.checkOut ?? '',
})
} else {
await onSave({ stopType, dwellMinutes: dwell })
@@ -178,44 +178,17 @@ export default function RoadtripStopPopup({
{asNight ? (
-
+ {/* Check-in only, and optional at that: it is the hour the drive can stop
+ for the night, which is the one thing about a booking the chain uses.
+ A check-out says when the room has to be back, never when anybody sets
+ off, so it is a booking detail and lives under Days with the rest of
+ them. A hotel found on a map has no idea when its reception opens. */}
+
- {t('roadtrip.stay.checkOutDay')}
+ {t('day.checkIn')}
-
- setEndDayId(Number(value))}
- options={overnight!.days.map(d => ({
- value: String(d.id),
- // Through the shared helper, like every other day date in
- // this feature. Interpolating the column gave this one
- // dropdown a bare ISO date while the rail beside it showed
- // the same day in the reader's own order.
- label: d.date
- ? `${t('roadtrip.stay.dayNumber', { number: d.number })} · ${formatDate(d.date, language)}`
- : t('roadtrip.stay.dayNumber', { number: d.number }),
- }))}
- size="sm"
- />
-
-
- {/* Both optional: leaving them empty is normal, and the server stores null.
- A hotel found on a map has no idea when its reception opens. */}
-
-
-
- {t('day.checkIn')}
-
-
-
-
-
- {t('day.checkOut')}
-
-
-
-
+
+
{websiteHref || draft.poi.phone ? (
{websiteHref ? (
@@ -299,7 +272,7 @@ export default function RoadtripStopPopup({
{overnight ?
day.id === draft.dayId)?.date}
- departure={overnight.days.find(day => day.id === (endDayId ?? overnight.defaultEndDayId))?.date} /> : null}
+ departure={overnight.days.find(day => day.id === overnight.defaultEndDayId)?.date} /> : null}
diff --git a/client/src/components/Roadtrip/useRoadtripRoutes.ts b/client/src/components/Roadtrip/useRoadtripRoutes.ts
index 2e1b27c1c..928e96596 100644
--- a/client/src/components/Roadtrip/useRoadtripRoutes.ts
+++ b/client/src/components/Roadtrip/useRoadtripRoutes.ts
@@ -3,7 +3,7 @@ import { assembleRoadtrip, foldRouteRun, type RoadtripStop, type RoadtripRoutes,
import { useEffect, useMemo, useRef, useState } from 'react'
import { calculateRouteWithLegs, RoutingRefusedError } from '../Map/RouteCalculator'
import { resolveLegMode } from '../Planner/legMode'
-import { splitIntoRuns, parseClock, parseAvoid, type DriveLimits } from './roadtripModel'
+import { splitIntoRuns, parseAvoid, type DriveLimits } from './roadtripModel'
import { spillChains } from './nightSpill'
import { useSettingsStore } from '../../store/settingsStore'
import { useVehicleRange } from './useVehicleRange'
@@ -71,14 +71,14 @@ const legKey = (from: RoadtripStop, to: RoadtripStop): string => `${stopKey(from
const EMPTY_ACCOMMODATIONS: Accommodation[] = []
-const asStop = (a: Assignment, ownerDayId: number, ownerIndex: number, accommodations: Accommodation[], days: Day[]): RoadtripStop | null => {
+const asStop = (a: Assignment, ownerDayId: number, ownerIndex: number, accommodations: Accommodation[]): RoadtripStop | null => {
const p = a.place
if (!p || typeof p.lat !== 'number' || typeof p.lng !== 'number') return null
+ // Check-in only. A check-out is the LATEST the room has to be handed back, not the
+ // earliest anybody may leave, so it says nothing about when the drive sets off and
+ // has no business in the chain. It stays a booking detail, shown under Days.
const stay = accommodations.find(stay => stay.place_id === a.place_id && stay.start_day_id === ownerDayId)
- const checkoutDay = days.find(day => day.id === stay?.end_day_id)?.day_number
- const checkoutTime = parseClock(stay?.check_out)
return {
- ...(checkoutDay != null && checkoutTime !== null ? { checkoutAt: checkoutDay * 1440 + checkoutTime } : {}),
assignmentId: a.id,
ownerDayId,
ownerIndex,
@@ -205,7 +205,7 @@ export function useRoadtripRoutes(
const stops = (assignments[String(d.id)] ?? [])
.slice()
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
- .map(a => asStop(a, d.id, 0, accommodations, days))
+ .map(a => asStop(a, d.id, 0, accommodations))
.filter((s): s is RoadtripStop => s !== null)
// The index is filled in after the drop, because it is the index into THIS
// list: an assignment whose place has no coordinates never becomes a stop, and
@@ -233,7 +233,7 @@ export function useRoadtripRoutes(
stops: (assignments[String(d.id)] ?? [])
.slice()
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
- .map(a => asStop(a, d.id, 0, accommodations, days))
+ .map(a => asStop(a, d.id, 0, accommodations))
.filter((s): s is RoadtripStop => s !== null)
.map((s, i) => ({ ...s, ownerIndex: i })),
}))
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index 8e6418d2a..769cdef12 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -96,10 +96,15 @@ const TransportModal = lazyWithRetry(() =>
* No label: ErrorBoundary lets label win over the panel level and would title a
* broken packing list "This plugin could not be shown".
*/
-function LazyPanel({ id, children }: { id: string; children: React.ReactNode }): React.ReactElement {
+function LazyPanel({ id, children, overlay }: { id: string; children: React.ReactNode; overlay?: boolean }): React.ReactElement {
return (
- }>
+ {/* A panel holds its place with a skeleton while its chunk arrives; a dialog has no
+ place to hold. Drawn in the page flow, that skeleton was a pale block flashing
+ under the planner the first time each dialog was ever opened, and never again
+ once the chunk was cached. Nothing is the right placeholder for something that
+ is about to cover the screen anyway. */}
+ }>
{children}
@@ -1025,7 +1030,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
{/* Which track a day drives along. Mounted only while it is open, so the chunk and
the parsing of every imported line stay out of an ordinary planner session. */}
{followTrack.dayId !== null && (
-
+
d.dayId === followTrack.dayId)?.dayNumber ?? 0}
@@ -1033,12 +1038,12 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
)}
{stayDraft && (
-
+
setStayDraft(null)} onSave={setRoadtripStay} />
)}
{stopDraft && (
-
+
) => {
- const stay = tripAccommodations.find(stay => stay.place_id === draft.placeId && stay.check_out)
- const place = places.find(place => place.id === draft.placeId)
- const visit = stay && assignments[String(stay.start_day_id)]?.find(visit => visit.place_id === draft.placeId)
- if (stay && place && visit) openPlaceEditor(place, visit.id)
- else setStayDraft(draft)
- }, [tripAccommodations, places, assignments, openPlaceEditor])
+ setStayDraft(draft)
+ }, [])
const handleDeletePlace = useCallback((placeId) => {
setDeletePlaceId(placeId)
diff --git a/shared/src/i18n/ar/roadtrip.ts b/shared/src/i18n/ar/roadtrip.ts
index 8ac3a19e0..eb0214c0a 100644
--- a/shared/src/i18n/ar/roadtrip.ts
+++ b/shared/src/i18n/ar/roadtrip.ts
@@ -143,8 +143,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'وجهة',
'roadtrip.stay.mode.pause': 'استراحة',
'roadtrip.stay.mode.night': 'مبيت',
- 'roadtrip.stay.checkOutDay': 'المغادرة في',
- 'roadtrip.stay.dayNumber': 'اليوم {number}',
'roadtrip.stay.nightAdded': 'أُضيف كمبيت',
'roadtrip.stay.nightAction': 'إضافة كمبيت',
'roadtrip.stop.makeService': 'اجعله توقفاً في الطريق',
diff --git a/shared/src/i18n/br/roadtrip.ts b/shared/src/i18n/br/roadtrip.ts
index ae6888f9d..02089c4e5 100644
--- a/shared/src/i18n/br/roadtrip.ts
+++ b/shared/src/i18n/br/roadtrip.ts
@@ -146,8 +146,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Um destino',
'roadtrip.stay.mode.pause': 'Pausa',
'roadtrip.stay.mode.night': 'Pernoite',
- 'roadtrip.stay.checkOutDay': 'Saída em',
- 'roadtrip.stay.dayNumber': 'Dia {number}',
'roadtrip.stay.nightAdded': 'Adicionado como pernoite',
'roadtrip.stay.nightAction': 'Adicionar como pernoite',
'roadtrip.stop.makeService': 'Tornar uma parada no caminho',
diff --git a/shared/src/i18n/ca/roadtrip.ts b/shared/src/i18n/ca/roadtrip.ts
index 50cec5f3a..24be49233 100644
--- a/shared/src/i18n/ca/roadtrip.ts
+++ b/shared/src/i18n/ca/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Una destinació',
'roadtrip.stay.mode.pause': 'Parada',
'roadtrip.stay.mode.night': 'Pernoctar',
- 'roadtrip.stay.checkOutDay': 'Sortida el',
- 'roadtrip.stay.dayNumber': 'Dia {number}',
'roadtrip.stay.nightAdded': 'Afegit com a pernoctació',
'roadtrip.stay.nightAction': 'Afegeix com a pernoctació',
'roadtrip.stop.makeService': 'Converteix en parada del camí',
diff --git a/shared/src/i18n/cs/roadtrip.ts b/shared/src/i18n/cs/roadtrip.ts
index 6d9146fee..3c115cdba 100644
--- a/shared/src/i18n/cs/roadtrip.ts
+++ b/shared/src/i18n/cs/roadtrip.ts
@@ -145,8 +145,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Cíl cesty',
'roadtrip.stay.mode.pause': 'Pauza',
'roadtrip.stay.mode.night': 'Nocleh',
- 'roadtrip.stay.checkOutDay': 'Odjezd',
- 'roadtrip.stay.dayNumber': 'Den {number}',
'roadtrip.stay.nightAdded': 'Přidáno jako nocleh',
'roadtrip.stay.nightAction': 'Přidat jako nocleh',
'roadtrip.stop.makeService': 'Změnit na zastávku po cestě',
diff --git a/shared/src/i18n/de/roadtrip.ts b/shared/src/i18n/de/roadtrip.ts
index eaf440e12..ff4e88a0c 100644
--- a/shared/src/i18n/de/roadtrip.ts
+++ b/shared/src/i18n/de/roadtrip.ts
@@ -143,8 +143,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Ein Ziel',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Übernachten',
- 'roadtrip.stay.checkOutDay': 'Abreise am',
- 'roadtrip.stay.dayNumber': 'Tag {number}',
'roadtrip.stay.nightAdded': 'Als Übernachtung hinzugefügt',
'roadtrip.stay.nightAction': 'Als Übernachtung hinzufügen',
'roadtrip.stop.makeService': 'Zum Halt unterwegs machen',
diff --git a/shared/src/i18n/en/roadtrip.ts b/shared/src/i18n/en/roadtrip.ts
index 0a369764f..bfec101a3 100644
--- a/shared/src/i18n/en/roadtrip.ts
+++ b/shared/src/i18n/en/roadtrip.ts
@@ -142,8 +142,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'A destination',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Overnight',
- 'roadtrip.stay.checkOutDay': 'Check out on',
- 'roadtrip.stay.dayNumber': 'Day {number}',
'roadtrip.stay.nightAdded': 'Added as an overnight stay',
'roadtrip.stay.nightAction': 'Add as an overnight stay',
'roadtrip.stop.makeService': 'Make it a stop on the way',
diff --git a/shared/src/i18n/es/roadtrip.ts b/shared/src/i18n/es/roadtrip.ts
index 5678884b2..4dc64dcb9 100644
--- a/shared/src/i18n/es/roadtrip.ts
+++ b/shared/src/i18n/es/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Un destino',
'roadtrip.stay.mode.pause': 'Parada',
'roadtrip.stay.mode.night': 'Pernoctar',
- 'roadtrip.stay.checkOutDay': 'Salida el',
- 'roadtrip.stay.dayNumber': 'Día {number}',
'roadtrip.stay.nightAdded': 'Añadido como pernoctación',
'roadtrip.stay.nightAction': 'Añadir como pernoctación',
'roadtrip.stop.makeService': 'Convertir en parada del camino',
diff --git a/shared/src/i18n/fr/roadtrip.ts b/shared/src/i18n/fr/roadtrip.ts
index a60999a0f..29353a7e0 100644
--- a/shared/src/i18n/fr/roadtrip.ts
+++ b/shared/src/i18n/fr/roadtrip.ts
@@ -148,8 +148,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Une destination',
'roadtrip.stay.mode.pause': 'Pause',
'roadtrip.stay.mode.night': 'Nuitée',
- 'roadtrip.stay.checkOutDay': 'Départ le',
- 'roadtrip.stay.dayNumber': 'Jour {number}',
'roadtrip.stay.nightAdded': 'Ajouté comme nuitée',
'roadtrip.stay.nightAction': 'Ajouter comme nuitée',
'roadtrip.stop.makeService': 'En faire un arrêt en chemin',
diff --git a/shared/src/i18n/gr/roadtrip.ts b/shared/src/i18n/gr/roadtrip.ts
index a92bf714f..22c443bcb 100644
--- a/shared/src/i18n/gr/roadtrip.ts
+++ b/shared/src/i18n/gr/roadtrip.ts
@@ -149,8 +149,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Προορισμός',
'roadtrip.stay.mode.pause': 'Στάση',
'roadtrip.stay.mode.night': 'Διανυκτέρευση',
- 'roadtrip.stay.checkOutDay': 'Αναχώρηση',
- 'roadtrip.stay.dayNumber': 'Ημέρα {number}',
'roadtrip.stay.nightAdded': 'Προστέθηκε ως διανυκτέρευση',
'roadtrip.stay.nightAction': 'Προσθήκη ως διανυκτέρευση',
'roadtrip.stop.makeService': 'Μετατροπή σε στάση στον δρόμο',
diff --git a/shared/src/i18n/hu/roadtrip.ts b/shared/src/i18n/hu/roadtrip.ts
index 25f768069..af2f577eb 100644
--- a/shared/src/i18n/hu/roadtrip.ts
+++ b/shared/src/i18n/hu/roadtrip.ts
@@ -145,8 +145,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Úti cél',
'roadtrip.stay.mode.pause': 'Szünet',
'roadtrip.stay.mode.night': 'Éjszakázás',
- 'roadtrip.stay.checkOutDay': 'Kijelentkezés',
- 'roadtrip.stay.dayNumber': '{number}. nap',
'roadtrip.stay.nightAdded': 'Hozzáadva éjszakázásként',
'roadtrip.stay.nightAction': 'Hozzáadás éjszakázásként',
'roadtrip.stop.makeService': 'Legyen útközbeni megálló',
diff --git a/shared/src/i18n/id/roadtrip.ts b/shared/src/i18n/id/roadtrip.ts
index e44a1f254..1b746dae6 100644
--- a/shared/src/i18n/id/roadtrip.ts
+++ b/shared/src/i18n/id/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Sebuah tujuan',
'roadtrip.stay.mode.pause': 'Istirahat',
'roadtrip.stay.mode.night': 'Menginap',
- 'roadtrip.stay.checkOutDay': 'Check out',
- 'roadtrip.stay.dayNumber': 'Hari {number}',
'roadtrip.stay.nightAdded': 'Ditambahkan sebagai menginap',
'roadtrip.stay.nightAction': 'Tambahkan sebagai menginap',
'roadtrip.stop.makeService': 'Jadikan perhentian di jalan',
diff --git a/shared/src/i18n/it/roadtrip.ts b/shared/src/i18n/it/roadtrip.ts
index 1b8f41cdc..9d7498128 100644
--- a/shared/src/i18n/it/roadtrip.ts
+++ b/shared/src/i18n/it/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Una destinazione',
'roadtrip.stay.mode.pause': 'Sosta',
'roadtrip.stay.mode.night': 'Pernottamento',
- 'roadtrip.stay.checkOutDay': 'Partenza il',
- 'roadtrip.stay.dayNumber': 'Giorno {number}',
'roadtrip.stay.nightAdded': 'Aggiunto come pernottamento',
'roadtrip.stay.nightAction': 'Aggiungi come pernottamento',
'roadtrip.stop.makeService': 'Trasforma in sosta lungo il percorso',
diff --git a/shared/src/i18n/ja/roadtrip.ts b/shared/src/i18n/ja/roadtrip.ts
index bf47acb55..e68b03e41 100644
--- a/shared/src/i18n/ja/roadtrip.ts
+++ b/shared/src/i18n/ja/roadtrip.ts
@@ -146,8 +146,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': '目的地',
'roadtrip.stay.mode.pause': '休憩',
'roadtrip.stay.mode.night': '宿泊',
- 'roadtrip.stay.checkOutDay': 'チェックアウト日',
- 'roadtrip.stay.dayNumber': '{number}日目',
'roadtrip.stay.nightAdded': '宿泊として追加しました',
'roadtrip.stay.nightAction': '宿泊として追加',
'roadtrip.stop.makeService': '途中の立ち寄りにする',
diff --git a/shared/src/i18n/ko/roadtrip.ts b/shared/src/i18n/ko/roadtrip.ts
index ff6409177..fe4be5971 100644
--- a/shared/src/i18n/ko/roadtrip.ts
+++ b/shared/src/i18n/ko/roadtrip.ts
@@ -145,8 +145,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': '목적지',
'roadtrip.stay.mode.pause': '휴식',
'roadtrip.stay.mode.night': '숙박',
- 'roadtrip.stay.checkOutDay': '체크아웃 날짜',
- 'roadtrip.stay.dayNumber': '{number}일차',
'roadtrip.stay.nightAdded': '숙박으로 추가했습니다',
'roadtrip.stay.nightAction': '숙박으로 추가',
'roadtrip.stop.makeService': '경유 정차로 변경',
diff --git a/shared/src/i18n/nl/roadtrip.ts b/shared/src/i18n/nl/roadtrip.ts
index fd3cb7e97..0e2d09307 100644
--- a/shared/src/i18n/nl/roadtrip.ts
+++ b/shared/src/i18n/nl/roadtrip.ts
@@ -148,8 +148,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Een bestemming',
'roadtrip.stay.mode.pause': 'Pauze',
'roadtrip.stay.mode.night': 'Overnachten',
- 'roadtrip.stay.checkOutDay': 'Uitchecken op',
- 'roadtrip.stay.dayNumber': 'Dag {number}',
'roadtrip.stay.nightAdded': 'Toegevoegd als overnachting',
'roadtrip.stay.nightAction': 'Toevoegen als overnachting',
'roadtrip.stop.makeService': 'Maak er een stop onderweg van',
diff --git a/shared/src/i18n/pl/roadtrip.ts b/shared/src/i18n/pl/roadtrip.ts
index e543abdbf..f1d35f8c8 100644
--- a/shared/src/i18n/pl/roadtrip.ts
+++ b/shared/src/i18n/pl/roadtrip.ts
@@ -146,8 +146,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Cel podróży',
'roadtrip.stay.mode.pause': 'Postój',
'roadtrip.stay.mode.night': 'Nocleg',
- 'roadtrip.stay.checkOutDay': 'Wymeldowanie',
- 'roadtrip.stay.dayNumber': 'Dzień {number}',
'roadtrip.stay.nightAdded': 'Dodano jako nocleg',
'roadtrip.stay.nightAction': 'Dodaj jako nocleg',
'roadtrip.stop.makeService': 'Zamień w postój po drodze',
diff --git a/shared/src/i18n/ru/roadtrip.ts b/shared/src/i18n/ru/roadtrip.ts
index a559c5703..e07f94e56 100644
--- a/shared/src/i18n/ru/roadtrip.ts
+++ b/shared/src/i18n/ru/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Пункт назначения',
'roadtrip.stay.mode.pause': 'Остановка',
'roadtrip.stay.mode.night': 'Ночёвка',
- 'roadtrip.stay.checkOutDay': 'Выезд',
- 'roadtrip.stay.dayNumber': 'День {number}',
'roadtrip.stay.nightAdded': 'Добавлено как ночёвка',
'roadtrip.stay.nightAction': 'Добавить как ночёвку',
'roadtrip.stop.makeService': 'Сделать остановкой в пути',
diff --git a/shared/src/i18n/sv/roadtrip.ts b/shared/src/i18n/sv/roadtrip.ts
index b17fcf481..47a1d1fc7 100644
--- a/shared/src/i18n/sv/roadtrip.ts
+++ b/shared/src/i18n/sv/roadtrip.ts
@@ -145,8 +145,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Ett resmål',
'roadtrip.stay.mode.pause': 'Paus',
'roadtrip.stay.mode.night': 'Övernattning',
- 'roadtrip.stay.checkOutDay': 'Utcheckning',
- 'roadtrip.stay.dayNumber': 'Dag {number}',
'roadtrip.stay.nightAdded': 'Tillagd som övernattning',
'roadtrip.stay.nightAction': 'Lägg till som övernattning',
'roadtrip.stop.makeService': 'Gör det till ett stopp på vägen',
diff --git a/shared/src/i18n/tr/roadtrip.ts b/shared/src/i18n/tr/roadtrip.ts
index ee5fc1608..b1d109999 100644
--- a/shared/src/i18n/tr/roadtrip.ts
+++ b/shared/src/i18n/tr/roadtrip.ts
@@ -147,8 +147,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Bir varış noktası',
'roadtrip.stay.mode.pause': 'Mola',
'roadtrip.stay.mode.night': 'Konaklama',
- 'roadtrip.stay.checkOutDay': 'Çıkış günü',
- 'roadtrip.stay.dayNumber': '{number}. gün',
'roadtrip.stay.nightAdded': 'Konaklama olarak eklendi',
'roadtrip.stay.nightAction': 'Konaklama olarak ekle',
'roadtrip.stop.makeService': 'Yol üzerinde molaya dönüştür',
diff --git a/shared/src/i18n/uk/roadtrip.ts b/shared/src/i18n/uk/roadtrip.ts
index 2e9dcf94b..0a8b59f4c 100644
--- a/shared/src/i18n/uk/roadtrip.ts
+++ b/shared/src/i18n/uk/roadtrip.ts
@@ -146,8 +146,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Пункт призначення',
'roadtrip.stay.mode.pause': 'Зупинка',
'roadtrip.stay.mode.night': 'Ночівля',
- 'roadtrip.stay.checkOutDay': 'Виїзд',
- 'roadtrip.stay.dayNumber': 'День {number}',
'roadtrip.stay.nightAdded': 'Додано як ночівлю',
'roadtrip.stay.nightAction': 'Додати як ночівлю',
'roadtrip.stop.makeService': 'Зробити зупинкою в дорозі',
diff --git a/shared/src/i18n/vi/roadtrip.ts b/shared/src/i18n/vi/roadtrip.ts
index f0eec0a96..4396cc815 100644
--- a/shared/src/i18n/vi/roadtrip.ts
+++ b/shared/src/i18n/vi/roadtrip.ts
@@ -146,8 +146,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': 'Một điểm đến',
'roadtrip.stay.mode.pause': 'Nghỉ',
'roadtrip.stay.mode.night': 'Qua đêm',
- 'roadtrip.stay.checkOutDay': 'Trả phòng ngày',
- 'roadtrip.stay.dayNumber': 'Ngày {number}',
'roadtrip.stay.nightAdded': 'Đã thêm làm nơi nghỉ đêm',
'roadtrip.stay.nightAction': 'Thêm làm nơi nghỉ đêm',
'roadtrip.stop.makeService': 'Biến thành điểm dừng dọc đường',
diff --git a/shared/src/i18n/zh-TW/roadtrip.ts b/shared/src/i18n/zh-TW/roadtrip.ts
index 90a4c6aa1..08a393e12 100644
--- a/shared/src/i18n/zh-TW/roadtrip.ts
+++ b/shared/src/i18n/zh-TW/roadtrip.ts
@@ -137,8 +137,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': '目的地',
'roadtrip.stay.mode.pause': '停留',
'roadtrip.stay.mode.night': '過夜',
- 'roadtrip.stay.checkOutDay': '退房日',
- 'roadtrip.stay.dayNumber': '第 {number} 天',
'roadtrip.stay.nightAdded': '已新增為過夜住宿',
'roadtrip.stay.nightAction': '新增為過夜住宿',
'roadtrip.stop.makeService': '設為途中停靠',
diff --git a/shared/src/i18n/zh/roadtrip.ts b/shared/src/i18n/zh/roadtrip.ts
index 1cc59fb0d..8cbca9a94 100644
--- a/shared/src/i18n/zh/roadtrip.ts
+++ b/shared/src/i18n/zh/roadtrip.ts
@@ -137,8 +137,6 @@ const roadtrip: TranslationStrings = {
'roadtrip.stop.none': '目的地',
'roadtrip.stay.mode.pause': '停留',
'roadtrip.stay.mode.night': '过夜',
- 'roadtrip.stay.checkOutDay': '退房日',
- 'roadtrip.stay.dayNumber': '第 {number} 天',
'roadtrip.stay.nightAdded': '已添加为过夜住宿',
'roadtrip.stay.nightAction': '添加为过夜住宿',
'roadtrip.stop.makeService': '设为途中停靠',
diff --git a/shared/src/roadtrip/assemble.ts b/shared/src/roadtrip/assemble.ts
index d5b8a3ebb..ac9986639 100644
--- a/shared/src/roadtrip/assemble.ts
+++ b/shared/src/roadtrip/assemble.ts
@@ -90,7 +90,6 @@ export function assembleRoadtrip({
d.stops.map((s) => ({
anchor: s.time ?? s.checkInTime ?? null,
dwellMinutes: s.dwellMinutes,
- departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440,
})),
d.stops.slice(0, -1).map((s, i) => storedLegFor(s, d.stops[i + 1]!)?.seg.duration),
),
@@ -160,15 +159,7 @@ export function assembleRoadtrip({
const snap = s.automaticNight ? undefined : allSnaps[stopKey(s)];
const line = spurFor(snap);
if (line) accessLines.push({ line, meters: snap!.meters, stopKey: stopKey(s) });
- const entry = schedule.entries[index];
- const arrival = parseClock(entry?.arrival);
- const dwellMinutes =
- s.checkoutAt !== undefined && arrival !== null
- ? Math.max(0, s.checkoutAt - ((chain.dayNumber + (entry?.dayOffset ?? 0)) * 1440 + arrival))
- : s.checkoutAt !== undefined
- ? null
- : s.dwellMinutes;
- return { ...s, dwellMinutes, offRoadMeters: line ? snap!.meters : null };
+ return { ...s, offRoadMeters: line ? snap!.meters : null };
});
const drive = {
diff --git a/shared/src/roadtrip/continueSchedule.ts b/shared/src/roadtrip/continueSchedule.ts
index c9cda06ac..15f40ebd2 100644
--- a/shared/src/roadtrip/continueSchedule.ts
+++ b/shared/src/roadtrip/continueSchedule.ts
@@ -18,15 +18,10 @@ export function continueSchedule(
if (departure === null || duration === undefined) continue;
const previousArrival = parseClock(previous.arrival);
const departureAt =
- stops[i - 1]!.checkoutAt !== undefined
- ? stops[i - 1]!.checkoutAt! - dayNumber * 1440
- : previous.dayOffset * 1440 + departure + (previousArrival !== null && departure < previousArrival ? 1440 : 0);
+ previous.dayOffset * 1440 + departure + (previousArrival !== null && departure < previousArrival ? 1440 : 0);
const arrivalAt = departureAt + Math.round(duration / 60);
const stop = stops[i]!;
- const leaveAt =
- stop.checkoutAt === undefined
- ? arrivalAt + (stop.dwellMinutes ?? 0)
- : Math.max(arrivalAt, stop.checkoutAt - dayNumber * 1440);
+ const leaveAt = arrivalAt + (stop.dwellMinutes ?? 0);
entry.arrival = formatClock(arrivalAt);
entry.departure = formatClock(leaveAt);
entry.dayOffset = Math.floor(arrivalAt / 1440);
diff --git a/shared/src/roadtrip/dayWindow.spec.ts b/shared/src/roadtrip/dayWindow.spec.ts
index 4ebe91f01..af89057d7 100644
--- a/shared/src/roadtrip/dayWindow.spec.ts
+++ b/shared/src/roadtrip/dayWindow.spec.ts
@@ -366,47 +366,28 @@ describe('daily travel window', () => {
});
});
-describe('booked checkout', () => {
- it('waits at the hotel until noon on the checkout day', () => {
- const hotel = stop(2, { checkoutAt: 2 * 1440 + 720, dwellMinutes: 30 });
- const stops = [stop(1), hotel, stop(3)];
- const plan = calculate(stops, [60, 60]);
- expect(plan.issue).toBeNull();
- expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('09:00');
- expect(plan.chains[0]!.schedule.entries[1]!.departure).toBe('12:00');
- expect(plan.chains[1]!.schedule.entries[0]!.arrival).toBe('12:00');
- expect(plan.chains[1]!.schedule.entries[1]!.arrival).toBe('13:00');
- });
- it('honours an early checkout and multiple nights', () => {
- const stops = [stop(1, { checkoutAt: 3 * 1440 + 420 }), stop(2)];
- const plan = calculate(stops, [60], [day(2), day(3)]);
- expect(plan.issue).toBeNull();
- expect(plan.chains[2]!.schedule.entries[0]!.arrival).toBe('07:00');
- expect(plan.chains[2]!.schedule.entries[1]!.arrival).toBe('08:00');
- });
- it('does not move time backwards when arrival is after checkout', () => {
- const stops = [stop(1, { time: '13:00', checkoutAt: 1440 + 720 }), stop(2)];
- const plan = calculate(stops, [60]);
- expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('14:00');
- });
-});
-
-it('keeps checkout when the drive arrives after suggested check-in', () => {
+it('waits for a check-in the drive overshoots rather than rushing it', () => {
const stops = [
stop(1, { time: '07:00', dwellMinutes: 60 }),
- stop(2, { checkInTime: '09:11', checkoutAt: 2 * 1440 + 600 }),
+ stop(2, { checkInTime: '09:11' }),
stop(3),
];
const plan = calculate(stops, [72.5, 29]);
expect(plan.issue).toBeNull();
+ // Arrives two minutes past the hour it may check in from, and is left there rather
+ // than pulled back to the round number.
expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('09:13');
- expect(plan.chains[1]!.stops.at(-1)!.placeId).toBe(3);
- expect(plan.chains[1]!.schedule.entries.at(-1)!.arrival).toBe('10:29');
+ const stops0 = plan.chains[0]!.stops;
+ expect(stops0.at(-1)!.placeId).toBe(3);
+ // And carries straight on from there: nothing holds the traveller at the stop.
+ expect(plan.chains[0]!.schedule.entries.at(-1)!.arrival).toBe('09:42');
});
it('waits for check-in without treating it as a fixed appointment', () => {
- const plan = calculate([stop(1), stop(2, { checkInTime: '15:00', checkoutAt: 2 * 1440 + 600 }), stop(3)], [60, 29]);
+ const plan = calculate([stop(1), stop(2, { checkInTime: '15:00' }), stop(3)], [60, 29]);
expect(plan.issue).toBeNull();
+ // The check-in is a door opening, not an appointment: the drive waits for it and then
+ // carries straight on, rather than the day being rebuilt around it.
expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('15:00');
- expect(plan.chains[1]!.schedule.entries.at(-1)!.arrival).toBe('10:29');
+ expect(plan.chains[0]!.schedule.entries.at(-1)!.arrival).toBe('15:29');
});
diff --git a/shared/src/roadtrip/dayWindow.ts b/shared/src/roadtrip/dayWindow.ts
index 8ba7edb53..196f8e63e 100644
--- a/shared/src/roadtrip/dayWindow.ts
+++ b/shared/src/roadtrip/dayWindow.ts
@@ -181,7 +181,6 @@ export function planDayWindow(
name: labels.end,
time: null,
dwellMinutes: 0,
- checkoutAt: undefined,
checkInTime: undefined,
stopType: null,
fillPercent: null,
@@ -278,26 +277,6 @@ export function planDayWindow(
append(stop, clock);
previous = stop;
- if (stop.checkoutAt !== undefined) {
- const checkoutDay = Math.floor(stop.checkoutAt / 1440);
- const checkoutTime = stop.checkoutAt % 1440;
- const arrivalAt = number * 1440 + clock;
- if (stop.checkoutAt > arrivalAt) {
- const entries = chainAt(number).schedule.entries;
- entries[entries.length - 1]!.departure = formatClock(checkoutTime);
- while (number < checkoutDay) {
- previous = night(previous);
- if (issue) return failed(issue);
- }
- clock = checkoutTime;
- if (previous.automaticNight?.phase === 'start') {
- const entries = chainAt(number).schedule.entries;
- entries[entries.length - 1]!.arrival = formatClock(clock);
- entries[entries.length - 1]!.departure = formatClock(clock);
- }
- }
- continue;
- }
let dwell = Math.max(0, stop.dwellMinutes ?? 0);
while (dwell > 0) {
const remaining = targets.has(number) ? dwell : Math.max(0, window.end - clock);
diff --git a/shared/src/roadtrip/nightSpill.spec.ts b/shared/src/roadtrip/nightSpill.spec.ts
index 3093959c1..000dfb910 100644
--- a/shared/src/roadtrip/nightSpill.spec.ts
+++ b/shared/src/roadtrip/nightSpill.spec.ts
@@ -276,4 +276,66 @@ describe('spillChains', () => {
expect(chains[1]!.spills).toHaveLength(1);
expect(chains[1]!.spills[0]!).toMatchObject({ at: 0, count: 2, fromDayNumber: 1 });
});
+
+ describe('a stay that runs past midnight carries the clock into the next day', () => {
+ // Standing somewhere for twenty-four hours is not over when the date changes. The
+ // day after used to begin at nothing, so a trip whose second day had no pinned time
+ // showed no times at all, right after the first day had said exactly when it ends.
+ it('FE-NIGHTSPILL-013: the next day starts where the stay ends', () => {
+ const plan = [
+ day(1, 1, [
+ stop({ assignmentId: 1, name: 'Hamburg', time: '07:56' }),
+ stop({ assignmentId: 2, name: 'Mercure', time: '08:00', dwellMinutes: 24 * 60 }),
+ ]),
+ day(2, 2, [stop({ assignmentId: 3, name: 'Panorama' }), stop({ assignmentId: 4, name: 'Bergedorf' })]),
+ ];
+
+ const chains = spillChains(plan, [], everyLeg(11));
+ const dayTwo = chains.find((c) => c.dayNumber === 2)!;
+
+ expect(dayTwo.schedule.entries[0]!.arrival).toBe('08:00');
+ expect(dayTwo.schedule.entries[1]!.arrival).toBe('08:11');
+ });
+
+ it('FE-NIGHTSPILL-014: an ordinary evening hands on nothing', () => {
+ // The night between two days is not a wait somebody is serving out. Only a stay
+ // that genuinely runs past midnight reaches into the morning.
+ const plan = [
+ day(1, 1, [stop({ assignmentId: 1, name: 'Hamburg', time: '18:00', dwellMinutes: 60 })]),
+ day(2, 2, [stop({ assignmentId: 2, name: 'Panorama' })]),
+ ];
+
+ const chains = spillChains(plan, [], everyLeg(11));
+
+ expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBeNull();
+ });
+
+ it('FE-NIGHTSPILL-015: a stop pinned before the stay ends keeps its clock and is flagged', () => {
+ const plan = [
+ day(1, 1, [stop({ assignmentId: 1, name: 'Mercure', time: '08:00', dwellMinutes: 24 * 60 })]),
+ day(2, 2, [stop({ assignmentId: 2, name: 'Panorama', time: '07:00' })]),
+ ];
+
+ const chains = spillChains(plan, [], everyLeg(11));
+ const dayTwo = chains.find((c) => c.dayNumber === 2)!;
+
+ // Never moved: the hour is the traveller's.
+ expect(dayTwo.schedule.entries[0]!.arrival).toBe('07:00');
+ expect(dayTwo.schedule.warnings).toContainEqual({ index: 0, code: 'late', minutes: 60 });
+ });
+
+ it('FE-NIGHTSPILL-016: a stay of two whole days walks the clock forward day by day', () => {
+ const plan = [
+ day(1, 1, [stop({ assignmentId: 1, name: 'Mercure', time: '08:00', dwellMinutes: 48 * 60 })]),
+ day(2, 2, [stop({ assignmentId: 2, name: 'Nothing planned' })]),
+ day(3, 3, [stop({ assignmentId: 3, name: 'Berlin' })]),
+ ];
+
+ const chains = spillChains(plan, [], everyLeg(11));
+
+ // Day two is still inside the stay, so it cannot begin at all.
+ expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBeNull();
+ expect(chains.find((c) => c.dayNumber === 3)!.schedule.entries[0]!.arrival).toBe('08:00');
+ });
+ });
});
diff --git a/shared/src/roadtrip/nightSpill.ts b/shared/src/roadtrip/nightSpill.ts
index 9c51af731..9c1264395 100644
--- a/shared/src/roadtrip/nightSpill.ts
+++ b/shared/src/roadtrip/nightSpill.ts
@@ -69,17 +69,40 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
const landing = new Map();
for (const d of all) landing.set(d.dayNumber, []);
+ /**
+ * How far into a day the one before it still reaches.
+ *
+ * A stop stood at for twenty-four hours is not over when the date changes: the day
+ * after it begins where it ends, not at nothing. Only ever a floor, and only when a
+ * stay genuinely runs past midnight — an ordinary day that finishes at six in the
+ * evening hands on nothing, because the night between them is not a wait, it is a
+ * night.
+ */
+ let carriedInto: { dayNumber: number; minute: number } | null = null;
+
for (const d of all) {
+ const notBefore = carriedInto?.dayNumber === d.dayNumber ? carriedInto.minute : null;
const routed = d.stops.slice(0, -1).map((s, i) => legFor(s, d.stops[i + 1]!));
const legs = routed.map((l) => l?.seg);
const schedule = computeSchedule(
d.stops.map((s) => ({
anchor: s.time ?? s.checkInTime ?? null,
dwellMinutes: s.dwellMinutes,
- departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440,
})),
legs.map((l) => l?.duration),
+ { notBefore },
);
+ // Where this day's last clock lands, carried to whichever day that turns out to be.
+ const ends = schedule.endsAt ?? null;
+ if (ends !== null && ends > 1440) {
+ carriedInto = { dayNumber: d.dayNumber + Math.floor(ends / 1440), minute: ends % 1440 };
+ } else if (carriedInto && carriedInto.dayNumber <= d.dayNumber) {
+ // Spent: this is the day it was pointing at.
+ carriedInto = null;
+ }
+ // Otherwise it is left alone. A stay of several days passes over the days in the
+ // middle without them having anything to say, and clearing it there would lose the
+ // morning it was aimed at.
let day = 0;
let previous: number | null = null;
@@ -90,7 +113,7 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
if (previous !== null && clock < previous) day += 1;
previous = clock;
}
- const offset = d.stops.some((s) => s.checkoutAt !== undefined) ? (entry?.dayOffset ?? day) : day;
+ const offset = day;
const marks = schedule.warnings.filter((w) => w.index === i);
const reachable = offset > 0 && numbers.has(d.dayNumber + offset);
diff --git a/shared/src/roadtrip/planning-types.ts b/shared/src/roadtrip/planning-types.ts
index 696777155..92d946aa3 100644
--- a/shared/src/roadtrip/planning-types.ts
+++ b/shared/src/roadtrip/planning-types.ts
@@ -18,7 +18,6 @@ export interface RoadtripStop {
time: string | null;
dwellMinutes: number | null;
- checkoutAt?: number;
checkInTime?: string | null;
endDay?: boolean;
diff --git a/shared/src/roadtrip/roadtripModel.spec.ts b/shared/src/roadtrip/roadtripModel.spec.ts
index 7b163cfda..2a2d1a1dd 100644
--- a/shared/src/roadtrip/roadtripModel.spec.ts
+++ b/shared/src/roadtrip/roadtripModel.spec.ts
@@ -252,7 +252,7 @@ describe('computeSchedule', () => {
});
it('handles an empty chain', () => {
- expect(computeSchedule([], [])).toEqual({ entries: [], warnings: [] });
+ expect(computeSchedule([], [])).toEqual({ entries: [], warnings: [], endsAt: null });
});
it('treats a missing dwell as no time spent', () => {
diff --git a/shared/src/roadtrip/roadtripModel.ts b/shared/src/roadtrip/roadtripModel.ts
index efcb9177a..a94cc6320 100644
--- a/shared/src/roadtrip/roadtripModel.ts
+++ b/shared/src/roadtrip/roadtripModel.ts
@@ -133,6 +133,15 @@ export interface ScheduleEntry {
export interface Schedule {
entries: ScheduleEntry[];
warnings: ScheduleWarning[];
+ /**
+ * Minute the last stop is left at, counted from the start of this day, so a stay
+ * running past midnight comes back as more than 1440.
+ *
+ * The one number the next day needs. A stop stood at for twenty-four hours is not
+ * over when the date changes, and the day after it cannot begin before it ends.
+ * Null or absent when nothing on this day has a time at all.
+ */
+ endsAt?: number | null;
}
const DAY_MINUTES = 24 * 60;
@@ -154,13 +163,24 @@ function resolveArrival(
};
}
-export function computeSchedule(stops: ScheduleStop[], legSeconds: (number | undefined)[]): Schedule {
+/**
+ * @param opts.notBefore Minute of this day the first stop cannot be reached before,
+ * because the day before is still running into it: a stop stood at past midnight ends
+ * where it ends, and nothing can happen ahead of that. Behaves like the arrival of an
+ * imaginary stop just before the first, so a stop pinned earlier keeps its clock and
+ * picks up the same late finding any leg it cannot make in time would give it.
+ */
+export function computeSchedule(
+ stops: ScheduleStop[],
+ legSeconds: (number | undefined)[],
+ opts: { notBefore?: number | null } = {},
+): Schedule {
const warnings: ScheduleWarning[] = [];
const arrivals: (number | null)[] = new Array(stops.length).fill(null);
const anchored: boolean[] = new Array(stops.length).fill(false);
- let cursor: number | null = null;
+ let cursor: number | null = opts.notBefore ?? null;
let dayOffset = 0;
for (let i = 0; i < stops.length; i++) {
@@ -231,7 +251,22 @@ export function computeSchedule(stops: ScheduleStop[], legSeconds: (number | und
});
}
- return { entries, warnings };
+ // Read off the last stop that actually has a clock: a trailing stop with no time of
+ // its own carries nothing forward, and a day whose times peter out halfway should
+ // hand on what it does know rather than nothing.
+ let endsAt: number | null = null;
+ for (let i = stops.length - 1; i >= 0; i--) {
+ const raw = arrivals[i];
+ if (raw === null || raw === undefined) continue;
+ const arrival = raw + shift;
+ endsAt =
+ stops[i]!.departureAt === undefined
+ ? arrival + (stops[i]!.dwellMinutes ?? 0)
+ : Math.max(arrival, stops[i]!.departureAt! + shift);
+ break;
+ }
+
+ return { entries, warnings, endsAt };
}
export function splitIntoRuns(stops: T[], modeOfLeg: (from: T, to: T) => string): { stops: T[]; mode: string }[] {