PageSourceSearch

https://energyexperts.ai/wp-content/plugins/energyExperts/assets/js/ee-fragebogen-popup.js?ver=2.25.616

js energyexperts.ai collected 2026-10-05 12:12:21 UTC 145,432 bytes, 3,188 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
2700      .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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.