diff --git a/client/src/pages/TeslaTripPlanner.tsx b/client/src/pages/TeslaTripPlanner.tsx
index a12c1b5..faa3d11 100644
--- a/client/src/pages/TeslaTripPlanner.tsx
+++ b/client/src/pages/TeslaTripPlanner.tsx
@@ -702,10 +702,11 @@ function ChargerSwapBlock({ options, onPick }: { options: ChargerOption[]; onPic
);
}
-function StopExpansion({ stop, onSwap, onRemove }: {
+function StopExpansion({ stop, onSwap, onRemove, onCustomise }: {
stop: Stop;
onSwap: (alt: AlternativeStop) => void;
onRemove: () => void;
+ onCustomise: () => void;
}) {
const isCharge = stop.type === 'supercharger' || stop.type === 'destination-charger';
const arrive = typeof stop.estArrivalBattery === 'number' ? stop.estArrivalBattery : null;
@@ -821,7 +822,7 @@ function StopExpansion({ stop, onSwap, onRemove }: {
)}
- {active && }
+ {active && }
@@ -1029,7 +1031,7 @@ function makePinIcon(color: string, active: boolean, hover: boolean): L.DivIcon
function TopBar({
origin, destination, onOriginChange, onDestinationChange, onODCommit,
chatInput, setChatInput, onChatSubmit, chips, onRemoveChip,
- vehicle, onVehicleChange, grokStatus,
+ vehicle, onVehicleChange, grokStatus, onOpenGpx,
}: {
origin: string; destination: string;
onOriginChange: (v: string) => void;
@@ -1040,6 +1042,7 @@ function TopBar({
chips: string[]; onRemoveChip: (i: number) => void;
vehicle: typeof VEHICLES[number]; onVehicleChange: (v: typeof VEHICLES[number]) => void;
grokStatus: { label?: string };
+ onOpenGpx: () => void;
}) {
return (
- toast.success('GPX exported for your Tesla')}>
+ onOpenGpx()}>
Export
@@ -1199,6 +1202,12 @@ export default function TeslaTripPlanner() {
const [selectedVariant, setSelectedVariant] = useState('fast');
const [variantSwitching, setVariantSwitching] = useState(false);
const [draggingId, setDraggingId] = useState(null);
+ const [modal, setModal] = useState<
+ | { kind: 'customise'; stopId: string }
+ | { kind: 'detour'; afterStopId?: string }
+ | { kind: 'gpx' }
+ | null
+ >(null);
React.useEffect(() => {
fetch('/api/grok/status').then(r => r.json()).then(setGrokStatus).catch(() => {});
@@ -1300,6 +1309,53 @@ export default function TeslaTripPlanner() {
}
};
+ const updateStop = (stopId: string, patch: Partial) => {
+ const next = structuredClone(itinerary);
+ for (const d of next.days) {
+ const idx = d.stops.findIndex(s => s.id === stopId);
+ if (idx !== -1) {
+ d.stops[idx] = { ...d.stops[idx], ...patch };
+ break;
+ }
+ }
+ setItinerary(next);
+ };
+
+ const insertDetour = (place: { name: string; lat: number; lng: number; type: StopType; description?: string }, afterStopId?: string) => {
+ const next = structuredClone(itinerary);
+ const newStop: Stop = {
+ id: `detour-${Date.now()}`,
+ name: place.name,
+ type: place.type,
+ lat: place.lat,
+ lng: place.lng,
+ day: 1,
+ order: 1,
+ description: place.description,
+ combo: null,
+ };
+ // Find insertion point — after afterStopId or at the end of the first day
+ if (afterStopId) {
+ for (const d of next.days) {
+ const idx = d.stops.findIndex(s => s.id === afterStopId);
+ if (idx !== -1) {
+ newStop.day = d.day;
+ d.stops.splice(idx + 1, 0, newStop);
+ break;
+ }
+ }
+ } else if (next.days.length > 0) {
+ const lastDay = next.days[next.days.length - 1];
+ newStop.day = lastDay.day;
+ lastDay.stops.push(newStop);
+ } else {
+ next.days = [{ day: 1, stops: [newStop] }];
+ }
+ for (const d of next.days) d.stops.forEach((s, i) => { s.order = i + 1; });
+ setItinerary(next);
+ toast.success(`Added ${place.name} to your trip`);
+ };
+
const reorderStops = (dragId: string, targetId: string) => {
if (dragId === targetId) return;
const next = structuredClone(itinerary);
@@ -1413,6 +1469,7 @@ export default function TeslaTripPlanner() {
chips={chips} onRemoveChip={(i) => setChips(chips.filter((_, idx) => idx !== i))}
vehicle={vehicle} onVehicleChange={setVehicle}
grokStatus={grokStatus}
+ onOpenGpx={() => setModal({ kind: 'gpx' })}
/>
{variants.length > 0 && (
@@ -1543,6 +1600,7 @@ export default function TeslaTripPlanner() {