/* charger-test.jsx — laddboxtest per laddbox (TILL_DESIGN_laddboxtest_2026-09-29, SPEC_laddboxtest v2, André 29/9).
CtCustomer: kundens block överst i laddboxen (K1–K4, K6). CtMark: märkningarna vid batterivalen i Bil-fliken (K5).
CtAdmin: Driftcentralens panel Kund (D1–D4). Skäl och villkorstexter kommer från servern och visas som de är.
Ström per box finns inte i mätningen (bara ev_w), så sidan visar uppmätt effekt och den ström HEMS begär, och räknar inte fram ampere. */
(function(){
const {useState,useEffect,useRef}=React;
const CT=()=>window.HemsCT;
const ORDER_ALL=['max','dynamic','min','stop','restart','battery_conditions','pause','house_only'];
const ORDER_BAT=['battery_conditions','pause','house_only'];
const LBL={max:'Högsta ström',dynamic:'Ändrad ström',min:'Lägsta ström',stop:'Stopp',restart:'Omstart',bat_probe:'Förbereder batteridelen',battery_conditions:'Batteriet: villkor',pause:'Paus',house_only:'Täck huset',done:'Avslutar'};
const LIM={box:'boxen','säkring':'säkringen',effekttak:'effekttaket',bil_eller_box:'bilen eller boxen'};
const RES={ok:'OK',fail:'Fel',not_tested:'Kunde inte testas'};
const num=(v)=>v!=null&&v!==''&&Number.isFinite(Number(v))?Number(v):null;
const dec=(v,d)=>String(Math.round(v*Math.pow(10,d))/Math.pow(10,d)).replace('.',',');
const W=(w)=>{ const n=num(w); if(n==null) return null; return Math.abs(n)>=1000?dec(n/1000,1)+' kW':Math.round(n)+' W'; };
const A=(a)=>{ const n=num(a); return n==null?null:dec(n,1)+' A'; };
const S=(s)=>{ const n=num(s); return n==null?null:Math.round(n)+' s'; };
const ph=(n)=>Number(n)===1?'1 fas':`${n} faser`;
const pad=(n)=>String(n).padStart(2,'0');
const tm=(sec)=>{ const n=num(sec); if(n==null) return '—'; const d=new Date(n*1000); return `${d.getDate()}/${d.getMonth()+1} ${pad(d.getHours())}:${pad(d.getMinutes())}`; };
const hms=(sec)=>{ const n=num(sec); if(n==null) return '—'; const d=new Date(n*1000); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; };
/* v763 (backend 29/9, TILL_DESIGN_laddboxtest_45min): testets längsta tid ur servern. test_max_s i approval, total_max_s i status. Saknas fältet: 45 min. */
const maxMin=(ap)=>{ const s=num(ap&&ap.test_max_s); return s>0?Math.round(s/60):45; };
const mmss=(s)=>{ const n=Math.max(0,Math.round(num(s)||0)); return `${Math.floor(n/60)}:${pad(n%60)}`; };
/* v761 (TILL_DESIGN_laddboxtest_nedrakning 29/9): tider per steg. Saknas ett fält visas inte delen. response_s null i paus/täck huset = växelriktaren följde aldrig. */
const up=(t)=>t?t[0].toUpperCase()+t.slice(1):t;
const tLine=(s)=>[num(s.response_s)!=null&&`svar efter ${S(s.response_s)}`,num(s.stable_s)!=null&&`jämn efter ${S(s.stable_s)}`].filter(Boolean).join(' · ')||null;
const bLine=(s)=>{ const p=[s.response_s===null?'inget svar':num(s.response_s)!=null&&`svar efter ${S(s.response_s)}`,num(s.stable_s)!=null&&`bedömd efter ${S(s.stable_s)}`].filter(Boolean); return p.length?'Efter sändningen: '+p.join(' · '):null; };
const cmdTxt=(c)=>{ if(!c) return null; const m=/^charge@(\d+(?:\.\d+)?)$/.exec(c); if(m) return dec(Number(m[1]),1)+' A'; return c==='stop'?'stopp':c; };
const bcmdTxt=(c)=>{ if(!c) return null; const m=/^(pause|selfconsumption)@(-?\d+(?:\.\d+)?)$/.exec(c); if(!m) return c; return m[1]==='pause'?'paus':'täck huset '+dec(Number(m[2]),2)+' kW'; };
const loadsTxt=(l)=>l&&typeof l==='object'&&Object.keys(l).length?Object.entries(l).map(([k,v])=>`${k} ${W(v)}`).join(', '):null;
const stTxt=(st,n)=>st==='approved'?`Godkänd på ${ph(n)}`:st==='not_approved'?`Ej godkänd på ${ph(n)}`:`${ph(n)}: ej testad`;
const stCls=(st)=>st==='approved'?'ok':st==='not_approved'?'bad':'';
const rCls=(r)=>r==='ok'?'ok':r==='fail'?'bad':'nt';
const msgOf=(e,fb)=>e&&e.message&&!/^HTTP \d+$/.test(e.message)?e.message:fb;
const statusIn=(ap,n)=>n===num(ap.phases)?(ap.status||'untested'):((ap.other_phases||{})[String(n)]||'untested');
const firstFail=(steps)=>{ const s=steps||{}; for(const k of ORDER_ALL){ if(s[k]&&s[k].result==='fail') return s[k].reason||null; } return null; };
function Btn({skin,pri,wide,children,onClick,disabled}){ return ; }
/* v758 (André 29/9: "Gör denna snyggare. ser väldigt konstig ut"): kundens status som rubrik med fasläget som etikett, samma rad om testet som i Bil-fliken. */
function CtHero({ap,n,res}){ const st=statusIn(ap,n), o=n===1?3:1, ost=statusIn(ap,o), e=res&&res.by_phases&&res.by_phases[String(n)], r=e&&e.last_result;
const why=st==='not_approved'&&r?firstFail(r.steps):null, grace=st==='untested'?num(ap.grace_days_left):null;
const title=st==='approved'?'Laddboxen är godkänd':st==='not_approved'?'Laddboxen är inte godkänd':'Laddboxen är inte testad';
const days=(g)=>`${g} ${g===1?'dag':'dagar'}`;
/* v759 (André 29/9: "Den behöver vara tydligare att detta är något som behöver göras. nu ser det ut som en vanlig inställning"): otestad box som en uppgift. */
if(st==='untested') return
!
Testa din laddbox{`Inte testad på ${ph(n)}`}{grace>0?{days(grace)} kvar:ap.gate===true?Fast ström:null}
{(ost==='approved'||ost==='not_approved')&&
{stTxt(ost,o)}
}
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
{grace>0&&
Testa inom {days(grace)}, annars laddar HEMS bara på fast ström.
}
{!(grace>0)&&ap.gate===true&&
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
}
;
return
{title}{ph(n)}
{(ost==='approved'||ost==='not_approved')&&
{stTxt(ost,o)}
}
{st==='untested'&&
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
}
{why&&
{why}
}
{st!=='untested'&&r&&num(r.finished_at)!=null&&
Testad {tm(r.finished_at)} av {r.started_by&&r.started_by.kind==='support'?'support':'dig'}
}
{grace>0&&
Testa inom {grace} dagar, annars laddar HEMS bara på fast ström.
}
{st!=='approved'&&!(grace>0)&&ap.gate===true&&
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
}
; }
/* K4: stegens värden. Saknas ett fält visas inte raden (spec §4: fält kan saknas när steget inte hann mäta). */
function stepLines(k,s,set){ const L=[], add=(t,cls)=>{ if(t) L.push({t,cls}); }; if(!s) return L;
if((s.result==='fail'||s.result==='not_tested')&&s.reason) add(s.reason,s.result==='fail'?'bad':'');
if(k==='max'&&s.result==='ok'&&num(s.measured_a)!=null) add(`Högsta ${A(s.measured_a)}${num(s.measured_w)!=null?` (${W(s.measured_w)})`:''}${s.limited_by?`, begränsad av: ${LIM[s.limited_by]||s.limited_by}`:''}`);
if(k==='max') add(up(tLine(s)));
if(k==='dynamic'){ const t=tLine(s); if(t) add(s.result==='ok'&&num(s.target_a)!=null?`Till ${A(s.target_a)}: ${t}`:up(t));
const pu=num(s.phases_used), pc=num(set&&set.phases); if(pu&&pc&&pu!==pc) add(`Bilen laddar på ${ph(pu)}. Din inställning säger ${ph(pc)}.`,'note'); }
if(k==='min'){ if(num(s.measured_min_a)!=null) add(`Lägsta ström ${A(s.measured_min_a)}${num(s.floor_a)!=null?` (golv ${A(s.floor_a)})`:''}`);
if(Array.isArray(s.levels)) s.levels.forEach(v=>{ const t=v&&tLine(v); if(t&&num(v.a)!=null) add(`${A(v.a)}: ${t}`); });
if(num(s.stopped_at_a)!=null&&num(s.measured_min_a)!=null) add(`Bilen slutade ladda på ${A(s.stopped_at_a)} — lägsta som fungerar är ${A(s.measured_min_a)}`,'note');
if(s.resumed===false) add('Bilen återupptog inte laddningen','note');
if(num(s.switched_to_1ph_at_a)!=null) add(`Boxen växlade till 1 fas vid ${A(s.switched_to_1ph_at_a)}${num(s.measured_min_w)!=null?` — lägsta effekt ${W(s.measured_min_w)}`:''}`,'find'); }
if(k==='stop'&&s.result==='ok'&&num(s.time_s)!=null) add(`Stannade efter ${S(s.time_s)}`);
if(k==='restart'){ const t=tLine({response_s:num(s.response_s)!=null?s.response_s:s.time_s,stable_s:s.stable_s}); if(t) add(s.result==='ok'&&num(s.requested_a)!=null?`På ${A(s.requested_a)}: ${t}`:up(t)); if(s.current_check==='not_tested') add('Strömkontrollen kunde inte testas',''); }
if(k==='battery_conditions'&&Array.isArray(s.failed)) s.failed.forEach(f=>add(f&&f.text,'note'));
if(k==='pause'&&s.result==='ok'){ const p=[num(s.discharge_w)!=null&&`Urladdning ${W(s.discharge_w)}`,num(s.grid_w)!=null&&`nät ${W(s.grid_w)}`,num(s.house_w)!=null&&`hus ${W(s.house_w)}`,num(s.ev_w)!=null&&`bil ${W(s.ev_w)}`,num(s.grid_charge_w)!=null&&`laddning från nätet ${W(s.grid_charge_w)}`].filter(Boolean); if(p.length) add(p.join(' · ')); }
if(k==='house_only'){ if(num(s.discharge_w)!=null&&num(s.target_w)!=null) add(`Urladdning ${W(s.discharge_w)} mot hus − sol ${W(s.target_w)}`); const lt=loadsTxt(s.loads_w); if(lt&&num(s.house_w)!=null) add(`Hus ${W(s.house_w)}, varav ${lt}`); }
if((k==='pause'||k==='house_only')&&s.result!=='not_tested'){ const sw=num(s.send_wait_s); if(sw!=null) add(sw>0?`Väntade ${S(sw)} på växelriktarens spärr`:'Skickades direkt'); add(bLine(s)); }
return L; }
/* K1: status för boxens fasläge stort, det andra läget litet om det är testat. */
function CtStatus({ap,n,res}){ const st=statusIn(ap,n), o=n===1?3:1, ost=statusIn(ap,o), e=res&&res.by_phases&&res.by_phases[String(n)];
const why=st==='not_approved'&&e&&e.last_result?firstFail(e.last_result.steps):null, grace=st==='untested'?num(ap.grace_days_left):null;
return
Testa inom {grace} dagar, annars laddar HEMS bara på fast ström.
}
{st!=='approved'&&!(grace>0)&&ap.gate===true&&
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
}
; }
/* K2 och D4: ruta före start. */
function CtAsk({scope,err,busy,onStart,onClose,skin,adm,mins}){ return
{adm?
Testet styr kundens laddbox och batteri i upp till {mins||45} minuter. Kunden ser att support testar.
:<>
Koppla in bilen. Den får inte vara fulladdad.
Testet tar upp till {mins||45} minuter och laddar in ungefär 1–3 kWh.
HEMS styr laddboxen och batteriet under testet. Du kan avbryta när som helst.
>}
{scope==='battery'&&
Bara batteridelen: paus och täck huset medan bilen laddar.
}
{err&&
{err}
}
{busy?'Startar…':'Starta testet'}Avbryt
; }
/* K3 och D2: förloppet. */
/* v761: fasen i steget som pågår. Serverns text som den är, tid kvar räknad ned lokalt varje sekund från svaret (nästa svar skriver över), stannar på 0:00 tills servern dömt. send = batterikommandot väntar på växelriktarens spärr. */
function CtPhase({st,ago}){ const p=st.phase; if(!p) return null; const max=num(st.phase_max_s), l0=num(st.phase_left_s), left=l0==null?null:Math.max(0,l0-ago), send=p==='send';
return
; }
/* K5: märkningen vid batterivalen i Bil-fliken. Informerar, spärrar inte. Kör batteridelen leder till laddboxen i Inställningar (André 29/9). */
function CtMark({ap,kind}){ if(!ap) return null; const m=(kind==='pause'?ap.pause_mark:ap.house_only_mark)||'not_tested'; if(m==='ok') return null;
if(m==='fail') return
{kind==='pause'?'Fungerar inte med din växelriktare.':'Fungerar inte med din växelriktare — HEMS pausar batteriet i stället.'}
; }
/* v756 (André 29/9: "Att testet inte är kört behöver synas på Bil fliken också. Och länka till inställningar. När testet är kört så ska knappen inte finnas"):
rutan i Bil-fliken när boxen är otestad i sitt fasläge. Godkänd eller ej godkänd = testet är kört, då visas ingenting. Pågår ett test visas ingenting heller. */
function CtBilNote({ap}){ if(!ap||ap.running||(ap.status||'untested')!=='untested') return null;
const n=num(ap.phases), grace=num(ap.grace_days_left), href='settings.html'+(CT().demo()?'?laddtest=demo':'')+'#grp-ev', days=(g)=>`${g} ${g===1?'dag':'dagar'}`;
return
!Testa din laddbox{`Inte testad på ${ph(n||3)}`}
{grace>0?{days(grace)} kvar:ap.gate===true?Fast ström:null}
HEMS provar laddboxen med din bil: högsta och lägsta ström, ändrad ström, stopp och omstart, och om batteriet kan pausas medan bilen laddar. Testet tar upp till {maxMin(ap)} minuter.
{grace>0?
Testa inom {days(grace)}, annars laddar HEMS bara på fast ström.
:ap.gate===true?
HEMS laddar på fast ström och startar/stoppar bara tills boxen är godkänd.
; }
/* D1–D4. Driftcentralen känner inte kundens laddar-id, så boxarna hämtas med de positionsid servern tar emot (ev_1, ev_2; högst två boxar).
404 med egen text = boxen finns inte. 404 utan text = anropet saknas hos servern. */
const mkWho=(nm)=>(sb)=>!sb?'—':sb.kind==='customer'?'kunden':'support'+(sb.admin?` (${nm(sb.admin)})`:'');
function CtLog({rows}){ const [open,setOpen]=useState(false); return