PageSourceSearch

https://tecdvr-v7.web.app/js/tec-tab3-ssot.js?v=20.374.20

js tecdvr-v7.web.app collected 2026-10-03 12:41:42 UTC 81,306 bytes, 1,727 lines download raw bytes

1/**
2 * tec-tab3-ssot.js — Classificazione P/S/C Tab 3 (Luoghi & Sopralluoghi) ↔ Tab 4 Principale
3 * P = mirror read-only da Luogo Principale (Tab 4); S = fonte solo Tab 3/sede; C = calcolato.
4 *
5 * CPI SSOT (P2): i campi antincendio/CPI del Luogo Principale vivono in
6 *   ambientiSede[principale=true].impianti.cpi  (legacy: luoghiLavoro[principale_home].impianti.cpi)
7 * Tab 2 (Sede Generale) = mirror read-only mono-Luogo; multi-sede = blocchi CPI editabili
8 * che scrivono sul bag cpi + sync bidirezionale verso d.* flat (legacy DVR/readiness).
9 */
10(function (root) {
11  'use strict';
12
13  var P = 'P';
14  var S = 'S';
15  var C = 'C';
16  var G = 'G';
17  var L = 'L';
18  var O = 'O';
19
20  function _t(tier, pin, nav, extra) {
21    var o = { tier: tier, pin: pin || '', nav: nav || (tier === P || tier === L ? 'luogo_principale' : tier === C ? 'computed' : 'tab3') };
22    if (extra) Object.keys(extra).forEach(function (k) { o[k] = extra[k]; });
23    return o;
24  }
25
26  /** Entity tier G/L/C/O (v3.2) — indipendente da mirror P/S legacy. */
27  root.TAB3_FIELD_ENTITY = {
28    areaEstPresente: L, areaEstDest: L, areaEstDelimitazione: L, nAccessiCarrabili: L, nAccessiPedonali: L,
29    areaEstSeparaPedCarr: L, problemaAccessoVeic: L, problemaAccessoVeicTipo: L,
30    problemaAccessoVeicMisure: L, descrizioneEdificio: G,
31    ingressoPrincVia: L, ingressoPrincTipo: L, ingressoPrincQuota: L, rampDisabili: L,
32    impLegionella_presente: G, impLegionella_pianoControllo: G, impLegionella_responsabile: G,
33    impLegionella_ultimaVerifica: G, impLegionella_esito: G, impLegionella_ditta: G,
34    sciaNumero: G, sciaData: G, sciaCategoria: G, sciaNotaInterna: G, sciaTestoDVR: G, sciaIncludiDVR: G,
35    osservAccessi: O, osservLocali: O, osservImpiantistica: O, osservRischi: O, osservNote: O,
36    nDipendenti: C, nBlocchiWCTotale: C, nServiziIgienici: C, nBlocchiWC: C, rischioIncendioVecchio: C,
37    tipoEdificio: L, tipologiaStrutturale: L, annoCostruzione: L, nPianiFT: L, nPianiInterrati: L,
38    superficie: L, superficieCoperta: L, superficieEsterna: L, maxPresenze: L, nPostazioniVdt: L,
39    destinazioneUso: L, descrizioneLuogo: L, piani: L, corpoScalaInterno: L, blocchiWC: L, conformitaDPR503: L,
40    soggettoVVF: L, statoCPI: L, cpiNumero: L, cpiScadenza: L, rischioIncendioNuovo: L, gruppoPS: L
41  };
42
43  root._tab3FieldEntityTier = function (key) {
44    return root.TAB3_FIELD_ENTITY[key] || null;
45  };
46
47  /** Marker esplicito P/S/C per campi Tab 3 (3A–3M) + nav SSOT Legal «Vai →». */
48  root.TAB3_FIELD_SSC = {
49    /* 3A — Struttura */
50    tipoEdificio: _t(P, '3A.1'),
51    tipologiaStrutturale: _t(P, '3A.2'),
52    annoCostruzione: _t(P, '3A.3'),
53    nPianiFT: _t(P, '3A.4'),
54    nPianiInterrati: _t(P, '3A.5'),
55    superficie: _t(P, '3A.6'),
56    superficieCoperta: _t(P, '3A.6'),
57    superficieEsterna: _t(P, '3A.7'),
58    maxPresenze: _t(P, '3A.8'),
59    nPostazioniVdt: _t(P, '3A.9'),
60    destinazioneUso: _t(P, '3A.10', 'luogo_principale', { openT2Sec: 'struct', luogoSubTab: 'struttura' }),
61
62    /* 3B — Area esterna (mirror Luogo Principale → Tab 4 Struttura) */
63    areaEstPresente: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
64    areaEstDest: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
65    areaEstDelimitazione: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
66    nAccessiCarrabili: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
67    nAccessiPedonali: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
68    areaEstSeparaPedCarr: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
69    accessiCarrabili: _t(S, '3B', 'tab3', { openT2Sec: 'area' }),
70    problemaAccessoVeic: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
71    problemaAccessoVeicTipo: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
72    problemaAccessoVeicMisure: _t(P, '3B', 'luogo_principale', { openT2Sec: 'area', luogoSubTab: 'struttura' }),
73    descrizioneEdificio: _t(S, '3B.2', 'tab3', { openT2Sec: 'note2i' }),
74    descrizioneAmbiente: _t(S, '3B.1', 'tab3'),
75    descrizioneLuogo: _t(S, '3B.1', 'tab3', { entity: L, aliasOf: 'descrizioneAmbiente' }),
76
77    /* 3C — Ingresso (mirror Luogo Principale → Tab 4 Struttura / Anagrafica) */
78    ingressoPrincVia: _t(P, '3C.1', 'luogo_principale', { openT2Sec: 'ingr', luogoSubTab: 'anagrafica', entity: L }),
79    ingressoPrincTipo: _t(P, '3C', 'luogo_principale', { openT2Sec: 'ingr_g', luogoSubTab: 'struttura', entity: L }),
80    ingressoPrincQuota: _t(P, '3C', 'luogo_principale', { openT2Sec: 'ingr_g', luogoSubTab: 'struttura', entity: L }),
81    rampDisabili: _t(P, '3C', 'luogo_principale', { openT2Sec: 'ingr_g', luogoSubTab: 'struttura', entity: L }),
82
83    /* 3D — Piani */
84    piani: _t(P, '3D'),
85
86    /* 3F — Verticali */
87    corpoScalaInterno: _t(P, '3F'),
88    nCorpiScala: _t(P, '3F'),
89    montascale: _t(P, '3F'),
90    montascaleTipo: _t(P, '3F'),
91    impAscensore_presente: _t(P, '3F'),
92    ascensore: _t(P, '3F'),
93    nAscensori: _t(P, '3F'),
94    nAscensors: _t(P, '3F'),
95    scalaEmergenzaEst: _t(P, '3F'),
96    nScaleEmergenza: _t(P, '3F'),
97    scalaEmergenzaUbicazione: _t(P, '3F.1'),
98
99    /* 3G — Servizi igienici */
100    blocchiWC: _t(P, '3G', 'luogo_principale', { openT2Sec: 'wc' }),
101    nBlocchiWCTotale: _t(C, '3G', 'tab3_wc', { compute: 'sum_wc_luoghi', openT2Sec: 'wc' }),
102    nServiziIgienici: _t(C, '3G', 'tab3_wc', { compute: 'sum_wc_luoghi', openT2Sec: 'wc' }),
103    nBlocchiWC: _t(C, '3G', 'tab3_wc', { compute: 'sum_wc_luoghi', openT2Sec: 'wc' }),
104    wcPavPiastrellati: _t(P, '3G'),
105    wcAltPiastrellatura: _t(P, '3G'),
106
107    /* 3H — Accessibilità */
108    conformitaDPR503: _t(P, '3H'),
109    barrierePresenti: _t(P, '3H'),
110    tipoBarriere: _t(P, '3H'),
111
112    /* 3I — Impiant; 3I.7 Legionella = Sede */
113    impLegionella_presente: _t(S, '3I.7', 'tab3', { openT2Sec: 'impianti' }),
114    impLegionella_pianoControllo: _t(S, '3I.7', 'tab3'),
115    impLegionella_responsabile: _t(S, '3I.7', 'tab3'),
116    impLegionella_ultimaVerifica: _t(S, '3I.7', 'tab3'),
117    impLegionella_esito: _t(S, '3I.7', 'tab3'),
118    impLegionella_ditta: _t(S, '3I.7', 'tab3'),
119
120    /* 3J/3K/3L — Antincendio principale + SCIA sede */
121    attivitaDPR151: _t(P, '3J'),
122    numAttivitaPrinc: _t(P, '3J'),
123    categoriaAttivita: _t(P, '3J'),
124    criterioClassifica: _t(P, '3J'),
125    attivitaSecondarie: _t(P, '3J'),
126    centraleTerm: _t(P, '3J'),
127    centraleTermCombust: _t(P, '3J'),
128    centraleTermKW: _t(P, '3J'),
129    rischioIncendioVecchio: _t(C, '3K', 'tab3', { compute: 'incendio_vecchio' }),
130    rischioIncendioNuovo: _t(P, '3K'),
131    classeRischioIncendio: _t(P, '3K'),
132    gruppoPS: _t(P, '3K'),
133    soggettoVVF: _t(P, '3L'),
134    statoCPI: _t(P, '3L'),
135    cpiNumero: _t(P, '3L'),
136    cpiScadenza: _t(P, '3L'),
137    sciaNumero: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
138    sciaData: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
139    sciaCategoria: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
140    sciaNotaInterna: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
141    sciaTestoDVR: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
142    sciaIncludiDVR: _t(S, '3L', 'tab3', { openT2Sec: 'fire' }),
143    porteRei: _t(P, '3I'),
144
145    /* 3M — Osservazioni sopralluogo (solo Tab 3) */
146    osservAccessi: _t(S, '3M.1', 'tab3', { openT2Sec: 'osserv_sopralluogo' }),
147    osservLocali: _t(S, '3M.2', 'tab3', { openT2Sec: 'osserv_sopralluogo' }),
148    osservImpiantistica: _t(S, '3M.3', 'tab3', { openT2Sec: 'osserv_sopralluogo' }),
149    osservRischi: _t(S, '3M.4', 'tab3', { openT2Sec: 'osserv_sopralluogo' }),
150    osservNote: _t(S, '3M.5', 'tab3', { openT2Sec: 'osserv_sopralluogo' }),
151
152    /* Calcolati organico */
153    nDipendenti: _t(C, '5A.1', 'triade_mansioni', { compute: 'organico' })
154  };
155
156  root._tab3FieldTier = function (key) {
157    var m = root.TAB3_FIELD_SSC[key];
158    return m ? m.tier : null;
159  };
160
161  root._tab3IsPrincipaleMirror = function (key) {
162    return root._tab3FieldTier(key) === P;
163  };
164
165  root._tab3IsSedeSource = function (key) {
166    return root._tab3FieldTier(key) === S;
167  };
168
169  root._tab3IsComputed = function (key) {
170    return root._tab3FieldTier(key) === C;
171  };
172
173  /** SSOT CPI — chiavi nel bag impianti.cpi del Luogo Principale (tier L, mirror P in Tab 3). */
174  root.TAB3_CPI_SSOT_KEYS = [
175    'soggettoVVF', 'statoCPI', 'cpiNumero', 'cpiScadenza',
176    'rischioIncendioVecchio', 'rischioIncendioNuovo', 'gruppoPS',
177    'attivitaDPR151', 'numAttivitaPrinc', 'categoriaAttivita', 'criterioClassifica',
178    'centraleTerm', 'centraleTermKW', 'centraleTermCombust'
179  ];
180  root.TAB3_CPI_SSOT_PATH = 'ambientiSede[principale].impianti.cpi';
181
182  function _sedeIdFromProject() {
183    try {
184      var p = typeof CP === 'function' ? CP() : null;
185      if (typeof root._projectSedeId === 'function') return root._projectSedeId(p) || '';
186      return (p && (p.sedeId || (p.data && p.data.sedeId))) || '';
187    } catch (_) { return ''; }
188  }
189
190  function _cpiBag(luogo) {
191    return (luogo && luogo.impianti && luogo.impianti.cpi) || {};
192  }
193
194  function _cpiVal(luogo, d, key) {
195    if (typeof root._tab2LuogoFieldVal === 'function') {
196      return root._tab2LuogoFieldVal(luogo, key, d || {});
197    }
198    var cpi = _cpiBag(luogo);
199    if (cpi[key] != null && cpi[key] !== '') return cpi[key];
200    return d ? d[key] : undefined;
201  }
202
203  var TAB2_LUOGO_BAG_KEYS = {
204    areaEstPresente: ['areeEsterne', 'areaEstPresente'],
205    areaEstDest: ['areeEsterne', 'areaEstDest'],
206    areaEstDelimitazione: ['areeEsterne', 'areaEstDelimitazione'],
207    nAccessiCarrabili: ['areeEsterne', 'nAccessiCarrabili'],
208    nAccessiPedonali: ['areeEsterne', 'nAccessiPedonali'],
209    areaEstSeparaPedCarr: ['areeEsterne', 'areaEstSeparaPedCarr'],
210    problemaAccessoVeic: ['areeEsterne', 'problemaAccessoVeic'],
211    problemaAccessoVeicTipo: ['areeEsterne', 'problemaAccessoVeicTipo'],
212    problemaAccessoVeicMisure: ['areeEsterne', 'problemaAccessoVeicMisure'],
213    ingressoPrincTipo: ['ingresso', 'ingressoPrincTipo'],
214    ingressoPrincQuota: ['ingresso', 'ingressoPrincQuota'],
215    rampDisabili: ['ingresso', 'rampDisabili'],
216    barrierePresenti: ['accessibilita', 'barrierePresenti'],
217    tipoBarriere: ['accessibilita', 'tipoBarriere'],
218    conformitaDPR503: ['accessibilita', 'conformitaDPR503'],
219    nBlocchiWCTotale: ['serviziIgienici', 'nBlocchiWCTotale'],
220    wcPavPiastrellati: ['serviziIgienici', 'wcPavPiastrellati'],
221    wcAltPiastrellatura: ['serviziIgienici', 'wcAltPiastrellatura'],
222    blocchiWC: ['serviziIgienici', 'blocchiWC']
223  };
224
225  function _hasLuogoVal(v) {
226    return v != null && v !== '' && !(Array.isArray(v) && !v.length);
227  }
228
229  /** SSOT lettura campo per ambiente/luogo Tab 18 — alias superficie ↔ superficie_mq, maxPresenze ↔ capienza_max. */
230  root._tab2LuogoFieldVal = function (luogo, key, d) {
231    if (!luogo || !key) return null;
232    d = d || (typeof root.D === 'function' ? root.D() : null);
233    if (key === 'superficie') {
234      if (_hasLuogoVal(luogo.superficie_mq)) return luogo.superficie_mq;
235      if (_hasLuogoVal(luogo.superficie)) return luogo.superficie;
236      if (_hasLuogoVal(luogo.superficieCoperta)) return luogo.superficieCoperta;
237      return null;
238    }
239    if (key === 'superficieCoperta') {
240      if (_hasLuogoVal(luogo.superficieCoperta)) return luogo.superficieCoperta;
241      if (_hasLuogoVal(luogo.superficie_mq)) return luogo.superficie_mq;
242      return null;
243    }
244    if (key === 'maxPresenze') {
245      if (_hasLuogoVal(luogo.capienza_max)) return luogo.capienza_max;
246      if (_hasLuogoVal(luogo.presenzeMassime)) return luogo.presenzeMassime;
247      if (_hasLuogoVal(luogo.maxPresenze)) return luogo.maxPresenze;
248      return null;
249    }
250    if (key === 'nPostazioniVdt') {
251      if (_hasLuogoVal(luogo.nPostazioniVdt)) return luogo.nPostazioniVdt;
252      var comfort = luogo.comfort || {};
253      if (_hasLuogoVal(comfort.nPostazioniVdt)) return comfort.nPostazioniVdt;
254      return null;
255    }
256    if (key === 'destinazioneUso') {
257      if (_hasLuogoVal(luogo.destinazioneUso)) return luogo.destinazioneUso;
258      if (_hasLuogoVal(luogo.descrizioneLuogo)) return luogo.descrizioneLuogo;
259      return null;
260    }
261    if (root.TAB3_CPI_SSOT_KEYS && root.TAB3_CPI_SSOT_KEYS.indexOf(key) >= 0) {
262      if (root._tecCpiEdificioScope && typeof root._tecCpiEdificioScope.getCpiField === 'function') {
263        var dCtx = d || (typeof root.D === 'function' ? root.D() : null);
264        var listCtx = (dCtx && dCtx.ambientiSede) || [];
265        var resolved = root._tecCpiEdificioScope.getCpiField(luogo, key, listCtx, dCtx);
266        if (_hasLuogoVal(resolved)) return resolved;
267      }
268      var cpi = _cpiBag(luogo);
269      if (_hasLuogoVal(cpi[key])) return cpi[key];
270      if (d && _hasLuogoVal(d[key])) return d[key];
271      return null;
272    }
273    var bagPath = TAB2_LUOGO_BAG_KEYS[key];
274    if (bagPath) {
275      var bag = luogo[bagPath[0]];
276      if (bag && _hasLuogoVal(bag[bagPath[1]])) return bag[bagPath[1]];
277    }
278    if (key === 'ingressoPrincVia' && _hasLuogoVal(luogo.posizione)) return luogo.posizione;
279    if (key === 'piani') return luogo.piani || null;
280    if (key === 'porteRei') {
281      // v20.373.102 — SSOT reale: luogo.porteRei. `impianti.porteRei` non e' mai scritto da nessuno
282      // (era una copia di d.porteRei innestata al build) → il lettore concludeva sempre «nessuna».
283      if (_hasLuogoVal(luogo.porteRei)) return luogo.porteRei;
284      var pr = luogo.impianti && luogo.impianti.porteRei;
285      return _hasLuogoVal(pr) ? pr : null;
286    }
287    if (key === 'attivitaSecondarie') {
288      var cpiAs = _cpiBag(luogo);
289      return _hasLuogoVal(cpiAs.attivitaSecondarie) ? cpiAs.attivitaSecondarie : null;
290    }
291    // v20.371.13 — SoT UI = flat sul luogo; bag impianti.ascensore è mirror legacy (fallback se flat vuoto).
292    if (_hasLuogoVal(luogo[key])) return luogo[key];
293    var imp = luogo.impianti || {};
294    var asc = imp.ascensore || {};
295    var ascKeys = ['corpoScalaInterno', 'nCorpiScala', 'montascale', 'montascaleTipo', 'scalaEmergenzaEst', 'nScaleEmergenza', 'scalaEmergenzaUbicazione'];
296    if (ascKeys.indexOf(key) >= 0 && _hasLuogoVal(asc[key])) return asc[key];
297    if ((key === 'ascensore' || key === 'impAscensore_presente') && _hasLuogoVal(asc.ascensore)) return asc.ascensore;
298    if ((key === 'impAscensore_presente') && _hasLuogoVal(asc.impAscensore_presente)) return asc.impAscensore_presente;
299    if ((key === 'nAscensors' || key === 'nAscensori') && _hasLuogoVal(asc.nAscensori)) return asc.nAscensori;
300    return null;
301  };
302
303  function _isEmptyCpiVal(v) {
304    return v == null || v === '' || (Array.isArray(v) && !v.length);
305  }
306
307  /** Luogo Principale (SoT) → mirror flat d.* per legacy DVR/readiness/scadenziario. */
308  root._tab3MirrorCpiFlatFromLuogo = function (d, luogo, opts) {
309    opts = opts || {};
310    if (!d) return false;
311    var cpi = _cpiBag(luogo);
312    var keys = opts.keys || root.TAB3_CPI_SSOT_KEYS;
313    var changed = false;
314    keys.forEach(function (key) {
315      var lv = cpi[key];
316      if (opts.singleKey && key !== opts.singleKey) return;
317      if (opts.singleKey && opts.value !== undefined) lv = opts.value;
318      if (opts.rewireAll && _isEmptyCpiVal(lv)) lv = '';
319      else if (_isEmptyCpiVal(lv)) return;
320      if (d[key] !== lv) {
321        d[key] = lv;
322        changed = true;
323      }
324    });
325    return changed;
326  };
327
328  /** Backfill bag cpi vuoto da mirror flat (migrazione progetti pre-Luogo). */
329  root._tab3CpiBackfillPatch = function (luogo, d) {
330    if (!luogo || !d) return null;
331    var cpi = Object.assign({}, _cpiBag(luogo));
332    var patch = {};
333    var needs = false;
334    root.TAB3_CPI_SSOT_KEYS.forEach(function (key) {
335      if (!_isEmptyCpiVal(cpi[key])) return;
336      var fv = d[key];
337      if (_isEmptyCpiVal(fv)) return;
338      cpi[key] = fv;
339      patch[key] = fv;
340      needs = true;
341    });
342    if (!needs) return null;
343    return { cpi: cpi, patch: patch, needsSave: true };
344  };
345
346  /** Sync bidirezionale: SoT luogo→flat; flat→luogo solo se bag cpi vuoto (backfill). */
347  root._tab3SyncCpiBidirectional = function (d, luogo, opts) {
348    opts = opts || {};
349    var out = { changed: false, backfill: null, directions: [] };
350    if (!d) return out;
351    if (luogo && root._tab3MirrorCpiFlatFromLuogo(d, luogo, opts)) {
352      out.changed = true;
353      out.directions.push('luogo→flat');
354    }
355    if (opts.backfillLuogo !== false && luogo) {
356      var bf = root._tab3CpiBackfillPatch(luogo, d);
357      if (bf && bf.needsSave) {
358        out.backfill = bf;
359        out.directions.push('flat→luogo(backfill)');
360        if (opts.persistBackfill && root._tecGrafo && root._tecGrafo.saveAmbiente) {
361          var sedeId = _sedeIdFromProject();
362          if (sedeId && luogo.id) {
363            var imp = Object.assign({}, luogo.impianti || {}, { cpi: bf.cpi });
364            try {
365              root._tecGrafo.saveAmbiente(sedeId, { id: luogo.id, impianti: imp });
366              if (root._tab2LuogoHomeCache && root._tab2LuogoHomeCache.id === luogo.id) {
367                root._tab2LuogoHomeCache.impianti = imp;
368              }
369            } catch (_) {}
370          }
371        }
372      }
373    }
374    return out;
375  };
376
377  /** Luoghi operativi per guard P13 / KPI Tab 3 — allineato a sidebar Tab 4 (no leak, no tombstone, no sopralluogo). */
378  root._tab3OperationalAmbientiList = function (d) {
379    d = d || (typeof D === 'function' ? D() : null);
380    var list = (d && d.ambientiSede) ? d.ambientiSede.slice() : [];
381    if (!list.length && typeof root._tab2MergedAmbientiList === 'function') {
382      list = root._tab2MergedAmbientiList(d) || [];
383    }
384    if (!list.length && typeof root._getEntitaForAssessment === 'function') {
385      try {
386        return (root._getEntitaForAssessment(d, 'luogo') || []).filter(function (ent) {
387          return ent && ent._grafoAmbienteId;
388        }).map(function (ent) {
389          return { id: ent._grafoAmbienteId || ent.id, nome: ent.nome, principale: ent.principale, sopralluogo: ent.sopralluogo };
390        });
391      } catch (_) {}
392      return [];
393    }
394    var G = root._tecGrafo;
395    if (G && typeof G.filterAmbientiByTombstones === 'function') {
396      list = G.filterAmbientiByTombstones(list, d);
397    }
398    var E = root._tecGmrEpcLuoghi;
399    if (E && typeof E.filterConsultantLuogoLeak === 'function') {
400      list = E.filterConsultantLuogoLeak(list, d);
401    }
402    var R = root._tecSopralluogoRegistro;
403    return list.filter(function (amb) {
404      if (!amb) return false;
405      if (R && typeof R.isAmbienteSopralluogo === 'function') return !R.isAmbienteSopralluogo(amb, d);
406      return amb.sopralluogo !== true;
407    });
408  };
409
410  root._tab3CountLuoghi = function (d) {
411    var ops = root._tab3OperationalAmbientiList(d);
412    if (ops.length) return ops.length;
413    if (typeof root._getEntitaForAssessment === 'function') {
414      try { return (root._getEntitaForAssessment(d, 'luogo') || []).length; } catch (_) {}
415    }
416    return 0;
417  };
418
419  /** Mono + multi: CPI editabile in Tab 2 (scrittura SSOT impianti.cpi Principale). Tab 4 ha editor baseline 3K. */
420  root._tab3CpiTab2Editable = function (d) {
421    return true;
422  };
423
424  /** Campi 3A UI — mirror chirurgico da Luogo Principale (Tab 4 Struttura / Anagrafica / Comfort). */
425  root.TAB3_MIRROR_3A_KEYS = [
426    'tipoEdificio', 'tipologiaStrutturale', 'annoCostruzione', 'destinazioneUso',
427    'nPianiFT', 'nPianiInterrati', 'superficie', 'superficieCoperta', 'superficieEsterna',
428    'maxPresenze', 'nPostazioniVdt'
429  ];
430
431  /** Campi 3B — solo quelli editabili su Tab 4 → Struttura → areeEsterne. */
432  root.TAB3_MIRROR_3B_KEYS = [
433    'areaEstPresente', 'areaEstDest', 'areaEstDelimitazione', 'nAccessiCarrabili', 'nAccessiPedonali',
434    'areaEstSeparaPedCarr', 'problemaAccessoVeic', 'problemaAccessoVeicTipo', 'problemaAccessoVeicMisure'
435  ];
436
437  /** Campi 3C — ingresso + posizione (via). */
438  root.TAB3_MIRROR_3C_KEYS = [
439    'ingressoPrincVia', 'ingressoPrincTipo', 'ingressoPrincQuota', 'rampDisabili'
440  ];
441
442  root.TAB3_MIRROR_3ABC_KEYS = (root.TAB3_MIRROR_3A_KEYS || [])
443    .concat(root.TAB3_MIRROR_3B_KEYS || [], root.TAB3_MIRROR_3C_KEYS || []);
444
445  /** P13 — Campi 3A struttura edificio: SSOT Tier L su ogni Luogo (Tab 4). */
446  root.TAB3_EDIFICIO_SSOT_KEYS = [
447    'tipoEdificio', 'tipologiaStrutturale', 'annoCostruzione', 'destinazioneUso',
448    'nPianiFT', 'nPianiInterrati', 'pianiFT', 'pianiInterrati',
449    'superficie', 'superficieCoperta', 'superficieEsterna', 'maxPresenze', 'nPostazioniVdt',
450    'caricoMaxSolaiKgMq', 'postiElevatiPresenti', 'postiElevatiAccessoSicuro',
451    'corpoScalaInterno', 'nCorpiScala', 'montascale', 'montascaleTipo',
452    'scalaCorrimano', 'scalaParapetti', 'scalaPedataCm', 'scalaAlzataCm', 'scalaConformita',
453    'impAscensore_presente', 'ascensore', 'nAscensori', 'nAscensors',
454    'scalaEmergenzaEst', 'nScaleEmergenza', 'scalaEmergenzaUbicazione'
455  ];
456
457  /** Bag annidati sul Luogo Principale → flat d.* per render Tab 3 (solo 3B/3C mirror-safe). */
458  root.TAB3_BAG_MIRROR_KEYS = root.TAB3_MIRROR_3B_KEYS.concat(root.TAB3_MIRROR_3C_KEYS).filter(function (k) {
459    return k !== 'ingressoPrincVia';
460  });
461
462  root._tab3IsMultiLuogo = function (d) {
463    return root._tab3CountLuoghi(d) > 1;
464  };
465
466  function _tab3RedirectToPrincipale(key, msg) {
467    try {
468      if (typeof toast === 'function') toast(msg || 'Campo SSOT Luogo Principale — modifica in Tab 4 Luoghi.', 'info');
469      if (typeof root._goToLuogoPrincipale === 'function') {
470        var _sub = (typeof window._dvrLuogoPrincipaleSubTabForField === 'function')
471          ? window._dvrLuogoPrincipaleSubTabForField(key) : 'struttura';
472        root._goToLuogoPrincipale({ subTab: _sub, fieldKey: key });
473      } else if (root._tecTriade && root._tecTriade.openTriadeTab) {
474        root._tecTriade.openTriadeTab('amb');
475      }
476    } catch (_) {}
477  }
478
479  /** Tier P (mirror Principale): vietata scrittura d.* se esiste almeno un Luogo operativo Tab 4. */
480  root._tab3GuardPrincipaleMirrorSetD = function (key, d) {
481    if (!root._tab3IsPrincipaleMirror(key)) return false;
482    d = d || (typeof D === 'function' ? D() : null);
483    if (root._tab3CountLuoghi(d) < 1) return false;
484    _tab3RedirectToPrincipale(key, 'Campo del Luogo Principale (SSOT) — modifica in Tab 4 Luoghi, non in Tab 3.');
485    return true;
486  };
487
488  /** Struttura edificio 3A/3D/3F/3G/3H: SSOT per-Luogo su Tab 4 — mai d.* se c'è un Luogo operativo. */
489  root._tab3GuardEdificioSetD = function (key, d) {
490    if (root.TAB3_EDIFICIO_SSOT_KEYS.indexOf(key) < 0) return false;
491    d = d || (typeof D === 'function' ? D() : null);
492    // v20.373.102 — QUI SERVONO I LUOGHI CENSITI, NON QUELLI SINTETIZZATI.
493    // _tab3CountLuoghi, quando la lista operativa e' vuota, ricade su _getEntitaForAssessment che
494    // COSTRUISCE un luogo dai dati di progetto: cosi' anche un progetto senza alcun luogo risultava
495    // averne uno, e la scrittura dei dati dell'edificio veniva bloccata «perche' si compila nel
496    // Luogo» — cioe' in un luogo che non esiste. Il tecnico non poteva ne' scrivere qui ne' andare
497    // altrove: bootstrap impossibile. Il gate ssot-audit-v3/P13 lo diceva da tempo, ma esplodeva
498    // prima di stamparlo per una funzione mancante nell'harness dei test.
499    var _cens = (typeof root._tab3OperationalAmbientiList === 'function')
500      ? (root._tab3OperationalAmbientiList(d) || []).length
501      : root._tab3CountLuoghi(d);
502    if (_cens < 1) return false;
503    _tab3RedirectToPrincipale(key, 'Struttura edificio (3A) — SSOT Luogo Principale in Tab 4 Luoghi.');
504    return true;
505  };
506
507  /** dvrStruct su Anagrafica sede: solo mono-Luogo (multi = struct per-Luogo). */
508  root._tab3EdificioPersistToSedeAllowed = function (d) {
509    d = d || (typeof D === 'function' ? D() : null);
510    return !root._tab3IsMultiLuogo(d);
511  };
512
513  /** Mono-Luogo: Principale → mirror flat d.* (legacy DVR/readiness). Multi: skip. */
514  root._tab3SanitizeHiddenProfileFlatFromLuogo = function (d, luogo) {
515    if (!d || !luogo) return false;
516    var F = root._tecLuogoTipoForm;
517    if (!F) return false;
518    var tipo = luogo.tipo;
519    var changed = false;
520    if (typeof F.isComfortFieldVisible === 'function' && !F.isComfortFieldVisible(tipo, 'nPostazioniVdt')) {
521      if (d.nPostazioniVdt != null && String(d.nPostazioniVdt).trim() !== '' && String(d.nPostazioniVdt).trim() !== '0') {
522        d.nPostazioniVdt = '0';
523        changed = true;
524      }
525    }
526    if (typeof F.isStrutturaFieldVisible === 'function') {
527      ['tipologiaStrutturale', 'annoCostruzione', 'nPianiFT', 'nPianiInterrati', 'superficie', 'superficieEsterna', 'destinazioneUso'].forEach(function (key) {
528        if (F.isStrutturaFieldVisible(tipo, key)) return;
529        if (d[key] != null && String(d[key]).trim() !== '') {
530          d[key] = '';
531          changed = true;
532        }
533      });
534    }
535    return changed;
536  };
537
538  root._tab3MirrorEdificioFlatFromLuogo = function (d, luogo, opts) {
539    opts = opts || {};
540    if (!d || !luogo || root._tab3IsMultiLuogo(d)) return false;
541    var keys = opts.keys || root.TAB3_EDIFICIO_SSOT_KEYS;
542    var changed = false;
543    keys.forEach(function (key) {
544      if (opts.singleKey && key !== opts.singleKey) return;
545      var lv;
546      if (typeof root._tab2LuogoFieldVal === 'function') {
547        lv = root._tab2LuogoFieldVal(luogo, key, d);
548      }
549      if ((lv == null || lv === '') && luogo[key] != null && luogo[key] !== '') {
550        lv = luogo[key];
551      }
552      if (lv == null || lv === '' || (Array.isArray(lv) && !lv.length)) return;
553      if (opts.singleKey && opts.value !== undefined) lv = opts.value;
554      if (d[key] !== lv) {
555        d[key] = lv;
556        changed = true;
557      }
558    });
559    if (root._tab3SanitizeHiddenProfileFlatFromLuogo(d, luogo)) changed = true;
560    return changed;
561  };
562
563  root._tab3SyncEdificioBidirectional = function (d, luogo, opts) {
564    opts = opts || {};
565    var out = { changed: false, skipped: false, directions: [] };
566    if (!d) return out;
567    if (root._tab3IsMultiLuogo(d)) {
568      out.skipped = true;
569      out.reason = 'multi_luogo';
570      return out;
571    }
572    if (luogo && root._tab3MirrorEdificioFlatFromLuogo(d, luogo, opts)) {
573      out.changed = true;
574      out.directions.push('luogo→flat');
575    }
576    return out;
577  };
578
579  /** Prima del render Tab 3: allinea d.* (flat legacy) ai valori tier P del Luogo Principale. */
580  root._tab3SyncPrincipaleMirrorToFlat = function (d, luogo, opts) {
581    opts = opts || {};
582    var out = { changed: false, keys: [] };
583    if (!d || !luogo) return out;
584    if (typeof root._tab3CountLuoghi === 'function' && root._tab3CountLuoghi(d) < 1) return out;
585
586    function _apply(key, lv) {
587      if (opts.rewireAll) {
588        var rv = (lv == null || (Array.isArray(lv) && !lv.length)) ? '' : lv;
589        if (typeof rv === 'number') rv = String(rv);
590        if (d[key] !== rv) {
591          d[key] = rv;
592          out.changed = true;
593          out.keys.push(key);
594        }
595        return;
596      }
597      if (lv == null || lv === '' || (Array.isArray(lv) && !lv.length)) return;
598      // v20.190.4 — coerce numeric primitives to string for consistency with HTML input values
599      // (prevents type mismatch: select option '1' vs mirror number 1)
600      if (typeof lv === 'number') lv = String(lv);
601      if (d[key] !== lv) {
602        d[key] = lv;
603        out.changed = true;
604        out.keys.push(key);
605      }
606    }
607
608    (root.TAB3_MIRROR_3A_KEYS || []).forEach(function (key) {
609      if (opts.singleKey && key !== opts.singleKey) return;
610      var lv = typeof root._tab2LuogoFieldVal === 'function'
611        ? root._tab2LuogoFieldVal(luogo, key, d) : luogo[key];
612      if ((lv == null || lv === '') && luogo[key] != null && luogo[key] !== '') lv = luogo[key];
613      if (opts.singleKey && opts.value !== undefined) lv = opts.value;
614      _apply(key, lv);
615    });
616
617    (root.TAB3_EDIFICIO_SSOT_KEYS || []).forEach(function (key) {
618      if (root.TAB3_MIRROR_3A_KEYS && root.TAB3_MIRROR_3A_KEYS.indexOf(key) >= 0) return;
619      if (opts.singleKey && key !== opts.singleKey) return;
620      var lv;
621      if (typeof root._tab2LuogoFieldVal === 'function') lv = root._tab2LuogoFieldVal(luogo, key, d);
622      if ((lv == null || lv === '') && luogo[key] != null && luogo[key] !== '') lv = luogo[key];
623      if (opts.singleKey && opts.value !== undefined) lv = opts.value;
624      _apply(key, lv);
625    });
626
627    if (typeof root._tab3MirrorCpiFlatFromLuogo === 'function') {
628      if (root._tab3MirrorCpiFlatFromLuogo(d, luogo, opts)) out.changed = true;
629    }
630
631    Object.keys(root.TAB3_FIELD_SSC || {}
631).forEach(function (key) {
632      var meta = root.TAB3_FIELD_SSC[key];
633      if (!meta || meta.tier !== P) return;
634      if (root.TAB3_EDIFICIO_SSOT_KEYS && root.TAB3_EDIFICIO_SSOT_KEYS.indexOf(key) >= 0) return;
635      if (root.TAB3_CPI_SSOT_KEYS && root.TAB3_CPI_SSOT_KEYS.indexOf(key) >= 0) return;
636      if (opts.singleKey && key !== opts.singleKey) return;
637      var lv = typeof root._tab2LuogoFieldVal === 'function'
638        ? root._tab2LuogoFieldVal(luogo, key, d) : luogo[key];
639      if (opts.singleKey && opts.value !== undefined) lv = opts.value;
640      _apply(key, lv);
641    });
642
643    (root.TAB3_BAG_MIRROR_KEYS || []).forEach(function (key) {
644      if (opts.singleKey && key !== opts.singleKey) return;
645      var lvBag = typeof root._tab2LuogoFieldVal === 'function'
646        ? root._tab2LuogoFieldVal(luogo, key, d) : null;
647      if (opts.singleKey && opts.value !== undefined) lvBag = opts.value;
648      _apply(key, lvBag);
649    });
650
651    if (!opts.singleKey || opts.singleKey === 'ingressoPrincVia') {
652      var lvVia = typeof root._tab2LuogoFieldVal === 'function'
653        ? root._tab2LuogoFieldVal(luogo, 'ingressoPrincVia', d) : (luogo.posizione || null);
654      if (opts.singleKey && opts.value !== undefined) lvVia = opts.value;
655      _apply('ingressoPrincVia', lvVia);
656    }
657
658    if (typeof root._tab3SanitizeHiddenProfileFlatFromLuogo === 'function') {
659      if (root._tab3SanitizeHiddenProfileFlatFromLuogo(d, luogo)) out.changed = true;
660    }
661    return out;
662  };
663
664  function _exportHasVal(v) {
665    return v != null && v !== '' && !(Array.isArray(v) && !v.length);
666  }
667
668  function _cloneArr(val) {
669    if (!Array.isArray(val)) return [];
670    try { return JSON.parse(JSON.stringify(val)); } catch (_) { return val.slice(); }
671  }
672
673  /** Anagrafica sede cliente per fallback export (Soluzione D). */
674  root._tab3ResolveSedeForExport = function (d) {
675    d = d || {};
676    try {
677      var p = typeof root.CP === 'function' ? root.CP() : null;
678      var cliId = p && (p.clienteMasterId || p.clienteId);
679      var sedeId = p && p.sedeId;
680      if (!cliId || !sedeId || !Array.isArray(root.CLIENTI_LIST)) return null;
681      var cl = root.CLIENTI_LIST.find(function (c) { return c && c.id === cliId; });
682      if (!cl || !Array.isArray(cl.sedi)) return null;
683      return cl.sedi.find(function (s) { return s && (s.sedeId || s.id || s.nome) === sedeId; }) || null;
684    } catch (_) {
685      return null;
686    }
687  };
688
689  /**
690   * Soluzione D — export DVR: Principale grafo → d.* → sede anagrafica.
691   * Tier S (es. accessiCarrabili[]) resta solo su d.* Tab 3.
692   */
693  root._tab3ResolveExportField = function (key, d, opts) {
694    opts = opts || {};
695    d = d || {};
696    if (!key) return null;
697    var meta = (root.TAB3_FIELD_SSC || {})[key];
698
699    if (typeof root._tab3ComputeField === 'function' && meta && meta.compute) {
700      try {
701        var c = root._tab3ComputeField(key, d);
702        if (c && _exportHasVal(c.value)) return c.value;
703      } catch (_) {}
704    }
705
706    if (meta && meta.tier === S) {
707      return _exportHasVal(d[key]) ? d[key] : null;
708    }
709
710    if (!opts.skipPrincipale && typeof root._tab3CountLuoghi === 'function' && root._tab3CountLuoghi(d) >= 1) {
711      var lp = typeof root._tab3ResolvePrincipaleAmb === 'function' ? root._tab3ResolvePrincipaleAmb(d) : null;
712      if (lp && typeof root._tab2LuogoFieldVal === 'function') {
713        try {
714          var lv = root._tab2LuogoFieldVal(lp, key, d);
715          if (!_exportHasVal(lv) && opts.altKeys) {
716            for (var ai = 0; ai < opts.altKeys.length; ai++) {
717              lv = root._tab2LuogoFieldVal(lp, opts.altKeys[ai], d);
718              if (_exportHasVal(lv)) break;
719            }
720          }
721          if (_exportHasVal(lv)) return lv;
722        } catch (_) {}
723      }
724    }
725
726    if (key === 'superficie') {
727      if (_exportHasVal(d.superficie)) return d.superficie;
728      if (_exportHasVal(d.superficieCoperta)) return d.superficieCoperta;
729    } else if (_exportHasVal(d[key])) {
730      return d[key];
731    }
732    if (opts.altKeys) {
733      for (var bi = 0; bi < opts.altKeys.length; bi++) {
734        var ak = opts.altKeys[bi];
735        if (_exportHasVal(d[ak])) return d[ak];
736      }
737    }
738
739    if (!opts.skipSede) {
740      var sede = opts.sede || root._tab3ResolveSedeForExport(d);
741      if (sede) {
742        var sk = opts.sedeKey || key;
743        if (_exportHasVal(sede[sk])) return sede[sk];
744        if (sk !== key && _exportHasVal(sede[key])) return sede[key];
745        var ds = sede.dvrStruct;
746        if (ds && _exportHasVal(ds[sk])) return ds[sk];
747        if (ds && sk !== key && _exportHasVal(ds[key])) return ds[key];
748      }
749    }
750    return null;
751  };
752
753  /** Array tier P su grafo Luogo Principale (3D piani, 3G blocchiWC). */
754  root.TAB3_GRAFO_ARRAY_KEYS = ['piani', 'blocchiWC'];
755
756  /** Tab 3 setD → patch array su Luogo Principale in RAM (+ grafo se disponibile). */
757  root._tab3SyncGrafoArrayToPrincipale = function (d, key, val) {
758    if (!d || !key || root.TAB3_GRAFO_ARRAY_KEYS.indexOf(key) < 0) {
759      return { changed: false };
760    }
761    if (typeof root._tab3CountLuoghi === 'function' && root._tab3CountLuoghi(d) !== 1) {
762      return { changed: false, reason: 'multi_luogo' };
763    }
764    var lp = typeof root._tab3ResolvePrincipaleAmb === 'function' ? root._tab3ResolvePrincipaleAmb(d) : null;
765    if (!lp || !lp.id) return { changed: false, reason: 'no_principale' };
766
767    var patch = { id: lp.id };
768    if (key === 'piani') {
769      patch.piani = _cloneArr(val);
770    } else if (key === 'blocchiWC') {
771      patch.serviziIgienici = Object.assign({}, lp.serviziIgienici || {}, { blocchiWC: _cloneArr(val) });
772    }
773
774    var list = d.ambientiSede;
775    if (Array.isArray(list)) {
776      for (var i = 0; i < list.length; i++) {
777        if (list[i] && list[i].id === lp.id) {
778          if (key === 'piani') list[i].piani = patch.piani;
779          else if (key === 'blocchiWC') {
780            list[i].serviziIgienici = Object.assign({}, list[i].serviziIgienici || {}, patch.serviziIgienici);
781          }
782          lp = list[i];
783          break;
784        }
785      }
786    }
787    if (root._tab2LuogoHomeCache && root._tab2LuogoHomeCache.id === lp.id) {
788      if (key === 'piani') root._tab2LuogoHomeCache.piani = patch.piani;
789      else if (key === 'blocchiWC') {
790        root._tab2LuogoHomeCache.serviziIgienici = Object.assign(
791          {}, root._tab2LuogoHomeCache.serviziIgienici || {}, patch.serviziIgienici
792        );
793      }
794    }
795
796    var sedeId = _sedeIdFromProject();
797    if (sedeId && root._tecGrafo && typeof root._tecGrafo.saveAmbiente === 'function') {
798      try { root._tecGrafo.saveAmbiente(sedeId, patch); } catch (_) {}
799    }
800    return { changed: true, key: key };
801  };
802
803  /** Risolve Luogo Principale operativo (Tab 4) per mirror Tab 3. */
804  root._tab3ResolvePrincipaleAmb = function (d) {
805    d = d || (typeof D === 'function' ? D() : null);
806    if (!d) return null;
807    if (typeof root._tab2ResolvePrincipaleAmb === 'function') {
808      try {
809        var hit = root._tab2ResolvePrincipaleAmb(d);
810        if (hit) return hit;
811      } catch (_) {}
812    }
813    if (typeof root._tab2LuogoHome === 'function') {
814      try {
815        var h = root._tab2LuogoHome();
816        if (h) return h;
817      } catch (_) {}
818    }
819    var list = root._tab3OperationalAmbientiList ? root._tab3OperationalAmbientiList(d) : (d.ambientiSede || []);
820    if (root._tecGrafo && typeof root._tecGrafo.resolveCanonicalPrincipaleAmb === 'function') {
821      var canon = root._tecGrafo.resolveCanonicalPrincipaleAmb(list, d);
822      if (canon) return canon;
823    }
824    return list.filter(function (a) { return a && a.principale === true; })[0] || null;
825  };
826
827  /** Prima dell'export DVR: allinea d.* al Luogo Principale (3A/3B/3C + CPI). */
828  root._tab3EnsureMirrorBeforeExport = function (d) {
829    d = d || (typeof D === 'function' ? D() : null);
830    if (!d || typeof root._tab3CountLuoghi !== 'function' || root._tab3CountLuoghi(d) < 1) {
831      return { changed: false };
832    }
833    var lp = root._tab3ResolvePrincipaleAmb(d);
834    if (!lp) return { changed: false };
835    return root._tab3SyncPrincipaleMirrorToFlat(d, lp, {}) || { changed: false };
836  };
837
838  /** Risolve currentTab (let in index.html — non sempre su window). */
839  function _resolveCurrentTabId() {
840    try {
841      var viaFn = (new Function('return (typeof currentTab !== "undefined" ? currentTab : null)'))();
842      if (viaFn != null) return viaFn;
843    } catch (_) {}
844    return typeof root.currentTab !== 'undefined' ? root.currentTab : null;
845  }
846
847  /** Cambio Luogo Principale / patch struttura — invalida cache e refresh Tab 3 se aperta. */
848  root._tab3RefreshAfterPrincipaleChange = function (opts) {
849    opts = opts || {};
850    try { root._tab2LuogoHomeCache = null; } catch (_) {}
851    var d = typeof D === 'function' ? D() : null;
852    if (!d) return;
853    var lp = null;
854    if (opts.ambId && Array.isArray(d.ambientiSede)) {
855      lp = d.ambientiSede.find(function (a) { return a && a.id === opts.ambId; }) || null;
856    }
857    if (!lp && opts.ambId && typeof root._tab2MergedAmbientiList === 'function') {
858      var merged = root._tab2MergedAmbientiList(d) || [];
859      lp = merged.find(function (a) { return a && a.id === opts.ambId; }) || null;
860    }
861    if (!lp) lp = root._tab3ResolvePrincipaleAmb(d);
862    if (lp && typeof root._tab3PushPrincipaleMirrorToFlat === 'function') {
863      root._tab3PushPrincipaleMirrorToFlat(lp, opts);
864    }
865    var ct = _resolveCurrentTabId();
866    if (opts.renderTab !== false && ct === 2) {
867      try {
868        var mainEl = typeof document !== 'undefined' ? document.getElementById('main') : null;
869        if (mainEl && typeof root.renderRaccolta === 'function') {
870          root.renderRaccolta(mainEl);
871        } else if (typeof root.renderTab === 'function') {
872          root.renderTab();
873        }
874      } catch (_) {}
875    }
876  };
877
878  /** Live push Luogo Principale → d.* (Tab 3 mirror) + impianti 3I flat. */
879  root._tab3PushPrincipaleMirrorToFlat = function (amb, opts) {
880    opts = opts || {};
881    if (!amb) return { changed: false };
882    var isPrinc = amb.principale === true;
883    if (!isPrinc && typeof root._tab3ResolvePrincipaleAmb === 'function') {
884      var dChk = typeof root.D === 'function' ? root.D() : null;
885      var hit = root._tab3ResolvePrincipaleAmb(dChk);
886      isPrinc = !!(hit && hit.id === amb.id);
887    }
888    if (!isPrinc) return { changed: false };
889    var d = typeof root.D === 'function' ? root.D() : null;
890    if (!d) return { changed: false };
891    var out = root._tab3SyncPrincipaleMirrorToFlat(d, amb, opts) || { changed: false };
892    if (root._tecImpiantiSync && typeof root._tecImpiantiSync.syncImpiantiGrafoToSede === 'function') {
893      root._tecImpiantiSync.syncImpiantiGrafoToSede(d, [amb]);
894      out.impianti = true;
895      out.changed = true;
896    }
897    return out;
898  };
899
900  var _CPI_ENUM_OPTS = {
901    rischioIncendioNuovo: [
902      ['1', 'Livello 1 — Basso'],
903      ['2', 'Livello 2 — Medio'],
904      ['3', 'Livello 3 — Alto']
905    ],
906    rischioIncendioVecchio: [
907      ['basso', 'Basso (ex D.M. 10/03/98 — abrogato)'],
908      ['medio', 'Medio (ex D.M. 10/03/98 — abrogato)'],
909      ['alto', 'Alto (ex D.M. 10/03/98 — abrogato)']
910    ],
911    gruppoPS: [
912      ['A', 'Gruppo A — rischio basso'],
913      ['B', 'Gruppo B — rischio medio'],
914      ['C', 'Gruppo C — rischio elevato']
915    ]
916  };
917
918  function _cpiNormSelectVal(key, raw) {
919    if (raw == null || raw === '') return '';
920    if (key === 'rischioIncendioNuovo') {
921      var s = String(raw).trim().toLowerCase();
922      var m = { basso: '1', medio: '2', alto: '3', elevato: '3', a: '3', b: '2', c: '1' };
923      if (m[s]) return m[s];
924      var dig = s.match(/^(\d)/);
925      return dig ? dig[1] : s;
926    }
927    if (key === 'rischioIncendioVecchio') return String(raw).trim().toLowerCase();
928    if (key === 'gruppoPS') return String(raw).trim().charAt(0).toUpperCase();
929    return String(raw);
930  }
931
932  /** Suggerimenti CPI: _proponiClasseIncendio (ATECO/edificio/sport) + _derivaGruppoPrimoSoccorso. */
933  root._tab3CpiSuggestFor = function (d) {
934    d = d || {};
935    var inc = typeof root._proponiClasseIncendio === 'function' ? root._proponiClasseIncendio(d) : {};
936    var ps = typeof root._derivaGruppoPrimoSoccorso === 'function' ? root._derivaGruppoPrimoSoccorso(d) : {};
937    return {
938      rischioIncendioNuovo: _cpiNormSelectVal('rischioIncendioNuovo', inc.propostaNuovo),
939      rischioIncendioVecchio: _cpiNormSelectVal('rischioIncendioVecchio', inc.propostaVecchio),
940      gruppoPS: _cpiNormSelectVal('gruppoPS', ps.gruppo),
941      incendio: inc,
942      ps: ps
943    };
944  };
945
946  /** Patch ottimistico SSOT impianti.cpi su tutte le cache Luogo Principale (evita revert al renderTab). */
947  function _patchCpiLuogoCaches(lh, key, value) {
948    if (!lh || !key) return;
949    if (!lh.impianti) lh.impianti = {};
950    if (!lh.impianti.cpi) lh.impianti.cpi = {};
951    lh.impianti.cpi[key] = value;
952    var lid = lh.id;
953    if (root._tab2LuogoHomeCache && (!lid || root._tab2LuogoHomeCache.id === lid)) {
954      if (!root._tab2LuogoHomeCache.impianti) root._tab2LuogoHomeCache.impianti = {};
955      if (!root._tab2LuogoHomeCache.impianti.cpi) root._tab2LuogoHomeCache.impianti.cpi = {};
956      root._tab2LuogoHomeCache.impianti.cpi[key] = value;
957    }
958    var eng = root._tecEngine;
959    if (eng && eng._luogoListCache && lid) {
960      for (var i = 0; i < eng._luogoListCache.length; i++) {
961        var x = eng._luogoListCache[i];
962        if (x && x.id === lid) {
963          if (!x.impianti) x.impianti = {};
964          if (!x.impianti.cpi) x.impianti.cpi = {};
965          x.impianti.cpi[key] = value;
966          break;
967        }
968      }
969    }
970  }
971
972  function _cpiWriteFlat(key, value, d) {
973    var run = function () {
974      if (typeof setD === 'function') setD(key, value);
975      else if (d) d[key] = value;
976    };
977    if (key === 'rischioIncendioVecchio') {
978      root._allowIncendioVecchioOverride = true;
979      try {
980        if (typeof root._withSsotBypass === 'function') root._withSsotBypass(run);
981        else run();
982      } finally {
983        root._allowIncendioVecchioOverride = false;
984      }
985    } else {
986      run();
987    }
988  }
989
990  function _cpiPersistOne(key, value, d, lh) {
991    _patchCpiLuogoCaches(lh, key, value);
992    _cpiWriteFlat(key, value, d);
993    var sedeId = _sedeIdFromProject();
994    if (lh && lh.id && sedeId && root._tecEngine && root._tecEngine._saveAmbImpiantoField) {
995      root._tecEngine._saveAmbImpiantoField(sedeId, lh.id, 'cpi', key, value, 0);
996    }
997  }
998
999  root._setCpiSsotField = function (key, value) {
1000    if (root.TAB3_CPI_SSOT_KEYS.indexOf(key) < 0) return;
1001    var d = typeof D === 'function' ? D() : null;
1002    if (!d) return;
1003    var pairs = [[key, value]];
1004    if (key === 'rischioIncendioNuovo' && typeof root._incendioNuovoToVecchio === 'function') {
1005      var dv = root._incendioNuovoToVecchio(value);
1006      if (dv) pairs.push(['rischioIncendioVecchio', dv]);
1007    } else if (key === 'rischioIncendioVecchio' && typeof root._incendioVecchioToNuovo === 'function') {
1008      var nv = root._incendioVecchioToNuovo(value);
1009      if (nv) pairs.push(['rischioIncendioNuovo', nv]);
1010    }
1011    var lh = typeof root._tab2LuogoHome === 'function' ? root._tab2LuogoHome() : null;
1012    pairs.forEach(function (pair) { _cpiPersistOne(pair[0], pair[1], d, lh); });
1013    if (root._tecTriParere && typeof root._tecTriParere.recordDecision === 'function') {
1014      root._tecTriParere.recordDecision(key, value, d);
1015    }
1016    if (pairs.length > 1 && typeof renderTab === 'function') renderTab();
1017  };
1018
1019  function _esc(s) {
1020    return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1021  }
1022
1023  function _fmtCpiDisp(raw) {
1024    if (raw == null || raw === '') return '—';
1025    if (raw === 'si') return 'Sì';
1026    if (raw === 'no') return 'No';
1027    var incLbl = {
1028      '1': 'Livello 1 — Basso', '2': 'Livello 2 — Medio', '3': 'Livello 3 — Alto',
1029      basso: 'Basso', medio: 'Medio', alto: 'Alto',
1030      A: 'Gruppo A', B: 'Gruppo B', C: 'Gruppo C'
1031    };
1032    if (incLbl[raw]) return incLbl[raw];
1033    return String(raw).replace(/_/g, ' ');
1034  }
1035
1036  function _tab3CpiSuggestBadge(key, suggestVal, currentVal, meta) {
1037    if (!suggestVal) return '';
1038    var cur = _cpiNormSelectVal(key, currentVal);
1039    var match = cur === String(suggestVal);
1040    var tip = (meta && meta.perche) ? String(meta.perche).replace(/"/g, "'") : '';
1041    var bg = match ? '#dcfce7' : '#eff6ff';
1042    var col = match ? '#166534' : '#1d4ed8';
1043    var border = match ? '#86efac' : '#93c5fd';
1044    var lbl = 'Suggerito: ' + _fmtCpiDisp(suggestVal) + (match ? ' ✓' : '');
1045    return '<span title="' + tip + '" style="font-size:8px;font-weight:700;color:' + col + ';background:' + bg + ';padding:1px 8px;border-radius:999px;margin-left:5px;border:1px solid ' + border + '">' + lbl + '</span>';
1046  }
1047
1048  /** Badge «Validato dal Legale» — stesso contratto di index.html `_legalValidatedBadge`. */
1049  function _tab3LegalValidatedBadge(key, val) {
1050    try {
1051      if (typeof root._legalFieldValidation !== 'function') return '';
1052      var lv = root._legalFieldValidation(key);
1053      if (!lv || lv.stato !== 'validato') return '';
1054      var norma = lv.norma ? (' · ' + String(lv.norma)) : '';
1055      var empty = val == null || (function () {
1056        var s = String(val).trim();
1057        return s === '' || s === '?' || s === '—' || s === '-';
1058      })();
1059      if (empty) {
1060        var tipE = ('Regola validata dal Legale, ma il campo è ancora da compilare' + (lv.validatoDa ? (' · ' + lv.validatoDa) : '')).replace(/"/g, "'");
1061        return '<span title="' + tipE + '" style="cursor:help;font-size:8px;padding:1px 6px;border-radius:4px;background:#fef3c7;color:#92400e;font-weight:700;margin-left:5px;border:1px solid #fcd34d">&#10003; Validato — campo da compilare</span>';
1062      }
1063      var tip = ('Validato dal Legale' + (lv.validatoDa ? (' · ' + lv.validatoDa) : '') + (lv.data ? (' · ' + String(lv.data).slice(0, 10)) : '') + (lv.nota ? ('\n' + lv.nota) : '')).replace(/"/g, "'");
1064      return '<span title="' + tip + '" style="cursor:help;font-size:8px;padding:1px 6px;border-radius:4px;background:#dcfce7;color:#166534;font-weight:700;margin-left:5px;border:1px solid #86efac">&#10003; Validato dal Legale' + norma.replace(/</g, '&lt;') + '</span>';
1065    } catch (_) { return ''; }
1066  }
1067
1068  var _CPI_CHIP_KEY = {
1069    rischioIncendioVecchio: 'rischioIncendio',
1070    rischioIncendioNuovo: 'rischioIncendio',
1071    cpiNumero: 'statoCPI',
1072    cpiScadenza: 'statoCPI'
1073  };
1074
1075  function _tab3CpiLabelTitle(key) {
1076    try {
1077      if (typeof root._fieldMeta !== 'function') return '';
1078      var m = root._fieldMeta(key);
1079      if (!m || !m.locator) return '';
1080      return ' title="' + String((m.categoria || '') + ' · ' + (m.norma || '—') + ' · ' + m.locator).replace(/"/g, "'") + '"';
1081    } catch (_) { return ''; }
1082  }
1083
1084  function _tab3CpiFieldBadges(key, val, d) {
1085    var html = '';
1086    try { if (typeof root._fieldMetaBadge === 'function') html += root._fieldMetaBadge(key); } catch (_) {}
1087    html += _tab3LegalValidatedBadge(key, val);
1088    try {
1089      var chipKey = _CPI_CHIP_KEY[key] || key;
1090      if (typeof root._renderLuoghiImpiantiChip === 'function') {
1091        var chip = root._renderLuoghiImpiantiChip(d, chipKey);
1092        if (chip) html += chip;
1093      }
1094    } catch (_) {}
1095    return html;
1096  }
1097
1098  /** Pannello 3J–3L CPI in Tab 2 Sede Generale: RO mono-Luogo · edit multi-sede. */
1099  root._tab3RenderCpiTab2Panel = function (d, ctx) {
1100    d = d || {};
1101    ctx = ctx || {};
1102    var SEC = ctx.SEC;
1103    if (!SEC) return '';
1104    var lh = typeof root._tab2LuogoHome === 'function' ? root._tab2LuogoHome() : null;
1105    root._tab3SyncCpiBidirectional(d, lh, { persistBackfill: true });
1106    var editable = root._tab3CpiTab2Editable(d);
1107    var lv = function (k) { return _cpiVal(lh, d, k); };
1108    var nLu = root._tab3CountLuoghi(d);
1109    var intro = '<div style="font-size:10.5px;color:#92400e;margin-bottom:10px;padding:8px 12px;background:#fff7ed;border-radius:8px;border:1px solid #fdba74">'
1110      + '<strong>' + (nLu > 1 ? 'Multi-sede (' + nLu + ' Luoghi)' : 'Mono-Luogo') + '</strong> — CPI del <em>Luogo Principale</em> editabile qui; SSOT su <code>'
1111      + root.TAB3_CPI_SSOT_PATH + '</code> + mirror <code>d.*</code>. '
1112      + 'Classe derivata per fattori luogo → '
1113      + '<button type="button" onclick="window._goToLuogoPrincipale({subTab:\'antincendio\',fieldKey:\'rischioIncendioNuovo\'})" style="margin-left:4px;padding:4px 10px;border-radius:6px;border:1px solid #2563eb;background:#fff;color:#1d4ed8;font-size:10px;font-weight:700;cursor:pointer">Vai → Tab 4 Antincendio</button></div>';
1114
1115    function fieldWrap(key, tier, inner, extraAttrs) {
1116      extraAttrs = extraAttrs || '';
1117      return '<div data-field-key="' + key + '" data-tab3-tier="' + (tier || 'P') + '"' + extraAttrs + '>' + inner + '</div>';
1118    }
1119
1120    function roField(label, key) {
1121      var raw = lv(key);
1122      return fieldWrap(key, 'P',
1123        '<label class="fl"' + _tab3CpiLabelTitle(key) + '><span class="dot dot-r"></span>' + label + _tab3CpiFieldBadges(key, raw, d) + '</label>'
1124        + '<div style="border:1.5px solid #e2e8f0;border-radius:10px;padding:9px 13px;font-size:13px;background:#f8fafc;font-weight:600">' + _esc(_fmtCpiDisp(raw)) + '</div>',
1125        ' data-ro-luogo="1"');
1126    }
1127
1128    function ynSelect(label, key) {
1129      var v = lv(key) || '';
1130      return fieldWrap(key, 'P',
1131        '<label class="fl"' + _tab3CpiLabelTitle(key) + '><span class="dot dot-r"></span>' + label + _tab3CpiFieldBadges(key, v, d) + '</label>'
1132        + '<select onchange="window._setCpiSsotField(\'' + key + '\',this.value)" style="width:100%;padding:9px 13px;border:1.5px solid #cbd5e1;border-radius:10px;font-size:13px">'
1133        + '<option value="">—</option><option value="si"' + (v === 'si' ? ' selected' : '') + '>Sì</option><option value="no"' + (v === 'no' ? ' selected' : '') + '>No</option></select>');
1134    }
1135
1136    function textInput(label, key, type) {
1137      type = type || 'text';
1138      var v = lv(key) || '';
1139      return fieldWrap(key, 'P',
1140        '<label class="fl"' + _tab3CpiLabelTitle(key) + '><span class="dot dot-r"></span>' + label + _tab3CpiFieldBadges(key, v, d) + '</label>'
1141        + '<input type="' + type + '" value="' + _esc(v) + '" onchange="window._setCpiSsotField(\'' + key + '\',this.value)" style="width:100%;padding:9px 13px;border:1.5px solid #cbd5e1;border-radius:10px;font-size:13px">');
1142    }
1143
1144    function statoCpiSelect(label, key) {
1145      var v = lv(key) || '';
1146      return fieldWrap(key, 'P',
1147        '<label class="fl"' + _tab3CpiLabelTitle(key) + '><span class="dot dot-r"></span>' + label + _tab3CpiFieldBadges(key, v, d) + '</label>'
1148        + '<select onchange="window._setCpiSsotField(\'' + key + '\',this.value)" style="width:100%;padding:9px 13px;border:1.5px solid #cbd5e1;border-radius:10px;font-size:13px">'
1149        + ['non_richiesto:Non richiesto', 'presente:CPI presente', 'scaduto:Scaduto', 'scia_completa:SCIA completa', 'scia_parziale:SCIA parziale', 'assente:Assente', 'in_iter:In iter', 'in_corso:In corso'].map(function (o) {
1150          var p = o.split(':');
1151          return '<option value="' + p[0] + '"' + (v === p[0] ? ' selected' : '') + '>' + p[1] + '</option>';
1152        }).join('')
1153        + '</select>');
1154    }
1155
1156    function enumSelect(label, key, suggestVal, suggestMeta) {
1157      var opts = _CPI_ENUM_OPTS[key] || [];
1158      var v = _cpiNormSelectVal(key, lv(key));
1159      var suggestBadge = _tab3CpiSuggestBadge(key, suggestVal, v, suggestMeta);
1160      var optHtml = opts.map(function (pair) {
1161        var val = pair[0];
1162        var lbl = pair[1];
1163        var isSugg = suggestVal && String(suggestVal) === String(val);
1164        return '<option value="' + val + '"' + (v === val ? ' selected' : '') + (isSugg ? ' data-suggested="1"' : '') + '>'
1165          + lbl + (isSugg ? ' ★' : '') + '</option>';
1166      }).join('');
1167      var hintStyle = !v && suggestVal ? 'border-color:#93c5fd;background:#f8fafc' : '';
1168      var tier = key === 'rischioIncendioVecchio' ? 'C' : 'P';
1169      return fieldWrap(key, tier,
1170        '<label class="fl"' + _tab3CpiLabelTitle(key) + '><span class="dot dot-r"></span>' + label + _tab3CpiFieldBadges(key, v, d) + suggestBadge + '</label>'
1171        + '<select onchange="window._setCpiSsotField(\'' + key + '\',this.value)" style="width:100%;padding:9px 13px;border:1.5px solid #cbd5e1;border-radius:10px;font-size:13px;' + hintStyle + '">'
1172        + '<option value=""' + (!v ? ' selected' : '') + '>— seleziona —</option>' + optHtml
1173        + '</select>'
1174        + ((root._tecTriParere && typeof root._tecTriParere.renderStrip === 'function')
1175          ? root._tecTriParere.renderStrip(key, d, { currentVal: v }) : ''));
1176    }
1177
1178    var suggests = root._tab3CpiSuggestFor(d);
1179
1180    var body = intro + '<div class="grid grid-cols-2 gap-4 mb-4">';
1181    if (editable) {
1182      body += ynSelect('2L.1 Soggetto a controllo VVF?', 'soggettoVVF');
1183      body += statoCpiSelect('Stato CPI', 'statoCPI');
1184    } else {
1185      body += roField('2L.1 Soggetto a controllo VVF?', 'soggettoVVF');
1186      body += roField('Stato CPI', 'statoCPI');
1187    }
1188    body += '</div>';
1189
1190    if (lv('soggettoVVF') === 'si' && (lv('statoCPI') === 'presente' || lv('statoCPI') === 'scaduto')) {
1191      body += '<div class="grid grid-cols-2 gap-4 mb-4">';
1192      if (editable) {
1193        body += textInput('Numero CPI', 'cpiNumero');
1194        body += textInput('Data scadenza CPI', 'cpiScadenza', 'date');
1195      } else {
1196        body += roField('Numero CPI', 'cpiNumero');
1197        body += roField('Data scadenza CPI', 'cpiScadenza');
1198      }
1199      body += '</div>';
1200    }
1201
1202    body += '<div class="grid grid-cols-2 gap-4 mt-2">';
1203    if (editable) {
1204      body += ynSelect('2J.1 Attività DPR 151/2011?', 'attivitaDPR151');
1205      body += enumSelect('2K.2 Livello rischio (D.M. 03/09/2021)', 'rischioIncendioNuovo', suggests.rischioIncendioNuovo, suggests.incendio);
1206      body += enumSelect((root._tecNormativa && root._tecNormativa.label('rischioIncendioVecchioField')) || '2K.1 Classificazione storica (ex D.M. 10/03/98 — abrogato)', 'rischioIncendioVecchio', suggests.rischioIncendioVecchio, suggests.incendio);
1207      body += enumSelect('2K.3 Gruppo Primo Soccorso', 'gruppoPS', suggests.gruppoPS, suggests.ps);
1208    } else {
1209      body += roField('2J.1 Attività DPR 151/2011?', 'attivitaDPR151');
1210      body += roField('2K.2 Livello rischio (D.M. 03/09/2021)', 'rischioIncendioNuovo');
1211      body += roField((root._tecNormativa && root._tecNormativa.label('rischioIncendioVecchioField')) || '2K.1 Classificazione storica (ex D.M. 10/03/98 — abrogato)', 'rischioIncendioVecchio');
1212      body += roField('2K.3 Gruppo Primo Soccorso', 'gruppoPS');
1213    }
1214    body += '</div>';
1215
1216    return SEC('cpi_ssot', '🔥', '3L — CPI / Antincendio (Luogo Principale)', body);
1217  };
1218
1219  function _luoghiList(sedeId) {
1220    var list = [];
1221    var eng = root._tecEngine;
1222    if (eng && eng._luogoListCache && (!sedeId || eng._luogoSedeId === sedeId)) {
1223      list = eng._luogoListCache.slice();
1224    }
1225    if (!list.length && root._tecGrafo && root._tecGrafo.getGrafoCache) {
1226      try {
1227        var g = root._tecGrafo.getGrafoCache();
1228        if (g && Array.isArray(g.luoghi)) list = g.luoghi.slice();
1229        if (!list.length && g && Array.isArray(g.ambientiSede)) list = g.ambientiSede.slice();
1230      } catch (_) {}
1231    }
1232    // v20.372.3 — fallback progetto: cache luoghi stale non deve far Σ=0 dopo save bag
1233    if (!list.length && typeof D === 'function') {
1234      try {
1235        var dL = D();
1236        if (dL && Array.isArray(dL.ambientiSede) && dL.ambientiSede.length) list = dL.ambientiSede.slice();
1237      } catch (_) {}
1238    }
1239    return list;
1240  }
1241
1242  root._tab3SumServiziIgieniciLuoghi = function (sedeId) {
1243    var sum = 0;
1244    var list = _luoghiList(sedeId);
1245    // v20.373.367 — ROLL-UP SOLO FOGLIE: non sommare i nodi CONTENITORE (con figli), altrimenti
1246    // Master + piani = doppio conteggio dei blocchi WC. Modello piatto: nessun figlio -> invariato.
1247    var _hasChildWc = function (id) { return !!id && list.some(function (x) { return x && (x.parentEdificioId === id || x.parentLuogoId === id); }); };
1248    list.forEach(function (l) {
1249      if (!l) return;
1250      if (_hasChildWc(l.id)) return;
1251      var si = l.serviziIgienici || {};
1252      var n = parseInt(si.nBlocchiWCTotale || si.nBlocchiWC || 0, 10);
1253      if (!n && Array.isArray(si.blocchiWC)) {
1254        n = si.blocchiWC.reduce(function (s, b) {
1255          return s + (parseInt(b && b.nBlocchi, 10) || 0);
1256        }, 0);
1257      }
1258      sum += n || 0;
1259    });
1260    if (sum === 0 && typeof root._tab2LuogoHome === 'function') {
1261      try {
1262        var h = root._tab2LuogoHome();
1263        if (h && h.serviziIgienici) {
1264          sum = parseInt(h.serviziIgienici.nBlocchiWCTotale || h.serviziIgienici.nBlocchiWC || 0, 10) || 0;
1265        }
1266      } catch (_) {}
1267    }
1268    if (sum === 0 && typeof D === 'function') {
1269      try {
1270        var d = D();
1271        sum = parseInt(d.nBlocchiWCTotale || d.nBlocchiWC || 0, 10) || 0;
1272      } catch (_) {}
1273    }
1274    return sum;
1275  };
1276
1277  root._tab3ComputeField = function (key, d) {
1278    d = d || (typeof D === 'function' ? D() : {});
1279    var meta = root.TAB3_FIELD_SSC[key] || {};
1280    if (key === 'nDipendenti' || meta.compute === 'organico') {
1281      var n = typeof root._countDipendentiOrganico === 'function'
1282        ? root._countDipendentiOrganico(d)
1283        : (parseInt(d.nDipendenti || 0, 10) || 0);
1284      return {
1285        value: n,
1286        provenance: 'Organico nominativo — Tab 3.3 Mansioni (Σ righe dipendenti)',
1287        navAction: 'triade_mansioni'
1288      };
1289    }
1290    if (key === 'nBlocchiWCTotale' || key === 'nServiziIgienici' || key === 'nBlocchiWC' || meta.compute === 'sum_wc_luoghi') {
1291      var sedeId = null;
1292      try {
1293        var p = typeof CP === 'function' ? CP() : null;
1294        sedeId = p && (p.sedeId || (p.data && p.data.sedeId));
1295      } catch (_) {}
1296      var tot = root._tab3SumServiziIgieniciLuoghi(sedeId);
1297      var nLu = _luoghiList(sedeId).length;
1298      return {
1299        value: tot,
1300        provenance: nLu > 1
1301          ? ('Σ servizi igienici su ' + nLu + ' Luoghi censiti')
1302          : 'Servizi igienici dai Luoghi (Tab 4) — totale blocchi WC',
1303        navAction: 'tab3_wc'
1304      };
1305    }
1306    if (key === 'rischioIncendioVecchio' || meta.compute === 'incendio_vecchio') {
1307      var v = d.rischioIncendioVecchio;
1308      if (!v && d.rischioIncendioNuovo && typeof root._incendioNuovoToVecchio === 'function') {
1309        v = root._incendioNuovoToVecchio(d.rischioIncendioNuovo);
1310      }
1311      return {
1312        value: v || '',
1313        provenance: 'Derivato da 2K.2 rischioIncendioNuovo (DM 03/09/2021)',
1314        navAction: 'luogo_principale'
1315      };
1316    }
1317    return { value: d[key], provenance: '', navAction: meta.nav || 'tab3' };
1318  };
1319
1320  /**
1321   * v20.372.6 — emptyData() storico seminava wcPavPiastrellati:'si' + wcAltPiastrellatura:'2'.
1322   * Su progetti già creati quei flat restano e hydrate/merge li rimettono nel bag al F5
1323   * (UI: WC vuoto, pavimenti «—», altezza «2»). Strip se nBlocchi assente/0 (pattern seed).
1324   */
1325  root._tab3StripWcEmptyDataSeeds = function (d) {
1326    d = d || (typeof D === 'function' ? D() : null);
1327    if (!d) return false;
1328    var n = String(d.nBlocchiWCTotale == null ? '' : d.nBlocchiWCTotale).trim();
1329    if (n === '' && d.nBlocchiWC != null) n = String(d.nBlocchiWC).trim();
1330    var nEmpty = !n || n === '0';
1331    if (!nEmpty) return false;
1332    var pav = String(d.wcPavPiastrellati == null ? '' : d.wcPavPiastrellati).trim().toLowerCase();
1333    var alt = String(d.wcAltPiastrellatura == null ? '' : d.wcAltPiastrellatura).trim();
1334    var stripPav = (pav === 'si');
1335    var stripAlt = (alt === '2');
1336    if (!stripPav && !stripAlt) {
1337      // bag-only poison (flat già vuoto, bag ha ancora seed)
1338    } else {
1339      /* flat poison */
1340    }
1341    var changed = false;
1342    function _clearFlat(key) {
1343      if (typeof setD === 'function') {
1344        var _prev = (typeof window !== 'undefined') ? window._tecSsotBypass : false;
1345        if (typeof window !== 'undefined') window._tecSsotBypass = true;
1346        try { setD(key, ''); } finally { if (typeof window !== 'undefined') window._tecSsotBypass = _prev; }
1347      } else {
1348        d[key] = '';
1349      }
1350      changed = true;
1351    }
1352    if (stripPav) _clearFlat('wcPavPiastrellati');
1353    if (stripAlt) _clearFlat('wcAltPiastrellatura');
1354
1355    function _clearBag(amb) {
1356      if (!amb || !amb.serviziIgienici) return;
1357      var si = amb.serviziIgienici;
1358      var bn = String(si.nBlocchiWCTotale == null ? '' : si.nBlocchiWCTotale).trim();
1359      if (bn && bn !== '0') return;
1360      if (stripPav || String(si.wcPavPiastrellati || '').toLowerCase() === 'si') {
1361        if (String(si.wcPavPiastrellati || '').toLowerCase() === 'si') { si.wcPavPiastrellati = ''; changed = true; }
1362      }
1363      if (stripAlt || String(si.wcAltPiastrellatura || '') === '2') {
1364        if (String(si.wcAltPiastrellatura || '') === '2') { si.wcAltPiastrellatura = ''; changed = true; }
1365      }
1366    }
1367    (d.ambientiSede || []).forEach(_clearBag);
1368    if (root._tab2LuogoHomeCache) _clearBag(root._tab2LuogoHomeCache);
1369    return changed;
1370  };
1371
1372  /**
1373   * v20.372.1/3/6 — flat progetto → bag Luogo Principale se bag vuoto/indebolito post-merge F5.
1374   * NON idratare da flat «0» né da seed emptyData (pav si / alt 2 con nBlocchi vuoto).
1375   */
1376  root._tab3HydrateWcBagFromFlat = function (d) {
1377    d = d || (typeof D === 'function' ? D() : null);
1378    if (!d) return false;
1379    try { root._tab3StripWcEmptyDataSeeds(d); } catch (_st) {}
1380    var lp = typeof root._tab3ResolvePrincipaleAmb === 'function' ? root._tab3ResolvePrincipaleAmb(d) : null;
1381    if (!lp) {
1382      var list = Array.isArray(d.ambientiSede) ? d.ambientiSede : [];
1383      for (var i = 0; i < list.length; i++) {
1384        if (list[i] && list[i].principale === true) { lp = list[i]; break; }
1385      }
1386      if (!lp && list.length === 1) lp = list[0];
1387    }
1388    if (!lp) return false;
1389    if (!lp.serviziIgienici) lp.serviziIgienici = {};
1390    var changed = false;
1391
1392    function _bagEmptyOrNa(v) {
1393      if (v == null || v === '') return true;
1394      return String(v).toLowerCase() === 'na';
1395    }
1396    function _isSynthZero(v) {
1397      if (v == null || v === '') return false;
1398      var n = parseInt(v, 10);
1399      return Number.isFinite(n) && n === 0;
1400    }
1401    function _applyBagKey(key, flatVal, allowOverwriteNa) {
1402      if (flatVal == null || String(flatVal).trim() === '') return;
1403      if (key === 'nBlocchiWCTotale' && _isSynthZero(flatVal)) return;
1404      // v20.372.6 — non ripopolare bag da seed emptyData
1405      var nFlat = String(d.nBlocchiWCTotale == null ? '' : d.nBlocchiWCTotale).trim();
1406      var nEmpty = !nFlat || nFlat === '0';
1407      if (nEmpty && key === 'wcPavPiastrellati' && String(flatVal).toLowerCase() === 'si') return;
1408      if (nEmpty && key === 'wcAltPiastrellatura' && String(flatVal).trim() === '2') return;
1409      var cur = lp.serviziIgienici[key];
1410      var empty = allowOverwriteNa ? _bagEmptyOrNa(cur) : (cur == null || String(cur).trim() === '');
1411      if (!empty && !(key === 'nBlocchiWCTotale' && _isSynthZero(cur) && !_isSynthZero(flatVal))) return;
1412      lp.serviziIgienici[key] = flatVal;
1413      changed = true;
1414      if (root._tab2LuogoHomeCache && root._tab2LuogoHomeCache.id === lp.id) {
1415        if (!root._tab2LuogoHomeCache.serviziIgienici) root._tab2LuogoHomeCache.serviziIgienici = {};
1416        var cCur = root._tab2LuogoHomeCache.serviziIgienici[key];
1417        var cEmpty = allowOverwriteNa ? _bagEmptyOrNa(cCur) : (cCur == null || String(cCur).trim() === '');
1418        if (cEmpty || (key === 'nBlocchiWCTotale' && _isSynthZero(cCur) && !_isSynthZero(flatVal))) {
1419          root._tab2LuogoHomeCache.serviziIgienici[key] = flatVal;
1420        }
1421      }
1422    }
1423
1424    var flatWc = d.nBlocchiWCTotale != null && String(d.nBlocchiWCTotale).trim() !== ''
1425      ? d.nBlocchiWCTotale
1426      : (d.nBlocchiWC != null && String(d.nBlocchiWC).trim() !== '' ? d.nBlocchiWC : null);
1427    _applyBagKey('nBlocchiWCTotale', flatWc, false);
1428    _applyBagKey('wcPavPiastrellati', d.wcPavPiastrellati, true);
1429    _applyBagKey('wcAltPiastrellatura', d.wcAltPiastrellatura, false);
1430    return changed;
1431  };
1432
1433  root._tab3SyncComputedFields = function (d, opts) {
1434    opts = opts || {};
1435    if (!d) return;
1436    // v20.148.0 — no _syncOrganicoDerivati su render (write storm Tab 2/3)
1437    // v20.371.13 — anti F5 overwrite: al backfill NON riscrivere un flat già valorizzato.
1438    // v20.372.1 — prima ripristina bag da flat se merge ha azzerato serviziIgienici
1439    // v20.372.3 — MAI scrivere «0» sintetico (onlyIfEmpty/force con Σ=0): crea ciclo F5
1440    //   flat=0 → hydrate bag=0 → merge azzera FS→UI mostra 0 / «—».
1441    try { root._tab3HydrateWcBagFromFlat(d); } catch (_hw) {}
1442    var wc = root._tab3SumServiziIgieniciLuoghi(null);
1443    if (wc < 0) return;
1444    if (wc === 0 && !opts.allowZero) {
1445      // v20.372.3 — rimuovi «0» sintetico già presente (non inventare, non lasciare in UI)
1446      var curZ = String(d.nBlocchiWCTotale == null ? '' : d.nBlocchiWCTotale).trim();
1447      if (curZ === '0') {
1448        var _clearZ = function () {
1449          if (typeof setD === 'function') {
1450            setD('nBlocchiWCTotale', '');
1451            setD('nServiziIgienici', '');
1452          } else {
1453            d.nBlocchiWCTotale = '';
1454            d.nServiziIgienici = '';
1455          }
1456        };
1457        if (typeof root._withSsotBypass === 'function') root._withSsotBypass(_clearZ);
1458        else {
1459          var _pbz = (typeof window !== 'undefined') ? window._tecSsotBypass : false;
1460          if (typeof window !== 'undefined') window._tecSsotBypass = true;
1461          try { _clearZ(); } finally { if (typeof window !== 'undefined') window._tecSsotBypass = _pbz; }
1462        }
1463        try {
1464          var lpZ = typeof root._tab3ResolvePrincipaleAmb === 'function' ? root._tab3ResolvePrincipaleAmb(d) : null;
1465          if (lpZ && lpZ.serviziIgienici
1466              && String(lpZ.serviziIgienici.nBlocchiWCTotale == null ? '' : lpZ.serviziIgienici.nBlocchiWCTotale).trim() === '0') {
1467            lpZ.serviziIgienici.nBlocchiWCTotale = '';
1468          }
1469        } catch (_cz) {}
1470      }
1471      return;
1472    }
1473    var wcStr = String(wc);
1474    var cur = String(d.nBlocchiWCTotale == null ? '' : d.nBlocchiWCTotale).trim();
1475    var curSvc = String(d.nServiziIgienici == null ? '' : d.nServiziIgienici).trim();
1476    if (cur === wcStr && curSvc === wcStr) return;
1477    if (!opts.force) {
1478      if (opts.onlyIfEmpty !== false && cur !== '') return;
1479    }
1480    if (opts.persist === false) {
1481      d.nBlocchiWCTotale = wcStr;
1482      d.nServiziIgienici = wcStr;
1483      return;
1484    }
1485    var _write = function () {
1486      if (typeof setD === 'function') {
1487        setD('nBlocchiWCTotale', wcStr);
1488        setD('nServiziIgienici', wcStr);
1489      } else {
1490        d.nBlocchiWCTotale = wcStr;
1491        d.nServiziIgienici = wcStr;
1492      }
1493    };
1494    if (typeof root._withSsotBypass === 'function') root._withSsotBypass(_write);
1495    else if (typeof window !== 'undefined' && window._tecSsotBypass !== undefined) {
1496      var _pb = window._tecSsotBypass;
1497      window._tecSsotBypass = true;
1498      try { _write(); } finally { window._tecSsotBypass = _pb; }
1499    } else {
1500      _write();
1501    }
1502  };
1503
1504  /** Gate assessment per-luogo: bag assente → non conforme «Da censire — Luogo X». */
1505  root._tab3PerLuogoFieldGate = function (luogo, key) {
1506    if (!luogo) return { ok: false, label: 'Da censire — Luogo (non selezionato)', conforme: false };
1507    var nome = luogo.nome || luogo.label || luogo.id || 'Luogo';
1508    if (typeof root._tab2LuogoFieldVal !== 'function') {
1509      return { ok: false, label: 'Da censire — ' + nome, conforme: false };
1510    }
1511    var d = typeof D === 'function' ? D() : {};
1512    var v = root._tab2LuogoFieldVal(luogo, key, d);
1513    var empty = v == null || v === '' || (Array.isArray(v) && !v.length);
1514    if (empty) return { ok: false, label: 'Da censire — ' + nome, conforme: false };
1515    return { ok: true, label: String(v), conforme: true, value: v };
1516  };
1517
1518  root._tab3TriadeNavTab = function (tabNum, fieldKey) {
1519    var meta = root.TAB3_FIELD_SSC[fieldKey];
1520    if (!meta) return null;
1521    if (meta.tier === P) return 18;
1522    if (meta.tier === C && meta.nav === 'triade_mansioni') return 20;
1523    if (meta.tier === C || meta.tier === S) return 2;
1524    return null;
1525  };
1526
1527  root._tab3LegalResolveNav = function (fieldKey) {
1528    if (!fieldKey) return null;
1529    var meta = root.TAB3_FIELD_SSC[fieldKey];
1530    if (!meta) return null;
1531    var subTabFn = root._legalLuogoSubTabForField || function () { return 'struttura'; };
1532    if (meta.tier === P) {
1533      var sub = meta.luogoSubTab;
1534      if (!sub && root.TAB3_CPI_SSOT_KEYS && root.TAB3_CPI_SSOT_KEYS.indexOf(fieldKey) >
1534= 0) sub = 'antincendio';
1535      if (!sub && typeof window !== 'undefined' && typeof window._dvrLuogoPrincipaleSubTabForField === 'function') {
1536        sub = window._dvrLuogoPrincipaleSubTabForField(fieldKey);
1537      }
1538      if (!sub) sub = subTabFn(fieldKey);
1539      return {
1540        kind: 'luogo',
1541        fieldKey: fieldKey,
1542        tab: 18,
1543        luogoSubTab: sub
1544      };
1545    }
1546    if (meta.tier === S) {
1547      return { kind: 'normal', fieldKey: fieldKey, tab: 2, openT2Sec: meta.openT2Sec };
1548    }
1549    if (meta.tier === C) {
1550      if (meta.nav === 'triade_mansioni') {
1551        return { kind: 'normal', fieldKey: fieldKey, tab: 20 };
1552      }
1553      return { kind: 'normal', fieldKey: fieldKey, tab: 2, openT2Sec: meta.openT2Sec || 'wc' };
1554    }
1555    return null;
1556  };
1557
1558  root._tab3SsotSelfTest = function () {
1559    var errs = [];
1560    var ssc = root.TAB3_FIELD_SSC;
1561    if (!ssc.destinazioneUso || ssc.destinazioneUso.tier !== P) errs.push('destinazioneUso must be P');
1562    if (!ssc.areaEstPresente || ssc.areaEstPresente.tier !== P) errs.push('areaEstPresente must be P');
1563    if (!ssc.ingressoPrincVia || ssc.ingressoPrincVia.tier !== P) errs.push('ingressoPrincVia must be P');
1564    if (!root.TAB3_MIRROR_3ABC_KEYS || root.TAB3_MIRROR_3ABC_KEYS.indexOf('tipoEdificio') < 0) {
1565      errs.push('TAB3_MIRROR_3ABC_KEYS');
1566    }
1567    if (!ssc.nDipendenti || ssc.nDipendenti.tier !== C) errs.push('nDipendenti must be C');
1568    if (!ssc.nBlocchiWCTotale || ssc.nBlocchiWCTotale.tier !== C) errs.push('nBlocchiWCTotale must be C');
1569    if (!ssc.tipoEdificio || ssc.tipoEdificio.tier !== P) errs.push('tipoEdificio must be P');
1570    if (!root.TAB3_FIELD_ENTITY || root.TAB3_FIELD_ENTITY.ingressoPrincVia !== L) errs.push('ingressoPrincVia entity L');
1571    if (root._tab3FieldEntityTier('areaEstPresente') !== L) errs.push('areaEstPresente entity L');
1572    if (root._tab3FieldEntityTier('ingressoPrincVia') !== L) errs.push('ingressoPrincVia entity L');
1573    if (root._tab3FieldEntityTier('osservAccessi') !== O) errs.push('osservAccessi entity O');
1574    if (root._tab3FieldEntityTier('descrizioneLuogo') !== L) errs.push('descrizioneLuogo entity L');
1575    if (!root.TAB3_CPI_SSOT_KEYS || root.TAB3_CPI_SSOT_KEYS.indexOf('statoCPI') < 0) errs.push('TAB3_CPI_SSOT_KEYS');
1576    if (!root.TAB3_CPI_SSOT_PATH || root.TAB3_CPI_SSOT_PATH.indexOf('impianti.cpi') < 0) errs.push('TAB3_CPI_SSOT_PATH');
1577    if (typeof root._tab3SyncCpiBidirectional !== 'function') errs.push('_tab3SyncCpiBidirectional missing');
1578    if (typeof root._setCpiSsotField !== 'function') errs.push('_setCpiSsotField missing');
1579    if (typeof root._tab3SyncEdificioBidirectional !== 'function') errs.push('_tab3SyncEdificioBidirectional missing');
1580    if (typeof root._tab3SyncPrincipaleMirrorToFlat !== 'function') errs.push('_tab3SyncPrincipaleMirrorToFlat missing');
1581    if (typeof root._tab3PushPrincipaleMirrorToFlat !== 'function') errs.push('_tab3PushPrincipaleMirrorToFlat missing');
1582    if (typeof root._tab3SyncPrincipaleMirrorToFlat === 'function') {
1583      var dMir = { tipoEdificio: 'Vecchio', ambientiSede: [{ id: 'p', principale: true }] };
1584      var lMir = { id: 'p', principale: true, tipoEdificio: 'Capannone industriale', annoCostruzione: '1990' };
1585      var mir = root._tab3SyncPrincipaleMirrorToFlat(dMir, lMir, {});
1586      if (!mir.changed || dMir.tipoEdificio !== 'Capannone industriale' || dMir.annoCostruzione !== '1990') {
1587        errs.push('_tab3SyncPrincipaleMirrorToFlat mono');
1588      }
1589      var dRew = { tipoEdificio: 'Vecchio', annoCostruzione: '1980', statoCPI: 'presente', ambientiSede: [{ id: 'n', principale: true }] };
1590      var lRew = { id: 'n', principale: true, tipoEdificio: 'Ufficio', impianti: { cpi: { statoCPI: '' } } };
1591      var rew = root._tab3SyncPrincipaleMirrorToFlat(dRew, lRew, { rewireAll: true });
1592      if (!rew.changed || dRew.tipoEdificio !== 'Ufficio' || dRew.annoCostruzione !== '' || dRew.statoCPI !== '') {
1593        errs.push('_tab3SyncPrincipaleMirrorToFlat rewireAll');
1594      }
1595    }
1596    if (!root.TAB3_EDIFICIO_SSOT_KEYS || root.TAB3_EDIFICIO_SSOT_KEYS.indexOf('nPianiFT') < 0) errs.push('TAB3_EDIFICIO_SSOT_KEYS');
1597    if (typeof root._tab3GuardEdificioSetD !== 'function') errs.push('_tab3GuardEdificioSetD missing');
1598    if (root._tab3GuardEdificioSetD('tipoEdificio', { ambientiSede: [{ id: 'a' }, { id: 'b' }] }) !== true) {
1599      errs.push('P13 guard tipoEdificio multi');
1600    }
1601    if (root._tab3GuardEdificioSetD('tipoEdificio', { ambientiSede: [{ id: 'a', principale: true }] }) !== true) {
1602      errs.push('P13 guard tipoEdificio mono con luogo');
1603    }
1604    if (root._tab3GuardEdificioSetD('tipoEdificio', { ambientiSede: [] }) !== false) {
1605      errs.push('P13 guard tipoEdificio senza luoghi deve passare');
1606    }
1607    if (typeof root._tab3GuardPrincipaleMirrorSetD !== 'function') errs.push('_tab3GuardPrincipaleMirrorSetD missing');
1608    if (root._tab3GuardPrincipaleMirrorSetD('statoCPI', { ambientiSede: [{ id: 'a' }] }) !== true) {
1609      errs.push('P13 mirror guard statoCPI');
1610    }
1611    if (typeof root._tab3OperationalAmbientiList !== 'function') errs.push('_tab3OperationalAmbientiList missing');
1612    if (root._tecGmrEpcLuoghi && typeof root._tab3CountLuoghi === 'function') {
1613      var dLeak = {
1614        ambientiSede: [
1615          { id: 'a', nome: 'Sede operativa', principale: true },
1616          { id: 'tec', nome: 'TECNOLOGICA SERVICE s.r.l.' }
1617        ],
1618        ragioneSociale: 'GMR s.r.l.'
1619      };
1620      if (root._tab3CountLuoghi(dLeak) !== 1) errs.push('operational count excludes consultant leak');
1621      if (root._tab3GuardEdificioSetD('tipoEdificio', dLeak) !== false) {
1622        errs.push('consultant leak must not trigger multi-luogo guard');
1623      }
1624    }
1625    if (typeof root._tab3LegalResolveNav !== 'function') errs.push('_tab3LegalResolveNav missing');
1626    var navS = root._tab3LegalResolveNav('destinazioneUso');
1627    if (!navS || navS.tab !== 18 || navS.luogoSubTab !== 'struttura') errs.push('Legal nav destinazioneUso → tab 18 struttura');
1628    var navB = root._tab3LegalResolveNav('areaEstPresente');
1629    if (!navB || navB.tab !== 18 || navB.luogoSubTab !== 'struttura') errs.push('Legal nav areaEstPresente → tab 18 struttura');
1630    var navC3 = root._tab3LegalResolveNav('ingressoPrincVia');
1631    if (!navC3 || navC3.tab !== 18) errs.push('Legal nav ingressoPrincVia â
1631†’ tab 18');
1632    var navP = root._tab3LegalResolveNav('tipoEdificio');
1633    if (!navP || navP.tab !== 18) errs.push('Legal nav tipoEdificio → tab 18');
1634    var navC = root._tab3LegalResolveNav('nDipendenti');
1635    if (!navC || navC.tab !== 20) errs.push('Legal nav nDipendenti → tab 20');
1636    ['soggettoVVF', 'statoCPI', 'attivitaDPR151', 'rischioIncendioNuovo', 'gruppoPS'].forEach(function (k) {
1637      var nav = root._tab3LegalResolveNav(k);
1638      if (!nav || nav.tab !== 18 || nav.luogoSubTab !== 'antincendio') errs.push('Legal nav CPI ' + k + ' → tab 18 antincendio');
1639    });
1640    if (typeof root._tab3RenderCpiTab2Panel === 'function') {
1641      var dMock = {
1642        soggettoVVF: 'si', statoCPI: 'presente', attivitaDPR151: 'no',
1643        rischioIncendioVecchio: 'medio', rischioIncendioNuovo: '2', gruppoPS: 'C',
1644        ambientiSede: [{ id: 'a1', principale: true }, { id: 'a2' }]
1645      };
1646      var html = root._tab3RenderCpiTab2Panel(dMock, { SEC: function (_id, _ic, _t, b) { return b; } });
1647      if (html.indexOf('data-field-key="soggettoVVF"') < 0) errs.push('CPI panel missing data-field-key soggettoVVF');
1648      if (html.indexOf('data-field-key="statoCPI"') < 0) errs.push('CPI panel missing data-field-key statoCPI');
1649      if (html.indexOf('data-field-key="rischioIncendioNuovo"') < 0) errs.push('CPI panel missing rischioIncendioNuovo');
1650      if (html.indexOf('<select') < 0 || html.indexOf('rischioIncendioNuovo') < 0) errs.push('CPI multi: rischioIncendioNuovo select');
1651      if (typeof root._proponiClasseIncendio === 'function') {
1652        if (html.indexOf('Suggerito:') < 0) errs.push('CPI multi: badge Suggerito');
1653        if (html.indexOf('data-suggested="1"') < 0) errs.push('CPI multi: option suggerita ★');
1654      }
1655    }
1656    if (typeof root._tab3CpiSuggestFor !== 'function') errs.push('_tab3CpiSuggestFor missing');
1657    if (typeof root._tab2LuogoFieldVal !== 'function') errs.push('_tab2LuogoFieldVal missing');
1658    else {
1659      var amb = { superficie_mq: 15, capienza_max: 8 };
1660      if (root._tab2LuogoFieldVal(amb, 'superficie') !== 15) errs.push('_tab2LuogoFieldVal superficie↔superficie_mq');
1661      if (root._tab2LuogoFieldVal(amb, 'maxPresenze') !== 8) errs.push('_tab2LuogoFieldVal maxPresenze↔capienza_max');
1662      var cop = { superficieCoperta: '120' };
1663      if (root._tab2LuogoFieldVal(cop, 'superficieCoperta') !== '120') errs.push('_tab2LuogoFieldVal superficieCoperta');
1664      var luArr = {
1665        id: 'p1', principale: true,
1666        piani: [{ nome: 'Piano Terra', locali: ['Reception'] }],
1667        serviziIgienici: { blocchiWC: [{ piano: 'PT', nBlocchi: '2' }] }
1668      };
1669      if (!Array.isArray(root._tab2LuogoFieldVal(luArr, 'piani')) || root._tab2LuogoFieldVal(luArr, 'piani').length !== 1) {
1670        errs.push('_tab2LuogoFieldVal piani array');
1671      }
1672      if (!Array.isArray(root._tab2LuogoFieldVal(luArr, 'blocchiWC')) || root._tab2LuogoFieldVal(luArr, 'blocchiWC').length !== 1) {
1673        errs.push('_tab2LuogoFieldVal blocchiWC array');
1674      }
1675    }
1676    if (typeof root._tab3ResolveExportField !== 'function') errs.push('_tab3ResolveExportField missing');
1677    else {
1678      var dExp = {
1679        tipoEdificio: 'Legacy flat',
1680        accessiCarrabili: [{ via: 'Via Tab3' }],
1681        ambientiSede: [{ id: 'p1', principale: true, tipoEdificio: 'Da grafo' }]
1682      };
1683      root._tab3CountLuoghi = function () { return 1; };
1684      if (root._tab3ResolveExportField('tipoEdificio', dExp) !== 'Da grafo') {
1685        errs.push('_tab3ResolveExportField principale before d.*');
1686      }
1687      if (!Array.isArray(root._tab3ResolveExportField('accessiCarrabili', dExp)) ||
1688          root._tab3ResolveExportField('accessiCarrabili', dExp)[0].via !== 'Via Tab3') {
1689        errs.push('_tab3ResolveExportField tier S accessiCarrabili');
1690      }
1691      var dSede = { tipoEdificio: '', ambientiSede: [{ id: 'p1', principale: true }] };
1692      root.CLIENTI_LIST = [{
1693        id: 'cl1',
1694        sedi: [{ sedeId: 's1', tipoEdificio: 'Da sede anagrafica' }]
1695      }];
1696      root.CP = function () { return { clienteMasterId: 'cl1', sedeId: 's1' }; };
1697      if (root._tab3ResolveExportField('tipoEdificio', dSede) !== 'Da sede anagrafica') {
1698        errs.push('_tab3ResolveExportField sede fallback');
1699      }
1700    }
1701    if (typeof root._tab3SyncGrafoArrayToPrincipale !== 'function') errs.push('_tab3SyncGrafoArrayToPrincipale missing');
1702    else {
1703      var dG = { ambientiSede: [{ id: 'p1', principale: true, piani: [] }] };
1704      root._tab3CountLuoghi = function () { return 1; };
1705      root._tab3ResolvePrincipaleAmb = function (dd) {
1706        return (dd.ambientiSede || []).filter(function (a) { return a && a.principale; })[0] || null;
1707      };
1708      var syncP = root._tab3SyncGrafoArrayToPrincipale(dG, 'piani', [{ nome: 'Terra' }]);
1709      if (!syncP.changed || !dG.ambientiSede[0].piani || dG.ambientiSede[0].piani[0].nome !== 'Terra') {
1710        errs.push('_tab3SyncGrafoArrayToPrincipale piani');
1711      }
1712    }
1713    return { ok: !errs.length, errors: errs, count: Object.keys(ssc).length };
1714  };
1715
1716  (function installTab3SetDHook() {
1717    var prevAfter = root._ssotAfterSetD;
1718    root._ssotAfterSetD = function (p, key, val) {
1719      if (typeof prevAfter === 'function') prevAfter(p, key, val);
1720      if (!p || !p.data || !key || root.TAB3_GRAFO_ARRAY_KEYS.indexOf(key) < 0) return;
1721      if (typeof root._tab3SyncGrafoArrayToPrincipale === 'function') {
1722        root._tab3SyncGrafoArrayToPrincipale(p.data, key, val);
1723      }
1724    };
1725  })();
1726
1727})(typeof window !== 'undefined' ? window : globalThis);

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.