/* ============================================================================
GC · LADDHISTORIK — hans sjudagarsblock, uppmätt data
----------------------------------------------------------------------------
GravioApp.jsx BilView avslutas med "Laddhistorik · 7 dagar". Hans rader kom
ur rnd(); våra kommer ur GET /api/ev/history?days=7 (live 16/8), som mäter
ur ev_actual.
Skillnader mot hans block, alla för att kontraktet säger något annat:
1. INGEN BIL, BARA BOX. car_id finns inte i svaret — vi visar laddboxens
namn. Hans "C40"/"G9P" är bilnamn.
2. TRE KÄLLOR, INTE TVÅ. Svaret skiljer solar_kwh, battery_kwh och grid_kwh.
Hans block har bara sol och nät; en laddning ur hembatteriet fanns inte i
hans värld.
3. KOSTNADEN ÄR BACKENDENS. cost_kr = nätdelen × köppris, solar_value_kr =
soldelen × säljpris (alltså vad solen hade gett vid försäljning — hans
"alt."). Vi räknar inte om dem.
4. within_plan och unknown_quarters syns. En laddning utanför styrningen är
inte samma sak som en planerad, och kvartar utan mätvärde ska inte tyst
räknas som noll.
========================================================================== */
const GH_SRC_COL = { sol:'#facc15', 'nät':'#60a5fa', batteri:'#fb923c', blandat:'#a3e635' };
function ghDayLabel(iso, today){
if (!iso) return '—';
const d = new Date(iso + 'T12:00:00');
if (isNaN(d.getTime())) return iso;
const t = today ? new Date(today + 'T12:00:00') : null;
if (t){
const diff = Math.round((t - d) / 86400000);
if (diff === 0) return 'I dag';
if (diff === 1) return 'I går';
}
return d.toLocaleDateString('sv-SE', { weekday:'short', day:'numeric', month:'short' });
}
function GcLaddhistorik({ settings }){
const [state, setState] = React.useState({ loading:true, data:null, err:null });
/* v766: Visa alla och Om siffrorna. Före de tidiga returnerna, så krokarnas ordning står still. */
const [allRows, setAllRows] = React.useState(false);
const [about, setAbout] = React.useState(false);
React.useEffect(() => {
if (!window.hems || !window.hems.getEvHistory){ setState({ loading:false, data:null, err:'saknas' }); return; }
/* Samma mount-race som Prognoser hade: kortet monteras före bootHems satt
nyckeln, och ett kast med noKeyYet är övergående. gcFetchReady väntar. */
return window.gcFetchReady(() => window.hems.getEvHistory(7), {
// null = endpointen svarade inte (t.ex. inte deployad på den rev
// sessionen träffar). Blocket ritas inte alls då — inget felmeddelande
// om något användaren inte kan påverka.
onOk: (d) => setState({ loading:false, data:d, err: d ? null : 'saknas' }),
onFail: (e) => setState({ loading:false, data:null, err: String(e && e.message || e) }),
});
}, []);
const boxName = React.useCallback((id) => {
const list = (window.evModel && window.evModel.normalize) ? window.evModel.normalize(settings) : [];
// Kanoniskt id är ev_1, ev_2 … alltså POSITIONEN. Matcha på id först, annars
// på ordningen — samma regel som ev_power_list och ev_per_car.
const byId = list.findIndex(c => String(c.id) === String(id));
const m = /^ev_(\d+)$/.exec(String(id || ''));
const k = byId >= 0 ? byId : (m ? Number(m[1]) - 1 : -1);
const c = k >= 0 ? list[k] : null;
return c ? ((window.evModel && window.evModel.displayName) ? window.evModel.displayName(c, k) : (c.name || 'Laddbox ' + (k+1)))
: String(id || 'Laddbox');
}, [settings]);
if (state.loading) {
return (
Hämtar laddhistorik…
);
}
if (state.err || !state.data) return null;
const sessions = Array.isArray(state.data.sessions) ? state.data.sessions : [];
/* Identitetsläget (TILL_DESIGN_laddbox_bil_identitet 3/10, D4): raden bär charger_uid, charger_name
och removed. Namnet är serverns, och en borttagen box märks "borttagen". charger_uid null = boxen
var okänd den dagen: då visas charger_id och inget namn. Utan identitet: som förut. */
const ident = !!(window.evModel?.identityMode && window.evModel.identityMode(settings));
const rowName = (r) => {
if (!ident || !r || !('charger_uid' in r)) return boxName(r && r.charger_id);
if (r.charger_uid == null) return String(r.charger_id || '—');
if (r.charger_name) return String(r.charger_name);
const list = window.evModel.normalize(settings);
const k = list.findIndex(c => c.uid === r.charger_uid);
return k >= 0 ? window.evModel.displayName(list[k], k) : String(r.charger_id || '—');
};
const tot = state.data.totals || {};
const today = window.hemsToday ? window.hemsToday() : null;
const kr = (v) => v == null ? '—' : (Math.abs(v) < 10 ? window.gcNum(v, 2) : window.gcNum(v, 0)) + ' kr';
/* Summa och uppdelning måste stänga vid VISAD precision. Med regeln
"under 10 en decimal, däröver heltal" fick totalen och delarna olika
precision — 52,4 skrevs "52" medan totalen 52,7 skrevs "53", och foten
påstod en uppdelning som inte gick ihop. Samma fälla som Statistik,
Avancerat och Last redan städat bort.
Regeln nu: EN precision för hela gruppen, satt av det minsta talet som
ska synas. Delarna rundas, de som blir 0 faller bort, och differensen
mot den avrundade totalen läggs på den största delen. Då summerar det
som står på skärmen alltid till det som står i rubriken — och en
batteridel under upplösningen försvinner inte ur summan, den hamnar
hos den största källan i stället för att tyst ätas. */
const parts = [
{ label:'Sol', v: tot.solar_kwh },
{ label:'batteri', v: tot.battery_kwh },
{ label:'nät', v: tot.grid_kwh },
].filter(p => typeof p.v === 'number' && p.v > 0);
const totKwh = typeof tot.kwh === 'number' ? tot.kwh
: parts.reduce((a,p) => a + p.v, 0);
const dec = parts.concat([{v:totKwh}]).some(p => p.v > 0 && p.v < 10) ? 1 : 0;
const kwh = (v) => window.gcNum(v, dec) + ' kWh';
const rTot = Number(totKwh.toFixed(dec));
const shown = parts.map(p => ({ ...p, r: Number(p.v.toFixed(dec)) })).filter(p => p.r > 0);
if (shown.length){
const diff = Number((rTot - shown.reduce((a,p) => a + p.r, 0)).toFixed(dec));
if (diff) shown.reduce((a,b) => b.r > a.r ? b : a).r += diff;
}
/* v766 (förenklad Bil-flik): summan överst med källorna som en stapel, fyra rader och Visa alla.
Förklaringen ligger under Om siffrorna; raden om kvartar utan mätvärde syns alltid när den gäller.
Boxens namn står på raden när historiken har flera boxar eller en borttagen. */
const colOf = (label) => ({ Sol: GH_SRC_COL.sol, batteri: GH_SRC_COL.batteri, 'nät': GH_SRC_COL['nät'] })[label] || '#6B7A94';
const boxIds = new Set(sessions.map(r => String((r && (r.charger_uid || r.charger_id)) || '')));
const multi = boxIds.size > 1 || (ident && sessions.some(r => r && r.removed === true));
const list = allRows ? sessions : sessions.slice(0, 4);
return (
Uppmätt laddning, inte plan. Kostnaden är nätdelen gånger köppriset;
solvärdet är vad soldelen hade gett vid försäljning. Kvartar utan
mätvärde räknas inte med.