/* ============================================================================ INSTÄLLNINGAR · LASTFÖNSTER — fönsterlistan i settings-formspråket ---------------------------------------------------------------------------- Samma logik som Last-fliken (lastfonster.js, lw*), samma regler, samma texter — men renderad med settings.css i stället för Tailwind. Två editorer får aldrig validera olika; därför delar de logiken och skiljer sig bara i utseende. Kravlista: DESIGN_KRAVLISTA_lastfonster_2026-09-02 p1–p3, p5. ========================================================================== */ const SLW_BOX = { border:'1px solid rgba(255,255,255,.07)', borderRadius:8, padding:'10px 10px 8px', marginBottom:8, background:'rgba(255,255,255,.015)' }; const SLW_BOX_BAD = { ...SLW_BOX, borderColor:'rgba(255,107,107,.45)', background:'rgba(255,107,107,.05)' }; const SLW_DAY = (on) => ({ font:'inherit', fontSize:11.5, fontWeight:700, flex:1, padding:'5px 0', cursor:'pointer', borderRadius:6, border: on ? '1px solid rgba(91,168,255,.55)' : '1px solid var(--line)', background: on ? 'rgba(91,168,255,.18)' : 'transparent', color: on ? '#BBD7FF' : 'var(--ink-3)' }); const SLW_PILL = (on) => ({ font:'inherit', fontSize:11.5, fontWeight:600, padding:'5px 10px', cursor:'pointer', borderRadius:6, border: on ? '1px solid rgba(244,200,75,.45)' : '1px solid var(--line)', background: on ? 'rgba(244,200,75,.14)' : 'transparent', color: on ? '#F4C84B' : 'var(--ink-2)' }); const SLW_PILL_B = (on) => ({ ...SLW_PILL(on), border: on ? '1px solid rgba(91,168,255,.5)' : '1px solid var(--line)', background: on ? 'rgba(91,168,255,.16)' : 'transparent', color: on ? '#BBD7FF' : 'var(--ink-2)' }); function SettingsWindowRow({ 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 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); const hist = (measured && Array.isArray(measured.history)) ? measured.history.slice(0, 10) : []; const [open, setOpen] = React.useState(false); const toggleDay = (d) => onChange({ days: days.indexOf(d) >= 0 ? days.filter(x => x !== d) : days.concat([d]).sort((a, b) => a - b) }); return (
Fönster {idx + 1} {count > 1 && ( )}
{window.LW_DAY_SHORT.map((s, d) => ( ))}
onChange({earliest: v})}/> onChange({latest: v})}/> onChange({duration_hours: v})} help="Körtid för DETTA fönster — HEMS lägger den på de billigaste kvartarna inom fönstret"/>
{w.mode === 'split' && ( )}
{w.solar_only === true &&
{window.LW_HELP_SOLAR}
} {span && span.wrap &&
{window.LW_HELP_WRAP}
} {err &&
{err}
} {/* Vad HEMS planerar med (Andrés beslut 3/9) — blocket finns bara när backend svarar med enabled:true. Flagga av eller endpoint saknas ⇒ ingenting (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 SettingsWindowList({ 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)])); }; // V6: backend avvisar windows: []. Sista raden går därför inte att ta bort. const del = (i) => { if (list.length <= 1) return; onChange(list.filter((_, k) => k !== i)); }; return (
Körfönster
{list.map((w, i) => ( upd(i, p)} onRemove={()=>del(i)}/> ))} {list.length < max ? ( ) : (
Max {max} fönster per last.
)}
); } Object.assign(window, { SettingsWindowRow, SettingsWindowList });