fix(roadtrip): keep a booked night out of the day list, and off the traveller's stops

Three things the first pass got wrong.

A stop a booking puts on the day is for road trip mode, which needs the
hotel in the driving chain. The day planner already shows that booking as
its own overnight block, so the row there would be the same hotel twice.
It is hidden in Days whatever the service-stop switch says, and the switch
goes back to meaning what it says: the petrol stations the traveller added.

Cancelling a night from the road trip popup took the stop with it. That
control means "this is a pause now, not a night", and the stop sits
mid-drive where re-adding it would land it at the end of the day. The
booking now hands the stop over instead of deleting it.

Moving a booking whose stop belongs to the traveller put a second one on
the new day and left the first behind, which is the duplicate this change
exists to remove. Only a booking's own stop moves now. Stays booked before
this shipped get theirs from the migration, not from the next edit they
happen to receive.
This commit is contained in:
Maurice
2026-09-13 01:42:40 +02:00
parent 462a208fdf
commit edf0538260
7 changed files with 110 additions and 24 deletions
+1 -1
View File
@@ -1428,7 +1428,7 @@ export const accommodationsApi = {
list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/accommodations`).then(r => r.data),
create: (tripId: number | string, data: AccommodationCreateRequest) => apiClient.post(`/trips/${tripId}/accommodations`, data).then(r => r.data),
update: (tripId: number | string, id: number, data: AccommodationUpdateRequest) => apiClient.put(`/trips/${tripId}/accommodations/${id}`, data).then(r => r.data),
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/accommodations/${id}`).then(r => r.data),
delete: (tripId: number | string, id: number, opts?: { keepStop?: boolean }) => apiClient.delete(`/trips/${tripId}/accommodations/${id}`, opts?.keepStop ? { params: { keepStop: 'true' } } : undefined).then(r => r.data),
}
export const dayNotesApi = {
+26 -5
View File
@@ -55,9 +55,10 @@ import { usePlannerHistory } from '../../hooks/usePlannerHistory'
import { useAirtrailConnection } from '../../hooks/useAirtrailConnection'
import { useIsTouch } from '../../hooks/useIsTouch'
import { usePluginStore } from '../../store/pluginStore'
import type { Accommodation, TripMember, Day, Place, Reservation } from '../../types'
import type { Accommodation, Assignment, TripMember, Day, Place, Reservation } from '../../types'
import { OFM_POSITRON, DEFAULT_MAP_LAT, DEFAULT_MAP_LNG, DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults'
import { useTileUrl } from '../../hooks/useTileUrl'
import { applyStayStops } from '../../store/stayStops'
import { resolvePoolAssignmentId } from './tripPlannerModel'
import { isDeepLinkableTripTab, TRIP_TAB_LABEL_KEYS } from '../../constants/tripTabs'
import { isRoutableReservation } from '../../utils/reservationRoutes'
@@ -128,9 +129,27 @@ export function useTripPlanner() {
// someone planning a road trip stays in it across reloads without it leaking into
// their next, non-driving trip.
const [roadtripMode, setRoadtripMode] = useState<boolean>(() => sessionStorage.getItem(`trip-roadtrip-${tripId}`) === '1')
const assignments = useMemo(() => roadtripMode || roadtripSettings.roadtrip_service_stops_in_days !== false ? storedAssignments : Object.fromEntries(
Object.entries(storedAssignments).map(([dayId, visits]) => [dayId, visits.filter(visit => !isServiceStopType(visit.place?.stop_type))]),
), [roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, storedAssignments])
// Two reasons a stop can be road-trip-only, and they are not the same reason.
//
// The switch is the traveller's: it hides the petrol stations and rest areas
// they added along the drive from a day list they want to read as a plan.
//
// A stop a lodging booking put there is hidden whatever the switch says,
// because the day already shows that booking as its own overnight block and
// the row would be the same hotel a second time. Road trip mode wants it: the
// drive has to end somewhere, and that somewhere is where you sleep.
const assignments = useMemo(() => {
if (roadtripMode) return storedAssignments
const hideServiceStops = roadtripSettings.roadtrip_service_stops_in_days === false
const hidden = (visit: Assignment) => visit.accommodation_id != null
|| (hideServiceStops && isServiceStopType(visit.place?.stop_type))
// Same object back when nothing is hidden, so a trip without bookings does not
// rebuild every day list on each render of this hook.
if (!Object.values(storedAssignments).some(visits => visits.some(hidden))) return storedAssignments
return Object.fromEntries(
Object.entries(storedAssignments).map(([dayId, visits]) => [dayId, visits.filter(v => !hidden(v))]),
)
}, [roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, storedAssignments])
const places = useMemo(() => roadtripMode || roadtripSettings.roadtrip_service_stops_in_days !== false ? allPlaces : allPlaces.filter(place => !isServiceStopType(place.stop_type)), [roadtripMode, roadtripSettings.roadtrip_service_stops_in_days, allPlaces])
const toggleRoadtripMode = useCallback(() => {
setRoadtripMode(prev => {
@@ -937,7 +956,9 @@ export function useTripPlanner() {
if (stopDraft.editing) {
await tripActions.updatePlace(tripId, stopDraft.editing.placeId, { stop_type: stopType, duration_minutes: dwellMinutes })
if (stopDraft.editing.accommodationId) {
await accommodationsApi.delete(tripId, stopDraft.editing.accommodationId)
// The night is what was switched off, not the stop: it stays where it is
// in the drive and becomes an ordinary pause.
applyStayStops(await accommodationsApi.delete(tripId, stopDraft.editing.accommodationId, { keepStop: true }))
await loadAccommodations()
}
updateRouteForDay(dayId)