/* ============================================================================
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 && (
Ta bort
)}
{window.LW_DAY_SHORT.map((s, d) => (
toggleDay(d)} aria-label={window.LW_DAY_NAME[d]}
aria-pressed={days.indexOf(d) >= 0}
className={`h-7 flex-1 rounded-md border text-[11px] font-bold transition-colors ${
days.indexOf(d) >= 0 ? 'border-sky-400 bg-sky-500/20 text-sky-200'
: 'border-slate-700 bg-slate-800/60 text-slate-500 hover:text-slate-300'}`}>
{s}
))}
onChange({ solar_only: !w.solar_only })} className={LW_PILL(w.solar_only === true)}>
☀ Bara sol
onChange({ mode: w.mode === 'split' ? 'consecutive' : 'split' })}
className={LW_PILL(w.mode === 'split')}>
Får delas upp
{w.mode === 'split' && (
onChange({ max_parts: Number(e.target.value) })}
aria-label="Max antal delar"
className="rounded-lg border border-slate-700 bg-slate-800/70 px-1.5 py-1 text-[11px] font-bold text-slate-100"
style={{ ...window.GC_MONO, colorScheme: 'dark' }}>
{[2, 3, 4, 5, 6, 7, 8].map(n => max {n} delar )}
)}
{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 && (
setOpen(!open)} className="ml-1.5 underline decoration-dotted underline-offset-2 hover:text-slate-300">
{open ? 'Dölj körningar' : `Visa körningar (${hist.length})`}
)}
)}
)}
{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 ? (
+ Lägg till fönster
) : (
Max {max} fönster
)}
);
}
Object.assign(window, { GcWindowRow, GcWindowList });