/* Powermieter — App shell: state, chrome, device frame, routing */
const { useState: useS, useEffect: useE, useRef: useR } = React;
const LS = {
get: (k, d) => { try { const v = localStorage.getItem('pm_' + k); return v == null ? d : JSON.parse(v); } catch { return d; } },
set: (k, v) => { try { localStorage.setItem('pm_' + k, JSON.stringify(v)); } catch {} },
};
const INITIAL_FORM = {
vorname: '', nachname: '', email: '', telefon: '', adresse: '', consent1: false,
geburt: '', strasse: '', hausnr: '', plz: '', ort: '', aufgang: '', etage: '', whgnr: '', klingel: '',
anbieter: '', kundennr: '', zaehler: '', zaehlerUnbekannt: false, verbrauch: '',
lieferbeginnTyp: 'fruehest', lieferbeginn: '', dsgvo: false, digital: true,
v_agb: false, v_widerruf: false, v_digital: false,
};
const SAMPLE_FORM = {
vorname: 'Max', nachname: 'Mustermann', email: 'max@beispiel.de', telefon: '+49 170 1234567',
adresse: 'Musterstraße 12, 2. OG rechts', consent1: true,
strasse: 'Musterstraße', hausnr: '12', plz: '10115', ort: 'Berlin', aufgang: 'Haus A', etage: '2. OG', whgnr: 'W04',
anbieter: 'Stadtwerke Berlin', zaehler: '1 ESY 0123 4567', verbrauch: '2.500',
lieferbeginnTyp: 'fruehest', dsgvo: true, digital: true, v_agb: true, v_widerruf: true, v_digital: true,
};
/* Tweaks: simulate the contract gatekeeper without touching the data */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"goLiveGate": "offen",
"datenfuelle": "lueckenhaft",
"quote": 58
}/*EDITMODE-END*/;
function TweaksUI({ t, setTweak }) {
if (!window.TweaksPanel) return null;
return (
setTweak('goLiveGate', v)} />
setTweak('datenfuelle', v)} />
setTweak('quote', v)} />
);
}
function renderScreen(step, props) {
if (step === 1) return ;
if (step === 2) return ;
if (step === 3 && window.Screen3) return ;
if (step === 4 && window.Screen4) return ;
return ;
}
/* Landing (Einstieg) rendered natively inside the prototype — reuses the
landing-page sections; CTAs hand off into the onboarding flow. */
function LandingEntry({ onEnter }) {
const [aud, setAud] = useS('mieter');
const [chat, setChat] = useS(false);
const toForm = () => window.lpScrollTo && window.lpScrollTo('lp-form');
if (!window.HeroSection) return ;
return (
setChat(true)} />
setChat(true)} />
onEnter(2)} />
setChat(true)} />
{window.PaulaChat &&
setChat(true)} onClose={() => setChat(false)} />}
{window.StickyCTA && }
);
}
function App() {
const [theme, setTheme] = useS(() => LS.get('theme', 'light'));
const [device, setDevice] = useS(() => LS.get('device', 'mobile'));
const [view, setView] = useS(() => LS.get('view', 'landing')); // 'landing' | 'flow' | 'admin'
const [step, setStep] = useS(() => LS.get('step', 1));
const [maxReached, setMax] = useS(() => LS.get('max', 1));
const [form, setForm] = useS(() => ({ ...INITIAL_FORM, ...LS.get('form', {}) }));
const [errors, setErrors] = useS({});
const [model, setModel] = useS(() => LS.get('model_v', 'mieterstrom'));
const [t, setTweak] = (window.useTweaks || (() => [TWEAK_DEFAULTS, () => {}]))(TWEAK_DEFAULTS);
useE(() => LS.set('model_v', model), [model]);
useE(() => { document.documentElement.setAttribute('data-theme', theme); LS.set('theme', theme); }, [theme]);
useE(() => LS.set('device', device), [device]);
useE(() => LS.set('view', view), [view]);
useE(() => LS.set('step', step), [step]);
useE(() => LS.set('max', maxReached), [maxReached]);
useE(() => LS.set('form', form), [form]);
// Landing (embedded iframe) → Flow handoff
useE(() => {
const onMsg = (e) => {
const d = e.data;
if (d && d.pm === 'enterFlow') {
const n = Math.min(Math.max(parseInt(d.step) || 1, 1), 4);
setView('flow'); setStep(n); setMax(m => Math.max(m, n));
}
};
window.addEventListener('message', onMsg);
return () => window.removeEventListener('message', onMsg);
}, []);
const scrollRef = useR(null);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
// safe scroll helper (no scrollIntoView)
useE(() => {
const scroller = (id) => {
const el = document.getElementById(id);
const sc = scrollRef.current;
if (!el || !sc) return;
const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - 16;
sc.scrollTo({ top, behavior: 'smooth' });
};
window.pmScrollTo = scroller;
window.lpScrollTo = scroller;
}, []);
const scrollTop = () => { if (scrollRef.current) scrollRef.current.scrollTo({ top: 0, behavior: 'smooth' }); };
const validate = (s) => {
const e = {};
const req = (k, msg) => { if (!String(form[k] || '').trim()) e[k] = msg || 'Pflichtfeld'; };
const email = () => { if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email || '')) e.email = 'Bitte gültige E-Mail eingeben'; };
if (s === 1) { req('vorname', 'Bitte Vornamen eingeben'); req('nachname', 'Bitte Nachnamen eingeben'); email(); req('adresse', 'Bitte Adresse / Wohnung angeben'); if (!form.consent1) e.consent1 = 'Bitte stimmen Sie der Kontaktaufnahme zu.'; }
if (s === 2) {
req('vorname'); req('nachname'); email();
req('strasse', 'Bitte Straße angeben'); req('hausnr', 'Pflichtfeld'); req('plz', 'PLZ angeben'); req('ort', 'Ort angeben');
if (!form.zaehlerUnbekannt) req('zaehler', 'Zählernummer angeben oder "kenne ich nicht" wählen');
if (form.lieferbeginnTyp !== 'fruehest' && !form.lieferbeginn) e.lieferbeginn = 'Bitte Datum wählen';
if (!form.dsgvo) e.dsgvo = 'Bitte dem Datenschutzhinweis zustimmen.';
}
if (s === 3) {
if (!form.v_agb) e.v_agb = 'Erforderlich';
if (!form.v_widerruf) e.v_widerruf = 'Erforderlich';
if (!form.v_digital) e.v_digital = 'Erforderlich';
}
return e;
};
const goNext = () => {
const e = validate(step);
setErrors(e);
if (Object.keys(e).length) {
// scroll to first error
requestAnimationFrame(() => {
const el = scrollRef.current?.querySelector('.input-err, .field-err');
if (el && scrollRef.current) {
const top = el.getBoundingClientRect().top - scrollRef.current.getBoundingClientRect().top + scrollRef.current.scrollTop - 100;
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
}
});
return;
}
const ns = Math.min(step + 1, 4);
setStep(ns); setMax(m => Math.max(m, ns)); scrollTop();
};
const goBack = () => { setStep(s => Math.max(1, s - 1)); scrollTop(); };
const jump = (n) => { if (n <= maxReached) { setStep(n); setErrors({}); scrollTop(); } };
const restart = () => { setForm({ ...INITIAL_FORM }); setStep(1); setMax(1); setErrors({}); setView('flow'); scrollTop(); };
const screenProps = { form, set, errors, onNext: goNext, onBack: goBack, device, jump, restart, model, tweaks: t };
const mobile = device === 'mobile';
const Product = (
{/* In-app top bar */}
{!mobile && (
)}
{mobile && }
{/* Stepper */}
{mobile && (
Schritt {step}/4
{STEPS[step - 1].label}
)}
{/* Screen */}
{renderScreen(step, screenProps)}
);
const Admin = window.AdminView ? : ;
return (
{/* Prototype chrome */}
Prototyp
{view === 'flow' && (
},
{ value: 'desktop', label: 'Desktop', icon: },
]} />
)}
{/* Body */}
{view === 'landing' ? (
{ setView('flow'); setStep(n); setMax(m => Math.max(m, n)); scrollTop(); }} />
) : view === 'admin' ? (
{Admin}
) : mobile ? (
Mobile Ansicht · QR-Einstieg vom Hausaushang
) : (
{Product}
)}
);
}
function Placeholder({ n, admin }) {
return (
{admin ? 'Hausverwaltung' : 'Schritt ' + n}
Wird gerade gebaut …
);
}
/* ---------- Embed mode: a single screen, no chrome, no persistence ----------
Used by the overview canvas via ?embed=1&view=flow&step=2&device=mobile&theme=dark */
function EmbedApp() {
const p = new URLSearchParams(location.search);
const theme = p.get('theme') || 'light';
const device = p.get('device') || 'desktop';
const view = p.get('view') || 'flow';
const step = parseInt(p.get('step') || '1');
const mobile = device === 'mobile';
const [form, setForm] = useS(() => ({ ...INITIAL_FORM, ...SAMPLE_FORM }));
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
useE(() => { document.documentElement.setAttribute('data-theme', theme); document.body.classList.add('pm-embed-body'); }, [theme]);
const noop = () => {};
const tweaks = { goLiveGate: p.get('gate') || 'offen', datenfuelle: p.get('data') || 'lueckenhaft', quote: parseInt(p.get('quote') || '58') };
const model = p.get('model') || 'mieterstrom';
const props = { form, set, errors: {}, onNext: noop, onBack: noop, device, jump: noop, restart: noop, model, tweaks };
if (view === 'wizard') return {window.ContractWizard ? u.whg === p.get('unit')) : null} onClose={noop} /> : null}
;
if (view === 'admin') return {window.AdminView ?
: null}
;
return (
{mobile &&
Schritt {step}/4{STEPS[step - 1].label}
}
{renderScreen(step, props)}
);
}
const IS_EMBED = new URLSearchParams(location.search).get('embed') === '1';
ReactDOM.createRoot(document.getElementById('root')).render(IS_EMBED ? : );