/* ============================================================================ GC · LASTFÖNSTER (vy) — fönsterlistan på Last-fliken ---------------------------------------------------------------------------- All logik ligger i lastfonster.js (lw*) och delas med inställningssidan. Den här filen är bara Tailwind-renderingen i GC-formspråket. Kravlista: DESIGN_KRAVLISTA_lastfonster_2026-09-02 p1–p3, p5. ========================================================================== */ const LW_F = 'w-full rounded-lg border border-slate-700 bg-slate-800/70 px-2 py-1.5 text-sm font-bold text-slate-100 outline-none focus:border-slate-500'; const LW_L = 'mb-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-500'; const LW_PILL = (on) => `rounded-lg border px-2 py-1 text-[11px] font-bold transition-colors ${ on ? 'border-lime-400 bg-lime-500/15 text-lime-300' : 'border-slate-700 bg-slate-800/70 text-slate-400 hover:text-slate-200'}`; function GcWindowRow({ win, idx, count, err, measured, reportsPower, showMeasured, load, onChange, onRemove }){ const w = { ...window.LW_WIN_DEFAULT, ...(win || {}) }; const days = Array.isArray(w.days) ? w.days : []; const span = window.lwSpan(w); const [open, setOpen] = React.useState(false); const toggleDay = (d) => { const next = days.indexOf(d) >= 0 ? days.filter(x => x !== d) : days.concat([d]).sort((a, b) => a - b); onChange({ days: next }); }; const hist = (measured && Array.isArray(measured.history)) ? measured.history.slice(0, 10) : []; const mTxt = window.lwRunsText(measured); const planned = showMeasured && reportsPower ? window.lwPlanned(measured, load, w) : null; const prog = showMeasured && reportsPower ? window.lwProgress(measured) : null; const running = !!(measured && measured.running === true); return (
Fönster {idx + 1} {count > 1 && ( )}
{window.LW_DAY_SHORT.map((s, d) => ( ))}
Tidigast
onChange({ earliest: e.target.value })} className={LW_F} style={{ ...window.GC_MONO, colorScheme: 'dark' }}/>
Senast klar
onChange({ latest: e.target.value })} className={LW_F} style={{ ...window.GC_MONO, colorScheme: 'dark' }}/>
Körtid (h)
onChange({ duration_hours: Math.max(0.25, Number(e.target.value) || 0.25) })} className={LW_F} style={{ ...window.GC_MONO, colorScheme: 'dark' }}/>
{w.mode === 'split' && ( )}
{w.solar_only === true &&
{window.LW_HELP_SOLAR}
} {span && span.wrap &&
{window.LW_HELP_WRAP}
} {err &&
{err}
} {/* Vad HEMS planerar med (p5, Andrés beslut 3/9). Blocket finns bara när backend svarar med enabled:true — är flaggan av (eller endpointen inte deployad) visas INGENTING enligt spec §5. */} {showMeasured && (
{!reportsPower ? (
Slå på förbrukningsläsning för att mäta
) : ( {planned ? (
Planerar med {window.lwNum(planned.kwh, planned.kwh < 10 ? 2 : 1)} kWh {window.LW_SRC_TXT[planned.src] || planned.src}
) : (
HEMS planerar efter inlärt värde
)} {prog && (
Inlärning {prog.n} av {prog.need} körningar{running ? ' · mäter nu' : ''}
)}
{mTxt || (running ? 'Mäter körningen just nu…' : 'Ingen körning mätt ännu')} {hist.length > 0 && ( )}
)}
)} {showMeasured && open && hist.length > 0 && (
{hist.map((h, i) => { const ex = h && h.excluded ? String(h.excluded) : null; return (
{h && h.day_key ? String(h.day_key).slice(5) : '—'} {h && h.kwh != null ? window.lwNum(h.kwh, 2) + ' kWh' : '—'} {ex && {window.LW_EXCL_TXT[ex] || 'räknas ej'}}
); })}
)}
); } function GcWindowList({ load, wins, errs, rowErrs, runs, onChange }){ const list = Array.isArray(wins) ? wins : []; const max = window.LW_MAX_ROWS; const reportsPower = load && load.reports_power === true; const showMeasured = !!(runs && runs.enabled === true); const upd = (i, patch) => onChange(list.map((w, k) => k === i ? { ...w, ...patch } : w)); const add = () => { if (list.length >= max) return; onChange(list.concat([window.lwNextFreeWindow(list)])); }; /* Minst ett fönster krävs (V6): backend avvisar windows: []. Sista raden går därför inte att ta bort — knappen finns inte, i stället för att finnas och ge 422. */ const del = (i) => { if (list.length <= 1) return; onChange(list.filter((_, k) => k !== i)); }; return (
{list.map((w, i) => ( upd(i, p)} onRemove={() => del(i)}/> ))} {list.length < max ? ( ) : (
Max {max} fönster
)}
); } Object.assign(window, { GcWindowRow, GcWindowList });