/* Powermieter — Admin / Hausverwaltung */
const { useState: useAS, useMemo: useAM, useEffect: useAE } = React;
const PM_STATUS = {
none: { label: 'Nicht eingeladen', tone: 'gray', dot: 'var(--text-3)' },
invited: { label: 'Eingeladen', tone: 'gray', dot: 'var(--text-3)' },
interest:{ label: 'Interesse', tone: 'accent', dot: 'var(--accent)' },
reg: { label: 'Registrierung', tone: 'accent', dot: 'var(--accent)' },
prep: { label: 'Vertrag vorbereitet', tone: 'warn', dot: 'var(--warn-500)' },
signed: { label: 'Abgeschlossen', tone: 'positive', dot: 'var(--positive)' },
active: { label: 'Aktiviert', tone: 'positive', dot: 'var(--positive-500)' },
};
const STATUS_ORDER = ['none', 'invited', 'interest', 'reg', 'prep', 'signed', 'active'];
const RESIDENTS = [
{ whg: 'W01', name: 'Anna Berger', auf: 'A', etage: 'EG', status: 'active', contact: 'a.berger@mail.de' },
{ whg: 'W02', name: 'Familie Çelik', auf: 'A', etage: 'EG', status: 'signed', contact: '+49 170 …' },
{ whg: 'W03', name: 'Tobias Wolf', auf: 'A', etage: '1. OG', status: 'prep', contact: 't.wolf@mail.de' },
{ whg: 'W04', name: 'Max Mustermann', auf: 'A', etage: '2. OG', status: 'reg', contact: 'max@beispiel.de', flag: true },
{ whg: 'W05', name: 'Sofia Russo', auf: 'A', etage: '2. OG', status: 'interest', contact: 's.russo@mail.de' },
{ whg: 'W06', name: 'Leon Fischer', auf: 'A', etage: '3. OG', status: 'invited', contact: 'l.fischer@mail.de' },
{ whg: 'W07', name: 'Hr. & Fr. Yılmaz', auf: 'B', etage: 'EG', status: 'active', contact: '+49 151 …' },
{ whg: 'W08', name: 'Clara Hoffmann', auf: 'B', etage: 'EG', status: 'signed', contact: 'c.hoffmann@mail.de' },
{ whg: 'W09', name: 'Jonas Krüger', auf: 'B', etage: '1. OG', status: 'interest', contact: 'j.krueger@mail.de' },
{ whg: 'W10', name: '— leer stehend', auf: 'B', etage: '1. OG', status: 'none', contact: '', vacant: true },
{ whg: 'W11', name: 'Mara Schulz', auf: 'B', etage: '2. OG', status: 'invited', contact: 'm.schulz@mail.de', flag: true },
{ whg: 'W12', name: 'David Becker', auf: 'B', etage: '3. OG', status: 'reg', contact: 'd.becker@mail.de' },
];
function StatusBadge({ s }) {
const st = PM_STATUS[s];
return {st.label} ;
}
function AdminView({ device, tweaks = { goLiveGate: 'offen', datenfuelle: 'lueckenhaft', quote: 58 }, model = 'mieterstrom', setModel = () => {}, initialView = 'uebersicht' }) {
const [adminView, setAdminView] = useAS(() => {
if (initialView && initialView !== 'uebersicht') return initialView;
try { return JSON.parse(localStorage.getItem('pm_adminview') || '"uebersicht"'); } catch { return 'uebersicht'; }
});
useAE(() => {
if (new URLSearchParams(location.search).get('embed') === '1') return;
try { localStorage.setItem('pm_adminview', JSON.stringify(adminView)); } catch {}
}, [adminView]);
const [wizardOpen, setWizardOpen] = useAS(false);
const [wizardUnit, setWizardUnit] = useAS(null);
const [drawer, setDrawer] = useAS(null);
const openWizard = (u) => { setWizardUnit(u || null); setWizardOpen(true); };
const [auf, setAuf] = useAS('all');
const [etage, setEtage] = useAS('all');
const [q, setQ] = useAS('');
const [statusFilter, setStatusFilter] = useAS('all');
const [noteFor, setNoteFor] = useAS(null);
const [notes, setNotes] = useAS({ W04: 'Rückruf gewünscht — Mo nachmittags.', W11: 'Zweite Einladung am 28.05. versendet.' });
const [showPoster, setShowPoster] = useAS(false);
const [view, setView] = useAS('list');
const etagen = useAM(() => [...new Set(RESIDENTS.map(r => r.etage))], []);
const rows = useAM(() => RESIDENTS.filter(r =>
(auf === 'all' || r.auf === auf) &&
(etage === 'all' || r.etage === etage) &&
(statusFilter === 'all' || r.status === statusFilter) &&
(!q || r.name.toLowerCase().includes(q.toLowerCase()) || r.whg.toLowerCase().includes(q.toLowerCase()))
), [auf, etage, q, statusFilter]);
const total = RESIDENTS.length;
const occupied = RESIDENTS.filter(r => !r.vacant).length;
const interested = RESIDENTS.filter(r => ['interest', 'reg', 'prep', 'signed', 'active'].includes(r.status)).length;
const contracts = RESIDENTS.filter(r => ['signed', 'active'].includes(r.status)).length;
const active = RESIDENTS.filter(r => r.status === 'active').length;
const quote = Math.round(contracts / occupied * 100);
const todo = RESIDENTS.filter(r => ['prep', 'reg'].includes(r.status) || r.flag).length;
const breakdown = STATUS_ORDER.map(s => ({ s, n: RESIDENTS.filter(r => r.status === s).length })).filter(b => b.n > 0);
return (
{/* Top bar */}
{[['uebersicht', '\u00dcbersicht', I.chart], ['cockpit', 'Vertrags-Cockpit', I.signature], ['lifecycle', 'Fristen & Wechsel', I.calendar], ['weg', 'WEG-Freigabe', I.users]].map(([v, l, Ic]) => (
setAdminView(v)}> {l}
))}
{adminView === 'cockpit' &&
}
{adminView === 'lifecycle' &&
{window.Lifecycle ? : null}
}
{adminView === 'weg' &&
}
{adminView === 'uebersicht' && (
{/* KPIs */}
} value={total} label="Wohnungen" sub={`${occupied} bewohnt · 1 leer`} />
} value={interested} label="Interessenten" tone="accent" sub={`${Math.round(interested / occupied * 100)}% der Haushalte`} />
} value={contracts} label="Verträge" tone="positive" sub={`${active} bereits aktiv`} />
} value={quote + '%'} label="Teilnahmequote" tone="positive" sub="Ziel: 60%" />
} value={todo} label="Handlungsbedarf" tone="warn" sub="offene Vorgänge" />
{/* Progress + action panel */}
Objekt-Fortschritt {quote}% abgeschlossen
{breakdown.map(b => )}
{breakdown.map(b => (
{PM_STATUS[b.s].label} {b.n}
))}
Offener Handlungsbedarf
Vorgänge, die auf Sie warten.
} text="3 Verträge in Vorbereitung" sub="Daten vollständig — bereit zum Versand" />
} text="2 Einladungen ohne Reaktion" sub="seit über 7 Tagen" warn />
} text="1 leer stehende Wohnung" sub="W10 · Aufgang B" />
{/* Conversion funnel */}
{/* Bewohner — Liste / Gebäude */}
Bewohner-Status
},
{ value: 'building', label: 'Gebäude', icon: },
]} />
{view === 'building' ?
{}} /> : (<>
{/* Filters */}
{/* Table */}
Wohnung
Bewohner
Lage
Status
Aktion
{rows.map(r => (
{r.whg}
{r.vacant ? '—' : initials(r.name)}
{r.name}{r.flag && }
{r.contact &&
{r.contact}
}
Aufgang {r.auf} · {r.etage}
{!r.vacant && (r.status === 'invited' || r.status === 'none' || r.flag
? Einladung
: )}
setNoteFor(noteFor === r.whg ? null : r.whg)}>
{noteFor === r.whg && (
)}
))}
{rows.length === 0 &&
Keine Bewohner für diese Filter.
}
>)}
)}
{wizardOpen && window.ContractWizard &&
setWizardOpen(false)} />}
{drawer && window.UnitDrawer && setDrawer(null)} onWizard={openWizard} />}
{showPoster && setShowPoster(false)} />}
);
}
function statusCat(s) {
if (s === 'signed' || s === 'active') return 'done';
if (s === 'interest' || s === 'reg' || s === 'prep') return 'progress';
return 'open';
}
function ConversionFunnel({ residents }) {
const idx = s => STATUS_ORDER.indexOf(s);
const cnt = min => residents.filter(r => idx(r.status) >= min).length;
const total = residents.length;
const stages = [
{ label: 'Wohnungen gesamt', n: total, c: 'var(--text-3)' },
{ label: 'Eingeladen', n: cnt(1), c: 'var(--text-2)' },
{ label: 'Interesse angemeldet', n: cnt(2), c: 'var(--accent-400)' },
{ label: 'Registriert', n: cnt(3), c: 'var(--accent)' },
{ label: 'Vertrag abgeschlossen', n: cnt(5), c: 'var(--positive)' },
{ label: 'Aktiviert', n: cnt(6), c: 'var(--positive-500)' },
];
return (
Conversion-Funnel
{stages[0].n} → {stages[stages.length - 1].n} aktiviert
{stages.map((s, i) => {
const pct = s.n / total * 100;
const conv = Math.round(s.n / total * 100);
const drop = i > 0 ? stages[i - 1].n - s.n : 0;
return (
{s.label}
{conv}%
{drop > 0 && −{drop} }
);
})}
);
}
function BuildingGraphic({ residents, onInvite }) {
const floors = ['3. OG', '2. OG', '1. OG', 'EG'];
const aufs = ['A', 'B'];
const [sel, setSel] = useAS(null);
const unitsOf = (auf, fl) => residents.filter(r => r.auf === auf && r.etage === fl);
return (
Gebäudeansicht
Abgeschlossen
In Bearbeitung
Offen
{aufs.map(auf => (
{Array.from({ length: 7 }).map((_, i) => )}
PV-Dach
{floors.map(fl => {
const u = unitsOf(auf, fl);
return (
{fl}
{u.length ? u.map((r, k) => (
setSel(r)} title={`${r.whg} · ${r.name} · ${PM_STATUS[r.status].label}`}>
{r.whg}
)) : }
);
})}
Aufgang {auf}
))}
{sel && (
{sel.vacant ? '—' : initials(sel.name)}
{sel.whg} · {sel.name}
Aufgang {sel.auf} · {sel.etage}{sel.contact ? ' · ' + sel.contact : ''}
{!sel.vacant && (statusCat(sel.status) === 'open'
?
} onClick={() => onInvite && onInvite(sel)}>Einladung
:
}>Öffnen)}
setSel(null)} aria-label="Schließen">
)}
);
}
function KPI({ icon, value, label, sub, tone }) {
return (
{icon}
{value}
{label}
{sub &&
{sub}
}
);
}
function TodoRow({ icon, text, sub, warn }) {
return (
);
}
function FilterSelect({ label, value, onChange, options }) {
return (
onChange(e.target.value)} aria-label={label}>
{options.map(([v, l]) => {l} )}
);
}
function PosterModal({ onClose }) {
return (
e.stopPropagation()}>
Aushang-Vorschau · DIN A4
}>PDF
Musterstraße 12
Neu für Ihr Gebäude
Günstiger Solarstrom direkt vom Dach.
Lokal erzeugt, transparent abgerechnet, in wenigen Minuten digital abgeschlossen. Die Teilnahme ist freiwillig — Ihr Strom bleibt jederzeit zuverlässig.
Scannen & starten
Meist günstiger als Grundversorgung
Solarstrom aus Ihrem eigenen Haus
Monatlich & transparent
Kein Aufwand in Ihrer Wohnung
powermieter.de/musterstrasse-12
Fragen? hilfe@powermieter.de
);
}
function initials(n) { return n.replace(/[^A-Za-zÀ-ÿ. ]/g, '').split(' ').filter(w => w && w !== '&' && !w.includes('.')).slice(0, 2).map(w => w[0]).join('').toUpperCase() || '–'; }
function avatarColor(n) { const h = [...n].reduce((a, c) => a + c.charCodeAt(0), 0) % 360; return `oklch(0.62 0.11 ${h})`; }
Object.assign(window, { AdminView });