/* ev-week.jsx — veckoschema per laddbox (SPEC §12, live i staging 15/8 23:49). ──────────────────────────────────────────────────────────────────────── Sju valfria fält per laddbox: departure_mon … departure_sun, värde "HH:MM" eller null. Kontraktets semantik, ordagrant: • Minst en satt dag ⇒ VECKOSCHEMAT gäller. Deadline är nästa (dag, tid) efter inkopplingen. • Inga dagar satta ⇒ som förut: departure_time + departure_day. • Per-tillfälle-overriden (ev_session.departure_time) byter TIDEN på nästa schemalagda dag — inte dagen. Det syns i vyn, annars ser det ut som att dagvalet slutat fungera. • I sol-läge räknas en satt veckodag som avresedag ⇒ nätgarantin öppnas, precis som departure_day gör. Med sju rader gäller varningen sju rader. • Fälten saknas i GET tills någon sparat dem ⇒ saknad nyckel = null. Sparas via window.hems.saveSettingsPatch med HELA ev_chargers-listan och varje posts samtliga fält — en partiell post nollställer allt backend inte fick med. Laddbox-id lämnas orört: kanoniskt id är ev_1, ev_2 (positionen), men det gamla evN accepteras fortfarande och att byta det här skulle skilja sig från vad resten av appen skriver. */ const EW_DAYS = [ ['departure_mon','Måndag','mon'], ['departure_tue','Tisdag','tue'], ['departure_wed','Onsdag','wed'], ['departure_thu','Torsdag','thu'], ['departure_fri','Fredag','fri'], ['departure_sat','Lördag','sat'], ['departure_sun','Söndag','sun'], ]; // JS getDay(): 0 = söndag, 1 = måndag … 6 = lördag. EW_DAYS är måndagsstartad, // så index 0 (måndag) → 1 och index 6 (söndag) → 0. const EW_JS_DAY = [1, 2, 3, 4, 5, 6, 0]; const EW_HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/; const ewTimeOk = (v) => !v || EW_HHMM.test(v); const ewPad = (v) => { const m = /^(\d{1,2}):(\d{1,2})$/.exec(String(v || '').trim()); return m ? String(m[1]).padStart(2,'0')+':'+String(m[2]).padStart(2,'0') : null; }; /* Nästa deadline ur schemat. Samma regel som backendens deadline-upplösning: nästa (dag, tid) efter nu. Visas som text så att användaren ser vad valen betyder utan att behöva vänta på en plan. */ function ewNextDeadline(week, now){ const base = now || new Date(); let best = null; EW_DAYS.forEach(([key], i) => { const t = week[key]; if (!t || !EW_HHMM.test(t)) return; const [hh, mm] = t.split(':').map(Number); const target = EW_JS_DAY[i]; const d = new Date(base); d.setHours(hh, mm, 0, 0); let add = (target - d.getDay() + 7) % 7; if (add === 0 && d <= base) add = 7; d.setDate(d.getDate() + add); if (!best || d < best.at) best = { at: d, dayIdx: i, time: t }; }); return best; } const ewRel = (dt, now) => { const a = new Date(now); a.setHours(0,0,0,0); const b = new Date(dt); b.setHours(0,0,0,0); const days = Math.round((b - a)/86400000); if (days === 0) return 'idag'; if (days === 1) return 'i morgon'; return 'om ' + days + ' dygn'; }; function EvWeek({ charger, index, allChargers, session, name, onSaved }){ const c = charger || {}; const now = window.hemsDate ? window.hemsDate() : new Date(); // Utkast: sju dagar plus det gamla ett-värdet. Saknad nyckel = null. const incoming = React.useMemo(() => { const w = {}; EW_DAYS.forEach(([key]) => { w[key] = c[key] || null; }); return w; }, [c]); const [draft, setDraft] = React.useState(null); const [toast, setToast] = React.useState(null); const w = draft || incoming; const dirty = !!draft; const anySet = EW_DAYS.some(([key]) => !!w[key]); const solarMode = c.solar_mode === true; const next = React.useMemo(() => anySet ? ewNextDeadline(w, now) : null, [w, anySet, now]); const invalid = EW_DAYS.filter(([key]) => w[key] && !ewTimeOk(w[key])); const set = (key, val) => setDraft({ ...w, [key]: val === '' ? null : val }); const clearAll = () => setDraft(EW_DAYS.reduce((a,[k]) => ({ ...a, [k]: null }), {})); const fillAll = () => { const first = EW_DAYS.map(([k]) => w[k]).find(Boolean) || c.departure_time || '07:00'; setDraft(EW_DAYS.reduce((a,[k]) => ({ ...a, [k]: first }), {})); }; const weekdaysOnly = () => { const first = EW_DAYS.map(([k]) => w[k]).find(Boolean) || c.departure_time || '07:00'; setDraft(EW_DAYS.reduce((a,[k],i) => ({ ...a, [k]: i < 5 ? first : null }), {})); }; const save = async () => { if (invalid.length){ setToast({ kind:'err', msg:'Tiden måste vara HH:MM' }); return; } const patch = {}; EW_DAYS.forEach(([key]) => { const v = w[key] ? ewPad(w[key]) : null; if ((v || null) !== (c[key] || null)) patch[key] = v; }); if (!Object.keys(patch).length){ setDraft(null); return; } try { setToast({ kind:'info', msg:'Sparar…' }); await window.hems.saveSettingsPatch((full) => { const list = Array.isArray(full?.ev_chargers) ? full.ev_chargers : null; if (!list || !list.length) throw new Error('Laddboxlistan saknas i inställningarna'); // Matcha på POSITION — kanoniskt id är positionen, och råa id-strängen // kan vara legacy (ev0 mot ev_1). Faller tillbaka på id om listan // skulle ha ändrat ordning mellan läsning och sparning. let k = (index != null && index < list.length) ? index : -1; if (k >= 0 && c.id != null && list[k] && String(list[k].id) !== String(c.id)){ const byId = list.findIndex(x => String(x?.id) === String(c.id)); if (byId >= 0) k = byId; } if (k < 0) throw new Error('Hittar inte laddboxen i inställningarna'); return { ev_chargers: list.map((x, i) => i === k ? { ...x, ...patch } : x) }; }); setDraft(null); setToast({ kind:'ok', msg:'Sparat — planeras om inom ~15 min' }); if (onSaved) onSaved(); } catch (e) { setToast({ kind:'err', msg: String(e && e.message || e) }); } }; React.useEffect(() => { if (!toast || toast.kind === 'info') return; const id = setTimeout(() => setToast(null), 4000); return () => clearTimeout(id); }, [toast]); const sessTime = session?.departure_time || null; const overridden = anySet && next && sessTime && sessTime !== next.time; return (