mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(reservations): keep a car booking's stops when it is saved on a phone
The desktop form has offered stops between pick-up and return since the road trip release (#1797). The phone sheet never knew about them: its car branch rebuilt the endpoint list from from/to alone, and because the server replaces every endpoint row of a booking, saving there silently dropped whatever the desktop had set. Its car branch now reads the stop endpoints back into an editor and writes them out again, the same shape TransportModal uses: location, time, reorder arrows, remove.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Bike, Bus, Car, CarTaxiFront, Check, Plane, Plus, Route, Sailboat, Ship, Train, TrainFront, TramFront, Trash2 } from 'lucide-react'
|
||||
import { Bike, Bus, Car, CarTaxiFront, Check, ChevronDown, ChevronUp, Plane, Plus, Route, Sailboat, Ship, Train, TrainFront, TramFront, Trash2, X } from 'lucide-react'
|
||||
import MSheet from '../../../components/MSheet'
|
||||
import { useAddonStore } from '../../../../store/addonStore'
|
||||
import { useTranslation } from '../../../../i18n'
|
||||
@@ -70,6 +70,16 @@ function locationFromEndpoint(e: ReservationEndpoint | undefined): LocationPoint
|
||||
return { name: e.name, lat: e.lat, lng: e.lng, address: null }
|
||||
}
|
||||
|
||||
// The places a driver planned between pick-up and return (#1797), the same shape the
|
||||
// desktop TransportModal edits. Without an editor here the save below would rebuild the
|
||||
// endpoint list from from/to alone and the server, which replaces every endpoint row of a
|
||||
// booking, would drop the stops a phone never showed.
|
||||
interface CarStopForm {
|
||||
location: LocationPoint | null
|
||||
time: string
|
||||
}
|
||||
const emptyCarStop = (): CarStopForm => ({ location: null, time: '' })
|
||||
|
||||
// A flight is an ordered list of airports; N waypoints = N-1 legs. The origin
|
||||
// only departs, the destination only arrives, each stop does both.
|
||||
interface WaypointForm {
|
||||
@@ -154,6 +164,7 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
const [toPick, setToPick] = useState<EndpointPick>({})
|
||||
const [waypoints, setWaypoints] = useState<WaypointForm[]>([emptyWaypoint(), emptyWaypoint()])
|
||||
const [trainWaypoints, setTrainWaypoints] = useState<StationWaypointForm[]>([emptyStationWaypoint(), emptyStationWaypoint()])
|
||||
const [carStops, setCarStops] = useState<CarStopForm[]>([])
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([])
|
||||
// Travelers assigned to this booking (#1517) — seeded from the editing
|
||||
// reservation on open, persisted separately after the save resolves.
|
||||
@@ -296,6 +307,17 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
setToPick({ location: locationFromEndpoint(to) || undefined })
|
||||
setWaypoints([emptyWaypoint(), emptyWaypoint()])
|
||||
setTrainWaypoints([emptyStationWaypoint(), emptyStationWaypoint()])
|
||||
// Stops persist for every type; only a car offers an editor for them, so only a
|
||||
// car reads them back into one. The others keep passing theirs through untouched.
|
||||
setCarStops(
|
||||
src.type === 'car'
|
||||
? (src.endpoints ?? [])
|
||||
.filter(e => e.role === 'stop')
|
||||
.slice()
|
||||
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
||||
.map(e => ({ location: locationFromEndpoint(e), time: e.local_time ?? '' }))
|
||||
: [],
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setForm({ ...EMPTY, start_day_id: transportModalDayId ?? '', end_day_id: transportModalDayId ?? '' })
|
||||
@@ -303,6 +325,7 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
setToPick({})
|
||||
setWaypoints([emptyWaypoint(transportModalDayId ?? ''), emptyWaypoint(transportModalDayId ?? '')])
|
||||
setTrainWaypoints([emptyStationWaypoint(transportModalDayId ?? ''), emptyStationWaypoint(transportModalDayId ?? '')])
|
||||
setCarStops([])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showTransportModal])
|
||||
@@ -311,6 +334,16 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
const prefill = snap.prefill
|
||||
const set = (field: keyof typeof EMPTY, value: string | number) => setForm(prev => ({ ...prev, [field]: value }))
|
||||
|
||||
const moveCarStop = (index: number, delta: number): void => {
|
||||
setCarStops(prev => {
|
||||
const to = index + delta
|
||||
if (to < 0 || to >= prev.length) return prev
|
||||
const next = [...prev]
|
||||
;[next[index], next[to]] = [next[to], next[index]]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toggleTraveler = (id: number) => setTravelerIds(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
@@ -491,7 +524,14 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
})
|
||||
} else {
|
||||
if (fromPick.location) endpoints.push(endpointFromLocation(fromPick.location, 'from', 0, startDate, form.departure_time || null))
|
||||
const stops = keepTransit
|
||||
// A car writes the stops the driver planned; every other type keeps passing the
|
||||
// itinerary's transfer stops through while the route is unchanged (#1065).
|
||||
const carEndpoints = form.type === 'car'
|
||||
? carStops
|
||||
.filter(s => s.location)
|
||||
.map((s, i) => endpointFromLocation(s.location!, 'stop', i + 1, startDate, s.time || null))
|
||||
: []
|
||||
const stops = keepTransit && form.type !== 'car'
|
||||
? prevEndpointsAll.filter(ep => ep.role === 'stop').slice().sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
|
||||
: []
|
||||
stops.forEach((s, i) => endpoints.push({
|
||||
@@ -499,7 +539,9 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
lat: s.lat, lng: s.lng, timezone: s.timezone ?? null,
|
||||
local_date: s.local_date ?? null, local_time: s.local_time ?? null,
|
||||
}))
|
||||
if (toPick.location) endpoints.push(endpointFromLocation(toPick.location, 'to', stops.length + 1, endDate, form.arrival_time || null))
|
||||
carEndpoints.forEach(e => endpoints.push(e))
|
||||
const stopCount = stops.length + carEndpoints.length
|
||||
if (toPick.location) endpoints.push(endpointFromLocation(toPick.location, 'to', stopCount + 1, endDate, form.arrival_time || null))
|
||||
}
|
||||
|
||||
const flightDepDay = firstWp && firstWp.depDayId ? Number(firstWp.depDayId) : null
|
||||
@@ -890,6 +932,72 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
|
||||
<Eyebrow className="mb-[5px] mt-3 uppercase">{t('reservations.meta.to')}</Eyebrow>
|
||||
<LocationSelect value={toPick.location || null} onChange={l => setToPick({ location: l || undefined })} />
|
||||
|
||||
{/* Stops along the drive — cars only (#1797). The rental frame above stays the
|
||||
pick-up and return; these are the places in between, in order. */}
|
||||
{form.type === 'car' && (
|
||||
<>
|
||||
<Eyebrow className="mb-[5px] mt-3 uppercase">{t('roadtrip.stops.label')}</Eyebrow>
|
||||
<div className="flex flex-col gap-2">
|
||||
{carStops.map((stop, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
{/* The order of these stops IS the route: `sequence` is the array
|
||||
index at save time. Arrows rather than dragging, because the
|
||||
row already carries a location picker and a time picker. */}
|
||||
{carStops.length > 1 && (
|
||||
<div className="flex shrink-0 flex-col">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => moveCarStop(i, -1)}
|
||||
disabled={i === 0}
|
||||
aria-label={t('dayplan.moveUp')}
|
||||
className="flex items-center px-1 py-[2px] text-m-muted disabled:opacity-30"
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2.2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => moveCarStop(i, 1)}
|
||||
disabled={i === carStops.length - 1}
|
||||
aria-label={t('dayplan.moveDown')}
|
||||
className="flex items-center px-1 py-[2px] text-m-muted disabled:opacity-30"
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2.2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<LocationSelect
|
||||
value={stop.location}
|
||||
onChange={l => setCarStops(prev => prev.map((s, j) => (j === i ? { ...s, location: l || null } : s)))}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[92px] shrink-0">
|
||||
<CustomTimePicker
|
||||
value={stop.time}
|
||||
onChange={v => setCarStops(prev => prev.map((s, j) => (j === i ? { ...s, time: v } : s)))}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCarStops(prev => prev.filter((_, j) => j !== i))}
|
||||
aria-label={t('roadtrip.stops.remove')}
|
||||
className="flex shrink-0 items-center p-1 text-m-muted"
|
||||
>
|
||||
<X size={15} strokeWidth={2.2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCarStops(prev => [...prev, emptyCarStop()])}
|
||||
className="flex w-full items-center justify-center gap-[5px] rounded-full border-[1.5px] border-dashed border-[color:var(--m-rowbr)] py-2 font-geist text-[0.6875rem] font-semibold text-m-muted"
|
||||
>
|
||||
<Plus size={12} strokeWidth={2.2} /> {t('reservations.layover.addStop')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Departure row */}
|
||||
<div className="mt-3 flex gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
Reference in New Issue
Block a user