/* gc-entitle.jsx — vyerna för planspärr, trappa och villkorsgrind. EntLock: gråad funktion bakom en panel med Uppgradera (K2/K3/K4, 15/9). LockedPage: EN sida som tar access + reason (trappan T2, 20/9) och väljer rubrik, styrningsbesked, nedräkning och knapp. Ersätter BlockedPage. TermsPage: godkännandesidan (villkor K3, 20/9). Samma skal som LockedPage. Ingen av dem hämtar data; allt går via window.HemsEnt. */ function EntUpgradeBtn({ label = 'Uppgradera' }){ const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); const go = async () => { setErr(null); setBusy(true); const r = await window.HemsEnt.openPortal(); if (!r.ok) setErr(r.error); setBusy(false); }; return (
{err &&
{err}
}
); } /* Gråat innehåll bakom panelen: konfigurationen syns kvar så kunden ser vad hen går miste om (André 15/9), men inget går att röra. */ function EntLock({ text, children }){ return (
{text}
); } function EntLockEmpty({ text }){ return (
); } /* Gemensamt skal: märke, rubrik, brödtext, innehåll, Logga ut. */ function EntLogout(){ ['hems_session_token', 'hems_user_email', 'hems_user_name', 'hems_user_id'] .forEach(k => { try { localStorage.removeItem(k); } catch(_){} }); window.location.href = 'login.html'; } /* Din supportkod på spärrsidan (backend 23/9, S2): ur samma /me. Inget när fältet saknas. */ function EntSupportCode(){ const E = window.HemsEnt; const [code, setCode] = React.useState(() => { const m = E && E.me(); return (m && m.support_code) || null; }); React.useEffect(() => { const on = () => { const m = E && E.me(); if (m && m.support_code) setCode(m.support_code); }; window.addEventListener('hems:entitlements', on); return () => window.removeEventListener('hems:entitlements', on); }, []); if (!code) return null; return (
Din supportkod: {code} – uppge den när du kontaktar oss.
); } function EntShell({ title, lede, children, wide }){ return (
Gravio HEMS AI Energy Solutions

{title}

{lede &&

{lede}

}
{children}
); } /* Kryssruta med länkade dokument (K3/K5). Länkarna öppnas i ny flik så rutan inte tappas när kunden läst klart. URL:erna ur /me när de finns. */ function EntCheck({ checked, onChange, children, id }){ return ( ); } function EntTermsLabel(){ const E = window.HemsEnt; const a = 'underline decoration-slate-600 underline-offset-2 hover:text-slate-50'; return ( <>Jag har läst och godkänner användarvillkoren och har tagit del av integritetspolicyn ); } /* Plankort. onPick(plan, interval). Samma kort för subscription_required (checkout) och subscription_ended (reactivate); texten "Första månaden gratis" hör inte hit — Checkout debiterar direkt. */ function EntPlanCard({ plan, onPick, busy, err, disabled }){ const [ival, setIval] = React.useState('month'); const yearly = plan.year && ival === 'year'; return (
{plan.name}
{plan.tag}
{yearly ? plan.year : plan.month} {yearly ? 'kr / år' : 'kr / mån'}
inkl. moms
    {plan.rows.map((r, i) =>
  • {r}
  • )}
{plan.year && (
{[['month', 'Månad'], ['year', 'År']].map(([v, l]) => ( ))}
)} {plan.year && ival === 'year' &&
Köp 12 månader, betala för 11
} {err &&
{err}
}
); } /* Styrningsbeskedet (T3) + nedräkningarna (T4). Sidans viktigaste mening: ligger FÖRE plankorten. Lugn rad, ingen varningspanel. */ function EntControlNotice({ ent }){ const E = window.HemsEnt; const c = E.daysPhrase(ent.days_until_control_stops, 'for'); const d = E.daysPhrase(ent.days_until_data_deleted, 'in'); let main = null, sub = null; if (ent.access === 'dash_locked'){ main = c ? `HEMS styr din anläggning som vanligt ${c}.` : 'HEMS styr din anläggning som vanligt.'; if (ent.days_until_control_stops === 0) main = 'HEMS styr din anläggning som vanligt i dag. Sedan upphör styrningen.'; sub = 'Batteriet, bilen och lasterna följer planen även om den här sidan är stängd.'; } else if (ent.access === 'stopped'){ main = 'HEMS har slutat styra din anläggning.'; /* D3: data sparas tolv månader efter avtalets slut. Nedräkningen visas bara när backend skickat ett tal; null ⇒ ingen raderingstext. */ const n = ent.days_until_data_deleted; sub = n == null ? 'Dina data och inställningar finns kvar i tolv månader, så du kan fortsätta där du var om du kommer tillbaka.' : n === 0 ? 'Dina data tas bort i dag.' : n === 1 ? 'Dina data finns kvar till i morgon.' : `Dina data finns kvar i ${n} dagar.`; } else if (ent.access === 'purged'){ main = 'HEMS har slutat styra din anläggning.'; sub = 'Dina data har tagits bort.'; } if (!main) return null; return (
{main}
{sub &&
{sub}
} {ent.access === 'dash_locked' && d && (
Dina data raderas {d} om du inte förnyar. Fram till dess finns allt kvar.
)}
); } /* Spärrsidan — ett läge per reason (2.2). */ function LockedPage({ ent }){ const E = window.HemsEnt; const e = ent || E.get(); const [busy, setBusy] = React.useState(null); const [err, setErr] = React.useState(null); const [termsOk, setTermsOk] = React.useState(false); const [deliverOk, setDeliverOk] = React.useState(false); const reason = e.reason; const pickReactivate = async (plan, interval) => { setErr(null); setBusy(plan); const r = await E.reactivate(plan, interval); if (!r.ok) setErr({ plan, msg:r.error }); setBusy(null); }; const pickCheckout = async (plan, interval) => { setErr(null); setBusy(plan); const r = await E.checkout(plan, interval, { terms_accepted:termsOk, delivery_start_now:deliverOk }); if (!r.ok) setErr({ plan, msg:r.error }); setBusy(null); }; /* payment_failed hit FÖRST efter varningsperioden (rev 2 D1): access är då stopped och styrningen har upphört. Under perioden är dashboarden öppen med PaymentBanner. */ if (reason === 'payment_failed'){ return (
Öppnar Stripes kundportal, där kortet byts.
); } if (reason === 'subscription_required'){ const ready = termsOk && deliverOk; return (
{E.DELIVERY_LABEL} {!ready &&
Kryssa båda rutorna för att kunna välja en nivå.
}
{E.PLANS.map(p => ( ))}
); } /* subscription_ended (v404) — plankort via reactivate. */ return (
{E.PLANS.map(p => ( ))}
); } /* Godkännandesidan (K3). Helsida före all annan rendering. En kryssruta, knappen inaktiv tills den är kryssad. Ingen väg förbi utom Logga ut. */ function TermsPage(){ const E = window.HemsEnt; const t = E.terms(); const [ok, setOk] = React.useState(false); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); const first = !t.accepted_version; const go = async () => { setErr(null); setBusy(true); const r = await E.acceptTerms(); if (!r.ok) { setErr(r.error); setBusy(false); } }; return (
Användarvillkor för HEMS och Integritetspolicy · PowerGravio AB · version {t.current_version}
{err &&
{err}
}
); } /* Banner vid utebliven betalning (rev 2 D1): dashboarden är öppen, styrningen pågår, kunden ska se problemet och kunna laga det. Nedräkning + datum ur backend (days_until_control_stops, control_stops_date), aldrig uträknat här. Lugn ton; närmare slutet (≤ 3 dagar) tydligare kant, men ingen röd panel. */ function PaymentBanner({ ent }){ const E = window.HemsEnt; const w = E && E.paymentWarning(ent); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); if (!w) return null; const go = async () => { setErr(null); setBusy(true); const r = await E.openPortal(); if (!r.ok) setErr(r.error); setBusy(false); }; return (
{w.text}
Fram till dess styr HEMS som vanligt. Dashboarden och inställningarna är öppna.{err ? ` ${err}` : ''}
); } /* Grinden i rot (K3 + T2): villkor först, sedan spärren, sedan appen. Lyssnar på hems:entitlements så 402/403 mitt i sessionen byter vy — och App avmonteras, så pollningen stannar. */ function EntGate({ children }){ const E = window.HemsEnt; const [, force] = React.useState(0); React.useEffect(() => { if (!E) return; const on = () => force(n => n + 1); window.addEventListener('hems:entitlements', on); return () => window.removeEventListener('hems:entitlements', on); }, []); if (E && E.termsRequired()) return ; if (E && E.locked()) return ; return children; } const BlockedPage = LockedPage; // bakåtkompatibelt namn Object.assign(window, { EntLock, EntLockEmpty, EntUpgradeBtn, EntPlanCard, EntCheck, EntTermsLabel, EntControlNotice, LockedPage, TermsPage, EntGate, BlockedPage, PaymentBanner });