/* 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 (
{step === 0 && ( { setMethod(m); if (m === "import") { setStep(1); } else { setRooms([]); setStep(1); } }} /> )} {step === 1 && method === "manual" && ( { setAssignments(applyDefaults(rooms, modules, ausstattungsklasse)); setStep(2); }} onBack={() => setStep(0)} /> )} {step === 1 && method === "import" && ( { setRooms(detected); setAssignments(applyDefaults(detected, modules, ausstattungsklasse)); setStep(2); }} onBack={() => setStep(0)} /> )} {step === 2 && ( setStep(3)} onBack={() => setStep(1)} /> )} {step === 3 && ( setStep(2)} onRestart={reset} /> )}
); } /* =========================================================== STYLES ============================================================ */ function KfgStyles() { return ( ); } /* =========================================================== HEADER + STEPPER ============================================================ */ function KfgHeader({ step, onJump, method, pathKey }) { const isGewerbe = pathKey === "gewerbe"; const steps = [ { n: "01", lbl: "Methode" }, { n: "02", lbl: method === "import" ? "Plan-Import" : (isGewerbe ? "Bereiche erfassen" : "Räume erfassen") }, { n: "03", lbl: isGewerbe ? "Funktionen wählen" : "Module wählen" }, { n: "04", lbl: "Empfehlung" }, ]; return ( <>
03 · KONFIGURATOR

{isGewerbe ? <>Lass uns dein Gewerbeobjekt planen. : <>Lass uns dein Smart-Home planen.}

{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."}

{steps.map((s, i) => ( ))}
); } /* =========================================================== STEP 0 — Methode wählen ============================================================ */ function KfgStep0Method({ pathKey, onPick }) { const isGewerbe = pathKey === "gewerbe"; return (

Wie möchtest du starten?

{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."}

); } function KfgIlluManual({ isGewerbe = false }) { return ( {isGewerbe ? "Büro" : "Wohnen"} 31m² {isGewerbe ? "Meeting" : "Küche"} {isGewerbe ? "Technik" : "Bad"} + {isGewerbe ? "Bereich" : "Raum"} hinzu- fügen ); } function KfgIlluImport({ isGewerbe = false }) { return ( {isGewerbe ? "OBJ" : "EFH"} PDF {/* Scan line */} {/* Arrow */} {/* Detected rooms */} {isGewerbe ? "Büro" : "Wohnen"} {isGewerbe ? "Meet" : "Küche"} {isGewerbe ? "Tech" : "Bad"} {isGewerbe ? "Hotel" : "Schlafz."} 4/4 erkannt ); } window.KonfiguratorSection = KonfiguratorSection; window.BASTING_ROOMS = BASTING_ROOMS; window.MODULE_SETS = MODULE_SETS; window.applyDefaults = applyDefaults; window.KFG_PALETTE = KFG_PALETTE;