/* Powermieter — Vertrags-Cockpit (Hausverwaltung) */ const { useState: useCS, useMemo: useCM } = React; function CStatusBadge({ s, size }) { const st = CSTATUS[s] || CSTATUS.entwurf; return {st.label}; } /* effective units given the "Datenfülle" tweak */ function effUnits(datenfuelle) { if (datenfuelle === 'vollstaendig') { return UNIT_CONTRACTS.map(u => u.vacant ? u : ({ ...u, missing: [], status: u.status === 'daten_fehlen' ? 'bereit' : u.status, flag: false, })); } return UNIT_CONTRACTS; } function ModelSelector({ model, onChange }) { const m = VERTRAGSMODELLE[model]; return (
Vertragsmodell des Objekts {m.legal}
{MODELL_ORDER.map(id => { const v = VERTRAGSMODELLE[id]; return ( ); })}

{m.desc}{!m.zuschlag && model !== 'pre' && · Kein Mieterstromzuschlag (BNetzA).}

); } function ContractCockpit({ model, setModel, tweaks, onWizard, onUnit }) { const gate = tweaks.goLiveGate; const units = useCM(() => effUnits(tweaks.datenfuelle), [tweaks.datenfuelle]); const live = units.filter(u => !u.vacant); const occupied = live.length; const signedRanks = u => (CSTATUS_RANK[u.status] || 0) >= 4; const activeU = units.filter(u => u.status === 'aktiv').length; const signedU = units.filter(signedRanks).length; const missingU = units.filter(u => !u.vacant && u.missing.length).length; const avgReady = Math.round(live.reduce((a, u) => a + readinessPct(u, model, gate), 0) / occupied); const realQuote = Math.round(signedU / occupied * 100); const quote = tweaks.quote != null ? tweaks.quote : realQuote; const ready = live.filter(u => readinessPct(u, model, gate) === 100).length; const objReady = gate === 'erfuellt' || ready === occupied; const breakdown = CSTATUS_ORDER.map(s => ({ s, n: units.filter(u => u.status === s).length })).filter(b => b.n > 0); return (
{/* Model selector */} {/* Go-Live gate banner */}
{objReady ? : }
{objReady ? 'Objekt ist go-live-bereit' : 'Go-Live-Gate noch nicht erfüllt'}
{objReady ? `Alle ${occupied} bewohnten Einheiten erfüllen die Vertrags- und Stammdatenlogik. Aktivierung & Abrechnung freigegeben.` : `${occupied - ready} von ${occupied} Einheiten erfüllen das Gate noch nicht. Powermieter rechnet erst ab, wenn Vertrag, Zustimmung und Stammdaten vollständig sind.`}
{ready}/{occupied}
Einheiten bereit
{/* KPIs */}
} value={occupied} label="Aktive Einheiten" sub={`${units.length} gesamt · 1 leer`} /> } value={signedU} label="Verträge unterzeichnet" tone="positive" sub={`${activeU} bereits aktiv`} /> } value={missingU} label="Fehlende Daten" tone={missingU ? 'warn' : 'positive'} sub={missingU ? 'Einheiten unvollständig' : 'alles vollständig'} /> } value={avgReady + '%'} label="Ø Go-Live-Readiness" tone={avgReady === 100 ? 'positive' : 'accent'} sub={`Teilnahmequote ${quote}%`} />
{/* Status pipeline */}

Vertrags-Pipeline

{breakdown.map(b => )}
{breakdown.map(b => (
{CSTATUS[b.s].label}{b.n}
))}
{/* Contract table */}
Einheit
Bewohner
Vertragsstatus
Dokumente
Go-Live
Aktion
{units.map(u => { const pct = readinessPct(u, model, gate); const docs = u.vacant ? [] : unitDocs(u, model); const done = docs.filter(d => d.doc === 'signiert' || d.doc === 'archiviert').length; return (
!u.vacant && onUnit(u)} style={{ cursor: u.vacant ? 'default' : 'pointer' }}>
{u.whg}{u.auf} · {u.etage}
{u.vacant ? '—' : initials(u.name)} {u.name}{u.flag && }
{u.vacant ? : ( {done}/{docs.length}{u.docVer} )}
{u.vacant ? : ( {pct}% )}
{!u.vacant && (u.missing.length || CSTATUS_RANK[u.status] === 0 ? : )}
); })}

IBAN, Steuernummer & Vertragspartnerdaten liegen auf Legal-Entity-Ebene ({LEGAL_ENTITY.name}), nicht je Gebäude. Jede Änderung wird revisionssicher im Audit-Log (contract_events) protokolliert.

); } /* ---------------- Unit drawer: documents, go-live, audit ---------------- */ function UnitDrawer({ unit, model, tweaks, onClose, onWizard }) { const docs = unitDocs(unit, model); const checks = goLiveChecks(unit, model, tweaks.goLiveGate); const events = unitEvents(unit); const pct = readinessPct(unit, model, tweaks.goLiveGate); return (
); } Object.assign(window, { ContractCockpit, UnitDrawer, CStatusBadge, ModelSelector, effUnits });