Files
TREK/client/src/components/Roadtrip/useVehicleRange.ts
T
Maurice 96ab748f34 feat(roadtrip): let one stop say how full it fills
The fill level was one figure for the whole trip, and a trip does not have one.
A motorway rapid charger is worth about 80 % because the last fifth costs as
long again; the charger at the hotel is worth all of it because the car stands
there all night. Between those two the difference is a whole leg, and a single
setting has to be wrong about one of them.

So the badge on a refuelling stop is now the way in: it shows what that stop
will actually fill to, its own figure or the traveller's, and opens a panel of
the six answers people actually give. Under them it says what the choice buys in
kilometres, because 80 % is an abstraction and "about 290 km from here" is the
thing being decided. Picking the figure a stop already has hands it back to the
setting, the same gesture the kind picker uses to un-pick a kind.

The value lives in places.fill_percent, beside stop_type, because it is a
property of the stop rather than of the person. Null means "follow the setting",
which is every stop that predates the column and every stop nobody has an
opinion about, so nothing changes for a trip that never touches it.

Two pieces of shared machinery came out of it rather than being copied. The
popover chrome — measure before showing, flip when the window is short, close on
an outside click, Escape or a scroll — is one component now, used by the kind
picker and this one. So is the assembly of the range from seven settings, which
the routes hook and the rail both need and which would have drifted the first
time a field was added.
2026-09-10 21:40:51 +02:00

47 lines
2.5 KiB
TypeScript

import { useMemo } from 'react'
import { useSettingsStore } from '../../store/settingsStore'
import { effectiveRangeKm } from './vehicleRange'
import type { VehicleKind } from './roadtripModel'
/**
* How far the traveller's car goes on one fill, and what kind of car it is.
*
* One place rather than two, because the answer is assembled from seven settings and two
* readers need it: the routes hook, which turns it into the range budget every warning is
* measured against, and the rail, which tells a traveller what a given fill buys at a
* given stop. Assembling it twice is the hand-mirrored state this codebase refuses
* everywhere else, and the two copies would drift the first time a field is added.
*
* Each setting is read as its own primitive so the memo compares numbers. An object
* rebuilt on every render would give the result a new identity every time, and what
* depends on it is the effect that recalculates every day's driving.
*/
export function useVehicleRange(): { vehicleKind: VehicleKind | null; rangeKm: number | null } {
const rangeKm = useSettingsStore(s => s.settings.roadtrip_range_km)
// Validated rather than trusted: a per-user setting gets no server-side check, and an
// unknown word here would silently stop both kinds of stop counting as a fill-up.
const vehicle = useSettingsStore(s => s.settings.roadtrip_vehicle)
const tankLitres = useSettingsStore(s => s.settings.roadtrip_tank_litres)
const litresPer100 = useSettingsStore(s => s.settings.roadtrip_litres_per_100)
const batteryKwh = useSettingsStore(s => s.settings.roadtrip_battery_kwh)
const kwhPer100 = useSettingsStore(s => s.settings.roadtrip_kwh_per_100)
const degradationPercent = useSettingsStore(s => s.settings.roadtrip_battery_degradation)
const vehicleKind: VehicleKind | null =
vehicle === 'combustion' || vehicle === 'electric' ? vehicle : null
// The parts win over the typed number when they add up, because they are the more
// specific answer; the settings dialog shows the result, so it is never a surprise.
// Zero and absent both mean "no limit", which is why the falsy fold happens here once.
const planningKm = useMemo(
() => effectiveRangeKm(
vehicleKind,
{ tankLitres, litresPer100, batteryKwh, kwhPer100, degradationPercent },
rangeKm,
) || null,
[vehicleKind, tankLitres, litresPer100, batteryKwh, kwhPer100, degradationPercent, rangeKm],
)
return useMemo(() => ({ vehicleKind, rangeKm: planningKm }), [vehicleKind, planningKm])
}