/* 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 (
onChange(id)}>
{v.kurz}
{v.legal}
);
})}
{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
}>Alle Unterlagen
} onClick={() => onWizard(null)}>Neuer Vertrag
{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
? { e.stopPropagation(); onWizard(u); }}> Vorbereiten
: { e.stopPropagation(); onUnit(u); }}> )}
);
})}
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 (
e.stopPropagation()}>
{initials(unit.name)}
{unit.whg} · {unit.name}
Aufgang {unit.auf} · {unit.etage} · {VERTRAGSMODELLE[model].kurz}
{pct}% bereit
{unit.missing.length > 0 && (
Fehlende Angaben: {unit.missing.join(', ')}. Vertrag kann erst nach Vervollständigung versendet werden.
)}
{/* Go-Live checklist */}
Go-Live-Checkliste
{checks.map(c => (
{c.ok ? : }
{c.label}
))}
{/* Documents */}
Vertragsdokumente {docs.length}
{docs.map((d, i) => {
const ds = DOC_STATUS[d.doc];
return (
{d.name}{d.req && Pflicht }
{d.tpl} · {d.ver}
{ds.label}
);
})}
{/* Audit log */}
Audit-Log contract_events
{events.map((e, i) => (
{(I[e.icon] || I.dot)({ size: 13 })}
))}
{unit.missing.length || CSTATUS_RANK[unit.status] === 0
? } onClick={() => { onClose(); onWizard(unit); }}>Vertrag im Wizard vorbereiten
: <> }>Erinnerung senden }>Unterlagen exportieren>}
);
}
Object.assign(window, { ContractCockpit, UnitDrawer, CStatusBadge, ModelSelector, effUnits });