/* Konfigurator — Smartmacherei.at
Pfad-aware (altbau/neubau/gewerbe), 4 Schritte:
0) Methode wählen 1) Räume erfassen 2) Module pro Raum 3) Empfehlung+Preis
*/
/* Cyan-on-dark — passend zum Rest der Website */
const KFG_PALETTE = {
ink: "#ffffff", // Vordergrund-Text (war: dunkel)
paper: "#0A1B28", // Sektion-Hintergrund (leicht heller als bg-deep)
card: "#0D2030", // Karten / Eingabe-Flächen
accent: "#3ABDD0", // Cyan-Akzent
warm: "#8DDBEA", // sekundärer Akzent
muted: "rgba(141,219,234,0.55)", // gedämpfter Text
line: "rgba(141,219,234,0.18)", // Linien / Borders
};
/* ----- Demo-Daten: EFH Basting (echte Werte aus Plan) -----
windowSides: ["top"|"right"|"bottom"|"left"] gibt die Wand pro Fenster an.
doorSide: Wand der (Haupt-)Tür. Falls leer → "bottom". */
const BASTING_ROOMS = [
{ id:"r1", name:"Wohnen/Essen", area:31.62, type:"living", floor:"EG", windows:3, windowSides:["bottom","bottom","right"], doorSide:"left" },
{ id:"r2", name:"Kochen", area:10.31, type:"kitchen", floor:"EG", windows:1, windowSides:["left"], doorSide:"bottom" },
{ id:"r3", name:"Speiß", area: 4.50, type:"storage", floor:"EG", windows:0, doorSide:"bottom" },
{ id:"r4", name:"VR", area:10.02, type:"hall", floor:"EG", windows:0, doorSide:"right" },
{ id:"r5", name:"WC", area: 2.16, type:"wc", floor:"EG", windows:1, windowSides:["right"], doorSide:"left" },
{ id:"r6", name:"WiRa/HT", area: 4.81, type:"tech", floor:"EG", windows:0, doorSide:"right" },
{ id:"r7", name:"Bad", area: 9.00, type:"bath", floor:"EG", windows:1, windowSides:["top"], doorSide:"bottom" },
{ id:"r8", name:"Schlafzimmer", area:19.22, type:"bedroom", floor:"EG", windows:2, windowSides:["top","left"], doorSide:"right" },
{ id:"r9", name:"K Zimmer 1", area:13.60, type:"bedroom", floor:"EG", windows:1, windowSides:["top"], doorSide:"bottom" },
{ id:"r10", name:"K Zimmer 2", area:13.13, type:"bedroom", floor:"EG", windows:1, windowSides:["top"], doorSide:"bottom" },
{ id:"r11", name:"Zimmer", area: 9.49, type:"room", floor:"EG", windows:1, windowSides:["right"], doorSide:"left" },
{ id:"r12", name:"Garderobe", area: 7.13, type:"hall", floor:"EG", windows:0, doorSide:"left" },
{ id:"r13", name:"Terrasse", area:31.88, type:"terrace", floor:"EG", windows:0, outdoor:true },
{ id:"r14", name:"Zugang", area: 3.55, type:"entry", floor:"EG", windows:0, doorSide:"bottom" },
];
/* ----- DIN 18015-2 / RAL-RG 678 Ausstattungsklassen -----
* Mindestempfehlung pro Raumtyp und Klasse. Werte sind
* Branchen-Faustregeln aus RAL-RG 678 + Hersteller-Empfehlungen
* (HEA, Hager, Elektro+, Voltimum). Annaeherung an die Norm,
* Detail-Auswahl im Konfigurator durch User justierbar.
*
* Indexen: 0 = Mindestausstattung (1*) DIN 18015-2 minimum
* 1 = Standardausstattung (2*) ueblich im EFH-Neubau
* 2 = Komfortausstattung (3*) gehobener Standard
*/
const DIN_18015_2 = {
living: { sockets: [4, 6, 8], light_circuits: [1, 2, 3], data: [1, 2, 3] },
bedroom: { sockets: [3, 5, 7], light_circuits: [1, 1, 2], data: [0, 1, 2] },
kitchen: { sockets: [6, 8, 10], light_circuits: [1, 2, 3], data: [0, 1, 1] },
bath: { sockets: [1, 2, 3], light_circuits: [1, 1, 2], data: [0, 0, 0] },
wc: { sockets: [0, 1, 1], light_circuits: [1, 1, 1], data: [0, 0, 0] },
hall: { sockets: [1, 2, 3], light_circuits: [1, 1, 2], data: [0, 0, 1] },
room: { sockets: [3, 4, 6], light_circuits: [1, 1, 2], data: [0, 1, 2] },
tech: { sockets: [2, 4, 6], light_circuits: [1, 1, 1], data: [0, 1, 1] },
storage: { sockets: [1, 1, 2], light_circuits: [1, 1, 1], data: [0, 0, 0] },
};
const KLASSE_LABELS = ["Mindest (DIN)", "Standard", "Komfort"];
function dinValue(roomType, axis, klasse) {
const row = DIN_18015_2[roomType] || DIN_18015_2.room;
return (row[axis] || [1, 1, 1])[klasse] ?? 1;
}
/* ----- WKO-Regiestundensaetze 2026 (Oesterreich, AT) -----
* Quelle: wko.at/oe/gewerbe-handwerk/elektro-gebaeude-alarm-kommunikation
* Stand Mai 2026.
*/
const WKO_STUNDENSATZ = {
monteur: 70.66, // Standard-Elektriker
spezial_monteur: 91.28, // KNX/Bus-Programmierung, Inbetriebnahme
spezial: 131.62, // SPS / Spezial-PLC
};
/* ----- Modul-Katalog (pfad-spezifisch) -----
* Erweiterte Felder fuer DIN-/Preis-Modell:
* material_unit EUR pro Stueck (Aktor-Anteil + Sensor + Zubehoer)
* material_base EUR einmalig pro Raum (Verkabelung, UV-Anschluss)
* install_minutes Min pro Stueck
* install_minutes_base Min einmalig pro Raum
* defaultCount(room, klasse) -- klasse=0|1|2 (Mindest/Standard/Komfort)
*
* Backwards-Compat: unitPrice/base/price-Wrapper bleiben (geben material_unit
* etc. zurueck), damit getModulePrice + KfgRoomPreview unveraendert
* weiterlaufen.
*/
/* Helper fuer Counts pro Raum + Klasse. Wenn Klasse nicht uebergeben,
* Standard-Klasse (1) verwenden -- Backwards-Compat fuer alte Aufrufe. */
const _din = (axis) => (r, klasse = 1) => dinValue(r.type, axis, klasse);
const MODULES_NEUBAU = {
licht: {
label: "Licht (KNX/DALI)", icon: "💡",
countable: true, unit: "Schaltkreise",
defaultCount: _din("light_circuits"),
// KNX-Aktor (4-fach Median ~150 EUR -> 38/Kanal) + Verkabelung pro Kreis.
material_unit: 60, // Aktor-Anteil + Bus-Kabel
material_base: 180, // Tastsensor + UV-Anteil + Verkabelung Raum
install_minutes: 35, // pro Schaltkreis
install_minutes_base: 60,
default: ["living","kitchen","bedroom","bath","hall","room","tech"],
},
steckdosen: {
label: "Schaltbare Steckdosen", icon: "⏻",
countable: true, unit: "Steckdosen",
defaultCount: _din("sockets"),
material_unit: 35, // Steckdose + Klemme
material_base: 0,
install_minutes: 22,
install_minutes_base: 0,
default: ["living","bedroom","kitchen","room","tech"],
},
beschattung: {
label: "Beschattung", icon: "⬇",
countable: true, unit: "Fenster",
defaultCount: (r) => r.windows || 0,
material_unit: 220, // Beschattungsaktor anteilig + Antrieb-Vorbereitung
material_base: 0,
install_minutes: 35,
install_minutes_base: 0,
default: ["living","bedroom","kitchen","room"],
},
heizung: {
label: "Einzelraum-Heizung", icon: "🌡",
price: () => 220, // legacy fixed -- ein Heizungsaktor-Kanal + Stellantrieb
material_unit: 0, material_base: 220,
install_minutes: 0, install_minutes_base: 50,
default: ["living","bedroom","bath","room","kitchen","wc"],
},
klima: {
label: "Klima/Lüftung", icon: "❄",
price: () => 580,
material_unit: 0, material_base: 580,
install_minutes: 0, install_minutes_base: 90,
default: ["living","bedroom"],
},
zutritt: {
label: "Zutritt/Sicherheit", icon: "🔐",
price: () => 420,
material_unit: 0, material_base: 420,
install_minutes: 0, install_minutes_base: 80,
default: ["entry","hall"],
},
multimedia: {
label: "Multimedia/Audio", icon: "♪",
price: () => 380,
material_unit: 0, material_base: 380,
install_minutes: 0, install_minutes_base: 60,
default: ["living"],
},
sensorik: {
label: "Sensorik (Bewegung/CO₂)", icon: "◉",
price: () => 180,
material_unit: 0, material_base: 180,
install_minutes: 0, install_minutes_base: 30,
default: ["living","bath","wc","entry","hall"],
},
};
const MODULES_ALTBAU = {
licht: {
label: "Licht (Funk)", icon: "💡",
countable: true, unit: "Schaltkreise",
defaultCount: _din("light_circuits"),
material_unit: 55, // Funk-Schaltaktor pro Kreis (Eltako/Loxone Air)
material_base: 100, // Funk-Sensor pro Raum
install_minutes: 25,
install_minutes_base: 30,
default: ["living","kitchen","bedroom","bath","hall","room","tech"],
},
steckdosen: {
label: "Smart-Steckdosen", icon: "⏻",
countable: true, unit: "Steckdosen",
defaultCount: _din("sockets"),
material_unit: 28, // Funk-/Zwischenstecker / Unterputz-Schaltaktor
material_base: 0,
install_minutes: 8, // bei Funk minimal -- nur tauschen
install_minutes_base: 0,
default: ["living","bedroom","kitchen","room"],
},
beschattung: {
label: "Beschattung (Funk)", icon: "⬇",
countable: true, unit: "Fenster",
defaultCount: (r) => r.windows || 0,
material_unit: 180,
material_base: 0,
install_minutes: 30,
install_minutes_base: 0,
default: ["living","bedroom","kitchen","room"],
},
heizung: {
label: "Smart-Heizkörper-Ventil", icon: "🌡",
price: () => 180,
material_unit: 0, material_base: 180,
install_minutes: 0, install_minutes_base: 25,
default: ["living","bedroom","bath","room","kitchen"],
},
zutritt: {
label: "Zutritt (Smart-Lock)", icon: "🔐",
price: () => 380,
material_unit: 0, material_base: 380,
install_minutes: 0, install_minutes_base: 60,
default: ["entry"],
},
multimedia: {
label: "Multimedia", icon: "♪",
price: () => 320,
material_unit: 0, material_base: 320,
install_minutes: 0, install_minutes_base: 45,
default: ["living"],
},
sensorik: {
label: "Sensorik (Funk)", icon: "◉",
price: () => 140,
material_unit: 0, material_base: 140,
install_minutes: 0, install_minutes_base: 15,
default: ["living","bath","entry"],
},
};
const MODULES_GEWERBE = {
licht: {
label: "Licht (DALI/KNX)", icon: "💡",
countable: true, unit: "Schaltkreise",
defaultCount: (r, klasse = 1) => Math.ceil(r.area / 10) + (klasse === 0 ? 0 : klasse), // mehr Kreise bei Komfort
material_unit: 90, // DALI-Treiber + KNX-Aktor anteilig
material_base: 320,
install_minutes: 45,
install_minutes_base: 90,
default: ["living","room","office","bedroom","kitchen","tech","hall","entry","storage"],
},
beschattung: {
label: "Beschattung wetterabh.", icon: "⬇",
countable: true, unit: "Fenster",
defaultCount: (r) => r.windows || 0,
material_unit: 280,
material_base: 220, // Wetterstation anteilig
install_minutes: 40,
install_minutes_base: 60,
default: ["living","room","office","bedroom"],
},
klima: {
label: "HLK-Steuerung", icon: "❄",
price: () => 720,
material_unit: 0, material_base: 720,
install_minutes: 0, install_minutes_base: 120,
default: ["living","room","office","bedroom"],
},
zutritt: {
label: "Zutritt + Rollen", icon: "🔐",
price: () => 880,
material_unit: 0, material_base: 880,
install_minutes: 0, install_minutes_base: 150,
default: ["entry","hall"],
},
energie: {
label: "Energie + Submetering", icon: "⚡",
price: () => 320,
material_unit: 0, material_base: 320,
install_minutes: 0, install_minutes_base: 45,
default: ["tech"],
},
sensorik: {
label: "Sensorik (Präsenz/CO₂)", icon: "◉",
price: () => 240,
material_unit: 0, material_base: 240,
install_minutes: 0, install_minutes_base: 30,
default: ["living","room","office","bedroom","hall","entry"],
},
alarm: {
label: "Status + Eskalation", icon: "⚠",
price: () => 540,
material_unit: 0, material_base: 540,
install_minutes: 0, install_minutes_base: 90,
},
};
const MODULE_SETS = {
altbau: MODULES_ALTBAU,
neubau: MODULES_NEUBAU,
gewerbe: MODULES_GEWERBE,
};
/* Resolve count for a (room,module): user override > default > 0.
* klasse=0|1|2 wird an mod.defaultCount durchgereicht. */
function getModuleCount(mod, room, counts, roomId, modKey, klasse = 1) {
if (!mod.countable) return 1;
const override = counts && counts[roomId] && counts[roomId][modKey];
if (typeof override === "number") return Math.max(0, override);
if (!mod.defaultCount) return 0;
// defaultCount kann (room) ODER (room, klasse) signature sein -- beide
// unterstuetzt fuer backwards-compat.
return Math.max(0, mod.defaultCount(room, klasse));
}
/* Compute price for a (room,module). Bevorzugt material_unit/material_base
* (neues Modell), fallback auf legacy unitPrice/base/price. */
function getModulePrice(mod, room, counts, roomId, modKey, klasse = 1) {
const baseMat = mod.material_base ?? (mod.base ? mod.base(room) : 0);
if (mod.countable) {
const c = getModuleCount(mod, room, counts, roomId, modKey, klasse);
const unitMat = mod.material_unit ?? (mod.unitPrice ? mod.unitPrice(room) : 0);
return baseMat + c * unitMat;
}
// non-countable: material_base bevorzugt, sonst price()
return baseMat || (mod.price ? mod.price(room) : 0);
}
/* Aufschluesselung: Material vs. Installation vs. Inbetriebnahme.
* Liefert {material, install_cost, commissioning_cost, total, range_low, range_high}. */
function priceBreakdown(rooms, assignments, modules, moduleCounts, klasse = 1, pathKey = "neubau") {
let material = 0;
let minutes = 0;
let base_minutes = 0;
const indoorRooms = (rooms || []).filter(r => r && !r.outdoor);
for (const r of indoorRooms) {
const mods = (assignments && assignments[r.id]) || [];
for (const k of mods) {
const m = modules[k]; if (!m) continue;
const count = getModuleCount(m, r, moduleCounts, r.id, k, klasse);
const matUnit = m.material_unit ?? (m.unitPrice ? m.unitPrice(r) : 0);
const matBase = m.material_base ?? (m.base ? m.base(r) : 0);
const flatPrice = (m.countable ? 0 : (m.price ? m.price(r) : 0));
// flatPrice nur wenn material_base nicht gesetzt ist
material += matBase + count * matUnit + (matBase ? 0 : flatPrice);
minutes += count * (m.install_minutes ?? 0);
base_minutes += (m.install_minutes_base ?? 0);
}
}
// Inbetriebnahme/Programmierung: 0.2 Stunden je Innenraum, mind. 4 h.
const commissioning_hours = Math.max(4, indoorRooms.length * 0.2);
const install_hours = (minutes + base_minutes) / 60;
const install_cost = install_hours * WKO_STUNDENSATZ.monteur;
const commissioning_cost = commissioning_hours * WKO_STUNDENSATZ.spezial_monteur;
// Pfad-Faktor (Komplexitaet, nicht Stundensatz):
const factor = pathKey === "gewerbe" ? 1.10 : 1.00;
const total = (material + install_cost + commissioning_cost) * factor;
return {
material: Math.round(material),
install_hours: Math.round(install_hours * 10) / 10,
install_cost: Math.round(install_cost),
commissioning_hours: Math.round(commissioning_hours * 10) / 10,
commissioning_cost: Math.round(commissioning_cost),
total: Math.round(total),
range_low: Math.round(total * 0.92 / 100) * 100,
range_high: Math.round(total * 1.18 / 100) * 100,
};
}
window.getModuleCount = getModuleCount;
window.getModulePrice = getModulePrice;
window.priceBreakdown = priceBreakdown;
window.WKO_STUNDENSATZ = WKO_STUNDENSATZ;
window.KLASSE_LABELS = KLASSE_LABELS;
/* Default-Module pro Raumtyp anwenden. klasse=0|1|2 (Mindest/Standard/Komfort)
* beeinflusst aktuell NICHT welche Module dabei sind, sondern nur die counts.
* Eventuell spaeter: bei Mindest -> Multimedia/Klima ausschliessen. */
function applyDefaults(rooms, modules, klasse = 1) {
const out = {};
for (const r of rooms) {
if (r.outdoor) { out[r.id] = []; continue; }
out[r.id] = Object.entries(modules)
.filter(([k, m]) => {
if (!m.default) return false;
if (!m.default.includes(r.type)) return false;
// Beschattung: nur defaultmaessig wenn Fenster vorhanden -- User kann manuell.
if (k === "beschattung" && (!r.windows || r.windows === 0)) return false;
// Bei Mindestklasse: Multimedia + Klima nicht voreingestellt
if (klasse === 0 && (k === "multimedia" || k === "klima")) return false;
return true;
})
.map(([k]) => k);
}
return out;
}
/* ===========================================================
HAUPT-KOMPONENTE
============================================================ */
function KonfiguratorSection({ pathKey, onStateChange }) {
const [step, setStep] = React.useState(0);
const [method, setMethod] = React.useState(null); // "manual" | "import"
const [rooms, setRooms] = React.useState([]);
const [activeFloor, setActiveFloor] = React.useState("EG");
const modules = MODULE_SETS[pathKey] || MODULES_NEUBAU;
const [assignments, setAssignments] = React.useState({});
const [moduleCounts, setModuleCounts] = React.useState({}); // { roomId: { modKey: number } }
// 0 = Mindest (DIN 18015-2), 1 = Standard, 2 = Komfort.
const [ausstattungsklasse, setAusstattungsklasse] = React.useState(1);
const reset = () => {
setStep(0); setMethod(null); setRooms([]);
setAssignments({}); setModuleCounts({});
setAusstattungsklasse(1);
};
React.useEffect(() => { reset(); }, [pathKey]);
// Beim Klassen-Wechsel: counts + assignments NICHT komplett ersetzen,
// sondern frische Defaults einrechnen fuer Raeume die noch keine
// manuellen User-Overrides haben. Aktuell vereinfacht: re-applyDefaults,
// manuelle Counts in moduleCounts bleiben unangetastet (override greift).
React.useEffect(() => {
if (rooms.length > 0) {
setAssignments(applyDefaults(rooms, modules, ausstattungsklasse));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ausstattungsklasse]);
// Push state up so the Grundriss viewer can read it
React.useEffect(() => {
if (onStateChange) onStateChange({ rooms, assignments, moduleCounts, modules, pathKey, ausstattungsklasse });
}, [rooms, assignments, moduleCounts, modules, pathKey, ausstattungsklasse]);
return (
{isGewerbe ? "In vier Schritten zur ersten Orientierung für Betriebsgebäude, Büro oder Hotel — Bereiche erfassen, Funktionen wählen, Grobschätzung erhalten." : "In vier Schritten zur ersten Schätzung — entweder Räume manuell erfassen oder einen Bauplan importieren. Du bekommst direkt eine Empfehlung mit Grobpreis."}
{isGewerbe ? "Beide Wege führen zur gleichen Orientierung — manuell über Objektbereiche oder per Plan-Import." : "Beide Wege führen zum gleichen Ergebnis — wähle was sich für dich angenehmer anfühlt."}