1/* ========================================================= 2 EE Fragebogen Popup v2 (Bug #140) 3 Step 2 - Phase A4/A5: 4 - JS-Proxy: neue Sidebar-Icon-Buttons triggern alte Tab-Buttons 5 - Synchronisation der active-Klasse via MutationObserver 6 - Side-Panel (Vollstaendigkeitsring) wird zur Laufzeit injiziert 7 8 Non-destruktiv: bestehende Tab-Switch-Logik (ee-project-view.js) 9 bleibt unveraendert. Die alte .projectSettingsTabNav ist per CSS 10 versteckt, aber im DOM vorhanden â wir delegieren Klicks dorthin. 11 ========================================================= */ 12 13(function () { 14 'use strict'; 15 16 var SCOPE_SELECTOR = '.projectSettingsPopupContainer.fragebogen-v2'; 17 18 /* ------------------------------------------------------------------ 19 Debug-Flag (2026-06-02): Standardmaessig AUS, damit die Konsole im 20 Normalbetrieb sauber bleibt. Zum Aktivieren in den DevTools: 21 window.EE_FB_DEBUG = true; 22 (am besten VOR dem Oeffnen des Fragebogen-Popups setzen). 23 Alle reinen Diagnose-/Status-Ausgaben laufen ueber dieses Flag. 24 Echte Fehler-Logs (catch-Bloecke, HTTP-Fehler) bleiben unabhaengig 25 vom Flag aktiv. 26 ------------------------------------------------------------------ */ 27 function eeFbDebugOn() { 28 return window.EE_FB_DEBUG === true; 29 } 30 function dbgLog() { 31 if (eeFbDebugOn() && window.console && console.log) { 32 console.log.apply(console, arguments); 33 } 34 } 35 function dbgInfo() { 36 if (eeFbDebugOn() && window.console && console.info) { 37 console.info.apply(console, arguments); 38 } 39 } 40 41 function $(sel, ctx) { 42 return (ctx || document).querySelector(sel); 43 } 44 function $$(sel, ctx) { 45 return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); 46 } 47 48 /* ------------------------------------------------------------------ 49 Merge a pane into another pane (generic, via data-fbv2-merge-source/ 50 -target on a sidebar button). Seit AP-B4 traegt kein Sidebar-Button 51 mehr diese Attribute (Webseite lebt im Studio-Pane) â die Funktion 52 bleibt generisch und ist dann ein No-op. 53 ------------------------------------------------------------------ */ 54 function mergeWebseiteIntoIntern(popup) { 55 var mergeBtn = $('.ee-sidebar .ee-nav-btn[data-fbv2-merge-source]', popup); 56 if (!mergeBtn) return; // not EE -> nothing to do 57 var sourceId = mergeBtn.getAttribute('data-fbv2-merge-source'); 58 var targetId = mergeBtn.getAttribute('data-fbv2-merge-target'); 59 var sourcePane = document.getElementById(sourceId); 60 var targetPane = document.getElementById(targetId); 61 if (!sourcePane || !targetPane) return; 62 if (targetPane.querySelector('[data-fbv2-merged="' + sourceId + '"]')) return; // already merged 63 64 var section = document.createElement('div'); 65 section.className = 'ee-form-section ee-fbv2-merged-section'; 66 section.setAttribute('data-fbv2-merged', sourceId); 67 var h3 = document.createElement('h3'); 68 h3.textContent = 'Webseite'; 69 section.appendChild(h3); 70 71 // Move all children of sourcePane into the new section (preserves IDs/listeners) 72 while (sourcePane.firstChild) { 73 section.appendChild(sourcePane.firstChild); 74 } 75 targetPane.appendChild(section); 76 77 // Hide the now-empty source pane (alte Tab-Logik wuerde es sonst leer anzeigen) 78 sourcePane.style.display = 'none'; 79 } 80 81 /* ------------------------------------------------------------------ 82 Sidebar-Proxy: Klick auf .ee-nav-btn[data-proxy-target] triggert 83 Klick auf den zugehoerigen alten .projectSettingsTabBtn (per ID). 84 ------------------------------------------------------------------ */ 85 function initSidebarProxy(popup) { 86 var navBtns = $$('.ee-sidebar .ee-nav-btn[data-proxy-target]', popup); 87 navBtns.forEach(function (btn) { 88 btn.addEventListener('click', function (e) { 89 e.preventDefault(); 90 var targetId = btn.getAttribute('data-proxy-target'); 91 var oldBtn = document.getElementById(targetId); 92 if (oldBtn && typeof oldBtn.click === 'function') { 93 oldBtn.click(); 94 } 95 }); 96 }); 97 } 98 99 /* ------------------------------------------------------------------ 100 Sync active-Klasse: wenn alte Tab-Buttons sich aendern (durch 101 bestehende Logik oder unseren Proxy-Click), spiegeln wir das
101in 102 die Sidebar. 103 ------------------------------------------------------------------ */ 104 function syncActiveFromOldNav(popup) { 105 var oldTabs = $$('.projectSettingsTabNav .projectSettingsTabBtn', popup); 106 var sidebarBtns = $$('.ee-sidebar .ee-nav-btn[data-proxy-target]', popup); 107 108 function syncOnce() { 109 var activeId = null; 110 for (var i = 0; i < oldTabs.length; i++) { 111 if (oldTabs[i].classList.contains('active')) { 112 activeId = oldTabs[i].id; 113 break; 114 } 115 } 116 // AP-B4: kein Sonder-Mapping mehr. "Webseite" ist nicht mehr in 117 // "Einstellungen" gemerged, sondern liegt mit "Presse" im Studio-Pane 118 // (eigener Sidebar-Button data-proxy-target="projectSettingsTabStudio"). 119 sidebarBtns.forEach(function (sb) { 120 if (sb.getAttribute('data-proxy-target') === activeId) { 121 sb.classList.add('active'); 122 } else { 123 sb.classList.remove('active'); 124 } 125 }); 126 } 127 128 syncOnce(); 129 130 var observer = new MutationObserver(function () { syncOnce(); }); 131 oldTabs.forEach(function (tab) { 132 observer.observe(tab, { attributes: true, attributeFilter: ['class'] }); 133 }); 134 return observer; 135 } 136 137 /* ------------------------------------------------------------------
138 Tab-Vollstaendigkeit (heuristisch). Pro Tab schauen wir ob 139 mindestens N% der sichtbaren Inputs befuellt sind. 140 ------------------------------------------------------------------ */ 141 function hasVal(id) { 142 var el = document.getElementById(id); 143 if (!el) return false; 144 return el.value && String(el.value).trim() !== ''; 145 } 146 function anyVal(ids) { 147 for (var i = 0; i < ids.length; i++) if (hasVal(ids[i])) return true; 148 return false; 149 } 150 function allVal(ids) { 151 for (var i = 0; i < ids.length; i++) if (!hasVal(ids[i])) return false; 152 return ids.length > 0; 153 } 154 155 // Bug-Fix 2026-05-29 â Komplettrefactor der TAB_RULES: 156 // * Rules liefern jetzt einen Score 0..100 statt Boolean (gewichtete Sub-Tasks). 157 // So "2 of 3 required fields filled" counts â nicht alles-oder-nichts. 158 // * Selektoren korrigiert: 159 // - Tab Flaeche: frueher anyVal(['projectSettingsFlaeche', 'projectSettingsFlur', 160 // 'projectSettingsGemarkung']) â diese IDs existieren GAR NICHT im DOM. 161 // Echte Felder sind class="flaeche-gemarkung|flaeche-flurstueck|flaeche-flurnummer" 162 // (Array-Inputs, mehrere Flurstuecke pro Projekt moeglich). 163 // - Tab Allgemein: Fallback auf #kontaktListe .ee-form-contact-item falls die 164 // dynamic [data-fbv2-tile] tiles are not yet rendered. 165 // * "completed"-Status (Sidebar-Haekchen) bleibt binaer und triggert bei score===100. 166 var TAB_RULES = { 167 projectSettingsTabAllgemein: function () { 168 var score = 0; 169 // 50% â mindestens 1 Kontakt (Kachel ODER Listen-Item als Fallback waehrend Hidden-Render) 170 var hasKontakt = 171 document.querySelectorAll('#kontaktKacheln [data-fbv2-tile]').length > 0 || 172 document.querySelectorAll('#kontaktListe .ee-form-contact-item').length > 0; 173 if (hasKontakt) score += 50; 174 // 50% â Anlagen-Adresse befuellt 175 var hasAdresse = anyVal([ 176 'ee-form-hidden-street-projectSettingsAdresse', 177 'ee-form-hidden-zip-projectSettingsAdresse' 178 ]); 179 if (hasAdresse) score += 50; 180 return score; 181 }, 182 projectSettingsTabFlaeche: function () { 183 var score = 0; 184 // 25% â irgendein Flaeche-Eintrag (echte Class-Selektoren, Array-Inputs) 185 var flaecheInputs = document.querySelectorAll( 186 'input.flaeche-gemarkung, input.flaeche-flurstueck, input.flaeche-flurnummer' 187 ); 188 var anyFlaeche = Array.prototype.some.call(flaecheInputs, function (el) { 189 return el.value && String(el.value).trim() !== ''; 190 }); 191 if (anyFlaeche) score += 25; 192 // 25% â Eigentum oder Pacht gewaehlt 193 if (document.querySelector('input[name="flaeche_eigentum"]:checked')) score += 25; 194 // 25% â Art der landwirtschaftlichen Nutzung 195 if (hasVal('projectFlaecheArtLandwirtschaft')) score += 25; 196 // 25% â Gesamtflaeche eingetragen 197 if (hasVal('projectFlaecheGesamtflaeche')) score += 25; 198 return score; 199 }, 200 projectSettingsTabNetzanschluss: function () { 201 // Conditional logic: NVP section only visible when 202 // netzvoranfrage_getaetigt === "ja". If "nein" or unanswered, 203 // die NVP-Frage komplett aus der Wertung (sonst koennte man nie 100% erreichen). 204 var hasNB = hasVal('projectSettingsNetzbetreiber'); 205 var nvJa = !!document.querySelector('input[name="netzvoranfrage_getaetigt"][value="ja"]:checked'); 206 var nvNein = !!document.querySelector('input[name="netzvoranfrage_getaetigt"][value="nein"]:checked'); 207 var nvAnswered = nvJa || nvNein; 208 209 // Case 1: "nein" selected â NVP section hidden, only 2 required fields 210 if (nvNein) { 211 return (hasNB ? 50 : 0) + 50; // nvAnswered ist hier true 212 } 213 // Fall 2: "ja" gewaehlt â alle 3 Felder zaehlen 214 if (nvJa) { 215 var nvpAnswered = !!document.querySelector('input[name="nvp_reserviert"]:checked'); 216 return (hasNB ? 34 : 0) + 33 + (nvpAnswered ? 33 : 0); 217 } 218 // Fall 3: noch nicht beantwortet â Netzbetreiber + offene NV-Frage zaehlen 219 return (hasNB ? 50 : 0); 220 }, 221 projectSettingsTabGenehmigung: function () { 222 // Data collection, not workflow status. 223 // Stand Baurecht zaehlt auch wenn "offen / In Bearbeitung" gewaehlt ist â 224 // the key is that the question was answered. 225 226 // Ansprechpartner bleibt optional. 227 var score = 0;
228 if (hasVal('projectSettingsNameLandratsamt')) score += 34; 229 if (hasVal('projectSettingsDownloadportal')) score += 33; 230 var stand = document.getElementById('projectSettingsStandBaurecht'); 231 if (stand && stand.value) score += 33; 232 return score; 233 }, 234 // v2.22.7 (Bug #140): Dokumente-Tab entfernt â Rule weg. 235 projectSettingsTabIntern: function () { 236 var pane = document.getElementById('projectSettingsPaneIntern'); 237 if (!pane) return 0; 238 var inputs = Array.prototype.slice.call( 239 pane.querySelectorAll('input[type="text"], select, textarea') 240 ); 241 if (inputs.length === 0) return 0; 242 // Anteil befuellter Inputs (linear gewichtet 0..100) 243 var filled = inputs.filter(function (el) { 244 return el.value && String(el.value).trim() !== ''; 245 }).length; 246 return Math.round((filled / inputs.length) * 100); 247 } 248 }; 249 250 // Normalize rule result to score 0..100 (backward compat for boolean). 251 function ruleScore(rule) { 252 var r; 253 try { r = rule(); } catch (err) { return 0; } 254 if (typeof r === 'number') return Math.max(0, Math.min(100, r)); 255 return r ? 100 : 0; 256 } 257 258 // v2.22.20: Tabs die NICHT in den Gesamtscore zaehlen (interne Notizen sind optional). 259 // Sidebar icon still marks "completed" at 100% â but excluded from avg. 260 var SCORE_EXCLUDED_TABS = { projectSettingsTabIntern: true }; 261 262 function updateSidebarCompletion(popup) { 263 var btns = $$('.ee-sidebar .ee-nav-btn[data-proxy-target]', popup); 264 var done = 0; 265 var total = 0; 266 btns.forEach(function (btn) { 267 var target = btn.getAttribute('data-proxy-target'); 268 var rule = TAB_RULES[target]; 269 if (!rule) return; 270 total++; 271 var score = ruleScore(rule); 272 // Sidebar-Haekchen erscheint nur bei 100% â wuerde sonst zu frueh "fertig" signalisieren. 273 if (score >= 100) { 274 btn.classList.add('completed'); 275 done++; 276 } else { 277 btn.classList.remove('completed'); 278 } 279 }); 280 return { done: done, total: total }; 281 } 282 283 /* ------------------------------------------------------------------ 284 v2.22.32: Einheitliche Progress-Berechnung â innen (Popup-Ring) und
285 aussen (Profil-Icon im Project-View) nutzen dieselbe Quelle: die 286 TAB_RULES (Pflichtfelder pro Tab). Damit verschwindet die Diskrepanz 287 "Popup 20% / aussen fast voll". 288 Berechnet ueber den DOM, nicht ueber DB â funktioniert auch wenn 289 das Popup noch nie geoeffnet wurde (Hidden-Render durch openPopup). 290 ------------------------------------------------------------------ */ 291 function computeFragebogenProgress() { 292 // Bug-Fix 2026-05-29: Gewichteter Score-Mittelwert ueber alle Tabs. 293 // Previously binary: only fully-completed tabs counted â daher "0% obwohl was ausgefuellt ist". 294 // Jetzt: jeder Tab liefert 0..100, Gesamtscore ist der Durchschnitt aller Tab-Scores. 295 var sumScore = 0; 296 var total = 0; 297 var done = 0; // count fully-completed tabs (score 100) 298 var breakdown = {}; // Debug: Score pro Tab 299 Object.keys(TAB_RULES).forEach(function (tabId) { 300 var rule = TAB_RULES[tabId]; 301 if (typeof rule !== 'function') return; 302 // v2.22.20: Excluded Tabs (z.B. Intern) gehen NICHT in den Gesamtschnitt ein. 303 // breakdown bekommt den Score zur Diagnose trotzdem, mit Marker. 304 var score = ruleScore(rule); 305 if (SCORE_EXCLUDED_TABS[tabId]) { 306 breakdown[tabId] = score + ' (excluded)'; 307 return; 308 } 309 total++; 310 breakdown[tabId] = score; 311 sumScore += score; 312 if (score >= 100) done++; 313 }); 314 var percent = total > 0 ? Math.round(sumScore / total) : 0; 315 return { done: done, total: total, percent: percent, breakdown: breakdown }; 316 } 317 // Global verfuegbar â Profil-Ring im Project-View liest hier (refreshProgressRing) 318 window.eeFragebogenProgress = computeFragebogenProgress; 319 320 // Diagnostic helper for browser console. 321 // Aufruf in DevTools-Console: eeDiagFragebogen() 322 // Shows score per tab + which required fields are empty. 323 window.eeDiagFragebogen = function () { 324 var r = computeFragebogenProgress(); 325 var get = function (id) { var el = document.getElementById(id); return el ? el.value : null; }; 326 console.group('%c[Fragebogen-Progress]', 'color:#3fa14d;font-weight:bold'); 327 console.table(r.breakdown); 328 var fl = document.querySelectorAll('input.flaeche-gemarkung, input.flaeche-flurstueck, input.flaeche-flurnummer'); 329 var flFilled = Array.prototype.filter.call(fl, function (el) { 330 return el.value && String(el.value).trim() !== ''; 331 }).length; 332 var pane = document.getElementById('projectSettingsPaneIntern'); 333 if (pane) { 334 var ins = pane.querySelectorAll('input[type="text"], select, textarea'); 335 var leer = Array.prototype.filter.call(ins, function (el) { 336 return !(el.value && String(el.value).trim() !== ''); 337 }).map(function (el) { return el.id || el.name || el.tagName; }); 338 } 339 console.groupEnd(); 340 return r; 341 }; 342 343 /* ------------------------------------------------------------------ 344 Logo-Progress: Ring im Sidebar-Logo + %-Label aktualisieren. 345 v2.22.32: Nutzt jetzt einheitlich TAB_RULES (computeFragebogenProgress). 346 Der frueher referenzierte ProjectProgressTracker.getOverallPercent gibt 347 es nicht â ProjectProgressTracker ist eine Klasse, kein Singleton. 348 ------------------------------------------------------------------ */ 349 function updateLogoProgress(popup, tabsStatus) { 350 var ringFg = popup.querySelector('#fbv2RingFg'); 351 var pctEl = popup.querySelector('#fbv2LogoPct'); 352 if (!ringFg || !pctEl) return; 353 354 // Bug-Fix 2026-05-29: IMMER ueber computeFragebogenProgress() rechnen. 355 // Previously the binary sidebar status (done/total) auf den Logo-Ring durch â 356 // jetzt nutzen wir den gewichteten Mittelwert ueber alle Tab-Sub-Scores. 357 var pct = computeFragebogenProgress().percent; 358 359 // Ring: r=22 -> Umfang ~138.23 360 var circumference = 2 * Math.PI * 22; 361 var offset = circumference - (pct / 100) * circumference; 362 ringFg.setAttribute('stroke-dashoffset', String(offset)); 363 pctEl.textContent = pct + '%'; 364 } 365 366 function refreshProgress(popup) { 367 var tabsStatus = updateSidebarCompletion(popup); 368 updateLogoProgress(popup, tabsStatus); 369 // v2.22.32: Aeusseren Ring (Profil-Icon) ebenfalls aktualisieren, damit 370 // always shows the same % as the popup. 371 // Bug-Fix 2026-05-29: NUR aktualisieren wenn Popup wirklich offen ist â 372 // Marker dafuer ist die CSS-Klasse `.active` auf #projectSettingsPopupContainer 373 // (gesetzt in projectSettingsOpenPopup, entfernt in projectSettingsClosePo
373pup). 374 // Frueher offsetParent/display/visibility-Check griff nicht, weil das Popup 375 // im "geschlossen"-State per `width:0;height:0` versteckt wird (display bleibt block). 376 // Dadurch feuerte boot() beim DOMContentLoaded refreshProgressRing() und setzte 377 // den aeusseren Ring auf den berechneten Score, bevor der User das Popup je sah. 378 try { 379 var popupContainer = document.getElementById('projectSettingsPopupContainer'); 380 var popupOpen = !!(popupContainer && popupContainer.classList.contains('active')); 381 if (popupOpen && typeof window.refreshProgressRing === 'function') { 382 window.refreshProgressRing(); 383 } 384 } catch (e) { /* refreshProgressRing ist optional */ } 385 } 386 387 /* ------------------------------------------------------------------ 388 Address-Sync: Wenn der User in einer der vier Adressen etwas 389 auswaehlt UND die anderen aktuell LEER oder IDENTISCH waren, 390 propagieren wir die neue Adresse dorthin. 391 ------------------------------------------------------------------ */ 392 // v2.22.7 (Bug #140): Adress-Sync entfaellt â Bauherr/Eigentuemer/Bewirtschafter 393 // No longer separate address fields â roles on a contact instead. 394 // Die Kontakt-Adresse kommt direkt aus wp_energyExperts_contact. 395 var ADDRESS_CONTEXTS = []; 396 var ADDRESS_FIELDS = ['street', 'streetNumber', 'zip', 'city', 'country', 'region']; 397 398 function getAddrSnapshot(ctx) { 399 var snap = {}; 400 ADDRESS_FIELDS.forEach(function (f) { 401 var el = document.getElementById('ee-form-hidden-' + f + '-' + ctx); 402 snap[f] = el ? (el.value || '') : ''; 403 }); 404 var visible = document.getElementById(ctx + '-address'); 405 snap._visible = visible ? (visible.value || '') : ''; 406 return snap; 407 } 408 409 function addrEqual(a, b) { 410 return ADDRESS_FIELDS.every(function (f) { return (a[f] || '') === (b[f] || ''); }); 411 } 412 function addrEmpty(a) { 413 return ADDRESS_FIELDS.every(function (f) { return !a[f]; }); 414 } 415 416 function setAddr(ctx, snap) { 417 ADDRESS_FIELDS.forEach(function (f) { 418 var el = document.getElementById('ee-form-hidden-' + f + '-' + ctx); 419 if (el && el.value !== snap[f]) { 420 el.value = snap[f] || ''; 421 el.dispatchEvent(new Event('input', { bubbles: true })); 422 el.dispatchEvent(new Event('change', { bubbles: true })); 423 } 424 }); 425 var visible = document.getElementById(ctx + '-address'); 426 if (visible && snap._visible && visible.value !== snap._visible) { 427 visible.value = snap._visible; 428 visible.dispatchEvent(new Event('input', { bubbles: true })); 429 } 430 } 431 432 function initAddressSync() { 433 // Snapshot aller vier Adressen vor dem Aendern 434 var snapshots = {}; 435 ADDRESS_CONTEXTS.forEach(function (ctx) { snapshots[ctx] = getAddrSnapshot(ctx); }); 436 437 ADDRESS_CONTEXTS.forEach(function (ctx) { 438 ADDRESS_FIELDS.forEach(function (f) { 439 var hid = document.getElementById('ee-form-hidden-' + f + '-' + ctx); 440 if (!hid) return; 441 hid.addEventListener('change', function () { 442 var newSnap = getAddrSnapshot(ctx); 443 var oldSnap = snapshots[ctx]; 444 445 // If no address field actually changed -> ignore 446 if (addrEqual(newSnap, oldSnap)) return; 447 448 // For all other addresses: if EMPTY or IDENTICAL to previous 449 // Source-Adresse -> uebernehmen 450 ADDRESS_CONTEXTS.forEach(function (other) { 451 if (other === ctx) return; 452 var otherSnap = snapshots[other]; 453 if (addrEmpty(otherSnap) || addrEqual(otherSnap, oldSnap)) { 454 setAddr(other, newSnap); 455 snapshots[other] = getAddrSnapshot(other); 456 } 457 }); 458 459 snapshots[ctx] = newSnap; 460 }); 461 }); 462 }); 463 } 464 465 /* ------------------------------------------------------------------ 466 Bug #140: Flaechen-Adresse via Reverse-Geocoding aus Polygon-Koordinaten. 467 projectData.coordinates ist ein JSON-Array '[lng, lat]' (Mittelpunkt der Flaeche). 468 Wir holen Strasse/PLZ/Ort von Mapbox und fuellen die hidden fields fuer 469 projectSettingsAdresse, sofern diese aktuell LEER sind. 470 ------------------------------------------------------------------ */ 471 function getPolygonCenter() { 472 // Bug #140: Quellen-Reihenfolge: 473 // 1) window.projectData.coordinates (JSON-String '[lng,lat]', vom REST-Call) 474 // 2) window.energyExpertsProjectData.coordinates (Fallback, falls je lokalisiert) 475 // 3) Erstes Polygon â centerCoordinates / Mittelwert aus coordinates 476 function parseLngLat(raw) { 477 if (raw == null) return null;
478 try { 479 var c = typeof raw === 'string' ? JSON.parse(raw) : raw; 480 if (Array.isArray(c) && c.length >= 2 && typeof c[0] === 'number') { 481 var lng = Number(c[0]), lat = Number(c[1]); 482 if (isFinite(lng) && isFinite(lat)) return { lng: lng, lat: lat }; 483 } 484 } catch (e) {} 485 return null; 486 } 487 try { 488 var pd = (typeof window.projectData !== 'undefined' && window.projectData) ? window.projectData : null; 489 var c; 490 if (pd && pd.coordinates) { 491 c = parseLngLat(pd.coordinates); 492 if (c) return c; 493 } 494 var loc = window.energyExpertsProjectData || {}; 495 if (loc.coordinates) { 496 c = parseLngLat(loc.coordinates); 497 if (c) return c; 498 } 499 // KRITISCH (Bug #140): Polygone liegen unter pd.variations[variation].polygons, 500 // NICHT direkt unter pd.polygons. Default variation = 0 (siehe ee-project-view.js). 501 // Wir bevorzugen Type 'area' (Flaechen-Polygon), sonst nehmen wir das erste. 502 var polygons = null; 503 try { 504 var vIdx = (typeof window.variation === 'number') ? window.variation : 0; 505 if (pd && pd.variations && pd.variations[vIdx] && Array.isArray(pd.variations[vIdx].polygons)) { 506 polygons = pd.variations[vIdx].polygons; 507 } else if (pd && Array.isArray(pd.polygons)) { 508 polygons = pd.polygons; 509 } 510 } catch (e) {} 511 if (polygons && polygons.length) { 512 var areaPoly = polygons.filter(function (p) { return p && p.type === 'area'; })[0] || polygons[0]; 513 if (areaPoly) { 514 // Reihenfolge: centerCoordinates â centerRotate â Mittelwert aus coordinates 515 c = parseLngLat(areaPoly.centerCoordinates); 516 if (c) return c; 517 c = parseLngLat(areaPoly.centerRotate); 518 if (c) return c; 519 var ring = areaPoly.polycoordinates || areaPoly.coordinates; 520 if (ring) { 521 try { 522 var parsed = typeof ring === 'string' ? JSON.parse(ring) : ring; 523 if (Array.isArray(parsed) && Array.isArray(parsed[0])) { 524 var sx = 0, sy = 0, n = 0; 525 parsed.forEach(function (p) { 526 if (Array.isArray(p) && p.length >= 2) { sx += Number(p[0]); sy += Number(p[1]); n++; } 527 }); 528 if (n > 0) return { lng: sx / n, lat: sy / n }; 529 } 530 } catch (e) {} 531 } 532 } 533 } 534 return null; 535 } catch (e) { return null; } 536 } 537 538 function parseMapboxFeature(feature) { 539 var out = { street: '', streetNumber: '', zip: '', city: '', country: '', region: '' }; 540 if (!feature) return out; 541 // Mapbox: feature.address = Hausnummer, feature.text = Strasse 542 out.street = feature.text || ''; 543 out.streetNumber = feature.address || ''; 544 (feature.context || []).forEach(function (c) { 545 var id = c.id || ''; 546 if (id.indexOf('postcode') === 0) out.zip = c.text || ''; 547 else if (id.indexOf('place') === 0) out.city = c.text || ''; 548 else if (id.indexOf('region') === 0) out.region = c.text || ''; 549 else if (id.indexOf('country') === 0) out.country = c.text || ''; 550 }); 551 return out; 552 } 553 554 function fillFlaecheAdresseFromCoords() { 555 var ctx = 'projectSettingsAdresse'; 556 // Nur fuellen wenn aktuell leer 557 var current = getAddrSnapshot(ctx); 558 if (!addrEmpty(current)) return; 559 560 var center = getPolygonCenter(); 561 if (!center) return; 562 563 var pd = window.energyExpertsProjectData || {}; 564 var token = pd.mapboxApiKey; 565 if (!token) return; 566 567 var url = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + 568 encodeURIComponent(center.lng + ',' + center.lat) +
569 '.json?access_token=' + encodeURIComponent(token) + 570 '&types=address,postcode,place&language=de&limit=1'; 571 572 fetch(url).then(function (r) { return r.json(); }).then(function (data) { 573 if (!data || !data.features || !data.features.length) return; 574 var snap = parseMapboxFeature(data.features[0]); 575 snap._visible = data.features[0].place_name || ''; 576 // Erneut pruefen (User koennte inzwischen getippt haben) 577 if (!addrEmpty(getAddrSnapshot(ctx))) return; 578 setAddr(ctx, snap); 579 }).catch(function () { /* silent */ }); 580 } 581 582 /* ------------------------------------------------------------------ 583 Logo-Klick proxied auf das Header-Profile-Icon (falls vorhanden). 584 ------------------------------------------------------------------ */ 585 function initLogoClick(popup) { 586 var logo = popup.querySelector('#fbv2SidebarLogo'); 587 if (!logo) return; 588 logo.addEventListener('click', function () { 589 var headerIcon = document.getElementById('projectProfileIcon'); 590 if (headerIcon) headerIcon.click(); 591 }); 592 } 593 594 /* ------------------------------------------------------------------ 595 Bug #140: Netzanschluss - Conditionals + E1/E8 Status-Sync 596 - NVP vorhanden=ja -> Entfernung + Koordinaten 597 - Netzvoranfrage=ja -> Details + NVP-Reserviert-Auswahl 598 + E1/E8 Status auf 'in_progress' 599 - NVP reserviert=ja -> Abschluss-Felder 600 + bei alle Abschluss-Felder befuellt: E1/E8 auf 'completed' 601 ------------------------------------------------------------------ */ 602 function getProjectId() { 603 // Previously this getter only read from 604 // window.energyExpertsProjectData (wp_localize_script). Das ist NICHT 605 // immer befuellt â `window.projectData` (async geladen aus ee-project-view) 606 // ist die zuverlaessigere Quelle. Folge des Bugs: updateBuildingFormsStatusOnBaurecht 607 // bekam null als pid, returnte still ohne Toast/POST, und der Baurecht->Widget 608 // Sync did not fire. Now: same logic as getCurrentProjectId(). 609 try { 610 var pd = window.projectData || (typeof projectData !== 'undefined' ? projectData : null); 611 if (pd && pd.id) return parseInt(pd.id, 10); 612 } catch (e) {} 613 try { 614 var loc = window.energyExpertsProjectData || {}; 615 if (loc.id) return parseInt(loc.id, 10); 616 } catch (e) {} 617 return null; 618 } 619 620 function updateE1E8Status(status) { 621 try { 622 var pid = getProjectId(); 623 if (!pid) return; 624 if (window.EEApi && window.EEApi.formStatus && typeof window.EEApi.formStatus.update === 'function') { 625 // Fire & forget â Erfolg/Fehler werden zentral geloggt 626 window.EEApi.formStatus.update('e1e8', pid, status).catch(function (err) { 627 console.warn('[fragebogen] E1/E8 status update failed:', err); 628 }); 629 } 630 } catch (e) { console.warn('[fragebogen] updateE1E8Status error:', e); } 631 } 632 633 /** 634 * 2026-05-28 / v2.22.35: Wenn der User "Baurecht erteilt" (stand_baurecht='genehmigt') 635 * waehlt, werden die Widgets "Bauantrag" (building_permit) und "Technische Zeichnung" 636 * (dxf_export) automatisch abgeschlossen. 637 * 638 * Frueher: EEApi.formStatus.update(slug, pid, 'completed') â PUT /module-form-status. 639 * Problem: Dieser Endpoint verweigert 'completed' fuer BEIDE User-Typen (EE + Kunde). 640 * Backend-Regel (api-functions.php Z. 19499-19505): "Status 'completed' kann nur ueber 641 * den Abschluss-Tab gesetzt werden". Calls wurden still mit 403 abgelehnt. 642 * 643 * Jetzt: POST /module-completion/{slug}/{pid} mit { action: 'submit' }. 644 * - EE-User â status='completed' (Widget zeigt "Abgeschlossen") 645 * - Kunde â status='review' (Widget zeigt "In Pruefung") 646 * Beides ist semantisch korrekt und besser als der frueher fehlschlagende Call. 647 */ 648 function submitModuleCompletion(slug, pid, formData) { 649 var url = getRestRoot() + '/module-completion/' + encodeURIComponent(slug) + '/' + encodeURIComponent(pid); 650 var headers = { 'Content-Type': 'application/json' }; 651 var nonce = getRestNonce(); 652 if (nonce) headers['X-WP-Nonce'] = nonce; 653 return fetch(url, { 654 method: 'POST', 655 credentials: 'same-origin', 656 headers: headers, 657 body: JSON.stringify({ action: 'submit', form_data: formData || {} }) 658 }).then(function (r) { 659 if (!r.ok) return r.text().then(function (t) { throw new Error('HTTP ' + r.status + ': ' + t); }); 660 return r.json(); 661 }); 662 } 663 664 // v2.22.36 (Bug-C Fix): Short-term Dedup damit change-listener + s
664ave-hook 665 // (both trigger on "approved") don't send duplicate module-completion POSTs 666 // abfeuern und 2x toasten. Reset nach 5s â danach wieder triggerbar. 667 var _baurechtSyncDebounce = {}; 668 function updateBuildingFormsStatusOnBaurecht(baurechtValue) { 669 if (baurechtValue !== 'genehmigt') return; 670 try { 671 var pid = getProjectId(); 672 if (!pid) return; 673 var dedupKey = pid + ':genehmigt'; 674 if (_baurechtSyncDebounce[dedupKey]) return; 675 _baurechtSyncDebounce[dedupKey] = true; 676 setTimeout(function () { delete _baurechtSyncDebounce[dedupKey]; }, 5000); 677 678 var modules = ['building_permit', 'dxf_export']; 679 var eventNames = { 'building_permit': 'bauantrag', 'dxf_export': 'dxf_export' }; 680 681 Promise.allSettled(modules.map(function (slug) { 682 return submitModuleCompletion(slug, pid).then(function (resp) { 683 var finalStatus = (resp && resp.status) || 'completed'; 684 if (window.EEEvents && typeof window.EEEvents.statusChanged === 'function') { 685 window.EEEvents.statusChanged(eventNames[slug], pid, finalStatus); 686 } 687 return { slug: slug, status: finalStatus }; 688 }); 689 })).then(function (results) { 690 var ok = results.filter(function (r) { return r.status === 'fulfilled'; }); 691 var fail = results.filter(function (r) { return r.status === 'rejected'; }); 692 if (ok.length === modules.length) { 693 // Beide gruen â kurzes Toast als Bestaetigung 694 if (typeof toast === 'function') toast('Bauantrag & Technische Zeichnung abgeschlossen', 'success'); 695 } else if (ok.length > 0) { 696 if (typeof toast === 'function') toast('Teilweise abgeschlossen â siehe Console', 'warning'); 697 } else { 698 if (typeof toast === 'function') toast('Konnte Widgets nicht aktualisieren', 'error'); 699 } 700 fail.forEach(function (r) { 701 console.warn('[fragebogen] module-completion submit failed:', r.reason); 702 }); 703 }); 704 } catch (e) { 705 console.warn('[fragebogen] updateBuildingFormsStatusOnBaurecht error:', e); 706 } 707 } 708 709 /** 710 * 2026-05-28: Wenn Bauherr === Eigentuemer (gleiche Personen-Menge), dann 711 * ist das Eigentumsverhaeltnis der Flaeche automatisch "Eigentum" â die 712 * manuelle Pill-Auswahl ist redundant. Section ausblenden + Wert hart setzen. 713 * 714 * Quelle: window.__fbv2Contacts (gesetzt in eeFbv2RenderContactTiles). 715 */ 716 function getRoleSets() { 717 var contacts = window.__fbv2Contacts || []; 718 var bauherr = {}; 719 var eigentuemer = {}; 720 var bauherrCount = 0, eigentuemerCount = 0;
721 contacts.forEach(function (c) { 722 if (c && c.roles) { 723 if (c.roles.bauherr) { bauherr[c.id] = true; bauherrCount++; } 724 if (c.roles.eigentuemer) { eigentuemer[c.id] = true; eigentuemerCount++; } 725 } 726 }); 727 return { bauherr: bauherr, eigentuemer: eigentuemer, bauherrCount: bauherrCount, eigentuemerCount: eigentuemerCount }; 728 } 729 function rolesEqual(setA, countA, setB, countB) { 730 if (countA !== countB) return false; 731 if (countA === 0) return false; // beide leer ist NICHT "gleich" 732 for (var k in setA) { if (!setB[k]) return false; } 733 return true; 734 } 735 function applyEigentumAutoLogic(popup) { 736 if (!popup) popup = $(SCOPE_SELECTOR); 737 if (!popup) return; 738 var pillGroup = popup.querySelector('#projectFlaecheEigentumsverhaeltnis'); 739 if (!pillGroup) return; 740 var section = pillGroup.closest('.ee-form-section'); 741 var roles = getRoleSets(); 742 var same = rolesEqual(roles.bauherr, roles.bauherrCount, roles.eigentuemer, roles.eigentuemerCount); 743 if (same) { 744 if (section) section.style.display = 'none'; 745 var eigentumRadio = popup.querySelector('#projectFlaecheEigentum'); 746 if (eigentumRadio && !eigentumRadio.checked) { 747 eigentumRadio.checked = true; 748 eigentumRadio.dispatchEvent(new Event('change', { bubbles: true })); 749 } 750 } else { 751 if (section) section.style.display = ''; 752 } 753 } 754 window.eeApplyEigentumAutoLogic = applyEigentumAutoLogic; 755 756 function initEigentumAutoLogic(popup) { 757 applyEigentumAutoLogic(popup); 758 var kk = document.getElementById('kontaktKacheln'); 759 if (kk) { 760 kk.addEventListener('click', function () { 761 setTimeout(function () { applyEigentumAutoLogic(popup); }, 50); 762 setTimeout(function () { applyEigentumAutoLogic(popup); }, 700); 763 }); 764 } 765 // Poll-Fallback: alle 1.5s die ersten ~10s 766 var tries = 0; 767 var iv = setInterval(function () { 768 tries++; 769 applyEigentumAutoLogic(popup); 770 if (tries >= 7) clearInterval(iv); 771 }, 1500); 772 } 773 774 function initBaurechtStatusSync(popup) { 775 var sel = popup.querySelector('#projectSettingsStandBaurecht'); 776 if (!sel) return; 777 var lastFired = null; 778 sel.addEventListener('change', function () { 779 var v = sel.value; 780 if (v === lastFired) return; // doppeltes Feuern vermeiden 781 lastFired = v; 782 updateBuildingFormsStatusOnBaurecht(v); 783 }); 784 } 785 786 /** 787 * Unlock the 5 Kernfelder that were set read-only when E1/E8 status was 'completed'. 788 * Called when the status is reset to 'in_progress' (user clicked "Noch nicht reserviert" 789 * or emptied a required Abschluss field). 790 */ 791 function _unlockCompletedFields(popup) { 792 var readOnlyFieldIds = [ 793 'projectSettingsNetzbetreiber', 794 'projectSettingsEinspeisezusageNr', 795 'projectSettingsEinspeisezusageDatum', 796 'projectSettingsAnschlussleistungKva', 797 'projectSettingsGueltigkeitReservierung' 798 ]; 799 readOnlyFieldIds.forEach(function (fid) { 800 var el = popup.querySelector('#' + fid); 801 if (el) { 802 el.disabled = false; 803 el.readOnly = false; 804 el.style.opacity = ''; 805 el.style.cursor = ''; 806 el.title = ''; 807 } 808 }); 809 // Remove any E1/E8 status notices (legacy read-only + new completed hint) 810 // so they stay consistent with the current status (e.g. after a downgrade 811 // completed -> in_progress via MODULE_STATUS_CHANGED). 812 var notices = popup.querySelectorAll('.ee-e1e8-readonly-notice, .ee-e1e8-completed-notice'); 813 notices.forEach(function (n) { n.remove(); }); 814 } 815 816 function initNetzanschlussConditionals(popup) { 817 // 2026-05-28 (Bug #140 Re-Order): 818 // 1. Netzvoranfrage zuerst (Pills + Details-Wrap) 819 // 2. NVP section only visible when grid pre-request = "Already filed" 820 // -> If "Not yet filed", NVP auto-set to "Not available". 821 // 3. NVP-Reservierung-Details nur wenn "Reserviert" (vorhanden/reserviert merged 2026-07-01) 822 823 // ----- 1. Netzvoranfrage gestellt? ----- 824 var nvJa = popup.querySelector('#projectSettingsNetzvoranfrageJa'); 825 var nvNein = popup.querySelector('#projectSettingsNetzvoranfrageNein'); 826 var detailsWrap = popup.querySelector('#projectSettingsNetzvoranfrageDetailsWrap'); 827 828 // ----- 2. NVP-Section + Pills ----- 829 var nvpSectionWrap = popup.querySelector('#projectSettingsNVPSectionWrap'); 830 831 // ONCE flag for the status trigger (this session) 832 var inProgressFired = false;
833 // Fields that live BELOW the Netzvoranfrage toggle and must be cleared 834 // when "Noch nicht gestellt" is selected (grid_operator + grid_operator_contact 835 // are ABOVE the toggle and intentionally kept). 836 var NV_CLEARABLE_FIELDS = [ 837 'projectSettingsDatumNetzvoranfrage', 838 'projectSettingsNetzvoranfrageDurch', 839 'projectSettingsAngefragteLeistung', 840 'projectSettingsEinspeisezusageNr', 841 'projectSettingsEinspeisezusageDatum', 842 'projectSettingsAnschlussleistungKva', 843 'projectSettingsZaehlpunkt', 844 'projectSettingsKoordinatenNVP', 845 'projectSettingsGueltigkeitReservierung' 846 ]; 847 848 function updateNV(userInitiated) { 849 var isJa = !!(nvJa && nvJa.checked); 850 if (detailsWrap) detailsWrap.style.display = isJa ? '' : 'none'; 851 // NVP section only visible when grid pre-request was filed 852 if (nvpSectionWrap) nvpSectionWrap.style.display = isJa ? '' : 'none'; 853 // Bug-Fix 2026-07-02 v2 (Status-Downgrade Kern-Pfad): Der in_progress-Trigger 854 // darf NUR bei ECHTER User-Interaktion feuern. Problem: loadNetzanschlussFromE1E8() 855 // setzt beim Laden nvJa.checked=true und dispatched ein 'change'-Event 856 // (ensureSectionsVisible), das den nvJa-change-Listener mit updateNV(true) 857 // aufruft â obwohl es KEINE User-Aktion ist. Der _lastE1E8AutoStatus !== 858 // 'completed'-Guard reichte nicht, weil der legitime Zustand hier 'draft' 859 // ist (Daten vollstaendig, aber module_completion.status nie auf completed 860 // gesetzt). Ergebnis: PUT module-form-status status:in_progress beim Laden. 861 // Fix: _netzanschlussLoading-Guard (analog saveNetzanschlussToE1E8), damit 862 // programmatische Dispatches waehrend des Ladens keinen Status-PUT ausloesen. 863 if (isJa && userInitiated && !_netzanschlussLoading && !inProgressFired && _lastE1E8AutoStatus !== 'completed') { 864 inProgressFired = true; 865 updateE1E8Status('in_progress'); 866 } 867 868 // When user selects "Noch nicht gestellt": clear all dependent fields, 869 // reset NVP toggle, and persist the empty values to the backend. 870 // Only on user-initiated changes (not on initial page load). 871 if (!isJa && userInitiated) { 872 _netzanschlussLoading = true; // suppress per-field auto-save 873 874 // 1. Clear text/date/number inputs 875 NV_CLEARABLE_FIELDS.forEach(function (elemId) { 876 var el = popup.querySelector('#' + elemId); 877 if (el) el.value = ''; 878 }); 879 880 // 2. Reset NVP toggle to "Noch nicht reserviert" 881 if (resNein) { 882 resNein.checked = true; 883 // Update NVP sub-section visibility 884 if (resDetailsWrap) resDetailsWrap.style.display = 'none'; 885 } 886 if (resJa) resJa.checked = false; 887 888 _netzanschlussLoading = false; 889 890 // 3. One save with all empty values 891 saveNetzanschlussToE1E8(popup); 892 } 893 } 894 if (nvJa) nvJa.addEventListener('change', function () { updateNV(true); }); 895 if (nvNein) nvNein.addEventListener('change', function () { updateNV(true); }); 896 updateNV(false); 897 898 // ----- 3. NVP reserviert? (2026-07-01: vorhanden/reserviert merged into one toggle) ----- 899 var resJa = popup.querySelector('#projectSettingsNVPReserviertJa'); 900 var resNein = popup.querySelector('#projectSettingsNVPReserviertNein'); 901 var resDetailsWrap = popup.querySelector('#projectSettingsNVPReservierungDetailsWrap'); 902 903 function updateRes() { 904 if (!resDetailsWrap) return; 905 resDetailsWrap.style.display = (resJa && resJa.checked) ? '' : 'none'; 906 } 907 if (resJa) resJa.addEventListener('change', updateRes); 908 if (resNein) resNein.addEventListener('change', updateRes); 909 updateRes(); 910 911 // ----- 4. BIDIRECTIONAL E1/E8 Status: completed <-> in_progress ----- 912 // 2026-07-01 Fix: Now bidirectional â status goes to 'completed' when all 913 // Abschluss fields are filled AND NVP is reserved, and BACK to 'in_progress' 914 // when user clicks "Noch nicht reserviert" or empties a required field. 915 // Uses submitModuleCompletion (POST /module-completion with action:'submit') 916 // for completed, and updateE1E8Status (PUT /module-form-status) for in_progress. 917 var abschlussFields = popup.querySelectorAll('.ee-abschluss-feld'); 918 // Initialize from projectData so we know if E1/E8 is already completed 919 // (set by loadNetzanschlussFromE1E8 from the GET /module-completion response) 920 // _lastE1E8AutoStatus is now declared at module scope (line ~1866) s
920o boot() listener can access it 921 _lastE1E8AutoStatus = (window.projectData && window.projectData.e1e8CompletionStatus) || null; 922 var _e1e8StatusUpdating = false; // prevent overlapping requests 923 function checkAndUpdateE1E8Status() { 924 if (_netzanschlussLoading) return; 925 if (_e1e8StatusUpdating) return; 926 var pid = getProjectId(); 927 if (!pid) return; 928 929 // Check: NVP reserved AND all Abschluss fields filled? 930 var nvpReserved = !!(resJa && resJa.checked); 931 var allFilled = abschlussFields.length > 0 && Array.prototype.every.call(abschlussFields, function (el) { 932 return el.value && String(el.value).trim() !== ''; 933 }); 934 935 if (nvpReserved && allFilled) { 936 // -> completed (only if not already set) 937 if (_lastE1E8AutoStatus === 'completed') return; 938 _e1e8StatusUpdating = true; 939 // Collect current DOM values and send them with the submit request. 940 // This fixes the race condition where the debounced save (1500ms) 941 // has not yet written form_data to the DB, causing the backend 942 // validation to fail with 422 (missing fields). 943 var currentFormData = _collectNetzanschlussFormData(popup); 944 // Cancel pending debounced save â submit includes all data anyway 945 if (_netzanschlussSaveTimer) { 946 clearTimeout(_netzanschlussSaveTimer); 947 _netzanschlussSaveTimer = null; 948 } 949 submitModuleCompletion('e1e8', pid, currentFormData).then(function (resp) { 950 var finalStatus = (resp && resp.status) || 'completed'; 951 _lastE1E8AutoStatus = 'completed'; 952 _e1e8StatusUpdating = false; 953 if (window.EEEvents && typeof window.EEEvents.statusChanged === 'function') { 954 window.EEEvents.statusChanged('e1e8', pid, finalStatus); 955 } 956 if (typeof toast === 'function') toast('E1/E8 Netzanschluss abgeschlossen', 'success'); 957 dbgLog('[Netzanschluss] Auto-completed E1/E8 via module-completion endpoint, status=' + finalStatus); 958 }).catch(function (err) { 959 _e1e8StatusUpdating = false; 960 console.warn('[fragebogen] E1/E8 auto-complete failed:', err); 961 }); 962 } else { 963 // -> in_progress (only if previously was completed) 964 if (_lastE1E8AutoStatus !== 'completed') return; 965 _e1e8StatusUpdating = true; 966 _lastE1E8AutoStatus = 'in_progress'; 967 updateE1E8Status('in_progress'); 968 969 // Auch module_completion.status zuruecksetzen (save statt submit = draft) 970 var resetPid = pid || (typeof _currentProjectId !== 'undefined' ? _currentProjectId : null) 971 || (document.getElementById('projectId') ? document.getElementById('projectId').value : null); 972 if (resetPid && typeof eeData !== 'undefined' && eeData.restRoot) { 973 var resetUrl = eeData.restRoot + 'energyExperts/v1/module-completion/e1e8/' + encodeURIComponent(resetPid); 974 fetch(resetUrl, { 975 method: 'POST', 976 credentials: 'same-origin', 977 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': eeData.restNonce }, 978 body: JSON.stringify({ action: 'save', form_data: {} }) 979 }).catch(function() { /* fire & forget */ }); 980 } 981 982 _e1e8StatusUpdating = false; 983 if (window.EEEvents && typeof window.EEEvents.statusChanged === 'function') { 984 window.EEEvents.statusChanged('e1e8', pid, 'in_progress'); 985 } 986 // Update projectData cache 987 if (window.projectData) { 988 window.projectData.e1e8CompletionStatus = 'in_progress'; 989 } 990 // Unlock read-only fields that were disabled when completed 991 _unlockCompletedFields(popup); 992 if (typeof toast === 'function') toast('E1/E8 Status: In Bearbeitung', 'info'); 993 dbgLog('[Netzanschluss] E1/E8 status reset to in_progress (NVP reserved=' + nvpReserved + ', allFilled=' + allFilled + ')'); 994 } 995 } 996 // Listen on Abschluss fields: input, change, AND focusout (catches clearing a field) 997 abschlussFields.forEach(function (el) { 998 el.addEventListener('input', checkAndUpdateE1E8Status); 999 el.addEventListener('change', checkAndUpdateE1E8Status); 1000 el.addEventListener('focusout', checkAndUpdateE1E8Status); 1001 }); 1002 // Listen on BOTH radio buttons (reserved = yes AND no) 1003 if (resJa) resJa.addEventListener('change', checkAndUpdateE1E8Status); 1004 if (resNein) resNein.addEventListener('change', checkAndUpdateE1E8Status); 1005 } 1006 1007 /* ------------------------------------------------------------------ 1008 Bug #140: Synchronisation Netzanschluss-Eckdaten <-> E1/E8 Generator. 1009 Bei Oeffnen des Popups: 1010 - Werte aus #e8pdf-{key} ins Fragebogen-Feld kopieren (Pre-Fill). 1011 Bei Aenderung im Fragebogen: 1012 - Wert ins entsprechende #e8pdf-{key} schreiben + change-Event feuern. 1013 Bei Aenderung im E1/E8: 1014 - via MutationObserver / event-Listener zurueck ins Fragebogen. 1015 ------------------------------------------------------------------ */ 1016 function initE1E8Sync(popup) { 1017 var syncFields = popup.querySelectorAll('.ee-e8-sync[data-e8-key]'); 1018 if (!syncFields.length) return; 1019 1020 function pullFromE8(input) { 1021 var key = input.getAttribute('data-e8-key'); 1022 var src = document.getElementById('e8pdf-' + key); 1023 if (src && src.value && !input.value) { 1024 input.value = src.value; 1025 } 1026 } 1027 1028 function pushToE8(input) { 1029 var key = input.getAttribute('data-e8-key'); 1030 var dst = document.getElementById('e8pdf-' + key); 1031 if (dst && dst.value !== input.value) { 1032 dst.value = input.value; 1033 dst.dispatchEvent(new Event('input', { bubbles: true })); 1034 dst.dispatchEvent(new Event('change', { bubbles: true })); 1035 } 1036 } 1037 1038 syncFields.forEach(function (input) { 1039 pullFromE8(input); 1040 input.addEventListener('input', function () { pushToE8(input); }); 1041 input.addEventListener('change', function () { pushToE8(input); }); 1042
1043 // Rueckweg: E8 -> Fragebogen 1044 var key = input.getAttribute('data-e8-key'); 1045 var src = document.getElementById('e8pdf-' + key); 1046 if (src) { 1047 ['input', 'change'].forEach(function (ev) { 1048 src.addEventListener(ev, function () { 1049 if (src.value && src.value !== input.value) { 1050 input.value = src.value; 1051 input.dispatchEvent(new Event('input', { bubbles: true })); 1052 } 1053 }); 1054 }); 1055 } 1056 }); 1057 } 1058 1059 /* ------------------------------------------------------------------ 1060 Bug #140: Netzbetreiber-Auto-Ermittlung aus Polygon-Koordinaten. 1061 Heuristik (Phase 1 ohne dedizierte API): 1062 - Reverse-Geocoding -> Bundesland aus Mapbox-Feature ableiten 1063 - Mapping Bundesland -> dominanter regionaler Netzbetreiber 1064 (grobe Schaetzung; User kann ueberschreiben) 1065 - Wird nur als VORSCHLAG eingetragen wenn Feld leer 1066 - Hint zeigt Quelle an 1067 ------------------------------------------------------------------ */ 1068 var BUNDESLAND_NETZBETREIBER = { 1069 'Bayern': 'Bayernwerk Netz / LEW Verteilnetz (regional)', 1070 'Baden-Wuerttemberg': 'Netze BW (regional)', 1071 'Baden-Württemberg': 'Netze BW (regional)', 1072 'Hessen': 'Syna / EnergieNetz Mitte (regional)', 1073 'Rheinland-Pfalz': 'Westnetz / Syna (regional)', 1074 'Saarland': 'VSE Verteilnetz (regional)', 1075 'Nordrhein-Westfalen': 'Westnetz (regional)', 1076 'Niedersachsen': 'Avacon / EWE Netz (regional)', 1077 'Bremen': 'wesernetz Bremen', 1078 'Hamburg': 'Stromnetz Hamburg', 1079 'Schleswig-Holstein': 'Schleswig-Holstein Netz / HanseWerk', 1080 'Mecklenburg-Vorpommern': 'WEMAG Netz / E.DIS', 1081 'Brandenburg': 'E.DIS Netz / edis (regional)', 1082 'Berlin': 'Stromnetz Berlin', 1083 'Sachsen': 'MITNETZ Strom / SachsenNetze', 1084 'Sachsen-Anhalt': 'Avacon Netz / MITNETZ Strom', 1085 'Thueringen': 'TEN Thueringer Energienetze', 1086 'Thüringen': 'TEN Thueringer Energienetze' 1087 }; 1088 1089 function initNetzbetreiberAuto(popup) { 1090 var input = popup.querySelector('#projectSettingsNetzbetreiber'); 1091 var btn = popup.querySelector('#projectSettingsNetzbetreiberAutoBtn'); 1092 var hint = popup.querySelector('#projectSettingsNetzbetreiberHint'); 1093 if (!input || !btn) return; 1094 1095 var center = getPolygonCenter(); 1096 var pd = window.energyExpertsProjectData || {}; 1097 if (!center || !pd.mapboxApiKey) return; 1098 1099 // Button nur zeigen wenn Feld leer ist 1100 function refreshBtn() { btn.style.display = input.value.trim() ? 'none' : ''; } 1101 refreshBtn(); 1102 input.addEventListener('input', refreshBtn); 1103 1104 btn.addEventListener('click', function () { 1105 btn.disabled = true; 1106 var origText = btn.querySelector('span').textContent; 1107 btn.querySelector('span').textContent = 'Ermittle...'; 1108 var url = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + 1109 encodeURIComponent(center.lng + ',' + center.lat) +
1110 '.json?access_token=' + encodeURIComponent(pd.mapboxApiKey) + 1111 '&types=region&language=de&limit=1'; 1112 fetch(url).then(function (r) { return r.json(); }).then(function (data) { 1113 var land = (data && data.features && data.features[0]) ? data.features[0].text : ''; 1114 var nb = BUNDESLAND_NETZBETREIBER[land] || ''; 1115 if (nb && !input.value.trim()) { 1116 input.value = nb; 1117 input.dispatchEvent(new Event('input', { bubbles: true })); 1118 input.dispatchEvent(new Event('change', { bubbles: true })); 1119 if (hint) { 1120 hint.textContent = 'Vorschlag basierend auf Bundesland "' + land + '" â bitte pruefen.'; 1121 hint.style.display = ''; 1122 } 1123 } else if (hint) { 1124 hint.textContent = land ? ('Bundesland: ' + land + ' (kein Mapping gefunden)') : 'Region konnte nicht ermittelt werden.'; 1125 hint.style.display = ''; 1126 } 1127 }).catch(function () { 1128 if (hint) { hint.textContent = 'Fehler bei der Ermittlung.'; hint.style.display = ''; } 1129 }).finally(function () { 1130 btn.disabled = false; 1131 btn.querySelector('span').textContent = origText; 1132 refreshBtn(); 1133 }); 1134 }); 1135 } 1136 1137 /* ------------------------------------------------------------------ 1138 Bug #140: Kontakt-Adress-Autocomplete 1139 Beim Hinzufuegen eines Kontakts werden die generischen Klassen- 1140 Felder mit eindeutigen IDs versehen und an die plugin-eigene 1141 setupAddressAutocomplete-Funktion uebergeben. 1142 ------------------------------------------------------------------ */ 1143 function attachKontaktAutocomplete(kontaktItem, idx) { 1144 if (!kontaktItem) return; 1145 var ctx = 'projectSettingsKontakt' + idx; 1146 var input = kontaktItem.querySelector('.kontakt-address'); 1147 var suggestions = kontaktItem.querySelector('.kontakt-address-suggestions'); 1148 if (!input || !suggestions) return; 1149 if (input.id) return; // already initialized 1150 1151 var inputId = ctx + '-address'; 1152 var sugId = 'ee-form-suggestions-' + inputId; 1153 input.id = inputId; 1154 suggestions.id = sugId; 1155 1156 var FIELDS = ['street', 'streetNumber', 'zip', 'city', 'country', 'region']; 1157 FIELDS.forEach(function (f) { 1158 var el = kontaktItem.querySelector('.kontakt-' + f); 1159 if (el) el.id = 'ee-form-hidden-' + f + '-' + ctx; 1160 }); 1161 1162 if (typeof window.setupAddressAutocomplete === 'function') { 1163 try { window.setupAddressAutocomplete(inputId); } catch (e) { console.warn('setupAddressAutocomplete failed', e); } 1164 } 1165 } 1166 1167 function initKontaktAutocompleteWatcher(popup) { 1168 var kontaktListe = popup.querySelector('#kontaktListe'); 1169 if (!kontaktListe) return; 1170 1171 function scan() { 1172 var items = kontaktListe.querySelectorAll('.ee-form-contact-item'); 1173 items.forEach(function (it, i) { attachKontaktAutocomplete(it, i + 1); }); 1174 } 1175 scan(); 1176 1177 var mo = new MutationObserver(function (muts) { 1178 var hasAdd = muts.some(function (m) { return m.addedNodes && m.addedNodes.length; }); 1179 if (hasAdd) scan(); 1180 }); 1181 mo.observe(kontaktListe, { childList: true }); 1182 } 1183 1184 /* ------------------------------------------------------------------ 1185 Bug #140: KI-Feld-Vorschlaege 1186 - Pro Feld mit bekanntem Key wird ein Vorschlag generiert (lokal aus 1187 projectData, Polygon, Mapbox). 1188 - Nur fuer LEERE Felder: kleiner Chip unter dem Input. 1189 - Klick auf Chip -> Wert wird ins Feld geschrieben, Chip entfernt. 1190 - Tippt der User selbst -> Chip blendet aus. 1191 - Datenquelle ist absichtlich lokal (keine Backend-Calls noetig). 1192 Falls spaeter echte KI gewuenscht: Provider durch async API ersetzen. 1193 ------------------------------------------------------------------ */ 1194 function getOrgContact() { 1195 try { 1196 var pd = window.energyExpertsProjectData || (typeof projectData !== 'undefined' ? projectData : null); 1197 return pd && pd.organization && pd.organization.connections && pd.organization.connections[0] 1198 ? pd.organization.connections[0].contact : null; 1199 } catch (e) { return null; } 1200 } 1201 function getOrg() { 1202 try { 1203 var pd = window.energyExpertsProjectData || (typeof projectData !== 'undefined' ? projectData : null); 1204 return pd && pd.organization ? pd.organization : null; 1205 } catch (e) { return null; } 1206 } 1207 1208 // Synchroner Cache fuer Mapbox-Reverse-Geocoding (wird beim Boot gefuellt) 1209 var _polygonAddressCache = null; 1210 function fetchPolygonAddress() { 1211 if (_polygonAddressCache !== null) return Promise.resolve(_polygonAddressCache); 1212 var center = getPolygonCenter(); 1213 var pd = window.energyExpertsProjectData || {}; 1214 if (!center || !pd.mapboxApiKey) {
1215 _polygonAddressCache = {}; 1216 return Promise.resolve(_polygonAddressCache); 1217 } 1218 var url = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + 1219 encodeURIComponent(center.lng + ',' + center.lat) + 1220 '.json?access_token=' + encodeURIComponent(pd.mapboxApiKey) + 1221 '&types=address,postcode,place,region&language=de&limit=1'; 1222 return fetch(url).then(function (r) { return r.json(); }).then(function (data) { 1223 var addr = {}; 1224 if (data && data.features && data.features[0]) { 1225 var snap = parseMapboxFeature(data.features[0]); 1226 addr = { 1227 full: data.features[0].place_name || '', 1228 street: snap.street, streetNumber: snap.streetNumber, 1229 zip: snap.zip, city: snap.city, country: snap.country, region: snap.region 1230 }; 1231 } 1232 _polygonAddressCache = addr; 1233 return addr; 1234 }).catch(function () { _polygonAddressCache = {}; return _polygonAddressCache; }); 1235 } 1236 1237 // Map: field ID -> function returning a suggestion (string) or null. 1238 // Bug #140: STRENG â nur konkrete Werte aus projectData/Mapbox, KEINE Heuristiken 1239 // die Strassen oder Personennamen erfinden koennten. Was hier nicht sicher ist, 1240 // bleibt leer; die KI im Backend versucht es ggf. besser. 1241 var SUGGESTION_PROVIDERS = { 1242 // v2.22.7 (Bug #140): Bauherr-Provider entfernt â Bauherr ist jetzt eine Rolle 1243 // auf einem Kontakt (Kontakt-Kacheln im Allgemein-Tab), kein eigenes Formularfeld. 1244 // Projekt-Adresse: nur wenn Mapbox eine konkrete Strasse mit Hausnummer liefert. 1245 'projectSettingsAdresse-address': function () { 1246 return fetchPolygonAddress().then(function (a) { 1247 if (!a) return null; 1248 // Strasse + Hausnummer muessen beide vorhanden sein, sonst leer 1249 if (a.street && a.streetNumber) { 1250 var line = a.street + ' ' + a.streetNumber; 1251 if (a.zip || a.city) line += ', ' + ((a.zip || '') + ' ' + (a.city || '')).trim(); 1252 return line.trim(); 1253 } 1254 return null; 1255 }); 1256 }, 1257 // Grundbuch-Kommune = Mapbox locality (Stadt/Gemeinde) 1258 projectSettingsGrundbuchKommune: function () { 1259 return fetchPolygonAddress().then(function (a) { 1260 return a && a.city ? a.city : null; 1261 }); 1262 }, 1263 // Netzbetreiber: Mapping ueber Bundesland 1264 projectSettingsNetzbetreiber: function () { 1265 return fetchPolygonAddress().then(function (a) { 1266 if (!a || !a.region) return null;
1267 return BUNDESLAND_NETZBETREIBER[a.region] || null; 1268 }); 1269 } 1270 }; 1271 1272 /** 1273 * Bug #269 (Feedback): Sichtbarer Status-Hinweis neben einem leeren Feld, 1274 * damit der User waehrend des langen KI-Calls (GPT ~20-30s) sieht, dass 1275 * gearbeitet wird â und danach, ob etwas gefunden wurde oder nicht. 1276 * kind: 'loading' | 'empty' | 'error'. Leerer text entfernt die Note. 1277 * Note und Vorschlags-Chip schliessen sich gegenseitig aus. 1278 */ 1279 function renderAiNote(input, kind, text) { 1280 if (!input || !input.parentElement) return; 1281 var prev = input.parentElement.querySelector(':scope > .ee-ai-note'); 1282 if (prev) prev.remove(); 1283 if (!text) return; 1284 var note = document.createElement('span'); 1285 note.className = 'ee-ai-note ee-ai-note--' + kind; 1286 note.setAttribute('data-target', input.id || ''); 1287 if (kind === 'loading') { 1288 var spin = document.createElement('span'); 1289 spin.className = 'ee-ai-note-spinner'; 1290 note.appendChild(spin); 1291 } 1292 var label = document.createElement('span'); 1293 label.className = 'ee-ai-note-text'; 1294 label.textContent = text; 1295 note.appendChild(label); 1296 if (input.nextSibling) { 1297 input.parentElement.insertBefore(note, input.nextSibling); 1298 } else { 1299 input.parentElement.appendChild(note); 1300 } 1301 } 1302 1303 function clearAiNote(input) { 1304 if (!input || !input.parentElement) return; 1305 var prev = input.parentElement.querySelector(':scope > .ee-ai-note'); 1306 if (prev) prev.remove(); 1307 } 1308 1309 // Wie lange der "Kein Vorschlag gefunden"-Hinweis stehen bleibt (ms). 1310 var NO_SUGGESTION_HINT_MS = 5000; 1311 // Dauer der Fade-Out-Animation (muss zur CSS-Transition passen). 1312 var NO_SUGGESTION_FADE_MS = 400; 1313 1314 /** 1315 * 2026-08-22: Zeigt unter einem LEEREN Feld den Hinweis "Kein Vorschlag 1316 * gefunden", wenn die KI fuer dieses Feld nichts liefern konnte. Vorher 1317 * passierte in dem Fall gar nichts â der User wusste nicht, ob die Suche 1318 * noch laeuft oder ergebnislos war. 1319 *
1320 * Wird in denselben Container wie der Vorschlags-Chip gehaengt (direkt 1321 * hinter das Input) und blendet sich nach NO_SUGGESTION_HINT_MS aus. 1322 */ 1323 function renderNoSuggestionHint(input) { 1324 if (!input || !input.parentElement) return; 1325 var parent = input.parentElement; 1326 // Feld wurde inzwischen befuellt -> kein Hinweis noetig 1327 if (input.value && String(input.value).trim() !== '') return; 1328 // Es gibt bereits einen Vorschlag (z.B. vom lokalen Provider) -> nichts melden 1329 if (parent.querySelector(':scope > .ee-ai-suggestion')) return; 1330 // Doppelte Hinweise vermeiden 1331 if (parent.querySelector(':scope > .ee-suggestion-empty')) return; 1332 // Bug #269: Es steht bereits eine Status-Note (loading/empty/error) am Feld. 1333 // Die ist aussagekraeftiger (unterscheidet Fehler von "nichts gefunden") â 1334 // kein zweiter, textgleicher Hinweis darunter. 1335 if (parent.querySelector(':scope > .ee-ai-note')) return; 1336 1337 var hint = document.createElement('span'); 1338 hint.className = 'ee-suggestion-empty'; 1339 hint.textContent = 'Kein Vorschlag gefunden'; 1340 1341 if (input.nextSibling) { 1342 parent.insertBefore(hint, input.nextSibling); 1343 } else { 1344 parent.appendChild(hint); 1345 } 1346 1347 setTimeout(function () { 1348 if (!hint.parentElement) return; 1349 hint.classList.add('ee-suggestion-empty--out'); 1350 setTimeout(function () { 1351 if (hint.parentElement) hint.remove(); 1352 }, NO_SUGGESTION_FADE_MS); 1353 }, NO_SUGGESTION_HINT_MS); 1354 } 1355 1356 /** 1357 * Setzt den "Kein Vorschlag gefunden"-Hinweis fuer alle angefragten Felder, 1358 * die am Ende keinen Chip bekommen haben. renderNoSuggestionHint() filtert 1359 * selbst: befuellte Felder und Felder mit Chip werden uebersprungen. 1360 */ 1361 function markFieldsWithoutSuggestion(popup, fieldIds) { 1362 if (!popup || !Array.isArray(fieldIds)) return; 1363 fieldIds.forEach(function (fid) { 1364 var input = popup.querySelector('#' + CSS.escape(fid)); 1365 if (input) renderNoSuggestionHint(input); 1366 }); 1367 } 1368
1369 function renderSuggestionChip(input, suggestion) { 1370 // Bug #269: Falls noch eine Status-Note (loading/empty) neben dem Feld 1371 // steht, ersetzt der Chip sie. 1372 clearAiNote(input); 1373 // Entferne alten Chip falls vorhanden 1374 var prev = input.parentElement && input.parentElement.querySelector(':scope > .ee-ai-suggestion'); 1375 if (prev) prev.remove(); 1376 // Ein echter Vorschlag schlaegt den "Kein Vorschlag gefunden"-Hinweis 1377 var staleHint = input.parentElement && input.parentElement.querySelector(':scope > .ee-suggestion-empty'); 1378 if (staleHint) staleHint.remove(); 1379 1380 if (!suggestion) return; 1381 1382 var chip = document.createElement('button'); 1383 chip.type = 'button'; 1384 chip.className = 'ee-ai-suggestion'; 1385 chip.setAttribute('data-target', input.id || ''); 1386 var sparkle = document.createElement('span'); 1387 sparkle.className = 'ee-ai-suggestion-icon'; 1388 sparkle.textContent = '\u2728'; 1389 var labelWrap = document.createElement('span'); 1390 labelWrap.className = 'ee-ai-suggestion-text'; 1391 labelWrap.innerHTML = 'Vorschlag: <span class="ee-ai-suggestion-value"></span>'; 1392 labelWrap.querySelector('.ee-ai-suggestion-value').textContent = suggestion; 1393 chip.appendChild(sparkle); 1394 chip.appendChild(labelWrap); 1395 1396 chip.addEventListener('click', function (ev) { 1397 ev.preventDefault(); 1398 input.value = suggestion; 1399 input.dispatchEvent(new Event('input', { bubbles: true })); 1400 input.dispatchEvent(new Event('change', { bubbles: true })); 1401 // v2.22.36 (Bug-A Fix): initAutoSave hoert NUR auf 'focusout'. Wenn der 1402 // User den Vorschlag per Klick uebernimmt, hat das Input nie den Fokus 1403 // bekommen -> kein focusout -> kein Auto-Save -> Wert ging beim Reload 1404 // verloren. Wir dispatchen 'focusout' explizit, damit der existierende 1405 // Auto-Save-Listener greift (DRY â kein direkter postFragebogenSave-Call). 1406 input.dispatchEvent(new Event('focusout', { bubbles: true })); 1407 // Bug-Fix 2026-05-29: User-Wunsch geaendert â beim Akzeptieren eines 1408 // Vorschlags NUR den eigenen Chip entfernen, die Chips anderer Felder 1409 // bleiben sichtbar (so kann der User pro Feld separat entscheiden). 1410 // Previously: all chips were removed globally. 1411 chip.remove(); 1412 }); 1413 1414 // Insert nach dem Input 1415 if (input.nextSibling) { 1416 input.parentElement.insertBefore(chip, input.nextSibling); 1417 } else { 1418 input.parentElement.appendChild(chip); 1419 } 1420 1421 // Wenn der User selbst tippt -> Chip raus 1422 var onInput = function () { 1423 if (input.value && input.value.trim() !== '' && input.value !== suggestion) { 1424 chip.remove(); 1425 input.removeEventListener('input', onInput); 1426 } 1427 }; 1428 input.addEventListener('input', onInput); 1429 } 1430 1431 function evaluateSuggestion(fieldId, input) { 1432 var provider = SUGGESTION_PROVIDERS[fieldId]; 1433 if (!provider) return; 1434 // Nur leere Felder 1435 if (input.value && String(input.value).trim() !== '') return; 1436 var result; 1437 try { result = provider(); } catch (e) { return; } 1438 if (result && typeof result.then === 'function') { 1439 result.then(function (val) { 1440 // Erneut pruefen â User koennte inzwischen getippt haben 1441 if (input.value && String(input.value).trim() !== '') return; 1442 if (val && String(val).trim() !== '') renderSuggestionChip(input, val); 1443 }).catch(function () { /* silent */ }); 1444 } else if (result && String(result).trim() !== '') { 1445 renderSuggestionChip(input, result); 1446 } 1447 } 1448 1449 function initFieldSuggestions(popup) { 1450 // Phase 1: lokale Provider sofort ausfuehren â schnelle Chips fuer Felder 1451 // die rein aus projectData / Mapbox ableitbar sind. AI ergaenzt das spaeter. 1452 Object.keys(SUGGESTION_PROVIDERS).forEach(function (fieldId) { 1453 var input = popup.querySelector('#' + CSS.escape(fieldId)); 1454 if (input) evaluateSuggestion(fieldId, input); 1455 }); 1456 1457 // Phase 2: echter KI-Call mit Projekt-Kontext. 1458 // Bug #140 â User-Wunsch: "bitte einen ki call machen mit dem was du hast". 1459 fetchAISuggestions(popup); 1460 } 1461 1462 /** 1463 * 2026-05-28 (Bug-Fix): Re-Render der KI-Vorschlaege beim Popup-Open. 1464 * 1465 * Problem: Nachdem das Popup einmal geoeffnet UND wieder geschlossen wurde, 1466 * verschwanden die KI-Chips beim naechsten Open â weil: 1467 * 1. _aiSuggestionsFetched=true verhindert neuen Backend-Call 1468 * 2. Akzeptieren eines Vorschlags entfernt ALLE anderen Chips (Zeile ~844) 1469 * 3. boot() laeuft nur 1x pro Page-Load â kein Re-Init 1470 * 1471 * Loesung: Cache (_aiSuggestionsCache) wird beim ersten Fetch gefuellt. 1472 * Beim Popup-Open rufen wir rerenderAISuggestions(popup) auf, was fuer jedes 1473 * Cache-Feld einen Chip rendert â sofern das Feld noch leer ist UND nicht 1474 * bereits ein Chip existiert. Lokale Provider (evaluateSuggestion) werden 1475 * auch erneut getriggert, da sie ggf. inzwischen mehr Daten haben (z.B. 1476 * projectData asynchron nachgeladen). 1477 */ 1478 function rerenderAISuggestions(popup) { 1479 if (!popup) popup = $(SCOPE_SELECTOR); 1480 if (!popup) return; 1481 DBG('rerenderAISuggestions â re-eval Local-Provider + Re-Render Cache'); 1482 1483 // 1. Lokale Provider erneut auswerten â leeren Feldern Chips geben
1484 Object.keys(SUGGESTION_PROVIDERS).forEach(function (fieldId) { 1485 var input = popup.querySelector('#' + CSS.escape(fieldId)); 1486 if (!input) return; 1487 // Skip wenn schon ein Chip neben dem Input existiert 1488 var existingChip = input.parentElement && input.parentElement.querySelector(':scope > .ee-ai-suggestion'); 1489 if (existingChip) return; 1490 evaluateSuggestion(fieldId, input); 1491 }); 1492 1493 // 2. KI-Cache neu ausrollen (falls vorhanden) 1494 if (_aiSuggestionsCache) { 1495 Object.keys(_aiSuggestionsCache).forEach(function (fid) { 1496 var input = popup.querySelector('#' + CSS.escape(fid)); 1497 if (!input) return; 1498 if (input.value && String(input.value).trim() !== '') return; // field already filled 1499 var existingChip = input.parentElement && input.parentElement.querySelector(':scope > .ee-ai-suggestion'); 1500 if (existingChip) return; // chip already exists 1501 renderSuggestionChip(input, _aiSuggestionsCache[fid]); 1502 }); 1503 } else if (!_aiSuggestionsFetched) { 1504 // Kein Cache UND noch kein Fetch gelaufen -> normalen Fetch starten 1505 fetchAISuggestions(popup); 1506 } 1507 1508 // 3. "Kein Vorschlag gefunden" erneut zeigen. Der Fetch laeuft 1x pro 1509 // Page-Load beim boot() â evtl. war das Popup da noch zu und der 1510 // Hinweis ist ungesehen ausgelaufen. 1511 if (_aiNoSuggestionFields && _aiNoSuggestionFields.length) { 1512 markFieldsWithoutSuggestion(popup, _aiNoSuggestionFields); 1513 } 1514 } 1515 // Global verfuegbar fuer projectSettingsOpenPopup() in ee-project-view.js 1516 window.eeRerenderAISuggestions = rerenderAISuggestions; 1517 1518 /* ------------------------------------------------------------------ 1519 Bug #140: Echter KI-Call an /wp-json/energyExperts/v1/fragebogen/suggest 1520 - Sammelt alle bekannten Feld-IDs die im Popup existieren UND leer sind 1521 - Schickt Projekt-Kontext (organization, coordinates, polygons, address) 1522 an das Backend, das GPT-4o-mini ueber EE_AI_Service::extractJson() 1523 anwirft. 1524 - Backend liefert { suggestions: { field_id: value } }. 1525 - Pro Eintrag wird ein Chip gerendert (nur wenn Feld noch leer ist). 1526 - Fallback: schlaegt der Call fehl, bleiben die lokalen Provider-Chips. 1527 ------------------------------------------------------------------ */ 1528 var AI_SUGGESTION_FIELDS = [ 1529 // v2.22.7 (Bug #140): Bauherr-Felder entfernt â Bauherr ist eine Kontakt-Rolle. 1530 'projectSettingsAdresse-address', 1531 'projectSettingsGrundbuchKommune', 1532 'projectSettingsGrundbuchBehoerde', 1533 'projectFlaecheArtLandwirtschaft', 1534 'projectSettingsNetzbetreiber', 1535 'projectSettingsAnsprechpartnerNetzbetreiber', 1536 'projectSettingsNameLandratsamt', 1537 'projectSettingsAnsprechpartnerLandratsamt', 1538 // v2.22.32: Downloadportal â URL des Online-Bauportals der zustaendigen Behoerde. 1539 'projectSettingsDownloadportal' 1540 ]; 1541 1542 // Bug #140: OSM landuse fuer Polygon-Center via Overpass. 1543 // Stufenweise: 100m â 300m â 800m. Polygon-Center kann auf einem Weg liegen, 1544 // dann brauchen wir mehr Radius bis ein landuse/natural-Way getroffen wird. 1545 var _overpassLanduseCache; 1546 function fetchOverpassLanduse() { 1547 if (typeof _overpassLanduseCache !== 'undefined') return Promise.resolve(_overpassLanduseCache); 1548 var center = getPolygonCenter(); 1549 if (!center) { 1550 _overpassLanduseCache = null; 1551 if (window.console) console.warn('[Fragebogen] Overpass: ABORT - no center (energyExpertsProjectData.coordinates missing)'); 1552 return Promise.resolve(null); 1553 } 1554 var radii = [100, 300, 800]; 1555 function tryRadius(idx) { 1556 if (idx >= radii.length) { 1557 _overpassLanduseCache = null; 1558 dbgInfo('[Fragebogen] Overpass: kein landuse-Tag im Umkreis gefunden (max ' + radii[radii.length - 1] + 'm)'); 1559 return null; 1560 } 1561 var rr = radii[idx]; 1562 // Bug #140: KEIN forest/wood â das ist meist der Wald NEBEN der Flaeche, 1563 // not the Agri-PV area itself. Explicitly filter on agri tags. 1564 var query = '[out:json][timeout:10];(' + 1565 'way["landuse"~"^(farmland|meadow|grass|vineyard|orchard|allotments|plant_nursery)$"](around:' + rr + ',' + center.lat + ',' + center.lng + ');' + 1566 'relation["landuse"~"^(farmland|meadow|grass|vineyard|orchard)$"](around:' + rr + ',' + center.lat + ',' + center.lng + ');' + 1567 'way["natural"~"^(grassland|scrub|heath)$"](around:' + rr + ',' + center.lat + ',' + center.lng + ');' + 1568 ');out tags 1;'; 1569 var url = 'https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(query); 1570 return fetch(url).then(function (r) { 1571 if (!r.ok) { 1572 if (window.console) console.warn('[Fragebogen] Overpass HTTP ' + r.status + ' bei radius ' + rr + 'm'); 1573 } 1574 return r.json(); 1575 }).then(function (data) { 1576 var elCount = (data && data.elements) ? data.elements.length : 0; 1577 var tags = (data && data.elements && data.elements[0] && data.elements[0].tags) || {}; 1578 var landuse = tags.landuse || tags.natural || null; 1579 dbgInfo('[Fragebogen] Overpass radius=' + rr + 'm elements=' + elCount + ' landuse=' + (landuse || 'kein-Treffer')); 1580 if (landuse) { 1581 _overpassLanduseCache = landuse; 1582 return landuse; 1583 }
1584 return tryRadius(idx + 1); 1585 }).catch(function (e) { 1586 if (window.console) console.warn('[Fragebogen] Overpass error at radius ' + rr + 'm:', e); 1587 return tryRadius(idx + 1); 1588 }); 1589 } 1590 return Promise.resolve(tryRadius(0)); 1591 } 1592 1593 // Bug #140: Nominatim Reverse-Geocoding â liefert village, town, suburb, 1594 // municipality, county separat (genauer als Mapbox bei laendlichen Lagen, 1595 // wo Mapbox oft nur "place" ohne Detail zurueckgibt). 1596 // Cache pro Popup-Session (Rate-Limit: 1 req/sec). 1597 var _nominatimCache; 1598 function fetchNominatimAddress() { 1599 if (typeof _nominatimCache !== 'undefined') return Promise.resolve(_nominatimCache); 1600 var center = getPolygonCenter(); 1601 if (!center) { 1602 _nominatimCache = null; 1603 if (window.console) console.warn('[Fragebogen] Nominatim: ABORT - no center'); 1604 return Promise.resolve(null); 1605 } 1606 var url = 'https://nominatim.openstreetmap.org/reverse?format=json' + 1607 '&lat=' + encodeURIComponent(center.lat) + 1608 '&lon=' + encodeURIComponent(center.lng) + 1609 '&zoom=14&addressdetails=1&accept-language=de'; 1610 dbgInfo('[Fragebogen] Nominatim: starte', url); 1611 return fetch(url, { headers: { 'Accept': 'application/json' } }).then(function (r) { 1612 if (!r.ok && window.console) console.warn('[Fragebogen] Nominatim HTTP ' + r.status); 1613 return r.json(); 1614 }).then(function (data) { 1615 dbgInfo('[Fragebogen] Nominatim RAW:', data); 1616 if (!data || !data.address) { 1617 _nominatimCache = null; 1618 if (window.console) console.warn('[Fragebogen] Nominatim: no address data in response'); 1619 return null; 1620 } 1621 var a = data.address; 1622 _nominatimCache = { 1623 display_name: data.display_name || null, 1624 municipality: a.municipality || a.town || a.city || a.village || null, 1625 suburb: a.suburb || a.neighbourhood || a.hamlet || a.locality || null, 1626 county: a.county || a.state_district || null, 1627 state: a.state || null, 1628 postcode: a.postcode || null, 1629 country: a.country || null, 1630 road: a.road || null, 1631 house_number: a.house_number || null 1632 }; 1633 dbgInfo('[Fragebogen] Nominatim PARSED:', _nominatimCache); 1634 return _nominatimCache; 1635 }).catch(function (e) { 1636 if (window.console) console.warn('[Fragebogen] Nominatim-Error:', e); 1637 _nominatimCache = null; 1638 return null; 1639 }); 1640 } 1641 1642 var _aiSuggestionsFetched = false; 1643 // AI suggestions were not re-rendered after popup close+reopen 1644 // mehr angezeigt â weil der Fetch nur 1x pro Page-Load lief (_aiSuggestionsFetched=true) 1645 // and accepting a suggestion removed ALL chips. 1646 // Loesung: Cache die Backend-Antwort und re-rendere Chips on demand 1647 // (beim Popup-Open ueber projectSettingsOpenPopup -> eeRerenderAISuggestions). 1648 var _aiSuggestionsCache = null; // { fieldId: value, ... } 1649 // 2026-08-22: Felder, fuer die die KI nachweislich NICHTS geliefert hat. 1650 // Wird beim Popup-Open erneut ausgerollt, damit der Hinweis "Kein Vorschlag 1651 // gefunden" auch dann sichtbar ist, wenn der Fetch lief waehrend das Popup 1652 // noch geschlossen war (Fetch laeuft 1x pro Page-Load beim boot()). 1653 var _aiNoSuggestionFields = null; // [ fieldId, ... ] 1654 var DBG = function () { 1655 if (eeFbDebugOn() && window.console && console.info) { 1656 console.info.apply(console, ['%c[Fragebogen-AI]', 'color:#3fa14d;font-weight:bold;'].concat([].slice.call(arguments))); 1657 } 1658 }; 1659 var DBG_WARN = function () { 1660 if (eeFbDebugOn() && window.console && console.warn) { 1661 console.warn.apply(console, ['[Fragebogen-AI]'].concat([].slice.call(arguments))); 1662 } 1663 }; 1664 1665 function fetchAISuggestions(popup) { 1666 DBG('fetchAISuggestions() aufgerufen'); 1667 if (_aiSuggestionsFetched) { 1668 DBG('Schon gelaufen â abgebrochen'); 1669 return; 1670 } 1671 1672 // WICHTIG: Es gibt ZWEI globale Datenstrukturen, die leicht zu verwechseln sind: 1673 // - window.energyExpertsProjectData (per wp_localize_script, KEINE id, KEINE polygons) 1674 // enthaelt: organization, user, contact, permissions, mapboxApiKey, etc. 1675 // - window.projectData (per fetchProjectDetails() async REST-Call) 1676 // enthaelt: id, polygons, variations, financeData, etc. 1677 // Fuer den KI-Call brauchen wir die polygons + id â also window.projectData. 1678 // Die organization ergaenzen wir aus energyExpertsProjectData (immer sofort da). 1679 var loc = window.energyExpertsProjectData || {}; 1680 var pd = (window.projectData && window.projectData.id) ? window.projectData : null; 1681 // Bug #269 Fix 1: Suggest-Call erst feuern, wenn NEBEN projectData.id auch ein 1682 // Polygon-Center (Koordinaten) verfuegbar ist. Sonst laufen die Geo-Lookups 1683 // (Mapbox/Overpass/Nominatim) leer und die KI bekommt keinen regionalen Kontext 1684 // -> "nichts kommt". Bounded Retry (max 30 * 500ms = 15s), danach Fallback ohne Center. 1685 var center0 = pd ? getPolygonCenter() : null; 1686 var tries = fetchAISuggestions._tries || 0; 1687 if ((!pd || !center0) && tries < 30) { 1688 fetchAISuggestions._tries = tries + 1;
1689 DBG_WARN('projectData/Center noch nicht bereit (Versuch ' + tries + '/30) â retry in 500ms', { 1690 hasProjectData: !!pd, 1691 hasCenter: !!center0, 1692 loc_hasId: !!(loc && loc.id), 1693 loc_keys: Object.keys(loc || {}) 1694 }); 1695 setTimeout(function () { fetchAISuggestions(popup); }, 500); 1696 return; 1697 } 1698 if (!pd) { 1699 DBG_WARN('ABBRUCH: window.projectData nach 15s immer noch ohne id. window.projectData =', window.projectData); 1700 return; 1701 } 1702 if (!center0) { 1703 DBG_WARN('Center nach 15s nicht verfuegbar â fahre ohne Geo-Kontext fort (Fallback).'); 1704 } 1705 _aiSuggestionsFetched = true; 1706 DBG('window.projectData OK', { id: pd.id, polyCount: (pd.polygons || []).length, hasFinance: !!pd.financeData }); 1707 DBG('energyExpertsProjectData (localize)', { hasOrg: !!loc.organization, hasContact: !!loc.contact, hasMapbox: !!loc.mapboxApiKey }); 1708 1709 // Welche Felder sind im DOM vorhanden UND leer? Nur fuer die fragen wir. 1710 var emptyFields = AI_SUGGESTION_FIELDS.filter(function (fid) { 1711 var el = popup.querySelector('#' + CSS.escape(fid)); 1712 return el && (!el.value || String(el.value).trim() === ''); 1713 }); 1714 var presentFields = AI_SUGGESTION_FIELDS.map(function (fid) { 1715 var el = popup.querySelector('#' + CSS.escape(fid)); 1716 return { id: fid, present: !!el, val: el ? el.value : null }; 1717 }); 1718 DBG('Field-Diagnose', presentFields); 1719 DBG('Leere Felder zum Befragen:', emptyFields); 1720 if (!emptyFields.length) { 1721 DBG_WARN('Keine leeren Felder â Abbruch (alle bereits gefuellt oder keines vorhanden)'); 1722 return; 1723 } 1724 1725 // Bug #269 (Feedback): Sofort einen sichtbaren Lade-Hinweis an jedem leeren 1726 // Feld setzen, das noch keinen Chip hat â der KI-Call dauert 20-30s (GPT), 1727 // vorher war das Popup in dieser Zeit komplett stumm. 1728 emptyFields.forEach(function (fid) { 1729 var el = popup.querySelector('#' + CSS.escape(fid)); 1730 if (!el) return; 1731 var hasChip = el.parentElement && el.parentElement.querySelector(':scope > .ee-ai-suggestion'); 1732 if (hasChip) return; // lokaler Provider war schneller â Chip nicht ueberdecken 1733 renderAiNote(el, 'loading', 'KI sucht Vorschlag\u2026'); 1734 }); 1735 1736 // Bug #269 (Feedback): Lade-Notes am Ende in einen Endzustand ueberfuehren. 1737 // Fuer jedes leere Feld ohne Vorschlags-Chip wird die Note gesetzt (empty/ 1738 // error). Felder die inzwischen einen Chip oder Wert haben, verlieren die Note. 1739 function setNotesForRemaining(kind, text) { 1740 emptyFields.forEach(function (fid) { 1741 var el = popup.querySelector('#' + CSS.escape(fid)); 1742 if (!el) return; 1743 if (el.value && String(el.value).trim() !== '') { clearAiNote(el); return; } 1744 var hasChip = el.parentElement && el.parentElement.querySelector(':scope > .ee-ai-suggestion'); 1745 if (hasChip) { clearAiNote(el); return; } 1746 renderAiNote(el, kind, text); 1747 }); 1748 } 1749 1750 // Drei Async-Quellen parallel: Mapbox-Reverse + Overpass-Landuse + Nominatim-Reverse. 1751 var _ctr = getPolygonCenter(); 1752 var _polyDiag = (function () { 1753 try { 1754 var vIdx = (typeof window.variation === 'number') ? window.variation : 0; 1755 var v = pd && pd.variations && pd.variations[vIdx]; 1756 var pgs = v && Array.isArray(v.polygons) ? v.polygons : []; 1757 return { 1758 variationIdx: vIdx, 1759 variationsCount: pd && pd.variations ? pd.variations.length : 0, 1760 polygonsCount: pgs.length, 1761 firstPolyKeys: pgs[0] ? Object.keys(pgs[0]) : [], 1762 firstPolyType: pgs[0] && pgs[0].type, 1763 firstPolyCenter: pgs[0] && pgs[0].centerCoordinates, 1764 pd_coordinates: pd && pd.coordinates, 1765 loc_coordinates: loc && loc.coordinates 1766 }; 1767 } catch (e) { return { err: String(e) }; } 1768 })(); 1769 DBG('Starte Geo-Lookups... center=', _ctr, 'hasMapboxKey=', !!loc.mapboxApiKey, 'polyDiag=', _polyDiag); 1770 if (!_ctr) DBG_WARN('Kein Center! Diagnose:', _polyDiag); 1771 Promise.all([ 1772 fetchPolygonAddress(), 1773 fetchOverpassLanduse(), 1774 fetchNominatimAddress() 1775 ]).then(function (results) { 1776 var addr = results[0]; 1777 var landuse = results[1]; 1778 var nominatim = results[2]; 1779 DBG('Geo-Lookups fertig:', { mapbox: addr, overpass_landuse: landuse, nominatim: nominatim }); 1780 1781 var org = (pd && pd.organization) || {}; 1782 var contact = (org.connections && org.connections[0] && org.connections[0].contact) || {}; 1783 var center = getPolygonCenter(); 1784 1785 // KRITISCH (Bug #140): Mapbox-Schluessel ins Backend-Schema mappen. 1786 // fetchPolygonAddress liefert {full, street, streetNumber, zip, city, country, region} 1787 // Backend erwartet aber {place_name, region, locality, postcode, district, address_line} 1788 // â vorher kam dadurch fast nichts in der KI an. 1789 var mappedAddress = null; 1790 if (addr && (addr.full || addr.city || addr.region)) { 1791 mappedAddress = { 1792 place_name: addr.full || null, 1793 region: addr.region || null, // Bundesland 1794 locality: addr.city || null, // Stadt/Gemeinde 1795 postcode: addr.zip || null, 1796 district: null, // Mapbox liefert keinen Landkreis 1797 address_line: ((addr.street || '') + (addr.streetNumber ? ' ' + addr.streetNumber : '')).trim() || null, 1798 country: addr.country || null 1799 }; 1800 } 1801 DBG('Mapped Mapbox -> Backend:', mappedAddress); 1802 1803 var payload = { 1804 project_id: pd.id, 1805 fields: emptyFields, 1806 context: { 1807 organization: { 1808 name: org.name || null, 1809 street: org.street || null, 1810 postalCode: org.postalCode || null, 1811 city: org.city || null, 1812 region: org.region || null, 1813 country: org.country || null, 1814 contact: { 1815 firstname: contact.firstname || null, 1816 lastname: contact.lastname || null, 1817 email: contact.email || null, 1818 phone: contact.phone || null 1819 } 1820 }, 1821 coordinates: center ? [center.lng, center.lat] : null, 1822 address: mappedAddress, 1823 nominatim: nominatim || null,
1824 polygons: Array.isArray(pd.polygons) && pd.polygons.length ? pd.polygons.slice(0, 5).map(function (p) { 1825 return { 1826 area_ha: p && p.area_ha ? p.area_ha : null, 1827 osm_landuse: landuse || null 1828 }; 1829 }) : (landuse ? [{ area_ha: null, osm_landuse: landuse }] : []) 1830 } 1831 }; 1832 1833 var settings = window.apiSettings || {}; 1834 var base = settings.root || '/wp-json/'; 1835 var url = base + 'energyExperts/v1/fragebogen/suggest'; 1836 var headers = { 'Content-Type': 'application/json' }; 1837 if (settings.nonce) { 1838 headers['X-WP-Nonce'] = settings.nonce; 1839 } else { 1840 DBG_WARN('apiSettings.nonce FEHLT! settings =', settings); 1841 } 1842 1843 DBG('POST', url, 'payload:', payload); 1844 1845 return fetch(url, { 1846 method: 'POST', 1847 credentials: 'include', 1848 headers: headers, 1849 body: JSON.stringify(payload) 1850 }).then(function (r) { 1851 DBG('Response Status:', r.status, r.statusText); 1852 if (!r.ok) { 1853 return r.text().then(function (t) { 1854 DBG_WARN('HTTP-Fehler Body:', t); 1855 throw new Error('HTTP ' + r.status); 1856 }); 1857 } 1858 return r.json(); 1859 }); 1860 }).then(function (data) { 1861 DBG('Response Body:', data); 1862 if (data && data._debug) { 1863 DBG('Backend-DEBUG (per-field GPT-Status):', data._debug); 1864 } 1865 if (!data) { 1866 DBG_WARN('Leere Antwort'); 1867 setNotesForRemaining('error', 'Vorschl\u00e4ge konnten nicht geladen werden'); 1868 // Merken fuer den Popup-Re-Open (rerenderAISuggestions): der Fetch laeuft 1869 // nur 1x pro Page-Load, evtl. war das Popup zu diesem Zeitpunkt zu. 1870 _aiNoSuggestionFields = emptyFields.slice(); 1871 return; 1872 } 1873 if (!data.success) { 1874 DBG_WARN('success=false', data.error || '(kein error-feld)'); 1875 setNotesForRemaining('error', 'Vorschl\u00e4ge konnten nicht geladen werden'); 1876 return; 1877 } 1878 if (!data.suggestions || (typeof data.suggestions === 'object' && Object.keys(data.suggestions).length === 0)) { 1879 DBG_WARN('Keine Suggestions in der Antwort â Backend hat nichts geliefert'); 1880 setNotesForRemaining('empty', 'Kein Vorschlag gefunden'); 1881 _aiNoSuggestionFields = emptyFields.slice(); 1882 return; 1883 } 1884 var suggestions = data.suggestions; 1885 // Bug-Fix: Suggestions cachen damit wir sie bei Popup re-open neu rendern koennen 1886 _aiSuggestionsCache = _aiSuggestionsCache || {}; 1887 Object.keys(suggestions).forEach(function (fid) { 1888 var val = suggestions[fid]; 1889 if (val && typeof val === 'string' && val.trim()) { 1890 _aiSuggestionsCache[fid] = val.trim(); 1891 } 1892 }); 1893 DBG('Suggestions empfangen, rendere Chips fuer:', Object.keys(suggestions)); 1894 Object.keys(suggestions).forEach(function (fid) { 1895 var val = suggestions[fid]; 1896 if (!val || typeof val !== 'string' || !val.trim()) { 1897 DBG_WARN('Leerer/ungueltiger Wert fuer', fid, '->', val); 1898 return; 1899 } 1900 var input = popup.querySelector('#' + CSS.escape(fid)); 1901 if (!input) { 1902 DBG_WARN('Input fuer', fid, 'nicht im DOM gefunden'); 1903 return; 1904 } 1905 // Nur leere Felder â User koennte zwischenzeitlich getippt haben 1906 if (input.value && String(input.value).trim() !== '') { 1907 DBG('Skip', fid, 'â inzwischen befuellt'); 1908 return; 1909 } 1910 DBG('Rendere Chip:', fid, '=', val.trim()); 1911 renderSuggestionChip(input, val.trim()); 1912 }); 1913 // Bug #269: verbleibende leere Felder ohne Vorschlag klar kennzeichnen, 1914 // damit der User sieht "hier gab es nichts" statt stiller Leere. 1915 setNotesForRemaining('empty', 'Kein Vorschlag gefunden'); 1916 1917 // 2026-08-22: Alle angefragten (= leeren) Felder, die keinen Chip 1918 // bekommen haben, fuer den Popup-Re-Open merken (rerenderAISuggestions 1919 // rollt den Hinweis dann erneut aus, falls die Note nicht mehr im DOM ist). 1920 _aiNoSuggestionFields = emptyFields.filter(function (fid) { 1921 var val = suggestions[fid]; 1922 return !(val && typeof val === 'string' && val.trim()); 1923 }); 1924 DBG('Ohne KI-Vorschlag:', _aiNoSuggestionFields); 1925 }).catch(function (err) { 1926 DBG_WARN('AI-Suggest fehlgeschlagen, Fallback aktiv:', err); 1927 // Auch im Fehlerfall Feedback geben statt stumm zu bleiben. 1928 setNotesForRemaining('error', 'Vorschl\u00e4ge konnten nicht geladen werden'); 1929 _aiNoSuggestionFields = emptyFields.slice(); 1930 }); 1931 } 1932 1933 /* ================================================================== 1934 Bug #140 Phase 3-6: Contact-Slots, Auto-Save, Massstab-Pills, 1935 Single-Source Netzbetreiber 1936 ================================================================== */ 1937 1938 function getCurrentProjectId() { 1939 try { 1940 var pd = window.projectData || (typeof projectData !== 'undefined' ? projectData : null); 1941 if (pd && pd.id) return parseInt(pd.id, 10); 1942 } catch (e) {} 1943 try { 1944 var loc = window.energyExpertsProjectData || {}; 1945 if (loc.id) return parseInt(loc.id, 10); 1946 } catch (e) {} 1947 return null; 1948 } 1949 1950 function getCurrentOrgId() { 1951 try { 1952 var pd = window.projectData || (typeof projectData !== 'undefined' ? projectData : null); 1953 if (pd && pd.organizationId) return parseInt(pd.organizationId, 10); 1954 } catch (e) {} 1955 try { 1956 var loc = window.energyExpertsProjectData || {}; 1957 if (loc.organizationId) return parseInt(loc.organizationId, 10); 1958 } catch (e) {} 1959 return null; 1960 } 1961 1962 function getRestRoot() { 1963 try { 1964 var loc = window.energyExpertsProjectData || {}; 1965 if (loc.restUrl) return String(loc.restUrl).replace(/\/$/, ''); 1966 } catch (e) {} 1967 try { 1968 if (window.wpApiSettings && window.wpApiSettings.root) { 1969 return String(window.wpApiSettings.root).replace(/\/$/, '') + '/energyExperts
1969/v1'; 1970 } 1971 } catch (e) {} 1972 return '/wp-json/energyExperts/v1'; 1973 } 1974 1975 function getRestNonce() { 1976 try { 1977 var loc = window.energyExpertsProjectData || {}; 1978 if (loc.nonce) return loc.nonce; 1979 } catch (e) {} 1980 // 2026-05-28 (Massstab 401-Fix): Plugin nutzt window.apiSettings (NICHT wpApiSettings). 1981 // Diese Variable wird via wp_localize_script in class-energyExperts.php gesetzt. 1982 // Ohne diesen Fallback waere der X-WP-Nonce-Header immer leer -> alle Saves 401. 1983 if (window.apiSettings && window.apiSettings.nonce) return window.apiSettings.nonce; 1984 if (window.wpApiSettings && window.wpApiSettings.nonce) return window.wpApiSettings.nonce; 1985 return ''; 1986 } 1987 1988 function toast(msg, type) { 1989 try { 1990 if (window.EEToast) { 1991 if (type === 'error' && window.EEToast.error) return window.EEToast.error(msg); 1992 if (type === 'info' && window.EEToast.info) return window.EEToast.info(msg); 1993 if (window.EEToast.success) return window.EEToast.success(msg); 1994 } 1995 } catch (e) {} 1996 dbgLog('[fragebogen-toast]', type || 'success', msg); 1997 } 1998 1999 /* ----- Phase 4: Auto-Save on-blur ----- */ 2000 // Mapping: HTML-Element-ID/name -> project_settings Spalte 2001 // Nur Felder fuer Bug #140 Phase 1 Spalten (die anderen Felder haben ihre 2002 // own save paths in ee-project-view.js). 2003 // v2.26.0: Netzanschluss-Felder (netzbetreiber, netzbetreiberAnsprechpartner, 2004 // netzvoranfrageDatum, nvpReservierungDetails, netzvoranfrageGestellt, nvpReserviert) 2005 // entfernt â diese werden jetzt ueber module_completion e1e8 gespeichert (SSOT). 2006 var AUTOSAVE_MAP = { 2007 'projectSettingsMassstabFlurkarten': 'flurkartenMassstab', 2008 'projectFlaecheArtLandwirtschaft': 'landUseType', 2009 // v2.22.20: Bewirtschaftete Hof-Gesamtflaeche (Tab Flaeche/Bewirtschaftung) 2010 'projectFlaecheGesamtflaeche': 'gesamtflaeche', 2011 'projectSettingsNameLandratsamt': 'landratsamtName', 2012 'projectSettingsAnsprechpartnerLandratsamt': 'landratsamtAnsprechpartner', 2013 'projectSettingsStandBaurecht': 'standBaurecht', 2014 'projectSettingsGrundbuchKommune': 'grundbuchKommune', 2015 'projectSettingsGrundbuchBehoerde': 'grundbuchBehoerde', 2016 // v2.22.20: Grundbuch-Blatt (Tab Flaeche/Grundbuchdaten) 2017 'projectSettingsGrundbuchBlatt': 'grundbuchBlatt', 2018 // Download portal link (AI-enabled, required for permit) 2019 'projectSettingsDownloadportal': 'downloadportal' 2020 }; 2021 var AUTOSAVE_RADIO_MAP = { 2022 'flaeche_eigentum': 'flaecheEigentum', 2023 'flurkarten_massstab': 'flurkartenMassstab' // Pills -> int 2024 }; 2025 2026 /* ----- Netzanschluss SSOT: DOM-ID -> module_completion e1e8 form_data key ----- 2027 All grid-connection fields now read/write from module_completion (e1e8). 2028 This is the single source of truth (SSOT) for Netzanschluss data. */ 2029 var NETZANSCHLUSS_E1E8_MAP = { 2030 'projectSettingsNetzbetreiber': 'grid_operator', 2031 'projectSettingsAnsprechpartnerNetzbetreiber': 'grid_operator_contact', 2032 'projectSettingsDatumNetzvoranfrage': 'grid_inquiry_date', 2033 'projectSettingsNetzvoranfrageDurch': 'grid_inquiry_by', 2034 'projectSettingsAngefragteLeistung': 'requested_capacity_kwp', 2035 'projectSettingsEinspeisezusageNr': 'feed_in_approval_nr', 2036 'projectSettingsEinspeisezusageDatum': 'feed_in_approval_date', 2037 'projectSettingsAnschlussleistungKva': 'connection_capacity_kva', 2038 'projectSettingsZaehlpunkt': 'meter_point_malo', 2039 'projectSettingsKoordinatenNVP': 'nvp_coordinates', 2040 'projectSettingsGueltigkeitReservierung': 'reserved_until' 2041 }; 2042 // Radio/pill groups -> e1e8 form_data key (boolean: ja->true, nein->false) 2043 // 2026-07-01: nvp_vorhanden removed â merged into nvp_reserviert. 2044 // nvp_available is set to same value as nvp_reserved in saveNetzanschlussToE1E8(). 2045 var NETZANSCHLUSS_E1E8_RADIO_MAP = { 2046 'netzvoranfrage_getaetigt': 'grid_inquiry_submitted', 2047 'nvp_reserviert': 'nvp_reserved' 2048 }; 2049 2050 /* ----- Netzanschluss: Load from module-completion e1e8 ----- */ 2051 var _netzanschlussLoading = false; // Guard: suppress auto-save during load 2052 var _netzanschlussInitialLoad = true; // First load: allow programmatic radio changes 2053 var _lastE1E8AutoStatus = (window.projectData && window.projectData.e1e8CompletionStatus) || null; // Shared across initNetzanschlussAutoSave + boot listener 2054 function loadNetzanschlussFromE1E8(popup) { 2055 if (!popup) popup = $(SCOPE_SELECTOR); 2056 if (!popup) return Promise.resolve(); 2057 // Bug-Fix 2026-07-01: Retry-Logik analog zu loadProjectSettingsIntoForm() â 2058 // boot() laeuft bei DOMContentLoaded, projectData.id ist zu dem Zeitpunkt 2059 // oft noch nicht gesetzt. Ohne Retry kehrt die Funktion sofort mit 2060 // Promise.resolve() zurueck und die Netzanschluss-Felder bleiben leer. 2061 if (!getCurrentProjectId()) { 2062 return new Promise(function (resolve) { 2063 var tries = 0; 2064 var maxTries = 30; // 30 * 500ms = 15s 2065 var iv = setInterval(function () { 2066 tries++; 2067 if (getCurrentProjectId()) { 2068 clearInterval(iv); 2069 loadNetzanschlussFromE1E8(popup).then(resolve); 2070 } else if (tries >= maxTries) { 2071 clearInterval(iv); 2072 if (window.console && console.warn) { 2073 console.warn('[Netzanschluss-SSOT] projectData.id not available after 15s â aborting e1e8 load'); 2074 } 2075 resolve(); 2076 } 2077 }, 500); 2078 }); 2079 } 2080 var pid = getCurrentProjectId(); 2081 if (!pid) return Promise.resolve();
2082 var url = getRestRoot() + '/module-completion/e1e8/' + encodeURIComponent(pid); 2083 var headers = {}; 2084 var nonce = getRestNonce(); 2085 if (nonce) headers['X-WP-Nonce'] = nonce; 2086 return fetch(url, { method: 'GET', credentials: 'same-origin', headers: headers }) 2087 .then(function (r) { return r.ok ? r.json() : null; }) 2088 .then(function (resp) { 2089 if (!resp || !resp.completion || !resp.completion.form_data) return; 2090 var fd = resp.completion.form_data; 2091 // Store in projectData for other widgets 2092 if (window.projectData) { 2093 window.projectData.e1e8Completion = fd; 2094 // Also store the completion status so bidirectional status logic 2095 // can detect if E1/E8 is already completed when popup opens 2096 window.projectData.e1e8CompletionStatus = (resp.completion && resp.completion.status) || 'draft'; 2097 } 2098 // Bug-Fix 2026-07-01: Suppress auto-save while filling fields from backend. 2099 // Without this guard, the change events dispatched below trigger 2100 // saveNetzanschlussToE1E8 (via initNetzanschlussAutoSave listeners), 2101 // which could re-POST partially filled data back to the backend. 2102 _netzanschlussLoading = true; 2103 // Fill text/number/date inputs 2104 Object.keys(NETZANSCHLUSS_E1E8_MAP).forEach(function (elemId) { 2105 var key = NETZANSCHLUSS_E1E8_MAP[elemId]; 2106 var el = popup.querySelector('#' + elemId); 2107 if (!el) return; 2108 var val = fd[key]; 2109 if (val != null && val !== '') { 2110 el.value = val; 2111 } 2112 }); 2113 // Fill radio/pill groups 2114 Object.keys(NETZANSCHLUSS_E1E8_RADIO_MAP).forEach(function (radioName) { 2115 var key = NETZANSCHLUSS_E1E8_RADIO_MAP[radioName]; 2116 var val = fd[key]; 2117 if (val == null) return; 2118 // Convert boolean/int to radio value: true/1 -> 'ja', false/0 -> 'nein' 2119 var radioVal = (val === true || val === 1 || val === '1' || val === 'ja') ? 'ja' : 'nein'; 2120 var radio = popup.querySelector('input[type="radio"][name="' + radioName + '"][value="' + radioVal + '"]'); 2121 if (radio) { 2122 radio.checked = true; 2123 radio.dispatchEvent(new Event('change', { bubbles: true })); 2124 } 2125 }); 2126 // Bug-Fix 2026-07-02 (Status-Downgrade B): _netzanschlussLoading NICHT hier 2127 // freigeben. Die ensureSectionsVisible()-IIFE unten setzt programmatisch 2128 // nvJa/resJa.checked und dispatched change-Events. Wuerde der Guard hier 2129 // schon auf false stehen, triggerten diese Dispatches updateNV()/ 2130 // checkAndUpdateE1E8Status() und koennten den Status faelschlich aendern. 2131 // Freigabe erfolgt daher erst NACH der IIFE (siehe unten). 2132 2133 // Bug-Fix QA-2026-07-01 (Bug 1): Sektionen sichtbar machen wenn Daten vorhanden. 2134 // Die Sichtbarkeit der NVP-/Einspeisezusage-Sections ist an den Toggle 2135 // "Netzvoranfrage: Bereits gestellt" gekoppelt. Wenn grid_inquiry_submitted 2136 // true ist ODER wenn Felder in den versteckten Sektionen Werte haben, 2137 // muessen die Sektionen programmatisch sichtbar gemacht werden. 2138 // 2139 // Bug-Fix 2026-07-01 (NVP-Toggle-Snap-Back): Radio-Buttons werden NUR 2140 // beim INITIALEN Laden programmatisch gesetzt (_netzanschlussInitialLoad). 2141 // Bei Reloads nach User-Interaktion werden nur die Sections sichtbar 2142 // gemacht, aber die Radios bleiben unangetastet â sonst wird ein vom 2143 // User geaenderter Toggle sofort zurueckgesetzt. 2144 (function ensureSectionsVisible() { 2145 var hasNVPData = !!(fd.grid_inquiry_submitted || fd.grid_inquiry_date || fd.grid_inquiry_by || fd.requested_capacity_kwp); 2146 var hasEinspeiseData = !!(fd.feed_in_approval_nr || fd.feed_in_approval_date || fd.connection_capacity_kva || fd.reserved_until || fd.nvp_reserved); 2147 2148 if (hasNVPData || hasEinspeiseData) { 2149 // Radios NUR beim initialen Laden setzen 2150 if (_netzanschlussInitialLoad) { 2151 var nvJa = popup.querySelector('#projectSettingsNetzvoranfrageJa'); 2152 if (nvJa && !nvJa.checked) { 2153 nvJa.checked = true; 2154 nvJa.dispatchEvent(new Event('change', { bubbles: true })); 2155 } 2156 } 2157 // Sektionen direkt sichtbar machen (Fallback falls change-Event nicht greift) 2158 var detailsWrap = popup.querySelector('#projectSettingsNetzvoranfrageDetailsWrap'); 2159 if (detailsWrap) detailsWrap.style.display = ''; 2160 var nvpSectionWrap = popup.querySelector('#projectSettingsNVPSectionWrap'); 2161 if (nvpSectionWrap) nvpSectionWrap.style.display = ''; 2162 } 2163 2164 if (hasEinspeiseData) { 2165 // NVP-Reservierung Radio NUR beim initialen Laden setzen 2166 if (_netzanschlussInitialLoad) { 2167 var resJa = popup.querySelector('#projectSettingsNVPReserviertJa'); 2168 if (resJa && !resJa.checked) { 2169 resJa.checked = true; 2170 resJa.dispatchEvent(new Event('change', { bubbles: true })); 2171 } 2172 } 2173 var resDetailsWrap = popup.querySelector('#projectSettingsNVPReservierungDetailsWrap'); 2174 if (resDetailsWrap) resDetailsWrap.style.display = ''; 2175 } 2176 })(); 2177 2178 // Bug-Fix 2026-07-02 (Status-Downgrade B): Guard jetzt freigeben â die 2179 // programmatischen Radio-Dispatches der IIFE oben sind durch. Ab hier 2180 // duerfen echte User-Interaktionen wieder Auto-Save/Status-Logik ausloesen. 2181 _netzanschlussLoading = false;
2182 2183 // Nach dem ersten Laden: Flag auf false setzen, damit bei Reloads 2184 // die Radios nicht mehr programmatisch gesetzt werden. 2185 _netzanschlussInitialLoad = false; 2186 2187 // 2026-07-02: Kernfelder bleiben bei abgeschlossenem E1/E8 EDITIERBAR. 2188 // Frueher (QA-2026-07-01 Bug 3) wurden bei status 'completed' die 5 2189 // Kernfelder disabled/readOnly gesetzt. Auf ausdruecklichen Wunsch 2190 // entfernt â der User will die Felder auch nach Abschluss bearbeiten 2191 // koennen. Es bleibt nur ein rein informativer Banner (keine Sperre). 2192 // Falls ein frueheres Locking (aus altem State) noch aktiv ist, hier 2193 // sicherheitshalber wieder loesen. 2194 var e1e8Status = resp.completion.status; 2195 2196 // Bug-Fix 2026-07-02 v3 (Anzeige-Sync, KEIN Write): _lastE1E8AutoStatus 2197 // nur mit dem frisch geladenen Status synchronisieren, damit die 2198 // Bidirektional-Logik (checkAndUpdateE1E8Status) korrekt weiss, ob bereits 2199 // abgeschlossen ist. Es wird beim Laden NICHTS in die DB geschrieben â 2200 // das Abschliessen passiert ausschliesslich ueber den Save-Pfad 2201 // (_doSaveNetzanschlussToE1E8 -> action:'submit' bei vollstaendigen Daten). 2202 _lastE1E8AutoStatus = e1e8Status || _lastE1E8AutoStatus || null; 2203 2204 if (e1e8Status === 'completed') { 2205 _unlockCompletedFields(popup); 2206 // Rein informativer Hinweis (einmalig, nicht duplizieren) â KEINE Sperre. 2207 var nvpResDW = popup.querySelector('#projectSettingsNVPReservierungDetailsWrap'); 2208 if (nvpResDW && !nvpResDW.querySelector('.ee-e1e8-completed-notice')) { 2209 var notice = document.createElement('p'); 2210 notice.className = 'ee-form-hint ee-form-sync-hint ee-e1e8-completed-notice'; 2211 notice.innerHTML = '<strong>E1/E8 ist abgeschlossen.</strong> Die Kernfelder bleiben bearbeitbar â Aenderungen werden gespeichert.'; 2212 nvpResDW.insertBefore(notice, nvpResDW.firstChild); 2213 } 2214 } 2215 2216 // v2.26.1: Nicht-EE-User duerfen ausgefuellte Kernfelder nicht bearbeiten. 2217 // window.isEE wird per PHP in ProjectShortcode.php gesetzt. 2218 var isEE = window.isEE || false; 2219 if (!isEE) { 2220 var protectedFieldIds = [ 2221 'projectSettingsNetzbetreiber', 2222 'projectSettingsEinspeisezusageNr', 2223 'projectSettingsEinspeisezusageDatum', 2224 'projectSettingsAnschlussleistungKva', 2225 'projectSettingsGueltigkeitReservierung' 2226 ]; 2227 var hasProtectedValue = false; 2228 protectedFieldIds.forEach(function (fid) { 2229 var el = popup.querySelector('#' + fid); 2230 if (el && el.value && el.value.trim() !== '') { 2231 hasProtectedValue = true; 2232 el.disabled = true; 2233 el.readOnly = true; 2234 el.classList.add('ee-netzanschluss-locked'); 2235 el.title = 'Dieses Feld wurde von Energy Experts ausgefuellt und kann nur von EE-Mitarbeitern bearbeitet werden.'; 2236 } 2237 }); 2238 if (hasProtectedValue) { 2239 // Info-Banner anzeigen (einmalig, nicht duplizieren) 2240 var netzPane = popup.querySelector('#projectSettingsNetzvoranfrageJa'); 2241 var bannerTarget = netzPane ? netzPane.closest('.ee-form-section, .projectSettingsNetzanschlussWrap, [class*="netzanschluss"], .ee-tab-pane') : null; 2242 if (!bannerTarget) { 2243 // Fallback: erstes geschuetztes Feld als Anker 2244 bannerTarget = popup.querySelector('#projectSettingsNetzbetreiber'); 2245 bannerTarget = bannerTarget ? bannerTarget.closest('.ee-form-section, .ee-form-row, .ee-form-group') : null; 2246 } 2247 if (bannerTarget && !bannerTarget.querySelector('.ee-netzanschluss-nonadmin-notice')) { 2248 var banner = document.createElement('div'); 2249 banner.className = 'ee-form-hint ee-form-sync-hint ee-netzanschluss-nonadmin-notice'; 2250 banner.style.cssText = 'background:rgba(33,150,243,0.08);border-left:3px solid #2196f3;padding:8px 12px;margin:8px 0 12px;font-size:12px;display:flex;align-items:center;gap:8px;'; 2251 banner.innerHTML = '<svg width="16" height="16" viewB
2251ox="0 0 16 16" fill="none" style="flex-shrink:0;"><path d="M8 1a3 3 0 0 0-3 3v2H4a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1V4a3 3 0 0 0-3-3zm1.5 5h-3V4a1.5 1.5 0 1 1 3 0v2z" fill="#2196f3"/></svg>' 2252 + '<span>Diese Felder wurden von Energy Experts ausgefuellt und koennen nur von EE-Mitarbeitern bearbeitet werden.</span>'; 2253 bannerTarget.parentNode.insertBefore(banner, bannerTarget); 2254 } 2255 dbgLog('[Netzanschluss-SSOT] Non-EE user: locked', protectedFieldIds.length, 'protected fields'); 2256 } 2257 } 2258 2259 dbgLog('[Netzanschluss-SSOT] Loaded from e1e8 module_completion:', Object.keys(fd).length, 'fields', '| status:', e1e8Status || 'n/a'); 2260 }) 2261 .catch(function (err) { 2262 if (window.console && console.warn) { 2263 console.warn('[Netzanschluss-SSOT] Failed to load e1e8 data:', err); 2264 } 2265 }); 2266 } 2267 2268 /* ----- Netzanschluss: Save to module-completion e1e8 (debounced) ----- */ 2269 var _netzanschlussSaveTimer = null; 2270 // Collect current Netzanschluss form values from the DOM into an object. 2271 // Used by both the immediate cache-update and the debounced server-save. 2272 function _collectNetzanschlussFormData(popup) { 2273 var formData = Object.assign({}, window.projectData && window.projectData.e1e8Completion || {}); 2274 // Collect text/number/date inputs 2275 Object.keys(NETZANSCHLUSS_E1E8_MAP).forEach(function (elemId) { 2276 var key = NETZANSCHLUSS_E1E8_MAP[elemId]; 2277 var el = popup.querySelector('#' + elemId); 2278 if (!el) return; 2279 var raw = el.value == null ? '' : String(el.value).trim(); 2280 if (raw === '') { 2281 formData[key] = null; 2282 } else if (el.type === 'number') { 2283 var n = parseFloat(raw); 2284 formData[key] = isNaN(n) ? null : n; 2285 } else { 2286 formData[key] = raw; 2287 } 2288 }); 2289 // Collect radio groups 2290 Object.keys(NETZANSCHLUSS_E1E8_RADIO_MAP).forEach(function (radioName) { 2291 var key = NETZANSCHLUSS_E1E8_RADIO_MAP[radioName]; 2292 var checked = popup.querySelector('input[type="radio"][name="' + radioName + '"]:checked'); 2293 if (checked) { 2294 formData[key] = (checked.value === 'ja'); 2295 } 2296 }); 2297 // 2026-07-01: Backward compat â nvp_available mirrors nvp_reserved 2298 if (formData.nvp_reserved != null) { 2299 formData.nvp_available = formData.nvp_reserved; 2300 } 2301 return formData; 2302 } 2303 2304 function saveNetzanschlussToE1E8(popup) { 2305 // Bug-Fix 2026-07-01: Don't save while loading â change events during 2306 // loadNetzanschlussFromE1E8 are programmatic, not user-initiated. 2307 if (_netzanschlussLoading) return; 2308 // Bug-Fix 2026-07-01 (NVP-Toggle-Snap-Back): Update the in-memory cache 2309 // IMMEDIATELY so that any concurrent reload (triggered by MutationObserver 2310 // or UI events) sees the user's latest values instead of stale server data. 2311 var formData = _collectNetzanschlussFormData(popup); 2312 if (window.projectData) { 2313 window.projectData.e1e8Completion = Object.assign({}, window.projectData.e1e8Completion || {}, formData); 2314 } 2315 if (_netzanschlussSaveTimer) clearTimeout(_netzanschlussSaveTimer); 2316 _netzanschlussSaveTimer = setTimeout(function () { 2317 _doSaveNetzanschlussToE1E8(popup); 2318 }, 1500); 2319 } 2320 function _doSaveNetzanschlussToE1E8(popup) { 2321 _netzanschlussSaveTimer = null; // Timer has fired â no longer pending 2322 var pid = getCurrentProjectId(); 2323 if (!pid) return; 2324 var formData = _collectNetzanschlussFormData(popup); 2325 var url = getRestRoot() + '/module-completion/e1e8/' + encodeURIComponent(pid); 2326 var headers = { 'Content-Type': 'application/json' }; 2327 var nonce = getRestNonce(); 2328 if (nonce) headers['X-WP-Nonce'] = nonce; 2329 2330 // Bug-Fix 2026-07-02 v3 (SSOT beim Speichern abschliessen): 2331 // Frueher wurde IMMER action:'save' gesendet -> module_completion.status 2332 // blieb 'draft', auch wenn der User alle Pflichtfelder ausgefuellt hatte. 2333 // Beim naechsten Laden interpretierte das Frontend 'draft' als "nicht fertig" 2334 // und stufte module_versions.status zurueck auf 'in_progress' (der berichtete 2335 // Downgrade-Bug). Fix: Wenn NVP reserviert ist UND alle Abschluss-Pflichtfelder 2336 // befuellt sind, senden wir action:'submit', sodass module_completion + 2337 // module_versions konsistent auf 'completed' stehen und der Status nach Reload 2338 // STABIL bleibt. Sonst weiterhin 'save' (Entwurf). Das Backend validiert die 2339 // Pflichtfelder erneut (422 bei unvollstaendig) â der Guard muss also mit dem 2340 // Backend-Pflichtfeld-Set uebereinstimmen. 2341 var resJaEl = popup.querySelector('#projectSettingsNVPReserviertJa'); 2342 var nvpReserved = !!(resJaEl && resJaEl.checked) || formData.nvp_reserved === true 2343 || formData.nvp_reserved === 1 || formData.nvp_reserved === '1'; 2344 var requiredKeys = ['feed_in_approval_nr', 'feed_in_approval_date', 2345 'connection_capacity_kva', 'reserved_until']; 2346 var allFilled = requiredKeys.every(function (k) { 2347 return formData[k] != null && String(formData[k]).trim() !== ''; 2348 }); 2349 var saveAction = (nvpReserved && allFilled) ? 'submit' : 'save'; 2350 2351 fetch(url, { 2352 method: 'POST', 2353 credentials: 'same-origin', 2354 headers: headers, 2355 body: JSON.stringify({ form_data: formData, action: saveAction }) 2356 }).then(function (r) { 2357 if (!r.ok) return r.text().then(function (t) { throw new Error('HTTP ' + r.status + ': ' + t); }); 2358 return r.json(); 2359 }).then(function (resp) {
2360 dbgLog('[Netzanschluss-SSOT] Saved to e1e8 module_completion (action=' + saveAction + ')'); 2361 // Wenn abgeschlossen wurde: lokalen Status-Cache aktualisieren + Widgets 2362 // informieren, damit die Anzeige sofort auf 'completed' geht (ohne Reload). 2363 if (saveAction === 'submit') { 2364 var newStatus = (resp && resp.status) || 'completed'; 2365 _lastE1E8AutoStatus = newStatus; 2366 if (window.projectData) window.projectData.e1e8CompletionStatus = newStatus; 2367 if (window.EEEvents && typeof window.EEEvents.statusChanged === 'function') { 2368 window.EEEvents.statusChanged('e1e8', pid, newStatus); 2369 } 2370 } 2371 // Dispatch event so other widgets know about the update 2372 try { 2373 window.dispatchEvent(new CustomEvent('ee:e1e8-form-saved', { 2374 detail: { project_id: pid, form_data: formData } 2375 })); 2376 } catch (e) { /* optional */ } 2377 }).catch(function (err) { 2378 if (window.console && console.warn) { 2379 console.warn('[Netzanschluss-SSOT] Save failed:', err); 2380 } 2381 }); 2382 } 2383 2384 /* ----- Netzanschluss: Attach auto-save listeners ----- */ 2385 function initNetzanschlussAutoSave(popup) { 2386 // Text/Number/Date fields: focusout 2387 Object.keys(NETZANSCHLUSS_E1E8_MAP).forEach(function (elemId) { 2388 var el = popup.querySelector('#' + elemId); 2389 if (!el) return; 2390 el.addEventListener('focusout', function () { 2391 saveNetzanschlussToE1E8(popup); 2392 }); 2393 // Date inputs: also on change (picker close) 2394 if (el.type === 'date') { 2395 el.addEventListener('change', function () { 2396 saveNetzanschlussToE1E8(popup); 2397 }); 2398 } 2399 }); 2400 // Radio groups: change 2401 Object.keys(NETZANSCHLUSS_E1E8_RADIO_MAP).forEach(function (radioName) { 2402 var radios = popup.querySelectorAll('input[type="radio"][name="' + radioName + '"]'); 2403 radios.forEach(function (r) { 2404 r.addEventListener('change', function () { 2405 if (!r.checked) return; 2406 saveNetzanschlussToE1E8(popup); 2407 }); 2408 }); 2409 }); 2410 } 2411 2412 function normalizeForSave(col, raw) { 2413 // v2.26.0: netzvoranfrageGestellt + nvpReserviert entfernt â jetzt in 2414 // module_completion e1e8 (SSOT via saveNetzanschlussToE1E8). 2415 if (col === 'flurkartenMassstab') { 2416 if (raw === 'custom' || raw === '' || raw == null) return null; 2417 var n = parseInt(raw, 10); 2418 return isNaN(n) ? null : n; 2419 } 2420 // v2.22.20: Hof-Gesamtflaeche â float, leer -> null 2421 if (col === 'gesamtflaeche') { 2422 if (raw === '' || raw == null) return null; 2423 // Komma als Dezimaltrenner tolerieren (DE-Locale) 2424 var f = parseFloat(String(raw).replace(',', '.')); 2425 return isNaN(f) ? null : f; 2426 } 2427 return raw; 2428 } 2429 2430 function postFragebogenSave(fields, opts) { 2431 var pid = getCurrentProjectId(); 2432 if (!pid || !fields || Object.keys(fields).length === 0) return Promise.resolve(); 2433 var url = getRestRoot() + '/fragebogen/save'; 2434 var headers = { 'Content-Type': 'application/json' }; 2435 var nonce = getRestNonce(); 2436 if (nonce) headers['X-WP-Nonce'] = nonce; 2437 return fetch(url, { 2438 method: 'POST', 2439 credentials: 'same-origin', 2440 headers: headers, 2441 body: JSON.stringify({ project_id: pid, fields: fields }) 2442 }).then(function (r) { 2443 if (!r.ok) return r.text().then(function (t) { throw new Error('HTTP ' + r.status + ': ' + t); }); 2444 return r.json(); 2445 }).then(function (j) { 2446 // 2026-05-28: CustomEvent dispatchen damit andere Widgets (DXF, PDF-Gen, etc.) 2447 // den neuen Wert sofort uebernehmen koennen â Single-Source-of-Truth Sync. 2448 try { 2449 window.dispatchEvent(new CustomEvent('ee:fragebogen-saved', { 2450 detail: { project_id: pid, fields: fields } 2451 })); 2452 // v2.22.36 (Bug-C Fix): Zweiter Trigger-Pfad fuer Baurecht-Sync. 2453 // initBaurechtStatusSync haengt am 'change' Event des Dropdowns â das 2454 // greift nicht wenn der Wert programmatisch gesetzt wird (z.B. KI-Chip, 2455 // API-Call, externe Quelle). Hier triggern wir den Module-Completion-Sync 2456 // direkt nach dem erfolgreichen DB-Save. Doppel-Trigger ist OK: Backend 2457 // ist idempotent (completed -> completed = no-op). lastFired-Guard im 2458 // change-Listener verhindert UI-Doppel-Toast. 2459 if (fields.standBaurecht === 'genehmigt' 2460 && typeof updateBuildingFormsStatusOnBaurecht === 'function') { 2461 updateBuildingFormsStatusOnBaurecht('genehmigt'); 2462 } 2463 } catch (e) { /* IE-Fallback nicht noetig */ } 2464 if (opts && opts.silent) return j; 2465 toast('Gespeichert', 'success'); 2466 return j; 2467 }).catch(function (err) { 2468 console.warn('[fragebogen] save failed:', err); 2469 toast('Speichern fehlgeschlagen', 'error'); 2470 throw err; 2471 }); 2472 } 2473 2474 // 2026-05-28: Async GET fuer project_settings â Single-Source-of-Truth-Reader. 2475 // Genutzt von loadProjectSettingsIntoForm (Reload-Bug-Fix) und vom DXF-Widget. 2476 // Promise resolves auf { settings: {...}, project_settings_id: int } oder null. 2477 function fetchFragebogenSettings(pid) { 2478 if (!pid) return Promise.resolve(null); 2479 var url = getRestRoot() + '/fragebogen/get?project_id=' + encodeURIComponent(pid); 2480 var headers = {}; 2481 var nonce = getRestNonce(); 2482 if (nonce) headers['X-WP-Nonce'] = nonce; 2483 return fetch(url, { method: 'GET', credentials: 'same-origin', headers: headers }) 2484 .then(function (r) { return r.ok ? r.json() : null; }) 2485 .catch(function () { return null; }); 2486 } 2487 // Global verfuegbar machen fuer andere Module (DXF-Widget etc.) 2488 window.eeFetchFragebogenSettings = fetchFragebogenSettings; 2489 2490 function initAutoSave(popup) {
2491 // Text/Number/Select-Felder: focusout 2492 Object.keys(AUTOSAVE_MAP).forEach(function (elemId) { 2493 var el = popup.querySelector('#' + elemId); 2494 if (!el) return; 2495 // Bug #269 Fix 2: Dirty-Tracking gegen Daten-Nulling. 2496 // Beim Fokussieren merken wir den Ausgangswert. Beim focusout speichern wir 2497 // NUR, wenn der User den Wert tatsaechlich geaendert hat. Verhindert, dass ein 2498 // (z.B. wegen Ladetiming) leer angezeigtes Feld beim blossen Durchtabben mit 2499 // NULL ueberschrieben wird und den bereits gespeicherten DB-Wert loescht. 2500 el.addEventListener('focusin', function () { 2501 el._eeDirtyBase = el.value == null ? '' : String(el.value).trim(); 2502 }); 2503 el.addEventListener('focusout', function () { 2504 var col = AUTOSAVE_MAP[elemId]; 2505 var raw = el.value == null ? '' : String(el.value).trim(); 2506 var base = (typeof el._eeDirtyBase === 'string') ? el._eeDirtyBase : ''; 2507 if (raw === base) return; // unveraendert -> nicht speichern (kein Null-Overwrite) 2508 var fields = {}; 2509 fields[col] = normalizeForSave(col, raw); 2510 postFragebogenSave(fields, { silent: false }); 2511 }); 2512 }); 2513 // Radio-Gruppen: change 2514 Object.keys(AUTOSAVE_RADIO_MAP).forEach(function (radioName) { 2515 var radios = popup.querySelectorAll('input[type="radio"][name="' + radioName + '"]'); 2516 radios.forEach(function (r) { 2517 r.addEventListener('change', function () { 2518 if (!r.checked) return; 2519 var col = AUTOSAVE_RADIO_MAP[radioName]; 2520 var fields = {}; 2521 fields[col] = normalizeForSave(col, r.value); 2522 postFragebogenSave(fields, { silent: false }); 2523 }); 2524 }); 2525 }); 2526 } 2527 2528 /* ----- Phase 5: Massstab-Pills (500/1000/.../Eigenes) ----- */ 2529 function initMassstabPills(popup) { 2530 var grp = popup.querySelector('[data-fbv2-massstab-pills]'); 2531 var customInput = popup.querySelector('#projectSettingsMassstabFlurkarten'); 2532 if (!grp || !customInput) return; 2533 var customRadio = popup.querySelector('#projectSettingsMassstabCustomToggle'); 2534 grp.addEventListener('change', function (e) { 2535 if (!e.target || e.target.name !== 'flurkarten_massstab') return; 2536 if (e.target.value === 'custom') { 2537 customInput.style.display = ''; 2538 customInput.focus(); 2539 } else { 2540 customInput.style.display = 'none'; 2541 customInput.value = ''; 2542 } 2543 }); 2544 // Custom-Input -> kein Pill-Reset, aber Auto-Save mit IntegerWert 2545 customInput.addEventListener('focusout', function () { 2546 if (!customRadio || !customRadio.checked) return; 2547 var v = parseInt(customInput.value, 10); 2548 if (!isNaN(v) && v > 0) { 2549 postFragebogenSave({ flurkartenMassstab: v }, { silent: false }); 2550 } 2551 }); 2552 } 2553 2554 /* ----- Phase 5: Koordinaten auf 6 Nachkommastellen runden bei Anzeige ----- */ 2555 function roundCoord(v) { 2556 var n = parseFloat(v); 2557 return isNaN(n) ? v : (Math.round(n * 1e6) / 1e6).toFixed(6); 2558 } 2559 function initCoordinateRounding(popup) { 2560 // KoordinatenNVP Feld: bei focusout auf 6 Nachkomma normalisieren 2561 var koordInput = popup.querySelector('#projectSettingsKoordinatenNVP'); 2562 if (koordInput) { 2563 koordInput.addEventListener('focusout', function () { 2564 var v = String(koordInput.value || '').trim(); 2565 if (!v) return; 2566 var parts = v.split(/[,;\s]+/).filter(Boolean); 2567 if (parts.length >= 2) { 2568 koordInput.value = roundCoord(parts[0]) + ', ' + roundCoord(parts[
25681]); 2569 } 2570 }); 2571 } 2572 } 2573 2574 /* ----- Phase 6: e1/e8 Single-Source â beim Oeffnen project_settings laden ----- 2575 2026-05-28 (Reload-Bug Fix v2): boot() laeuft am DOMContentLoaded, 2576 window.projectData.id wird aber asynchron NACH boot() gesetzt. 2577 Daher: warte (Retry) bis projectData.id verfuegbar ist (max 15s), 2578 dann lade Settings und befuelle das Formular. */ 2579 function loadProjectSettingsIntoForm(popup) { 2580 if (!popup) popup = $(SCOPE_SELECTOR); 2581 if (!popup) return Promise.resolve(); 2582 // Wenn projectData.id noch nicht ready: kurze Retry-Schleife (max 15s) 2583 if (!window.projectData || !window.projectData.id) { 2584 return new Promise(function (resolve) { 2585 var tries = 0; 2586 var maxTries = 30; // 30 * 500ms = 15s 2587 var iv = setInterval(function () { 2588 tries++; 2589 if (window.projectData && window.projectData.id) { 2590 clearInterval(iv); 2591 loadProjectSettingsIntoForm(popup).then(resolve); 2592 } else if (tries >= maxTries) { 2593 clearInterval(iv); 2594 if (window.console && console.warn) { 2595 console.warn('[Fragebogen-AI] loadProjectSettingsIntoForm: projectData.id not available after 15s â aborting'); 2596 } 2597 resolve(); 2598 } 2599 }, 500); 2600 }); 2601 } 2602 var pid = getCurrentProjectId(); 2603 if (!pid) return Promise.resolve(); 2604 // Falls projectData.project_settings (oder ähnliche Property) existiert, 2605 // direkt verwenden â sonst fetch. 2606 var ps = window.projectData.projectSettings || window.projectData.project_settings || null; 2607 function applyPS(settings) { 2608 if (!settings) return; 2609 // v2.26.0: Netzanschluss-Felder (Netzbetreiber, Ansprechpartner, Netzvoranfrage, 2610 // NVP-Reservierung etc.) werden jetzt aus module_completion e1e8 geladen â 2611 // siehe loadNetzanschlussFromE1E8(). Hier entfernt (SSOT). 2612 // v2.22.20: Bewirtschaftete Hof-Gesamtflaeche (ha) 2613 var gf = popup.querySelector('#projectFlaecheGesamtflaeche'); 2614 if (gf && !gf.value && settings.gesamtflaeche != null && settings.gesamtflaeche !== '') { 2615 gf.value = settings.gesamtflaeche; 2616 } 2617 // Massstab Pills 2618 if (settings.flurkartenMassstab) { 2619 var v = String(settings.flurkartenMassstab); 2620 var pillRadio = popup.querySelector('input[name="flurkarten_massstab"][value="' + v + '"]'); 2621 var customInput = popup.querySelector('#projectSettingsMassstabFlurkarten'); 2622 if (pillRadio) { 2623 pillRadio.checked = true; 2624 } else if (customInput) { 2625 var customRadio = popup.querySelector('#projectSettingsMassstabCustomToggle'); 2626 if (customRadio) customRadio.checked = true; 2627 customInput.value = v; 2628 customInput.style.display = ''; 2629 } 2630 } 2631 // landUseType 2632 var art = popup.querySelector('#projectFlaecheArtLandwirtschaft'); 2633 if (art && !art.value && settings.landUseType) art.value = settings.landUseType; 2634 // Behoerden 2635 var lrName = popup.querySelector('#projectSettingsNameLandratsamt'); 2636 if (lrName && !lrName.value && settings.landratsamtName) lrName.value = settings.landratsamtName; 2637 var lrAnspr = popup.querySelector('#projectSettingsAnsprechpartnerLandratsamt'); 2638 if (lrAnspr && !lrAnspr.value && settings.landratsamtAnsprechpartner) lrAnspr.value = settings.landratsamtAnsprechpartner; 2639 // v2.22.32: Downloadportal-Link aus DB nachladen 2640 var dlp = popup.querySelector('#projectSettingsDownloadportal'); 2641 if (dlp && !dlp.value && settings.downloadportal) dlp.value = settings.downloadportal; 2642 var stand = popup.querySelector('#projectSettingsStandBaurecht'); 2643 if (stand && (!stand.value || stand.value === 'offen') && settings.standBaurecht) stand.value = settings.standBaurecht; 2644 // Grundbuch 2645 var gbK = popup.querySelector('#projectSettingsGrundbuchKommune'); 2646 if (gbK && !gbK.value && settings.grundbuchKommune) gbK.value = settings.grundbuchKommune; 2647 var gbB = popup.querySelector('#projectSettingsGrundbuchBehoerde'); 2648 if (gbB && !gbB.value && settings.grundbuchBehoerde) gbB.value = settings.grundbuchBehoerde; 2649 // v2.22.20: Grundbuch-Blatt 2650 var gbBlatt = popup.querySelector('#projectSettingsGrundbuchBlatt'); 2651 if (gbBlatt && !gbBlatt.value && settings.grundbuchBlatt) gbBlatt.value = settings.grundbuchBlatt; 2652 // Eigentum-Pill 2653 if (settings.flaecheEigentum) { 2654 var er = popup.querySelector('input[name="flaeche_eigentum"][value="' + settings.flaecheEigentum + '"]'); 2655 if (er) er.checked = true; 2656 } 2657 // v2.26.0: Netzvoranfrage + NVP-Reservierung Pills entfernt â jetzt aus 2658 // module_completion e1e8 geladen (SSOT via loadNetzanschlussFromE1E8). 2659 } 2660 if (ps) { 2661 applyPS(ps); 2662 return Promise.resolve(); 2663 } 2664 // 2026-05-28 (Reload-Bug-Fix): Wenn projectData.projectSettings NICHT mitgeliefert 2665 // wurde (alter PHP-Bootstrap), explizit vom Backend nachladen. 2666 return fetchFragebogenSettings(window.projectData.id).then(function (resp) { 2667 if (resp && resp.settings) applyPS(resp.settings); 2668 }); 2669 } 2670 2671 /* ----- Phase 3 (Re-Design): Kontakt-Kacheln + Edit-Modal + Picker ----- 2672 Datenquelle: window.projectData.organization.connections[].contact 2673 Save: window.updateEntity({ entity:'contact', id, action:'update', ... }) 2674 "+" Hinzufuegen: 2675 - intern: bestehenden Org-Kontakt (nicht im Projekt) per neuer 2676 connection an die Project-Org haengen 2677 - extern: neuen Kontakt anlegen via updateEntity(contact, action:'create') 2678 */ 2679 2680 var TYP_LABELS = { 2681 bauherr: 'Bauherr', 2682 eigentuemer: 'Eigentuemer', 2683 bewirtschafter: 'Bewirtschafter' 2684 }; 2685 2686 function getProjectConnections() { 2687 try { 2688 var pd = window.projectData; 2689 if (pd && pd.organization && Array.isArray(pd.organization.connections)) { 2690 return pd.organization.connections; 2691 } 2692 } catch (e) {} 2693 return []; 2694 } 2695 2696 function escapeHtml(s) { 2697 if (s == null) return ''; 2698 return String(s) 2699 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 2700 .replace(/"/g, '"').replace(/'/g, '''); 2701 } 2702 2703 function getContactDisplayName(c) { 2704 if (!c) return 'Kontakt'; 2705 var v = (c.firstname || c.vorname || '').trim(); 2706 var n = (c.lastname || c.nachname || '').trim(); 2707 var name = (v + ' ' + n).trim(); 2708 if (name) return name; 2709 if (c.email) return c.email; 2710 return 'Kontakt #' + (c.id || '?'); 2711 } 2712 2713 function renderContactTile(connection) { 2714 var c = (connection && connection.contact) || {}; 2715 var typ = (c.typ || '').toLowerCase(); 2716 var label = getContactDisplayName(c); 2717 var meta = c.email ? escapeHtml(c.email) 2718 : (c.telefon ? escapeHtml(c.telefon) : ''); 2719 var badgeHtml = ''; 2720 if (typ && TYP_LABELS[typ]) { 2721 badgeHtml = '<span class="ee-kontakt-tile-badge --' + typ + '">' + TYP_LABELS[typ] + '</span>'; 2722 } 2723 var tile = document.createElement('div'); 2724 tile.className = 'ee-kontakt-tile'; 2725 tile.setAttribute('role', 'button'); 2726 tile.setAttribute('tabindex', '0'); 2727 tile.setAttribute('data-contact-id', String(c.id || '')); 2728 tile.setAttribute('data-connection-id', String(connection.id || '')); 2729 tile.innerHTML = 2730 badgeHtml + 2731 '<div class="ee-kontakt-tile-name">' + escapeHtml(label) + '</div>' + 2732 (meta ? '<div class="ee-kontakt-tile-meta">' + meta + '</div>' : ''); 2733 return tile; 2734 } 2735 2736 function renderAddTile() { 2737 var tile = document.createElement('div'); 2738 tile.className = 'ee-kontakt-tile --add'; 2739 tile.setAttribute('role', 'button'); 2740 tile.setAttribute('tabindex', '0'); 2741 tile.setAttribute('data-add-tile', '1'); 2742 tile.innerHTML = '<span class="ee-kontakt-tile-plus">+</span><span>Hinzufuegen</span>'; 2743 return tile; 2744 } 2745 2746 function renderContactTiles(popup) { 2747 var grid = popup.querySelector('#kontaktKacheln'); 2748 if (!grid) return; 2749 grid.innerHTML = ''; 2750 var connections = getProjectConnections(); 2751 connections.forEach(function (conn) { 2752 grid.appendChild(renderContactTile(conn)); 2753 }); 2754 grid.appendChild(renderAddTile()); 2755 } 2756 2757 /* ----- Edit-Modal ----- */ 2758 function openEditModal(popup, connectionId, contactId) { 2759 var modal = popup.querySelector('#kontaktEditModal'); 2760 if (!modal) return; 2761 var conn = null, contact = null; 2762 var conns = getProjectConnections(); 2763 for (var i = 0; i < conns.length; i++) { 2764 if (String(conns[i].id) === String(connectionId) ||
2765 (conns[i].contact && String(conns[i].contact.id) === String(contactId))) { 2766 conn = conns[i]; 2767 contact = conns[i].contact || {}; 2768 break; 2769 } 2770 } 2771 contact = contact || {}; 2772 var setVal = function (id, v) { 2773 var el = popup.querySelector('#' + id); 2774 if (el) el.value = (v == null ? '' : v); 2775 }; 2776 setVal('kemContactId', contact.id || ''); 2777 setVal('kemConnectionId', (conn && conn.id) || ''); 2778 setVal('kemSalutation', contact.salutation || 'Mr.'); 2779 setVal('kemTyp', (contact.typ || '').toLowerCase()); 2780 setVal('kemFirstname', contact.firstname || contact.vorname || ''); 2781 setVal('kemLastname', contact.lastname || contact.nachname || ''); 2782 setVal('kemEmail', contact.email || ''); 2783 setVal('kemPhone', contact.telefon || ''); 2784 modal.hidden = false; 2785 // Delete-Button nur sichtbar wenn nicht der einzige Kontakt 2786 var delBtn = popup.querySelector('#kemDeleteBtn'); 2787 if (delBtn) delBtn.style.display = (conns.length > 1) ? '' : 'none'; 2788 var first = popup.querySelector('#kemFirstname'); 2789 if (first) setTimeout(function(){ first.focus(); }, 50); 2790 } 2791 2792 function closeModal(modal) { 2793 if (modal) modal.hidden = true; 2794 } 2795 2796 function saveContactFromEditModal(popup) { 2797 var get = function (id) { 2798 var el = popup.querySelector('#' + id); 2799 return el ? el.value : ''; 2800 }; 2801 var contactId = parseInt(get('kemContactId'), 10); 2802 if (!contactId) { 2803 toast('Kein Kontakt-ID', 'error'); 2804 return Promise.resolve(); 2805 } 2806 var payload = { 2807 entity: 'contact', 2808 id: contactId, 2809 action: 'update', 2810 salutation: get('kemSalutation'), 2811 typ: get('kemTyp') || null, 2812 firstname: get('kemFirstname'), 2813 lastname: get('kemLastname'), 2814 email: get('kemEmail'), 2815 telefon: get('kemPhone') 2816 }; 2817 if (typeof window.updateEntity !== 'function') { 2818 toast('updateEntity nicht verfuegbar', 'error'); 2819 return Promise.resolve(); 2820 } 2821 return window.updateEntity(payload).then(function () { 2822 // projectData lokal aktualisieren 2823 var conns = getProjectConnections(); 2824 for (var i = 0; i < conns.length; i++) { 2825 if (conns[i].contact && String(conns[i].contact.id) === String(contactId)) { 2826 conns[i].contact.salutation = payload.salutation; 2827 conns[i].contact.typ = payload.typ; 2828 conns[i].contact.firstname = payload.firstname; 2829 conns[i].contact.lastname = payload.lastname; 2830 conns[i].contact.email = payload.email; 2831 conns[i].contact.telefon = payload.telefon; 2832 break; 2833 } 2834 } 2835 renderContactTiles(popup); 2836 toast('Kontakt gespeichert'); 2837 }).catch(function (err) { 2838 console.error('[ee-fragebogen-popup] saveContact error', err); 2839 toast('Speichern fehlgeschlagen', 'error'); 2840 }); 2841 } 2842 2843 function deleteContactFromEditModal(popup) { 2844 var contactId = parseInt((popup.querySelector('#kemContactId') || {}).value, 10); 2845 var connectionId = parseInt((popup.querySelector('#kemConnectionId') || {}).value, 10); 2846 if (!contactId) return Promise.resolve(); 2847 if (typeof window.updateEntity !== 'function') return Promise.resolve(); 2848 var p = Promise.resolve(); 2849 if (connectionId) { 2850 p = p.then(function(){ 2851 return window.updateEntity({ entity:'connections', id: connectionId, action:'delete' }); 2852 }); 2853 } 2854 p = p.then(function(){ 2855 return window.updateEntity({ entity:'contact', id: contactId, action:'delete' }); 2856 }); 2857 return p.then(function(){ 2858 // projectData lokal aktualisieren 2859 var conns = getProjectConnections(); 2860 for (var i = conns.length - 1; i >= 0; i--) { 2861 if (conns[i].contact && String(conns[i].contact.id) === String(contactId)) { 2862 conns.splice(i, 1); 2863 } 2864 } 2865 renderContactTiles(popup); 2866 toast('Kontakt geloescht'); 2867 }).catch(function(err){ 2868 console.error('[ee-fragebogen-popup] deleteContact error', err); 2869 toast('Loeschen fehlgeschlagen', 'error'); 2870 }); 2871 } 2872 2873 /* ----- Picker-Modal ("+") ----- */ 2874 function openPickerModal(popup) { 2875 var modal = popup.querySelector('#kontaktPickerModal'); 2876 if (!modal) return; 2877 // Intern-Select: aktuell nur Project-Kontakte verfuegbar in projectData. 2878 // Da wir keinen Endpoint fuer "alle Org-Kontakte" haben, zeigen wir 2879 // den intern-Slot nur wenn die Org weitere bekannte Kontakte enthaelt. 2880 var internSel = popup.querySelector('#kpInternSelect'); 2881 if (internSel) { 2882 internSel.innerHTML = '<option value="">â bitte waehlen â</option>'; 2883 // Fallback: leere Liste -> Hinweis im Select 2884 var empty = document.createElement('option'); 2885 empty.value = ''; 2886 empty.textContent = '(keine weiteren Kontakte gefunden)'; 2887 empty.disabled = true; 2888 internSel.appendChild(empty); 2889 } 2890 modal.hidden = false;
2891 } 2892 2893 function createNewContactFromPicker(popup) { 2894 if (typeof window.updateEntity !== 'function') { 2895 toast('updateEntity nicht verfuegbar', 'error'); 2896 return; 2897 } 2898 var orgId = getCurrentOrgId(); 2899 if (!orgId) { 2900 toast('Keine Organisation gefunden', 'error'); 2901 return; 2902 } 2903 // Minimaler Stub-Kontakt, sofort danach Edit-Modal oeffnen. 2904 // Email muss eindeutig sein -> Platzhalter mit Timestamp. 2905 var placeholderEmail = 'kontakt-' + Date.now() + '@platzhalter.local'; 2906 var payload = { 2907 entity: 'contact', 2908 action: 'create', 2909 salutation: 'Mr.', 2910 firstname: '', 2911 lastname: '', 2912 email: placeholderEmail, 2913 organizationId: orgId 2914 }; 2915 window.updateEntity(payload).then(function (resp) { 2916 // Erwartet: resp.id oder resp.contact.id 2917 var newId = (resp && (resp.id || (resp.contact && resp.contact.id))) || null; 2918 if (!newId) { 2919 toast('Neuer Kontakt angelegt â bitte Liste neu laden', 'info'); 2920 return; 2921 } 2922 // Die Connection zur Project-Org legt der Server beim Anlegen an 2923 // (organizationId im Payload) und meldet ihre Id als connectionId. 2924 // Kein zweiter POST /connections mehr: der legte bisher eine doppelte 2925 // Connection an und wird fuer fremde Kontakte abgelehnt (#486 F-003). 2926 var newConnId = (resp && resp.connectionId) || null; 2927 var newContact = { id: newId, salutation: 'Mr.', firstname: '', lastname: '', email: placeholderEmail, telefon: '', typ: null }; 2928 var conns = getProjectConnections(); 2929 conns.push({ id: newConnId, contact: newContact }); 2930 closeModal(popup.querySelector('#kontaktPickerModal')); 2931 renderContactTiles(popup); 2932 // Edit-Modal direkt oeffnen 2933 openEditModal(popup, newConnId, newId); 2934 }).catch(function (err) { 2935 console.error('[ee-fragebogen-popup] createContact error', err); 2936 toast('Anlegen fehlgeschlagen', 'error'); 2937 }); 2938 } 2939 2940 /* ----- Wire-Up ----- */ 2941 function initContactSlots(popup) { 2942 // Name "initContactSlots" beibehalten (boot ruft das auf). 2943 // Macht jetzt das Kachel-Setup. 2944 var grid = popup.querySelector('#kontaktKacheln'); 2945 if (!grid) return; 2946 renderContactTiles(popup); 2947 2948 // Falls projectData asynchron eintrifft, neu rendern wenn #kontaktListe sich aendert 2949 var legacyList = popup.querySelector('#kontaktListe'); 2950 if (legacyList) { 2951 var moDeb = null; 2952 var mo = new MutationObserver(function () { 2953 if (moDeb) clearTimeout(moDeb); 2954 moDeb = setTimeout(function () { renderContactTiles(popup); }, 150); 2955 }); 2956 mo.observe(legacyList, { childList: true, subtree: true }); 2957 } 2958 2959 // Tile-Click 2960 grid.addEventListener('click', function (e) { 2961 var addTile = e.target.closest('[data-add-tile]'); 2962 if (addTile) { 2963 openPickerModal(popup); 2964 return; 2965 } 2966 var tile = e.target.closest('.ee-kontakt-tile'); 2967 if (tile) { 2968 var cid = tile.getAttribute('data-contact-id'); 2969 var conn = tile.getAttribute('data-connection-id'); 2970 openEditModal(popup, conn, cid); 2971 } 2972 }); 2973 grid.addEventListener('keydown', function (e) { 2974 if (e.key !== 'Enter' && e.key !== ' ') return; 2975 var tile = e.target.closest('.ee-kontakt-tile'); 2976 if (!tile) return; 2977 e.preventDefault(); 2978 tile.click(); 2979 }); 2980 2981 // Edit-Modal Buttons 2982 var editModal = popup.querySelector('#kontaktEditModal'); 2983 if (editModal) { 2984 editModal.addEventListener('click', function (e) { 2985 if (e.target.closest('[data-close]')) { 2986 closeModal(editModal); 2987 } 2988 }); 2989 var saveBtn = popup.querySelector('#kemSaveBtn'); 2990 if (saveBtn) saveBtn.addEventListener('click', function () { 2991 saveContactFromEditModal(popup).then(function(){ closeModal(editModal); }); 2992 }); 2993 var delBtn = popup.querySelector('#kemDeleteBtn'); 2994 if (delBtn) delBtn.addEventListener('click', function () { 2995 if (!window.confirm('Diesen Kontakt wirklich loeschen?')) return; 2996 deleteContactFromEditModal(popup).then(function(){ closeModal(editModal); }); 2997 }); 2998 } 2999 3000 // Picker-Modal Buttons 3001 var pickerModal = popup.querySelector('#kontaktPickerModal'); 3002 if (pickerModal) { 3003 pickerModal.addEventListener('click', function (e) { 3004 if (e.target.closest('[data-close]')) closeModal(pickerModal); 3005 }); 3006 var externBtn = popup.querySelector('#kpExternBtn'); 3007 if (externBtn) externBtn.addEventListener('click', function () { 3008 createNewContactFromPicker(popup); 3009 }); 3010 var internBtn = popup.querySelector('#kpInternAddBtn'); 3011 if (internBtn) internBtn.addEventListener('click', function () { 3012 // Vorerst Hinweis: Endpoint fuer "alle Org-Kontakte" fehlt. 3013 toast('Funktion "bestehenden Kontakt waehlen" folgt â aktuell bitte "Neuen Kontakt anlegen".', 'info'); 3014 }); 3015 } 3016 3017 // ESC schliesst beide Modals 3018 document.addEventListener('keydown', function (e) { 3019 if (e.key !== 'Escape') return; 3020 [editModal, pickerModal].forEach(function (m) { 3021 if (m && !m.hidden) closeModal(m); 3022 }); 3023 }); 3024 } 3025 3026 /* ------------------------------------------------------------------ 3027 Boot 3028 ------------------------------------------------------------------ */ 3029 function boot() { 3030 var popup = $(SCOPE_SELECTOR); 3031 dbgInfo('%c[Fragebogen-AI]', 'color:#3fa14d;font-weight:bold;', 'boot() â popup =', popup); 3032 if (!popup) return; 3033
3034 mergeWebseiteIntoIntern(popup); 3035 initSidebarProxy(popup); 3036 syncActiveFromOldNav(popup); 3037 initLogoClick(popup); 3038 initAddressSync(); 3039 fillFlaecheAdresseFromCoords(); 3040 initNetzanschlussConditionals(popup); 3041 initNetzbetreiberAuto(popup); 3042 initKontaktAutocompleteWatcher(popup); 3043 initFieldSuggestions(popup); 3044 // v2.22.7 (Bug #140): Alter Single-Role-Slot-Code (initContactSlots) 3045 // deaktiviert â durch Multi-Role-Kachel-Renderer in ee-project-view.js 3046 // ersetzt. Wir triggern den neuen Renderer hier explizit, damit er 3047 // beim Popup-Open garantiert laeuft, plus bei Sidebar-Klick auf "Allgemein". 3048 if (typeof window.eeFbv2RenderContactTiles === 'function') { 3049 window.eeFbv2RenderContactTiles(); 3050 } 3051 var allgemeinBtn = popup.querySelector('.ee-sidebar .ee-nav-btn[data-proxy-target="projectSettingsTabAllgemein"]'); 3052 if (allgemeinBtn) { 3053 allgemeinBtn.addEventListener('click', function () { 3054 // Kurze Verzoegerung damit der Tab-Switch durch ist 3055 setTimeout(function () { 3056 if (typeof window.eeFbv2RenderContactTiles === 'function') { 3057 window.eeFbv2RenderContactTiles(); 3058 } 3059 }, 50); 3060 }); 3061 } 3062 initMassstabPills(popup); 3063 initCoordinateRounding(popup); 3064 initBaurechtStatusSync(popup); 3065 initEigentumAutoLogic(popup); 3066 var psLoadPromise = loadProjectSettingsIntoForm(popup); 3067 // Global verfuegbar fuer projectSettingsOpenPopup() in ee-project-view.js, 3068 // damit beim Popup-Oeffnen die DB-Werte (Massstab-Pill etc.) garantiert 3069 // gesetzt werden â auch wenn boot() vor projectData.id-Verfuegbarkeit lief. 3070 window.eeReloadFragebogenSettings = function () { 3071 return loadProjectSettingsIntoForm(popup); 3072 }; 3073 // v2.26.0 Bug-Fix 2026-07-01: Netzanschluss e1e8 Daten beim Popup-Open 3074 // nachladen â analog zu eeReloadFragebogenSettings fuer project_settings. 3075 window.eeReloadNetzanschlussE1E8 = function () { 3076 // Bug-Fix 2026-07-01 (NVP-Toggle-Snap-Back): Skip reload if a save is 3077 // pending â the server still has stale data, and reloading would overwrite 3078 // the user's in-flight changes. 3079 if (_netzanschlussSaveTimer) { 3080 dbgLog('[Netzanschluss-SSOT] Reload skipped â save pending'); 3081 return Promise.resolve(); 3082 } 3083 return loadNetzanschlussFromE1E8(popup); 3084 }; 3085 initAutoSave(popup); 3086 // v2.26.0: Netzanschluss SSOT â load from module_completion e1e8 + attach auto-save 3087 initNetzanschlussAutoSave(popup); 3088 var e1e8LoadPromise = loadNetzanschlussFromE1E8(popup); 3089 3090 // v2.25.29: Listen for MODULE_STATUS_CHANGED from widget stepper. 3091 // When EE clicks "In Bearbeitung" in the E1/E8 widget while the Fragebogen popup 3092 // is open, we must immediately remove the green "E1/E8 ist abgeschlossen" banner 3093 // and unlock the 5 Kernfelder so they become editable again â without page reload. 3094 if (window.EEEvents && typeof window.EEEvents.on === 'function') { 3095 window.EEEvents.on(window.EEEvents.EVENTS.MODULE_STATUS_CHANGED, function (data) { 3096 // Only react to e1e8 module events (key can be 'e1e8', 'e1-e8', or 'e1_e8_generator') 3097 var mod = data && data.module; 3098 if (!mod || (mod !== 'e1e8' && mod !== 'e1-e8' && mod !== 'e1_e8_generator')) return; 3099 dbgLog('[Netzanschluss] MODULE_STATUS_CHANGED received: module=' + data.module + ', status=' + data.status); 3100 3101 if (data.status !== 'completed') { 3102 // Status went back from completed â unlock fields + remove banner 3103 _unlockCompletedFields(popup); 3104 _lastE1E8AutoStatus = data.status || 'in_progress'; 3105 if (window.projectData) { 3106 window.projectData.e1e8CompletionStatus = data.status || 'in_progress'; 3107 } 3108 dbgLog('[Netzanschluss] Unlocked Kernfelder after widget status change to: ' + data.status); 3109 } else { 3110 // Status set to completed from widget â reload to apply read-only + banner 3111 loadNetzanschlussFromE1E8(popup); 3112 dbgLog('[Netzanschluss] Reloading Netzanschluss data after widget completion'); 3113 } 3114 }); 3115 } 3116 // Erster Refresh (Tabs evtl. noch leer â DB-Daten kommen erst nachher). 3117 refreshProgress(popup); 3118 3119 // Bug-Fix 2026-05-29 (v22.20): Sidebar-Icons + Avatar-Ring blieben nach Reload 3120 // alle grau / der Ring auf 0% â Ursache: loadProjectSettingsIntoForm() ist async 3121 // (Retry-Schleife bis projectData.id verfuegbar, dann fetch). refreshProgress 3122 // lief vorher synchron und sah ein leeres DOM â alle TAB_RULES = 0. 3123 // Loesung: NACH dem Promise-Resolve nochmal sauber durchrechnen. 3124 if (psLoadPromise && typeof psLoadPromise.then === 'function') { 3125 psLoadPromise.then(function () { 3126 // Mikro-Delay damit DOM-Mutations aus applyPS (Pills, Conditionals, dispatchEvent) 3127 // bereits durch sind, bevor wir den Score berechnen. 3128 setTimeout(function () { 3129 refreshProgress(popup); 3130 try { 3131 if (typeof window.refreshProgressRing === 'function') { 3132 window.refreshProgressRing(); 3133 } 3134 } catch (e) { /* optional */ } 3135 }, 50); 3136 }).catch(function () { /* silent */ }); 3137 } 3138 // v2.26.0: Netzanschluss e1e8-Load ebenfalls refreshen nach Load 3139 if (e1e8LoadPromise && typeof e1e8LoadPromise.then === 'function') { 3140 e1e8LoadPromise.then(function () { 3141 setTimeout(function () { 3142 refreshProgress(popup); 3143 }, 80); 3144 }).catch(function () { /* silent */ }); 3145 } 3146 3147 // Bug-Fix 2026-05-29: Initial-Sync des aeusseren Avatar-Rings beim Page-Load. 3148 // refreshProgress(popup) ruft window.refreshProgressRing() NUR wenn das Popup 3149 // offen ist (.active-Klasse) â beim Page-Load ist das aber noch nicht der Fall. 3150 try { 3151 if (typeof window.refreshProgressRing === 'function') { 3152 window.refreshProgressRing(); 3153 } 3154 } catch (e) { /* optional */ } 3155 3156 // 2026-05-29 (v22.20): Zusaetzliche Re-Refresh-Welle fuer asynchrone DOM-Renderer: 3157 // - eeFbv2RenderContactTiles laedt Kontakte ueber REST â erst nach ~300-800ms im DOM 3158 // - Flaeche-Cards werden ueber renderFlaechen async aufgebaut 3159 // - Conditional-Sections (NVP, Netzvoranfrage-Details) reagieren auf dispatchEvent 3160 // Ohne Re-Refresh sieht der User initial graue Icons obwohl alle Daten da sind.
3161 [400, 1200, 2500].forEach(function (delay) { 3162 setTimeout(function () { 3163 refreshProgress(popup); 3164 try { 3165 if (typeof window.refreshProgressRing === 'function') { 3166 window.refreshProgressRing(); 3167 } 3168 } catch (e) { /* optional */ } 3169 }, delay); 3170 }); 3171 3172 var debounceTimer = null; 3173 popup.addEventListener('input', function () { 3174 if (debounceTimer) clearTimeout(debounceTimer); 3175 debounceTimer = setTimeout(function () { refreshProgress(popup); }, 400); 3176 }); 3177 popup.addEventListener('change', function () { 3178 if (debounceTimer) clearTimeout(debounceTimer); 3179 debounceTimer = setTimeout(function () { refreshProgress(popup); }, 200); 3180 }); 3181 } 3182 3183 if (document.readyState === 'loading') { 3184 document.addEventListener('DOMContentLoaded', boot); 3185 } else { 3186 boot(); 3187 } 3188})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.