343 lines
26 KiB
Diff
343 lines
26 KiB
Diff
--- a/stripe.mjs
|
||
+++ b/stripe.mjs
|
||
@@ -22,7 +22,7 @@
|
||
cost_meter_aux: { sku: "ENX-BILLING-AUX", name: "Wärme- oder Wasserzähler Abrechnung", unitAmount: 300, setupAmount: 0, entitlement: true }
|
||
});
|
||
|
||
-const moduleKeys = ["battery", "buffer_storage", "ev_charger", "consumer_single", "boiler_multi"];
|
||
+const moduleKeys = ["battery", "buffer_storage", "ev_charger", "consumer_single", "boiler_multi", "heat_pump"];
|
||
const forecastKeys = ["forecast_pv", "forecast_load", "grid_schedule"];
|
||
const licenseKeys = [...moduleKeys, ...forecastKeys];
|
||
|
||
--- a/portal-domain.mjs
|
||
+++ b/portal-domain.mjs
|
||
@@ -5,7 +5,8 @@
|
||
buffer_storage: { name: "Pufferspeicher", catalogKey: "buffer_storage", moduleId: "{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}" },
|
||
ev_charger: { name: "EV-Ladestation", catalogKey: "ev_charger", moduleId: "{0D94913C-0F31-4C29-A685-6EB4AE58E55D}", gatewayModuleId: "{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}" },
|
||
consumer_single: { name: "Verbraucher einstufig", catalogKey: "consumer_single", moduleId: "{15879A4E-D0C2-4495-83DE-46E1E462591E}" },
|
||
- boiler_multi: { name: "Boiler mehrstufig", catalogKey: "boiler_multi", moduleId: "{B7C54AF4-AD7D-4FE4-B75D-203693906251}" }
|
||
+ boiler_multi: { name: "Boiler mehrstufig", catalogKey: "boiler_multi", moduleId: "{B7C54AF4-AD7D-4FE4-B75D-203693906251}" },
|
||
+ heat_pump: { name: "Wärmepumpe", catalogKey: "heat_pump", moduleId: "{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}" }
|
||
});
|
||
|
||
export const integrationProfiles = Object.freeze({
|
||
@@ -210,6 +211,23 @@
|
||
f("Mindesteinschaltdauer", "Mindest-Einschaltdauer (s)", "integer", 5, "Verbraucher", { min: 0 }),
|
||
f("Mindestausschaltdauer", "Mindest-Ausschaltdauer (s)", "integer", 5, "Verbraucher", { min: 0 })
|
||
],
|
||
+ heat_pump: [...commonConsumerFields,
|
||
+ f("Kontaktart", "Kontaktart", "select", 0, "Kontakte", { min: 0, max: 1, options: [{ value: 0, label: "Sperre und Erhöhung" }, { value: 1, label: "SG Ready" }] }),
|
||
+ f("Kontakt1VariableID", "Kontakt 1: Sperre / SG Ready 1 (Variable-ID)", "integer", 0, "Kontakte", { min: 0 }),
|
||
+ f("Kontakt2VariableID", "Kontakt 2: Erhöhung / SG Ready 2 (Variable-ID)", "integer", 0, "Kontakte", { min: 0 }),
|
||
+ f("Kontakt1Invertiert", "Kontakt 1 invertieren", "boolean", false, "Kontakte"),
|
||
+ f("Kontakt2Invertiert", "Kontakt 2 invertieren", "boolean", false, "Kontakte"),
|
||
+ f("Nennleistung", "Elektrische Nennleistung (W)", "integer", 0, "Wärmepumpe", { min: 0 }),
|
||
+ f("Rueckmeldungsart", "Rückmeldungsart", "select", 0, "Betriebsrückmeldung", { min: 0, max: 1, options: [{ value: 0, label: "Gemessene Leistung" }, { value: 1, label: "Boolean-Betriebsstatus" }] }),
|
||
+ f("IstleistungVariableID", "Leistung oder Betriebsstatus (Variable-ID)", "integer", 0, "Betriebsrückmeldung", { min: 0 }),
|
||
+ f("Laufschwelle", "Laufschwelle bei Leistungsmessung (W)", "number", 100, "Betriebsrückmeldung", { min: 0 }),
|
||
+ f("Anlaufwartezeit", "Anlaufwartezeit (s)", "integer", 30, "Schutzzeiten", { min: 0 }),
|
||
+ f("Wiederholsperre", "Wiederholsperre nach erfolglosem Anlauf (s)", "integer", 300, "Schutzzeiten", { min: 0 }),
|
||
+ f("Mindestlaufzeit", "Mindestlaufzeit nach bestätigtem Start (s)", "integer", 1200, "Schutzzeiten", { min: 0 }),
|
||
+ f("Mindestsperrzeit", "Mindest-Sperrzeit (s)", "integer", 1200, "Schutzzeiten", { min: 0 }),
|
||
+ f("MaximaleSperrzeit", "Maximale Sperrzeit am Stück (s; 0 = aus)", "integer", 7200, "Schutzzeiten", { min: 0 }),
|
||
+ f("MaximaleSperrzeit24h", "Maximale Sperrzeit in 24 Stunden (s; 0 = aus)", "integer", 21600, "Schutzzeiten", { min: 0, max: 86400 })
|
||
+ ],
|
||
boiler_multi: [...commonConsumerFields,
|
||
f("LeistungsStufen", "Leistungsstufen (JSON)", "json", "[]", "Warmwassererwärmer"),
|
||
f("Boilerfuehler_PT1", "Boilerfühler Variable-ID", "integer", 0, "Warmwassererwärmer", { min: 0 }),
|
||
@@ -253,6 +271,12 @@
|
||
"Nennleistung", "SchaltkontaktInvertiert", "Mindestlaufzeit",
|
||
"Mindesteinschaltdauer", "Mindestausschaltdauer"
|
||
]),
|
||
+ heat_pump: new Set([
|
||
+ "Kontaktart", "Kontakt1VariableID", "Kontakt2VariableID", "Kontakt1Invertiert",
|
||
+ "Kontakt2Invertiert", "Nennleistung", "Rueckmeldungsart", "IstleistungVariableID",
|
||
+ "Laufschwelle", "Anlaufwartezeit", "Wiederholsperre", "Mindestlaufzeit",
|
||
+ "Mindestsperrzeit", "MaximaleSperrzeit", "MaximaleSperrzeit24h"
|
||
+ ]),
|
||
boiler_multi: new Set([
|
||
"Boilervolumen", "Hysterese", "LegionellenfunktionAktiv",
|
||
"LegionellenMinimalintervallTage", "LegionellenMaximalintervallTage"
|
||
@@ -455,8 +479,8 @@
|
||
while (rawItems.length < requested) rawItems.push(legacyItem(key, source, rawItems.length));
|
||
const items = rawItems.map((item, index) => {
|
||
const variant = key === "ev_charger" && item.variant === "gateway" ? "gateway" : "standalone";
|
||
- const kind = key === "battery" ? "storage" : (["buffer_storage", "consumer_single", "boiler_multi"].includes(key) ? "actuator" : null);
|
||
- const integration = kind ? normalizeIntegration(item.integration, kind) : {
|
||
+ const kind = key === "battery" ? "storage" : (["buffer_storage", "consumer_single", "boiler_multi", "heat_pump"].includes(key) ? "actuator" : null);
|
||
+ const integration = kind ? normalizeIntegration(key === "heat_pump" ? { profile: "manual" } : item.integration, kind) : {
|
||
profile: text(item.integration?.profile || "device_api", 60),
|
||
host: text(item.integration?.host, 120),
|
||
port: finite(item.integration?.port, 0, 0, 65535, true),
|
||
@@ -588,6 +612,14 @@
|
||
return result;
|
||
}
|
||
|
||
+export function licenseCoverage(requested = {}, owned = {}) {
|
||
+ const coverage = { ...owned };
|
||
+ const spareConsumers = Math.max(0, Number(owned.consumer_single || 0) - Number(requested.consumer_single || 0));
|
||
+ coverage.heat_pump = Number(owned.heat_pump || 0) + Math.min(spareConsumers,
|
||
+ Math.max(0, Number(requested.heat_pump || 0) - Number(owned.heat_pump || 0)));
|
||
+ return coverage;
|
||
+}
|
||
+
|
||
export function recommendLicenses(configuration) {
|
||
const modules = {};
|
||
for (const key of Object.keys(systemDeviceTypes)) modules[key] = configuration.devices[key].quantity;
|
||
--- a/server.mjs
|
||
+++ b/server.mjs
|
||
@@ -19,6 +19,7 @@
|
||
createActivationCode,
|
||
normalizeActivationCode,
|
||
normalizeSystemConfiguration,
|
||
+ licenseCoverage,
|
||
publicWizardCatalog,
|
||
recommendLicenses
|
||
} from "./portal-domain.mjs";
|
||
@@ -2126,8 +2127,9 @@
|
||
const setup = ownedSetupState(plantId);
|
||
const modules = {};
|
||
const setupModules = {};
|
||
+ const coverage = licenseCoverage(recommendation.modules, owned.quantities);
|
||
for (const [key, requested] of Object.entries(recommendation.modules || {})) {
|
||
- modules[key] = Math.max(0, Number(requested || 0) - Number(owned.quantities[key] || 0));
|
||
+ modules[key] = Math.max(0, Number(requested || 0) - Number(coverage[key] || 0));
|
||
setupModules[key] = Math.max(0, Number(requested || 0) - Number(setup.quantities[key] || 0));
|
||
}
|
||
let managerAction = null;
|
||
--- a/symcon-package.mjs
|
||
+++ b/symcon-package.mjs
|
||
@@ -207,6 +207,33 @@
|
||
if (!in_array($moduleID, IPS_GetModuleList(), true)) throw new RuntimeException('Erforderliches Modul fehlt: ' . $moduleID);
|
||
}
|
||
|
||
+// Validate heat-pump bindings before creating or changing any system objects.
|
||
+foreach (($config['devices']['heat_pump']['items'] ?? []) as $item) {
|
||
+ if (!in_array($moduleIDs['heat_pump'], IPS_GetModuleList(), true)) {
|
||
+ throw new RuntimeException('Das Enelix-Waermepumpenmodul fehlt.');
|
||
+ }
|
||
+ $properties = $item['properties'];
|
||
+ $contact1 = (int) $properties['Kontakt1VariableID'];
|
||
+ $contact2 = (int) $properties['Kontakt2VariableID'];
|
||
+ if ($contact1 === $contact2 || (int) $properties['Nennleistung'] <= 0) {
|
||
+ throw new RuntimeException('Waermepumpe: zwei unterschiedliche Kontakte und Nennleistung erforderlich.');
|
||
+ }
|
||
+ foreach ([$contact1, $contact2] as $contact) {
|
||
+ if (!IPS_VariableExists($contact)) throw new RuntimeException('Waermepumpe: Kontaktvariable fehlt.');
|
||
+ $variable = IPS_GetVariable($contact);
|
||
+ if ($variable['VariableType'] !== 0 || max($variable['VariableAction'], $variable['VariableCustomAction']) <= 0) {
|
||
+ throw new RuntimeException('Waermepumpe: Kontakte muessen schaltbare Boolean-Variablen sein.');
|
||
+ }
|
||
+ }
|
||
+ $feedback = (int) $properties['IstleistungVariableID'];
|
||
+ if (!IPS_VariableExists($feedback)) throw new RuntimeException('Waermepumpe: Betriebsrueckmeldung fehlt.');
|
||
+ $feedbackType = IPS_GetVariable($feedback)['VariableType'];
|
||
+ $allowedTypes = (int) $properties['Rueckmeldungsart'] === 1 ? [0] : [1, 2];
|
||
+ if (!in_array($feedbackType, $allowedTypes, true)) {
|
||
+ throw new RuntimeException('Waermepumpe: Typ der Betriebsrueckmeldung passt nicht zur Rueckmeldungsart.');
|
||
+ }
|
||
+}
|
||
+
|
||
$root = enxCategory(0, 'ENELIX_System', (string) $config['name']);
|
||
$visualisation = enxCategory($root, 'Visualisierung', 'Visualisierung');
|
||
$energyView = enxCategory($visualisation, 'Energie', 'Energie und Bilanz');
|
||
--- a/public/index.html
|
||
+++ b/public/index.html
|
||
@@ -5,7 +5,7 @@
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="description" content="Enelix EMS Portal für Systeme, Lizenzen und Prognosemanager">
|
||
<title>Enelix EMS Portal</title>
|
||
- <link rel="stylesheet" href="/styles.css?v=20260929-3">
|
||
+ <link rel="stylesheet" href="/styles.css?v=20261006-heat-pump">
|
||
<link rel="stylesheet" href="/docs-portal.css?v=20261006">
|
||
</head>
|
||
<body>
|
||
@@ -65,7 +65,7 @@
|
||
<div id="portalApp" class="portal hidden">
|
||
<header class="topbar">
|
||
<a class="brand" href="#overview"><span class="brand-mark">E</span><span><strong>enelix</strong><small>EMS Portal</small></span></a>
|
||
- <div class="top-actions"><span class="environment">Entwicklung</span><button class="account-button" id="accountButton" type="button"><span id="topInitials">EN</span><strong id="topName">Konto</strong></button></div>
|
||
+ <div class="top-actions"><nav class="header-links" aria-label="Dokumentation und Preise"><a href="/docs/index.html">Dokumentation</a><a href="/docs/preise.html">Preise</a></nav><span class="environment">Entwicklung</span><button class="account-button" id="accountButton" type="button"><span id="topInitials">EN</span><strong id="topName">Konto</strong></button></div>
|
||
</header>
|
||
|
||
<div class="app-shell">
|
||
@@ -75,8 +75,6 @@
|
||
<button class="nav-item" data-view="forecast">Prognose</button>
|
||
<button class="nav-item" data-view="faults">Störüberwachung</button>
|
||
<button class="nav-item" data-view="account">Konto</button>
|
||
- <a class="nav-item public-doc-link" href="/docs/index.html">Dokumentation</a>
|
||
- <a class="nav-item public-doc-link" href="/docs/preise.html">Preise</a>
|
||
</nav>
|
||
<div class="workspace-switcher"><label>Organisation<select id="workspaceSelect"></select></label><small id="workspaceRole">Eigentümer</small></div>
|
||
</aside>
|
||
@@ -100,7 +98,8 @@
|
||
<label class="module-row" data-key="battery" data-name="Batteriespeicher" data-price="41.95" data-setup="54.55"><span><input type="checkbox" class="module-check"><strong>Batteriespeicher</strong><small>Batterie</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 41.95</span><span>CHF 54.55</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
<label class="module-row" data-key="buffer_storage" data-name="Pufferspeicher" data-price="25.15" data-setup="32.70"><span><input type="checkbox" class="module-check"><strong>Pufferspeicher</strong><small>VerbraucherPufferspeicher</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 25.15</span><span>CHF 32.70</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
<label class="module-row" data-key="ev_charger" data-name="EV-Ladestation" data-price="33.65" data-setup="43.75"><span><input type="checkbox" class="module-check"><strong>EV-Ladestation</strong><small>Stand-Alone oder Gateway</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 33.65</span><span>CHF 43.75</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
- <label class="module-row" data-key="consumer_single" data-name="Verbraucher einstufig" data-price="20.95" data-setup="27.25"><span><input type="checkbox" class="module-check"><strong>Verbraucher einstufig</strong><small>z. B. Wärmepumpe</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 20.95</span><span>CHF 27.25</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
+ <label class="module-row" data-key="consumer_single" data-name="Verbraucher einstufig" data-price="20.95" data-setup="27.25"><span><input type="checkbox" class="module-check"><strong>Verbraucher einstufig</strong><small>Ein schaltbarer Verbraucher</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 20.95</span><span>CHF 27.25</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
+ <label class="module-row" data-key="heat_pump" data-name="Wärmepumpe" data-price="20.95" data-setup="27.25"><span><input type="checkbox" class="module-check"><strong>Wärmepumpe</strong><small>Sperre / Erhöhung oder SG Ready</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 20.95</span><span>CHF 27.25</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
<label class="module-row" data-key="boiler_multi" data-name="Boiler mehrstufig" data-price="25.15" data-setup="32.70"><span><input type="checkbox" class="module-check"><strong>Boiler mehrstufig</strong><small>Warmwassererwärmer</small><small class="owned-count">Vorhanden: <b>0</b></small></span><span>CHF 25.15</span><span>CHF 32.70</span><input class="qty" type="number" min="1" max="999" value="1"></label>
|
||
<div class="module-header"><span>Prognoselizenz</span><span>Jahrespreis</span><span>Einrichtung</span><span>Menge</span></div>
|
||
<label class="module-row" data-key="forecast_pv" data-name="PV-Ertragsprognose" data-price="0" data-setup="0" data-term-months="12"><span><input type="checkbox" class="module-check"><strong>PV-Ertragsprognose</strong><small>Jahreslizenz</small><small class="owned-count">Nicht aktiviert</small></span><span>CHF 0.00 / Jahr</span><span>–</span><input class="qty" type="number" min="1" max="1" value="1"></label>
|
||
@@ -117,13 +116,13 @@
|
||
<form id="systemForm" class="system-configurator">
|
||
<section class="config-form wizard">
|
||
<header class="wizard-header">
|
||
- <div><p class="eyebrow" id="wizardStepCount">Schritt 1 von 9</p><h2 id="wizardStepTitle">Anlage</h2></div>
|
||
+ <div><p class="eyebrow" id="wizardStepCount">Schritt 1 von 10</p><h2 id="wizardStepTitle">Anlage</h2></div>
|
||
<ol class="wizard-progress" aria-label="Fortschritt">
|
||
<li class="active" data-wizard-marker="0">Anlage</li><li data-wizard-marker="1">Energiequellen</li>
|
||
<li data-wizard-marker="2">Batterie</li><li data-wizard-marker="3">Pufferspeicher</li>
|
||
<li data-wizard-marker="4">Ladestation</li><li data-wizard-marker="5">Verbraucher</li>
|
||
- <li data-wizard-marker="6">Boiler</li><li data-wizard-marker="7">Abrechnung</li>
|
||
- <li data-wizard-marker="8">Prüfen</li>
|
||
+ <li data-wizard-marker="6">Boiler</li><li data-wizard-marker="7">Wärmepumpe</li>
|
||
+ <li data-wizard-marker="8">Abrechnung</li><li data-wizard-marker="9">Prüfen</li>
|
||
</ol>
|
||
</header>
|
||
<section class="wizard-step active" data-wizard-step="0">
|
||
@@ -190,11 +189,16 @@
|
||
<div class="device-editor" data-device-editor="boiler_multi"></div>
|
||
</section>
|
||
<section class="wizard-step" data-wizard-step="7">
|
||
+ <p class="wizard-question">Welche Wärmepumpen sollen eingerichtet werden?</p>
|
||
+ <div class="quantity-line"><label>Anzahl<input name="heatPumpQuantity" data-device-quantity="heat_pump" type="number" min="0" max="50" value="0"></label></div>
|
||
+ <div class="device-editor" data-device-editor="heat_pump"></div>
|
||
+ </section>
|
||
+ <section class="wizard-step" data-wizard-step="8">
|
||
<p class="wizard-question">Soll im selben Symcon-System ein Verbrauchskostenreport eingerichtet werden?</p>
|
||
<label class="setup-option"><input name="costReportEnabled" type="checkbox"><span><strong>Verbrauchskostenreport hinzufügen</strong><small>Wird als eigene Instanz eingerichtet und verwendet einen separaten Lizenzcode.</small></span></label>
|
||
<div class="field-grid"><label>Stromzähler<input name="costPowerMeters" type="number" min="0" max="999" value="0"></label><label>Wärme- oder Wasserzähler<input name="costAuxMeters" type="number" min="0" max="999" value="0"></label></div>
|
||
</section>
|
||
- <section class="wizard-step" data-wizard-step="8">
|
||
+ <section class="wizard-step" data-wizard-step="9">
|
||
<p class="wizard-question">Bitte prüfe die Angaben vor dem Speichern.</p>
|
||
<div id="wizardReview" class="wizard-review"></div>
|
||
</section>
|
||
@@ -367,6 +371,6 @@
|
||
|
||
<dialog id="deletePaidDialog" class="warning-dialog"><div class="dialog-body"><div class="dialog-head"><div><p class="eyebrow">Zweite Bestätigung</p><h2>Auch Lizenz löschen?</h2></div><button class="icon-button" type="button" data-close-dialog aria-label="Schließen">×</button></div><div class="replacement-warning critical-warning"><strong>Achtung: Für diese Anlage besteht eine bezahlte Lizenz.</strong><p>Sollen <b id="deletePaidSystem">die Anlage</b> und die zugehörigen Lizenzcodes, Berechtigungen, Bestellungen und Sandbox-Zahlungsdaten wirklich endgültig gelöscht werden?</p><p>Diese Aktion kann nicht rückgängig gemacht werden.</p></div><p class="form-error" id="deletePaidError"></p><div class="dialog-actions"><button class="button secondary" type="button" data-close-dialog>Abbrechen</button><button class="button danger" id="confirmDeletePaid" type="button">Anlage und Lizenz löschen</button></div></div></dialog>
|
||
|
||
- <script src="/app.js?v=20260929-3"></script>
|
||
+ <script src="/app.js?v=20261006-heat-pump"></script>
|
||
</body>
|
||
</html>
|
||
--- a/public/app.js
|
||
+++ b/public/app.js
|
||
@@ -7,7 +7,7 @@
|
||
manager_peak: { name: "Manager mit Peak Shaving", price: 329.05, setup: 141.80 },
|
||
manager_peak_upgrade: { name: "Upgrade auf Peak Shaving", price: 20.90, setup: 0 }
|
||
};
|
||
- var deviceLabels = { battery: "Batteriespeicher", buffer_storage: "Pufferspeicher", ev_charger: "EV-Ladestation", consumer_single: "Verbraucher einstufig", boiler_multi: "Boiler mehrstufig" };
|
||
+ var deviceLabels = { battery: "Batteriespeicher", buffer_storage: "Pufferspeicher", ev_charger: "EV-Ladestation", consumer_single: "Verbraucher einstufig", boiler_multi: "Boiler mehrstufig", heat_pump: "Wärmepumpe" };
|
||
var roleLabels = { owner: "Eigentümer", admin: "Administrator", operator: "Operator", billing: "Abrechnung", viewer: "Lesend" };
|
||
var money = new Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" });
|
||
var byId = function (id) { return document.getElementById(id); };
|
||
@@ -437,6 +437,14 @@
|
||
return ownedManager;
|
||
}
|
||
|
||
+ function licenseCoverage(requested, owned) {
|
||
+ var coverage = Object.assign({}, owned);
|
||
+ var spareConsumers = Math.max(0, Number(owned.consumer_single || 0) - Number(requested.consumer_single || 0));
|
||
+ coverage.heat_pump = Number(owned.heat_pump || 0) + Math.min(spareConsumers,
|
||
+ Math.max(0, Number(requested.heat_pump || 0) - Number(owned.heat_pump || 0)));
|
||
+ return coverage;
|
||
+ }
|
||
+
|
||
function prepareOrderEditor(configurationId) {
|
||
var plant = selectedPlant("orderPlant");
|
||
if (!plant) return;
|
||
@@ -445,6 +453,7 @@
|
||
var recommendation = plantRecommendation(plant);
|
||
var owned = paidLicenseSummary(plant.id);
|
||
var setupOwned = paidSetupSummary(plant.id);
|
||
+ var coverage = licenseCoverage(recommendation?.modules || {}, owned);
|
||
var ownedManager = paidManagerVariant(plant);
|
||
var desiredManager = recommendation?.managerVariant || plant.manager_variant || "manager_standard";
|
||
byId("managerCurrentName").textContent = ownedManager ? managerLabel(ownedManager) : managerLabel(desiredManager);
|
||
@@ -455,7 +464,7 @@
|
||
document.querySelectorAll(".module-row").forEach(function (row) {
|
||
var recommendedQuantity = recommendation ? Number(recommendation.modules[row.dataset.key] || 0) : 0;
|
||
var ownedQuantity = Number(owned[row.dataset.key] || 0);
|
||
- var licenseQuantity = Math.max(0, recommendedQuantity - ownedQuantity);
|
||
+ var licenseQuantity = Math.max(0, recommendedQuantity - Number(coverage[row.dataset.key] || 0));
|
||
var setupQuantity = configurationPurchase
|
||
? Math.max(0, recommendedQuantity - Number(setupOwned[row.dataset.key] || 0))
|
||
: 0;
|
||
@@ -1877,7 +1886,7 @@
|
||
renderPvSurfaceEditor(pvSurfaceQuantity, pvSurfaces);
|
||
updatePvTopologyVisibility(form);
|
||
renderManagerFields(configuration.manager?.properties || {});
|
||
- var quantityNames = { battery: "batteryQuantity", buffer_storage: "bufferQuantity", ev_charger: "evQuantity", consumer_single: "consumerQuantity", boiler_multi: "boilerQuantity" };
|
||
+ var quantityNames = { battery: "batteryQuantity", buffer_storage: "bufferQuantity", ev_charger: "evQuantity", consumer_single: "consumerQuantity", boiler_multi: "boilerQuantity", heat_pump: "heatPumpQuantity" };
|
||
Object.keys(deviceLabels).forEach(function (key) {
|
||
var source = configuration.devices[key] || {};
|
||
var quantity = Math.max(Number(source.quantity || 0), Number(owned[key] || 0));
|
||
@@ -1908,7 +1917,7 @@
|
||
|
||
function systemPayload(form) {
|
||
var values = Object.fromEntries(new FormData(form).entries());
|
||
- var quantityNames = { battery: "batteryQuantity", buffer_storage: "bufferQuantity", ev_charger: "evQuantity", consumer_single: "consumerQuantity", boiler_multi: "boilerQuantity" };
|
||
+ var quantityNames = { battery: "batteryQuantity", buffer_storage: "bufferQuantity", ev_charger: "evQuantity", consumer_single: "consumerQuantity", boiler_multi: "boilerQuantity", heat_pump: "heatPumpQuantity" };
|
||
var devices = {};
|
||
Object.keys(deviceLabels).forEach(function (key) {
|
||
devices[key] = { quantity: Number(values[quantityNames[key]] || 0), items: collectDeviceItems(key) };
|
||
@@ -1932,7 +1941,7 @@
|
||
};
|
||
}
|
||
|
||
- var wizardTitles = ["Anlage", "Manager, Netz und PV", "Batteriespeicher", "Pufferspeicher", "EV-Ladestation", "Einstufige Verbraucher", "Boiler", "Verbrauchskostenreport", "Angaben prüfen"];
|
||
+ var wizardTitles = ["Anlage", "Manager, Netz und PV", "Batteriespeicher", "Pufferspeicher", "EV-Ladestation", "Einstufige Verbraucher", "Boiler", "Wärmepumpe", "Verbrauchskostenreport", "Angaben prüfen"];
|
||
|
||
function updateRecommendation() {
|
||
var payload = systemPayload(byId("systemForm"));
|
||
@@ -1941,9 +1950,11 @@
|
||
var selectedDevices = Object.keys(deviceLabels).filter(function (key) { return payload.devices[key].quantity > 0; });
|
||
var configuredPlant = state.plants.find(function (plant) { return plant.id === byId("systemForm").dataset.plantId; });
|
||
var owned = configuredPlant ? paidLicenseSummary(configuredPlant.id) : {};
|
||
+ var requestedModules = Object.fromEntries(Object.entries(payload.devices).map(function (entry) { return [entry[0], entry[1].quantity]; }));
|
||
+ var coverage = licenseCoverage(requestedModules, owned);
|
||
var rows = selectedDevices.map(function (key) {
|
||
var requested = Number(payload.devices[key].quantity || 0);
|
||
- var licensed = Number(owned[key] || 0);
|
||
+ var licensed = Number(coverage[key] || 0);
|
||
var missing = Math.max(0, requested - licensed);
|
||
var licenseText = missing
|
||
? licensed + " lizenziert · " + missing + " zusätzliche " + (missing === 1 ? "Lizenz" : "Lizenzen") + " erforderlich"
|
||
--- a/public/styles.css
|
||
+++ b/public/styles.css
|
||
@@ -85,6 +85,10 @@
|
||
.portal { min-height: 100vh; }
|
||
.topbar { position: sticky; z-index: 20; top: 0; display: flex; height: 68px; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.96); padding: 0 28px; }
|
||
.top-actions { display: flex; align-items: center; gap: 13px; }
|
||
+.header-links { display: flex; align-items: center; gap: 24px; margin-right: 16px; }
|
||
+.header-links a { color: var(--petrol); font-size: .88rem; font-weight: 700; text-decoration: none; padding: 10px 0; white-space: nowrap; }
|
||
+.header-links a:hover { text-decoration: underline; text-underline-offset: 5px; }
|
||
+.header-links a:focus-visible { outline: 2px solid var(--petrol); outline-offset: 4px; }
|
||
.environment, .status { display: inline-flex; align-items: center; border-radius: 999px; white-space: nowrap; font-size: .7rem; font-weight: 800; }
|
||
.environment { background: #edf2f1; color: #566568; padding: 5px 9px; text-transform: uppercase; letter-spacing: .07em; }
|
||
.account-button { display: flex; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--ink); }
|
||
@@ -239,8 +243,8 @@
|
||
.wizard { min-height: 520px; align-content: start; border-top: 3px solid var(--petrol); background: #fff; padding: 24px; }
|
||
.wizard-header { display: grid; gap: 20px; border-bottom: 1px solid var(--line); padding-bottom: 20px; }
|
||
.wizard-header h2 { font-size: 1.45rem; }
|
||
-.wizard-progress { display: grid; grid-template-columns: repeat(9, minmax(72px, 1fr)); overflow-x: auto; margin: 0; padding: 0; list-style: none; }
|
||
-.wizard-progress li { min-width: 72px; border-top: 3px solid var(--line); padding: 8px 5px 0; color: var(--muted); font-size: .7rem; font-weight: 750; text-align: center; }
|
||
+.wizard-progress { display: grid; grid-template-columns: repeat(10, minmax(max-content, 1fr)); overflow-x: auto; margin: 0; padding: 0; list-style: none; }
|
||
+.wizard-progress li { min-width: 72px; border-top: 3px solid var(--line); padding: 8px 8px 0; color: var(--muted); font-size: .7rem; font-weight: 750; text-align: center; white-space: nowrap; }
|
||
.wizard-progress li.active { border-color: var(--lime); color: var(--ink); }
|
||
.wizard-progress li.done { border-color: var(--petrol); color: var(--petrol); }
|
||
.wizard-step { display: none; min-height: 250px; padding: 28px 0 8px; }
|
||
@@ -325,6 +329,9 @@
|
||
|
||
@media (max-width: 680px) {
|
||
.topbar { height: 62px; padding: 0 15px; }
|
||
+ .top-actions { gap: 10px; }
|
||
+ .header-links { gap: 12px; margin-right: 0; }
|
||
+ .header-links a { font-size: .75rem; }
|
||
.brand strong { font-size: 1.05rem; }
|
||
.brand-mark { width: 32px; height: 32px; }
|
||
.environment, .account-button strong { display: none; }
|