From 8523bbb7240e81c4adc7bbfacc8c0ae13ca137e3 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 21 Oct 2026 20:46:33 +0200 Subject: [PATCH] fix(journey): say when a stay runs into the next day, leave the doubtful stays of a day unticked and pin a day report to a stay of the day, not the night --- .../Dawarich/dawarichSuggestionModel.test.ts | 5 ++++ .../Dawarich/dawarichSuggestionModel.ts | 7 +++++- .../Journey/journeyDayImportModel.test.ts | 23 ++++++++++++++++++- .../Journey/journeyDayImportModel.ts | 23 ++++++++++++++----- client/tests/helpers/journeyRecording.ts | 3 ++- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/client/src/components/Dawarich/dawarichSuggestionModel.test.ts b/client/src/components/Dawarich/dawarichSuggestionModel.test.ts index 2620d5eec..1cbb056e9 100644 --- a/client/src/components/Dawarich/dawarichSuggestionModel.test.ts +++ b/client/src/components/Dawarich/dawarichSuggestionModel.test.ts @@ -111,6 +111,11 @@ describe('clockOf / timeRange', () => { expect(timeRange('2026-09-10T10:15:00+02:00', 'nonsense', false)).toBe('10:15') }) + it('FE-DAWARICH-SUGMODEL-020: a stay into the next day carries the days it ran over', () => { + expect(timeRange('2026-10-24T18:30:00+02:00', '2026-10-25T08:30:00+02:00', false)).toBe('18:30 – 08:30 +1') + expect(timeRange('2026-10-24T18:30:00+02:00', '2026-10-26T08:30:00+02:00', false)).toBe('18:30 – 08:30 +2') + }) + it('FE-DAWARICH-SUGMODEL-007: no arrival, no range', () => { expect(timeRange('nonsense', '2026-09-10T12:40:00+02:00', false)).toBe('') }) diff --git a/client/src/components/Dawarich/dawarichSuggestionModel.ts b/client/src/components/Dawarich/dawarichSuggestionModel.ts index 50944bd4b..9aea6e87c 100644 --- a/client/src/components/Dawarich/dawarichSuggestionModel.ts +++ b/client/src/components/Dawarich/dawarichSuggestionModel.ts @@ -102,12 +102,17 @@ export function clockOf(iso: string): string { * timestamp and printed, so a traveller on a 12-hour clock read every Dawarich time in * 24-hour while the rest of the app obeyed the setting. A required argument makes the * next caller answer the question rather than inherit the bug. + * + * A stay that ends on a later day says so (`18:30 – 08:30 +1`), as a timetable + * does, or a night at the hotel reads as fourteen hours that never happened. */ export function timeRange(startedAt: string, endedAt: string, is12h: boolean): string { const from = formatClockTime(clockOf(startedAt), is12h) const to = formatClockTime(clockOf(endedAt), is12h) if (!from) return '' - return to ? `${from} – ${to}` : from + if (!to) return from + const days = Math.round((Date.parse(endedAt.slice(0, 10)) - Date.parse(startedAt.slice(0, 10))) / 86_400_000) + return days > 0 ? `${from} – ${to} +${days}` : `${from} – ${to}` } /** diff --git a/client/src/components/Journey/journeyDayImportModel.test.ts b/client/src/components/Journey/journeyDayImportModel.test.ts index 4829edf47..e40ce86ea 100644 --- a/client/src/components/Journey/journeyDayImportModel.test.ts +++ b/client/src/components/Journey/journeyDayImportModel.test.ts @@ -1,4 +1,4 @@ -// FE-JRN-DAYIMPORT-MODEL-001 to FE-JRN-DAYIMPORT-MODEL-011 +// FE-JRN-DAYIMPORT-MODEL-001 to FE-JRN-DAYIMPORT-MODEL-013 // // JRN-02: what the traveller decided in the day take-over, before anything is // written. Two stays at San Marco, a parking stop and Rialto, as the review @@ -144,4 +144,25 @@ describe('journeyDayImportModel', () => { expect(canJoin(choice, stays, 2)).toBe(false); expect(groupsOf(choice, stays).map((g) => g.stays.map((s) => s.suggestionId))).toEqual([[1], [3]]); }); + + it('FE-JRN-DAYIMPORT-MODEL-012: a stay Dawarich itself doubts starts unchosen, unless it was confirmed there', () => { + const stays = [ + buildDayStay({ suggestionId: 1, name: 'Car park', confidenceBand: 'low', sourceStatus: 'suggested' }), + buildDayStay({ suggestionId: 2, name: 'Bridge', confidenceBand: 'low', sourceStatus: 'confirmed' }), + buildDayStay({ suggestionId: 3, name: 'Cathedral', confidenceBand: 'medium', sourceStatus: 'suggested' }), + ]; + expect([...initialChoice(stays).selected]).toEqual([2, 3]); + }); + + it('FE-JRN-DAYIMPORT-MODEL-013: a night at the hotel is the position only when no stay of the day has one', () => { + const night = buildDayStay({ + suggestionId: 1, + startedAt: '2026-05-14T19:00:00+02:00', + endedAt: '2026-05-15T08:00:00+02:00', + durationMinutes: 780, + }); + const sight = buildDayStay({ suggestionId: 2, durationMinutes: 50 }); + expect(defaultAnchor([sight, night])).toBe(2); + expect(defaultAnchor([night, buildDayStay({ suggestionId: 3, lat: null, lng: null })])).toBe(1); + }); }); diff --git a/client/src/components/Journey/journeyDayImportModel.ts b/client/src/components/Journey/journeyDayImportModel.ts index a58963c8a..32ba410f6 100644 --- a/client/src/components/Journey/journeyDayImportModel.ts +++ b/client/src/components/Journey/journeyDayImportModel.ts @@ -36,11 +36,16 @@ export function isTakeable(stay: JourneyDayStay): boolean { } /** - * Every open stay chosen, and the same place twice in a row already joined: - * the square somebody left for a coffee and came back to is one memory. + * Every open stay chosen but those Dawarich itself doubts (the car park passed + * on the way, more often than not), and the same place twice in a row already + * joined: the square somebody left for a coffee and came back to is one memory. */ export function initialChoice(stays: JourneyDayStay[]): DayImportChoice { - const selected = new Set(stays.filter((stay) => stay.status === 'open').map((stay) => stay.suggestionId)); + const selected = new Set( + stays + .filter((stay) => stay.status === 'open' && !(stay.confidenceBand === 'low' && stay.sourceStatus !== 'confirmed')) + .map((stay) => stay.suggestionId) + ); const joined = new Set(); stays.forEach((stay, index) => { const before = stays[index - 1]; @@ -99,11 +104,17 @@ export function groupsOf(choice: DayImportChoice, stays: JourneyDayStay[]): DayI return groups; } -/** The longest stay that has a place: where a group sits on the map unless somebody picks another. */ +/** + * The longest stay that has a place: where a group sits on the map unless + * somebody picks another. A night somewhere outlasts any sight of the day, so + * a stay that ends on the same day goes first and the hotel only when there is + * nothing else. + */ export function defaultAnchor(stays: JourneyDayStay[]): number | null { + const placed = stays.filter((stay) => stay.lat !== null && stay.lng !== null); + const sameDay = placed.filter((stay) => stay.startedAt.slice(0, 10) === stay.endedAt.slice(0, 10)); let best: JourneyDayStay | null = null; - for (const stay of stays) { - if (stay.lat === null || stay.lng === null) continue; + for (const stay of sameDay.length > 0 ? sameDay : placed) { if (!best || stay.durationMinutes > best.durationMinutes) best = stay; } return best?.suggestionId ?? null; diff --git a/client/tests/helpers/journeyRecording.ts b/client/tests/helpers/journeyRecording.ts index 6163ee1c1..1c9749e02 100644 --- a/client/tests/helpers/journeyRecording.ts +++ b/client/tests/helpers/journeyRecording.ts @@ -94,7 +94,8 @@ export function buildDayStays(over: Partial = {}): JourneyDaySt endedAt: '2026-05-14T08:40:00+02:00', durationMinutes: 10, sourceStatus: 'suggested', - confidenceBand: 'low', + // Doubtful enough to ask about, not so doubtful that the review leaves it out. + confidenceBand: 'medium', }), buildDayStay(), buildDayStay({