+
{formatKm(leg.distanceKm)}
·
{formatDuration(leg.durationMin)} drive
+ {arrivalSoC != null && (
+ <>
+
·
+
+ arrive {Math.max(0, Math.round(arrivalSoC))}%
+
+ >
+ )}
+ {risk === 'danger' && (
+
+ won't reach
+
+ )}
+ {risk === 'warn' && (
+
+ tight
+
+ )}
);
}
@@ -2333,6 +2369,21 @@ export default function TeslaTripPlanner() {
const isDriving = forceDrivingMode || (
!!tesla.state?.shiftState && tesla.state.shiftState !== 'P'
);
+ // Project SoC through the itinerary so we can flag risky legs.
+ const batteryPlan = React.useMemo(() => {
+ if (allStops.length === 0) return null;
+ const startSoC = tesla.state?.battery ?? 80;
+ // 85% of rated range is a realistic motorway figure (HVAC, motorway speeds, weather).
+ const effectiveRangeKm = (vehicle.rangeKm || 400) * 0.85;
+ return computeBatteryPlan(allStops, legs, startSoC, effectiveRangeKm);
+ }, [allStops, legs, tesla.state?.battery, vehicle.rangeKm]);
+
+ const batteryByStop = React.useMemo(() => {
+ const m = new Map
();
+ if (batteryPlan) for (const entry of batteryPlan.perStop) m.set(entry.stopId, entry);
+ return m;
+ }, [batteryPlan]);
+
// Closest planned stop to the car's current position, used by driving mode.
const nextStop = React.useMemo(() => {
if (!isDriving || !tesla.state?.lat || !tesla.state?.lng) return null;
@@ -2627,6 +2678,46 @@ export default function TeslaTripPlanner() {
+ {/* Battery nudge — only fires when the plan would actually leave the driver stranded. */}
+ {batteryPlan?.hasDanger && allStops.length > 0 && (
+