diff --git a/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx b/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx index f96886d85..01daa1878 100644 --- a/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx +++ b/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx @@ -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({}) const [waypoints, setWaypoints] = useState([emptyWaypoint(), emptyWaypoint()]) const [trainWaypoints, setTrainWaypoints] = useState([emptyStationWaypoint(), emptyStationWaypoint()]) + const [carStops, setCarStops] = useState([]) const [pendingFiles, setPendingFiles] = useState([]) // 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 {t('reservations.meta.to')} 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' && ( + <> + {t('roadtrip.stops.label')} +
+ {carStops.map((stop, i) => ( +
+ {/* 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 && ( +
+ + +
+ )} +
+ setCarStops(prev => prev.map((s, j) => (j === i ? { ...s, location: l || null } : s)))} + /> +
+
+ setCarStops(prev => prev.map((s, j) => (j === i ? { ...s, time: v } : s)))} + /> +
+ +
+ ))} + +
+ + )} + {/* Departure row */}