PageSourceSearch

https://tecdvr-v7.web.app/js/tec-acam-piano.js?v=20.374.20

js tecdvr-v7.web.app collected 2026-10-03 12:41:05 UTC 134,500 bytes, 1,990 lines download raw bytes

1/**
2 * tec-acam-piano.js — IL PIANO DELLE AZIONI CORRETTIVE E DI MIGLIORAMENTO (ACAM).
3 *
4 * ── LA RICHIESTA, NELLE SUE PAROLE ────────────────────────────────────────────────────────────
5 * Beniamino Macchiarola: «apprezzerei se la integrassi anche con il piano di ACAM. Attendo al
6 * contempo la tua proposta che costituirà l'allegato nuovo al pacchetto DVR. Questa dovrà essere
7 * corredata anche dalle ACAM. Naturalmente, la versione allegata al pacchetto dovrà essere ed
8 * apparire outstanding!»
9 *
10 * ── CHE COSA E' ACAM QUI DENTRO, E DA DOVE VIENE ─────────────────────────────────────────────
11 * Non e' una parola nuova: e' il modello che questo progetto si e' dato nell'assessment ACAM
12 * (v20.373.118). Cinque requisiti, e questo file esiste per renderli MISURABILI e VISIBILI:
13 *   R1  ogni deviazione propone un'azione — un rilievo senza azione e' un'accusa che non porta
14 *       da nessuna parte
15 *   R2  l'azione e' assegnabile e formalizzabile: chi, entro quando, con quale gravita'
16 *       (art. 28 c.2 lett. c e d)
17 *   R3  l'azione aggredisce la CAUSA e sceglie il livello PIU' ALTO PRATICABILE della gerarchia
18 *       (art. 15 c.1 lett. c, e, f, i · ISO 45001 §10.2 d)
19 *   R4  la chiusura e' DIMOSTRATA efficace, non dichiarata (ISO 45001 §10.2 f)
20 *   R5  il tutto protegge il cliente: linguaggio da consulente, qualificazione giuridica corretta
21 *
22 * ── LA RIGA DELLA LEGGE CHE NESSUNO CITA, E CHE E' LA PIU' PESANTE ──────────────────────────
23 * Art. 28 c.2 lett. d): i ruoli che devono provvedere alle misure vanno assegnati «unicamente a
24 * soggetti in possesso di adeguate COMPETENZE E POTERI». Un campo «responsabile» con dentro un
25 * nome non basta: un'azione che costa, assegnata a chi non ha potere di spesa, e' formalmente
26 * viziata. E' il requisito piu' trascurato del prodotto e del mercato.
27 * Art. 55 c.3: il programma di miglioramento mancante e' una contravvenzione autonomamente
28 * sanzionata — l'unico gap di questo modulo che ha un capo d'imputazione.
29 *
30 * ── NON CALCOLA: INTERROGA. E NON SCRIVE ────────────────────────────────────────────────────
31 * Ogni numero ha gia' il suo proprietario e questo file non ne fabbrica di nuovi:
32 *   · l'elenco unico delle azioni      → _tecDvrProgramma.collectAzioniUnified
33 *   · puntualita', efficacia, cause    → _tecProgrammaMisura.calcola
34 *   · la gerarchia delle misure        → _tecMisuraGerarchia.distribuzione
35 *   · la copertura dei rilievi         → _tecProgrammaCopertura.copertura
36 *   · corsia, stato reale, blocco 55.3 → _tecAzioneProposta
37 *   · la scala di gravita' e i colori  → _tecDashboardRischi.FASCE / coloreGravita
38 * Se un proprietario manca, l'indicatore si DICHIARA non misurabile invece di valere zero: uno
39 * zero inventato e' peggio di un buco dichiarato, perche' sembra un risultato.
40 *
41 * ── UNA VISTA, DUE LETTORI ──────────────────────────────────────────────────────────────────
42 * `vista(d)` produce la struttura; `html(d)` la disegna per il tecnico; `righeDocumento(v)` la
43 * traduce per l'allegato del pacchetto. Nessuno dei due ricalcola: e' la regola nata il giorno in
44 * cui abbiamo misurato 23 motori che parlavano solo al documento.
45 *
46 * Espone window._tecAcamPiano.
47 */
48(function (win) {
49  'use strict';
50  if (typeof win === 'undefined') return;
51
52  var VERSION = '20.373.142';
53
54  function _s(v) { return String(v == null ? '' : v).trim(); }
55  function _n(v) { var x = Number(v); return isNaN(x) ? 0 : x; }
56  function _arr(v) { return Array.isArray(v) ? v.filter(Boolean) : []; }
57  function _pct(a, b) { return b > 0 ? Math.round((a / b) * 100) : null; }
58
59  /**
60   * I NOVE INDICATORI, CIASCUNO CON IL SUO BERSAGLIO DICHIARATO.
61   * ISO 45001 §9.1.1 c) impone di determinare i CRITERI rispetto ai quali valutare, non solo di
62   * misurare. Un indicatore senza bersaglio e' un numero: non dice se va bene. I bersagli sono
63   * qui, leggibili e discutibili — un numero che nessuno puo' discutere e' un numero di cui
64   * nessuno si fida.
65   */
66  var KPI_DEF = [
67    { id: 'copertura', label: 'Copertura degli obblighi', verso: 'su',
68      definizione: '% di obblighi di trattamento con almeno un’azione', target: 100,
69      norma: 'art. 28 c.2 lett. b, c e d · art. 29 c.1 · R1',
70      perche: 'Il denominatore sono TUTTI gli obblighi: rischi oltre soglia, non conformità rilevate e adempimenti di legge. Sotto il 100% c’è un obbligo orfano — nominato dalla valutazione e chiuso da nessuno.' },
71    { id: 'formalizzazione', label: 'Formalizzazione', verso: 'su',
72      definizione: '% di azioni con responsabile E scadenza', target: 100,
73      norma: 'art. 28 c.2 lett. c e d · art. 55 c.3 · R2',
74      perche: 'È l’unico gap penalmente sanzionato: un impegno senza nome e senza data non impegna nessuno.' },
75    { id: 'poteri', label: 'Competenze e poteri', verso: 'su',
76      definizione: '% di azioni onerose il cui responsabile ha potere di spesa', target: 100,
77      norma: 'art. 28 c.2 lett. d, inciso finale',
78      perche: 'La legge non chiede un nome: chiede un soggetto con poteri adeguati. Un’azione che costa, assegnata a chi non può spendere, è formalmente viziata.' },
79    { id: 'causa', label: 'Analisi della causa', verso: 'su',
80      definizione: '% di azioni con la causa registrata', target: 80,
81      norma: 'ISO 45001 §10.2 b) · R3',
82      perche: 'Senza causa ogni azione è una correzione travestita da azione correttiva, e la ricorrenza resta invisibile.' },
83    { id: 'indiceGerarchico', label: 'Indice gerarchico', verso: 'su',
84      definizione: '% di azioni sopra il livello amministrativo (gradini 1-3)', target: 50,
85      norma: 'art. 15 c.1 lett. c), f) e i) · ISO 45001 §8.1.2',
86      perche: 'È l’unica metrica che misura la QUALITÀ dell’azione anziché la sua puntualità. Una coda concentrata su procedure e DPI è il segnale precoce di un programma giuridicamente esposto.',
87      proposta: true },
88    { id: 'puntualita', label: 'Chiusura puntuale', verso: 'su',
89      definizione: '% di azioni attuate entro la data attesa', target: 85,
90      norma: 'art. 28 c.2 lett. c',
91      perche: 'Sotto questa soglia il programma non governa più le proprie scadenze: le subisce.' },
92    { id: 'scadute', label: 'Scadute', verso: 'giu',
93      definizione: '% di azioni aperte oltre la scadenza', target: 10,
94      norma: 'art. 28 c.2 lett. c · art. 29 c.3',
95      perche: 'Una scadenza passata resta scritta nel documento, ed è la prima cosa che un ispettore confronta con la data odierna.' },
96    { id: 'efficacia', label: 'Efficacia verificata', verso: 'su',
97      definizione: '% di azioni verificate che risultano efficaci', target: 90,
98      norma: 'ISO 45001 §10.2 f) · Cass. pen. Sez. 4 n. 12122/2023 · R4',
99      perche: 'La Cassazione misura la responsabilità del datore sull’IDONEITÀ della misura, non sulla sua adozione. Questo è l’unico indicatore che la documenta.' },
100    { id: 'ricorrenza', label: 'Ricorrenza', verso: 'giu',
101      definizione: '% di azioni riaperte dopo la chiusura', target: 5,
102      norma: 'ISO 45001 §10.2 b) 2)',
103      perche: 'Ricorrenza alta con puntualità alta significa che si stanno chiudendo CORREZIONI spacciandole per azioni correttive.' }
104  ];
105
106  function _kpiDef(id) {
107    for (var i = 0; i < KPI_DEF.length; i++) if (KPI_DEF[i].id === id) return KPI_DEF[i];
108    return null;
109  }
110
111  /**
112   * Un indicatore. `valore` null significa NON MISURABILE, e in quel caso porta il perche':
113   * la differenza fra «zero» e «non lo so» e' la differenza fra un cruscotto e un oroscopo.
114   */
115  function _kpi(id, valore, base, perche) {
116    var def = _kpiDef(id) || {};
117    var v = (valore == null || base === 0) ? null : Math.round(_n(valore));
118    var esito = 'non_misurabile';
119    if (v != null) {
120      esito = (def.verso === 'giu') ? (v <= _n(def.target) ? 'centrato' : 'sotto')
121                                    : (v >= _n(def.target) ? 'centrato' : 'sotto');
122    }
123    return {
124      id: id, label: def.label, definizione: def.definizione, norma: def.norma, perche: def.perche,
125      proposta: !!def.proposta, verso: def.verso, target: _n(def.target),
126      targetTesto: (def.verso === 'giu' ? '≤ ' : '≥ ') + _n(def.target) + '%',
127      valore: v, base: _n(base), esito: esito,
128      nonMisurabilePerche: v == null ? _s(perche || 'nessuna azione su cui misurare') : ''
129    };
130  }
131
132  // ── LE AZIONI: UN SOLO ELENCO = Registro Unico (stesso codice di Tab 11 e Quadro) ─
133  function azioniDi(d) {
134    var RR = win.RegistroRilievi || win.TecRegistroRilievi;
135    if (RR) {
136      try {
137        if (typeof RR.migra === 'function') RR.migra(d || {});
138        if (typeof RR.backfillCodici === 'function') RR.backfillCodici(d || {});
139        if (typeof RR.lista === 'function') {
140          /* Piano = solo accettate (le proposte restano nella Decisione). */
141          return _arr(RR.lista(d, { accettato: true }));
142        }
143      } catch (_rr) {}
144    }
145    var P = win._tecDvrProgramma;
146    if (P && typeof P.collectAzioniUnified === 'function') {
147      try { return _arr(P.collectAzioniUnified(d, null, {})); } catch (_e) {}
148    }
149    return _arr(d && d.azioniMiglioramento);
150  }
151
152  function _haResp(a) { return !!_s(a && (a.responsabile || a.chi || (a.ownerOperativo && a.ownerOperativo.nome))); }
153  function _haScad(a) { return !!_s(a && (a.scadenza || a.dataPrevista || a.entro)); }
154  function _costo(a) {
155    var c = a && (a.costoStimato != null ? a.costoStimato : a.costo);
156    var n = parseFloat(String(c == null ? '' : c).replace(/[^\d.,-]/g, '').replace(',', '.'));
157    return isNaN(n) ? 0 : n;
158  }
159  /**
160   * La gravita' di un'azione, letta solo da FATTI. `priorita` non e' un fatto: l'elenco unico la
161   * normalizza a «Bassa» quando manca il rischio residuo, e quel ripiego, letto come gravita',
162   * mandava in corsia rapida azioni di cui nessuno sapeva nulla. Il rischio residuo invece e'
163   * misurato, e la scala e' quella del prodotto (P x D).
164   */
165  function _gravitaDi(a) {
166    var g = _s(a && a.gravita).toLowerCase();
167    if (['bassa', 'media', 'alta', 'critica'].indexOf(g) >= 0) return g;
168    var r = Number(a && (a.rResiduoPre != null ? a.rResiduoPre : a.R));
169    if (!isNaN(r) && r > 0) {
170      if (r >= 12) return 'critica';
171      if (r >= 8) return 'alta';
172      if (r >= 4) return 'media';
173      return 'bassa';
174    }
175    return '';
176  }
177
178  function _poteriOk(a) {
179    var o = a && a.ownerDecisore;
180    if (o && typeof o.potereDiSpesa === 'boolean') return o.potereDiSpesa;
181    return null; // non dichiarato: non e' un no, e' un buco
182  }
183
184  /**
185   * LA VISTA. Un solo calcolo, due lettori.
186   */
187  function vista(d) {
188    d = d || {};
189    var az = azioniDi(d);
190    var out = {
191      VERSION: VERSION,
192      norma: 'art. 28 c.2 lett. c e d · art. 15 c.1 · art. 55 c.3 D.Lgs. 81/08 · ISO 45001 §10.2',
193      totale: az.length,
194      kpi: [], allarmi: [], corsie: { rapida: 0, standard: 0, estesa: 0, nonDeterminata: 0 },
195      gerarchia: null, misura: null, copertura: null, generabile: null,
196      responsabilita: { conResponsabile: 0, conScadenza: 0, formalizzate: 0,
197                        onerose: 0, oneroseConPotere: 0, onerosePotereNonDichiarato: 0 }
198    };
199
200    // corsie — il rigore proporzionato alla gravita' (ICH Q10, ripreso nell'assessment)
201    var AP = win._tecAzioneProposta;
202    az.forEach(function (a) {
203      var g = _gravitaDi(a);
204      var c = null;
205      // senza gravita' NON si sceglie una corsia: si dichiara di non averla. Il difetto misurato
206      // il 21/08/2026 in browser: l'elenco unico normalizza la priorita' a «Bassa» quando manca
207      // il rischio residuo, e quel valore di ripiego finiva letto come gravita' bassa — quindi
208      // corsia RAPIDA, la meno rigorosa, su azioni di cui nessuno conosceva la gravita'. Un
209      // ripiego di una colonna non puo' diventare il criterio di un'altra.
210      if (g && AP && typeof AP.corsiaDi === 'function') {
211        try { c = AP.corsiaDi(g, { ricorrente: _n(a && a.riaperture) > 0 }); } catch (_eC) { c = null; }
212      }
213      if (c && out.corsie[c] !== undefined) out.corsie[c]++; else out.corsie.nonDeterminata++;
214      if (_haResp(a)) out.responsabilita.conResponsabile++;
215      if (_haScad(a)) out.responsabilita.conScadenza++;
216      if (_haResp(a) && _haScad(a)) out.responsabilita.formalizzate++;
217      if (_costo(a) > 0) {
218        out.responsabilita.onerose++;
219        var p = _poteriOk(a);
220        if (p === true) out.responsabilita.oneroseConPotere++;
221        else if (p === null) out.responsabilita.onerosePotereNonDichiarato++;
222      }
223    });
224
225    // gli indicatori del programma — proprietario: _tecProgrammaMisura
226    var PM = win._tecProgrammaMisura;
227    if (PM && typeof PM.calcola === 'function') {
228      try { out.misura = PM.calcola(az, {}); } catch (_eM) { out.misura = null; }
229    }
230    if (out.misura) out.gerarchia = out.misura.gerarchia || null;
231
232    // ── v20.373.139 — LA COPERTURA SI LEGGE DAL REGISTRO DEGLI OBBLIGHI ────────────────────
233    // Prima si chiedeva la copertura dei soli RILIEVI. Ma un rilievo non e' l'unico modo in cui
234    // nasce un obbligo: ci sono i rischi oltre soglia e gli adempimenti di legge, e quelli non
235    // comparivano nel denominatore. Risultato: «copertura 100%» era possibile con undici rischi
236    // in banda ALARP e tre registri estintori mai aperti.
237    // Ora il denominatore e' l'UNIVERSO DEGLI OBBLIGHI, e questo file smette di avere una propria
238    // idea di che cosa vada coperto: la chiede a chi la possiede.
239    var OB = win._tecAcamObblighi;
240    if (OB && typeof OB.obblighi === 'function') {
241      try {
242        var reg = OB.obblighi(d);
243        out.obblighi = reg;
244        out.copertura = { rilievi: reg.totale, coperte: reg.coperti, scoperte: reg.scoperti,
245                          dovuti: reg.dovuti, dovutiScoperti: reg.dovutiScoperti };
246      } catch (_eK) { out.obblighi = null; out.copertura = null; }
247    } else {
248      out.obblighi = null;
249      out.copertura = null;   // dichiarato non misurabile: il proprietario degli obblighi non c'e'
250    }
251
252    // il blocco dell'art. 55 c.3 — proprietario: _tecAzioneProposta
253    if (AP && typeof AP.programmaGenerabile === 'function') {
254      try { out.generabile = AP.programmaGenerabile(az); } catch (_eG) { out.generabile = null; }
255    }
256
257    var m = out.misura, R = out.responsabilita;
258    var sopra = out.gerarchia ? (_n(out.gerarchia.totale) - _n(out.gerarchia.quotaBassa) - _n(out.gerarchia.senzaLivello)) : null;
259
260    out.kpi = [
261      _kpi('copertura', out.copertura ? _pct(out.copertura.coperte, out.copertura.rilievi) : null,
262           out.copertura ? out.copertura.rilievi : 0,
263           out.copertura ? 'nessun obbligo di trattamento aperto' : 'il registro degli obblighi non è caricato'),
264      _kpi('formalizzazione', _pct(R.formalizzate, out.totale), out.totale, 'nessuna azione programmata'),
265      _kpi('poteri', R.onerose ? _pct(R.oneroseConPotere, R.onerose) : null, R.onerose,
266           R.onerose ? '' : 'nessuna azione dichiara un costo: il potere di spesa non è ancora in gioco'),
267      _kpi('causa', m ? (m.indicatori.causaRegistrata.su ? m.indicatori.causaRegistrata.valore : null) : null,
268           m ? m.totale : 0, 'nessuna azione su cui misurare'),
269      _kpi('indiceGerarchico', (out.gerarchia && out.gerarchia.totale) ? _pct(sopra, out.gerarchia.totale) : null,
270           out.gerarchia ? out.gerarchia.totale : 0, 'la gerarchia non è classificata su alcuna azione'),
271      _kpi('puntualita', (m && m.attuate) ? m.indicatori.puntualita.valore : null, m ? m.attuate : 0,
272           'nessuna azione è ancora stata attuata: la puntualità non ha base'),
273      _kpi('scadute', (m && m.totale) ? _pct(m.scadute, m.totale) : null, m ? m.totale : 0, 'nessuna azione programmata'),
274      _kpi('efficacia', (m && m.verificate) ? _pct(m.verificateEfficaci, m.verificate) : null, m ? m.verificate : 0,
275           'nessuna verifica di efficacia è ancora stata registrata'),
276      _kpi('ricorrenza', (m && m.attuate) ? _pct(m.conRiaperture, m.attuate) : null, m ? m.attuate : 0,
277           'nessuna azione chiusa: la ricorrenza non ha base')
278    ];
279
280    out.nCentrati = out.kpi.filter(function (k) { return k.esito === 'centrato'; }).length;
281    out.nSotto = out.kpi.filter(function (k) { return k.esito === 'sotto'; }).length;
282    out.nNonMisurabili = out.kpi.filter(function (k) { return k.esito === 'non_misurabile'; }).length;
283    out.allarmi = allarmi(out);
284    out.nAllarmiGravi = out.allarmi.filter(function (a) { return a.gravita === 1; }).length;
285    return out;
286  }
287
288  /**
289   * Gli allarmi ACAM. Stessa forma e stessa porta di quelli del cruscotto rischi — il tecnico
290   * non deve imparare due linguaggi per due elenchi che gli chiedono la stessa cosa: vai li' e
291   * chiudila. Il colore lo possiede _tecDashboardRischi: qui NON si apre una seconda tavolozza.
292   */
293  function allarmi(v) {
294    var out = [];
295    var D = win._tecDashboardRischi;
296    function colore(g) {
297      if (D && typeof D.coloreGravita === 'function') return D.coloreGravita(g);
298      return '#64748B'; // il proprietario del colore non c'e': grigio dichiarato, non un rosso inventato
299    }
300    function tabProgramma() {
301      return (D && D.PORTE && D.PORTE.programma) ? D.PORTE.programma.tab : null;
302    }
303    function A(g, t, p, n) {
304      var spec = { tab: tabProgramma(), campo: 'azioniMiglioramento', ancora: 'programma-azioni',
305                   colore: colore(g), titolo: t, norma: n };
306      out.push({ gravita: g, titolo: t, perche: p, norma: n, colore: spec.colore,
307                 dove: 'Linguetta «11. Programma Miglioramento»', tab: spec.tab,
308                 vai: spec.tab == null ? '' : ('window._tecDashboardRischi.apri(' + JSON.stringify(spec) + ')') });
309    }
310
311    if (v.generabile && v.generabile.ok === false) {
312      _arr(v.generabile.motivi).forEach(function (mo) {
313        A(1, 'Il programma non è generabile', mo, v.generabile.norma || 'art. 55 c.3');
314      });
315    }
316    var senzaForm = _n(v.totale) - _n(v.responsabilita.formalizzate);
317    if (senzaForm > 0) {
318      A(1, senzaForm + ' azioni su ' + v.totale + ' senza responsabile o senza data',
319        'L’art. 28 c.2 lett. c) chiede le misure «per il miglioramento nel tempo» e la lett. d) i ruoli che vi debbono '
320        + 'provvedere. Un impegno senza nome e senza data non impegna nessuno, e la carenza è sanzionata dall’art. 55 c.3.',
321        'art. 28 c.2 lett. c e d · art. 55 c.3');
322    }
323    if (v.responsabilita.onerosePotereNonDichiarato > 0) {
324      A(2, v.responsabilita.onerosePotereNonDichiarato + ' azioni che costano, senza potere di spesa dichiarato',
325        'L’art. 28 c.2 lett. d) chiede soggetti «in possesso di adeguate competenze E POTERI». Un nome non basta: '
326        + 'un’azione onerosa assegnata a chi non può spendere è formalmente viziata, ed è il requisito che nessuno controlla.',
327        'art. 28 c.2 lett. d');
328    }
329    var ig = null;
330    v.kpi.forEach(function (k) { if (k.id === 'indiceGerarchico') ig = k; });
331    if (ig && ig.esito === 'sotto') {
332      A(2, 'Indice gerarchico al ' + ig.valore + '%: le misure stanno in basso nella gerarchia',
333        'Meno di metà delle azioni agisce sopra il livello amministrativo. Il rischio viene gestito con il '
334        + 'comportamento del lavoratore invece che rimosso: è la scelta che l’art. 15 c.1 lett. i) mette per ultima.',
335        'art. 15 c.1 lett. c), f) e i)');
336    }
337    var eff = null, ric = null;
338    v.kpi.forEach(function (k) { if (k.id === 'efficacia') eff = k; if (k.id === 'ricorrenza') ric = k; });
339    if (eff && eff.esito === 'non_misurabile' && _n(v.totale) > 0 && v.misura && _n(v.misura.attuate) > 0) {
340      A(1, v.misura.attuate + ' azioni risultano attuate, nessuna verificata efficace',
341        'La chiusura dichiarata non è una chiusura dimostrata. È esattamente il punto su cui la Cassazione misura '
342        + 'la responsabilità del datore: non l’adozione della misura, la sua IDONEITÀ.',
343        'ISO 45001 §10.2 f) · Cass. pen. Sez. 4 n. 12122/2023');
344    }
345    if (ric && ric.esito === 'sotto') {
346      A(2, 'Ricorrenza al ' + ric.valore + '%: azioni chiuse e riaperte',
347        'Ogni riapertura è la prova che la causa individuata non era quella. Si stanno chiudendo correzioni '
348        + 'spacciandole per azioni correttive.', 'ISO 45001 §10.2 b) 2)');
349    }
350    var ca = null;
351    v.kpi.forEach(function (k) { if (k.id === 'causa') ca = k; });
352    if (ca && ca.esito === 'sotto') {
353      A(3, 'Causa registrata solo sul ' + ca.valore + '% delle azioni',
354        'Senza la causa, l’azione agisce sull’effetto e il rilievo tornerà.', 'ISO 45001 §10.2 b)');
355    }
356    return out.sort(function (a, b) { return a.gravita - b.gravita; });
357  }
358
359  /**
360   * Le righe per il PACCHETTO. Stessa vista, seconda lettura: il documento non ricalcola nulla.
361   * @returns Array<[indicatore, valore, bersaglio, norma]>
362   */
363  function righeDocumento(v) {
364    v = v || {};
365    return _arr(v.kpi).map(function (k) {
366      return [
367        k.label + ' — ' + k.definizione,
368        k.valore == null ? ('non misurabile: ' + k.nonMisurabilePerche)
369                         : (k.valore + '%' + (k.base ? ' (base: ' + k.base + ')' : '')),
370        k.targetTesto,
371        k.norma
372      ];
373    });
374  }
375
376  /** Le azioni, pronte per la tabella dell'allegato: i cinque dati minimi dell'art. 28 c.2 c/d. */
377  function righeAzioni(d) {
378    return azioniDi(d).map(function (a, i) {
379      var G = win._tecMisuraGerarchia;
380      var liv = '';
381      var _urgTempo = (!_s(a.scadenza || a.dataPrevista || a.entro) && win._tecAzioniRilievi && win._tecAzioniRilievi.classificaUrgenza)
382        ? win._tecAzioniRilievi.classificaUrgenza(Number(a.rResiduoPre != null ? a.rResiduoPre : a.R)) : null;
383      if (G && typeof G.livelloEffettivo === 'function') {
384        try { liv = _s(G.livelloEffettivo(a).livello); } catch (_e) { liv = ''; }
385      }
386      var lm = null;
387      if (liv && G && typeof G.livello === 'function') { try { lm = G.livello(liv); } catch (_e2) { lm = null; } }
388      return {
389        codice: _s(a.codice) || ('AM-' + String(i + 1).padStart(2, '0')),
390        cosa: _s(a.descrizione || a.azione || a.misura),
391        nelTempo: _s(a.scadenza || a.dataPrevista || a.entro),
392        come: lm ? _s(lm.label) : (liv || ''),
393        comeRango: lm ? _n(lm.rango) : null,
394        chi: _s(a.responsabile || a.chi || (a.ownerOperativo && a.ownerOperativo.nome)),
395        potereDiSpesa: _poteriOk(a),
396        oneroso: _costo(a) > 0,
397        stato: _s(a.stato),
398        rischio: _s(a.rischioCod), norma: _s(a.norma),
399        causa: _s(a.causa && a.causa.testo), causaTipo: _s(a.causa && a.causa.tipo),
400        assegnazione: (win._tecAcamPersone ? win._tecAcamPersone.risolvi(_s(a.responsabile || a.chi || (a.ownerOperativo && a.ownerOperativo.nome)), d) : null),
401        avvisiAssegnazione: (win._tecAcamPersone ? win._tecAcamPersone.avvisi(a, d) : []),
402        nelTempoPropostoGiorni: _urgTempo ? _urgTempo.giorni : null,
403        nelTempoPropostoMotivo: _urgTempo ? _urgTempo.motivo : '',
404        efficaciaSulRischio: (a.efficaciaSulRischio || null),
405        efficaciaEsito: _s(a.verifica && a.verifica.esito)
406      };
407    });
408  }
409
410  function selfTest() {
411    var e = [];
412    if (KPI_DEF.length !== 9) e.push('gli indicatori dichiarati non sono nove');
413    KPI_DEF.forEach(function (k) {
414      if (!_s(k.norma)) e.push('l indicatore ' + k.id + ' non porta la norma che lo fonda');
415      if (!_s(k.perche)) e.push('l indicatore ' + k.id + ' non dice perche il bersaglio e quello');
416      if (['su', 'giu'].indexOf(k.verso) < 0) e.push('l indicatore ' + k.id + ' non dichiara in che verso si legge');
417    });
418    // un indicatore senza base NON vale zero: vale «non lo so», e lo dice
419    var vuoto = _kpi('puntualita', null, 0, 'nessuna azione attuata');
420    if (vuoto.valore !== null) e.push('un indicatore senza base produce un numero inventato');
421    if (vuoto.esito !== 'non_misurabile') e.push('un indicatore senza base si pronuncia lo stesso');
422    if (!_s(vuoto.nonMisurabilePerche)) e.push('un indicatore non misurabile non dice perche');
423    // il verso conta: 10% di scadute e' un successo, 10% di efficacia e' un disastro
424    if (_kpi('scadute', 5, 10).esito !== 'centrato') e.push('un indicatore al ribasso viene letto al rialzo');
425    if (_kpi('scadute', 40, 10).esito !== 'sotto') e.push('il 40% di azioni scadute risulta accettabile');
426    if (_kpi('efficacia', 40, 10).esito !== 'sotto') e.push('un efficacia del 40% risulta accettabile');
427    if (_kpi('efficacia', 95, 10).esito !== 'centrato') e.push('un efficacia del 95% risulta insufficiente');
428
429    // il colore NON e' di questo file: se il proprietario c'e', si usa il suo
430    var D = win._tecDashboardRischi;
431    if (D && typeof D.coloreGravita === 'function') {
432      var vf = { totale: 4, responsabilita: { formalizzate: 0, onerose: 0, oneroseConPotere: 0, onerosePotereNonDichiarato: 0 },
433                 kpi: [], generabile: null, misura: null };
434      var a = allarmi(vf);
435      if (!a.length) e.push('quattro azioni senza responsabile ne data non producono alcun allarme');
436      if (a[0].colore !== D.coloreGravita(a[0].gravita)) e.push('il colore dell allarme ACAM non viene dal proprietario della scala');
437      if (!_s(a[0].vai)) e.push('un allarme ACAM non porta la porta per andarci');
438      if (!/art\. 55/.test(a[0].norma + a[0].perche)) e.push('l allarme sulla formalizzazione non cita la contravvenzione che la sanziona');
439    }
440    // la gravita si legge dai fatti: la priorita di ripiego NON e un fatto
441    if (_gravitaDi({ priorita: 'Bassa' }) !== '') e.push('la priorita di ripiego viene letta come gravita: azioni ignote finirebbero in corsia rapida');
442    if (_gravitaDi({ gravita: 'critica' }) !== 'critica') e.push('una gravita dichiarata non viene letta');
443    if (_gravitaDi({ rResiduoPre: 12 }) !== 'critica') e.push('un rischio residuo 12 non produce gravita critica');
444    if (_gravitaDi({ rResiduoPre: 2 }) !== 'bassa') e.push('un rischio residuo 2 non produce gravita bassa');
445    if (_gravitaDi({}) !== '') e.push('un azione senza alcun segnale di gravita ne riceve una comunque');
446    // il potere di spesa NON dichiarato non e' un no
447    if (_poteriOk({}) !== null) e.push('un potere di spesa non dichiarato viene letto come asse
447nte');
448    if (_poteriOk({ ownerDecisore: { potereDiSpesa: false } }) !== false) e.push('un potere di spesa negato non viene letto');
449    // le righe del documento devono dichiarare la base: una percentuale senza base non e verificabile
450    var v = vista({});
451    var rd = righeDocumento(v);
452    if (rd.length !== 9) e.push('le righe per il documento non sono nove');
453    if (!rd.every(function (r) { return _s(r[2]); })) e.push('una riga del documento non porta il proprio bersaglio');
454    return { ok: e.length === 0, errors: e };
455  }
456
457  /**
458   * ── IL PIANO OPERATIVO (onda 6) — «CUI PRODEST?» ────────────────────────────────────────
459   *
460   * Il committente, davanti all'allegato precedente: «non trovo alcuna altra tabella sulle ACAM
461   * se non quella. Non dice niente di concreto. Non capisco il fine di quel documento, a parte
462   * centinaia di parole e bla bla bla sulle norme. Va bene le norme, ma alla fine, se ho un
463   * documento che come consulente voglio leggere e magari condividere con il cliente, genero
464   * quel documento? Cui prodest?»
465   *
466   * Risposta: a nessuno. Quell'allegato era un'AUTOVALUTAZIONE DEL CONSULENTE travestita da
467   * deliverable per il cliente. I nove indicatori misurano quanto bene il consulente ha
468   * formalizzato il programma: un datore di lavoro che li legge impara solo che il suo
469   * consulente ha preso 4 su 9. Non gli serve, e al consulente fa danno. E c'era l'aggravante:
470   * nella v20.213 i KPI di controllo erano stati tolti dal DVR proprio perche' sono strumenti
471   * del consulente — decisione che avevo citato con approvazione e poi violato chiamando
472   * l'allegato «aggiuntivo». L'etichetta non cambia chi lo legge.
473   *
474   * ── CHE COSA IL DVR NON DA', E CHE UN CONSULENTE VORREBBE CONSEGNARE ───────────────────
475   * Il §8 del DVR e' un ATTO GIURIDICO: elenca le misure perche' l'art. 28 c.2 lett. c) lo
476   * impone. Non e' uno strumento di lavoro, e nessun datore di lavoro lo usa per lavorare.
477   * Quello che userebbe e' un piano operativo: che cosa devo fare, in che ordine, entro quando,
478   * chi ci mette la firma, che cosa scade nei prossimi 30-60-90 giorni.
479   * ORDINATO PER SCADENZA, non per articolo di legge. E' l'unica cosa che cambia davvero.
480   *
481   * ── DUE PARTI, E LA PRIMA E' QUELLA CHE MANCA SEMPRE ──────────────────────────────────
482   *   1. DA ASSEGNARE — gli obblighi dovuti che nessuna azione prende in carico. Nel documento
483   *      del cliente non ci va la pagella; ci va la richiesta: queste cose aspettano da te un
484   *      nome e una data.
485   *   2. SCADENZARIO — cio' che e' gia' assegnato, raggruppato per finestra temporale.
486   *
487   * La data si INIETTA (`opts.oggi`): un piano che dipende dall'orologio non e' riproducibile,
488   * e due letture dello stesso progetto darebbero due documenti diversi.
489   */
490  var FINESTRE = [
491    { id: 'scadute', label: 'Scadute', da: null, a: -1, urgenza: 1,
492      dice: 'La data è passata e l’intervento non risulta chiuso. Resta scritto nel documento, ed è la prima cosa che un controllo confronta con la data odierna.' },
493    { id: 'entro30', label: 'Entro 30 giorni', da: 0, a: 30, urgenza: 2,
494      dice: 'Da chiudere adesso: è il mese in corso.' },
495    { id: 'entro90', label: 'Entro 90 giorni', da: 31, a: 90, urgenza: 3,
496      dice: 'Il trimestre: va organizzato ora, non alla scadenza.' },
497    { id: 'oltre', label: 'Oltre i 90 giorni', da: 91, a: 99999, urgenza: 4,
498      dice: 'Programmato più avanti: si riguarda al prossimo riesame.' },
499    { id: 'senzaData', label: 'Senza data', da: null, a: null, urgenza: 9,
500      dice: 'Un intervento senza data non è programmato: l’art. 28 c.2 lett. c) chiede il miglioramento NEL TEMPO, e senza una data quel tempo non esiste.' }
501  ];
502  function finestra(id) {
503    for (var i = 0; i < FINESTRE.length; i++) if (FINESTRE[i].id === id) return FINESTRE[i];
504    return null;
505  }
506
507  function _data(v) {
508    var t = _s(v);
509    if (!t) return null;
510    var iso = t.match(/^(\d{4})-(\d{2})-(\d{2})/);
511    if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
512    var ita = t.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/);
513    if (ita) return new Date(+ita[3], +ita[2] - 1, +ita[1]);
514    var d = new Date(t);
515    return isNaN(d.getTime()) ? null : d;
516  }
517  function _giorni(scad, oggi) {
518    if (!scad) return null;
519    return Math.round((scad.getTime() - oggi.getTime()) / 86400000);
520  }
521  function _finestraDi(gg, chiuso) {
522    if (gg == null) return 'senzaData';
523    if (gg < 0) return chiuso ? 'oltre' : 'scadute';
524    if (gg <= 30) return 'entro30';
525    if (gg <= 90) return 'entro90';
526    return 'oltre';
527  }
528
529  /**
530   * IL PIANO OPERATIVO. Non calcola: riordina cio' che il registro degli obblighi ha concluso.
531   */
532  /** Un titolo che comincia in minuscolo, in una colonna di un documento consegnato, sembra un
533   *  frammento incollato. Non si riscrive il testo: gli si da' la maiuscola iniziale. */
534  function _titolo(t) {
535    var x = _s(t);
536    return x ? x.charAt(0).toUpperCase() + x.slice(1) : x;
537  }
538
539  function pianoOperativo(d, opts) {
540    /* v20.373.921 — ancora usato da generate-chunk Sez.VIII; non rimuovere finché il generatore non legge RegistroRilievi. */
541    opts = opts || {};
542    var oggi = opts.oggi || new Date();
543    var OB = win._tecAcamObblighi;
544    if (!OB || typeof OB.obblighi !== 'function') {
545      return { disponibile: false, perche: 'il registro degli obblighi di trattamento non è caricato' };
546    }
547    var reg;
548    try { reg = OB.obblighi(d); } catch (_e) { return { disponibile: false, perche: 'il registro degli obblighi non è calcolabile' }; }
549    if (!reg || !reg.disponibile) return { disponibile: false, perche: 'il registro degli obblighi non è disponibile' };
550
551    var daAssegnare = [], voci = [];
552    _arr(reg.righe).forEach(function (o) {
553      var dovuto = (o.soglia && o.soglia.obbliga) || o.sorgente === 'obbligo_legge' || o.sorgente === 'non_conformita';
554      if (!o.coperto) {
555        // ── UNA COSA DA DECIDERE, UNA RIGA ─────────────────────────────────────────────────
556        // Lo stesso pericolo su due mansioni sono due obblighi distinti — nel REGISTRO devono
557        // restare due righe, ognuna col suo verdetto. In un PIANO no: chi deve decidere legge
558        // «Stress lavoro-correlato» quattro volte e conclude che il documento e' gonfiato.
559        // Si accorpa e si dice DOVE, esattamente come fa lo scadenzario piu' sotto.
560        var kA = _s(o.titolo) + '|' + _s(o.norma);
561        var giaA = null;
562        for (var ai = 0; ai < daAssegnare.length; ai++) if (daAssegnare[ai]._chiave === kA) { giaA = daAssegnare[ai]; break; }
563        var dv = _s(o.entitaTipo) + ' · ' + _s(o.entitaNome);
564        if (giaA) {
565          if (giaA.dovePiu.indexOf(dv) < 0) giaA.dovePiu.push(dv);
566          if (dovuto) giaA.dovuto = true;
567          if (o.soglia && _n(o.soglia.rango) < giaA.urgenza) {
568            giaA.urgenza = _n(o.soglia.rango);
569            giaA.entroSuggerito = _s(o.soglia.termine);
570            giaA.chiAccetta = _s(o.soglia.accetta);
571          }
572          return;
573        }
574        daAssegnare.push({
575          _chiave: kA,
576          cosa: _titolo(o.titolo), dove: dv, dovePiu: [dv],
577          // la banda di rischio puo' mancare (un rilievo non ha P x D), il TERMINE no: sono due
578          // fatti diversi, e tenerli legati stampava «da definire» sulle righe piu' urgenti.
579          entroSuggerito: o.soglia ? _s(o.soglia.termine) : (_s(o.termineDovuto) || 'da definire'),
580          chiAccetta: o.soglia ? _s(o.soglia.accetta) : '',
581          norma: _s(o.norma), dovuto: dovuto, urgenza: o.soglia ? _n(o.soglia.rango) : 3
582        });
583        return;
584      }
585      _arr(o.azioni).forEach(function (a) {
586        var scad = _data(a.scadenza || a.dataPrevista || a.entro);
587        var StF = win.TecAcamStato;
588        var chiuso = StF ? (StF.fatta({ stato: o.stato }) || StF.chiusa({ stato: o.stato })) : false;
589        var gg = _giorni(scad, oggi);
590        var fid = _finestraDi(gg, chiuso);
591        // ── UNA COSA DA FARE, UNA RIGA ────────────────────────────────────────────────────
592        // La stessa misura puo' coprire piu' obblighi (due mansioni esposte allo stesso rischio).
593        // Nel REGISTRO e' giusto che compaia due volte — sono due obblighi distinti, e ognuno ha
594        // il suo verdetto. In un PIANO OPERATIVO no: chi deve eseguire non vuole leggere due
595        // volte lo stesso intervento. Si accorpa, e si dice a quanti obblighi risponde.
596        var chiave = _s(a.codice) + '|' + _s(a.descrizione || a.azione || a.misura) + '|' + _s(a.scadenza);
597        var gia = null;
598        for (var vi = 0; vi < voci.length; vi++) if (voci[vi]._chiave === chiave) { gia = voci[vi]; break; }
599        if (gia) {
600          gia.obblighi.push(_s(o.titolo));
601          gia.dovePiu.push(_s(o.entitaTipo) + ' · ' + _s(o.entitaNome));
602          if (dovuto) gia.dovuto = true;
603          if (o.efficaciaSulRischio && o.efficaciaSulRischio.esito === 'non_sceso') gia.esitoRischio = o.efficaciaSulRischio;
604          return;
605        }
606        voci.push({
607          _chiave: chiave,
608          cosa: _titolo(a.descrizione || a.azione || a.misura),
609          dove: _s(o.entitaTipo) + ' · ' + _s(o.entitaNome),
610          dovePiu: [_s(o.entitaTipo) + ' · ' + _s(o.entitaNome)],
611          obbligo: _s(o.titolo), obblighi: [_s(o.titolo)],
612          chi: (o.assegnazione && o.assegnazione.persona) ? (o.assegnazione.persona.nome + ' (' + o.assegnazione.persona.ruolo + ')')
613             : (_s(a.responsabile) || ''),
614          chiRisolto: !!(o.assegnazione && o.assegnazione.esito === 'trovata'),
615          entro: _s(a.scadenza || a.dataPrevista || a.entro),
616          giorni: gg, finestra: fid,
617          stato: o.stato, dovuto: dovuto, norma: _s(a.norma) || _s(o.norma),
618          prova: _s(a.verifica && a.verifica.evidenza) || _s(a.verificaEfficacia) || '',
619          esitoRischio: o.efficaciaSulRischio || null,
620          avvisi: _arr(o.avvisiAssegnazione)
621        });
622      });
623    });
624
625    var gruppi = FINESTRE.map(function (f) {
626      return { finestra: f, voci: voci.filter(function (v) { return v.finestra === f.id; }) };
627    }).filter(function (g) { return g.voci.length; });
628    gruppi.sort(function (a, b) { return a.finestra.urgenza - b.finestra.urgenza; });
629
630    daAssegnare.sort(function (a, b) { return (a.dovuto === b.dovuto ? 0 : (a.dovuto ? -1 : 1)) || a.urgenza - b.urgenza; });
631
632    var n = function (id) { var g = gruppi.filter(function (x) { return x.finestra.id === id; })[0]; return g ? g.voci.length : 0; };
633    return {
634      disponibile: true,
635      norma: 'art. 28 c.2 lett. c e d D.Lgs. 81/08',
636      oggi: oggi,
637      daAssegnare: daAssegnare, nDaAssegnare: daAssegnare.length,
638      nDaAssegnareDovuti: daAssegnare.filter(function (x) { return x.dovuto; }).length,
639      gruppi: gruppi, voci: voci, totale: voci.length,
640      scadute: n('scadute'), entro30: n('entro30'), entro90: n('entro90'),
641      oltre: n('oltre'), senzaData: n('senzaData'),
642      senzaChi: voci.filter(function (v) { return !_s(v.chi); }).length,
643      chiNonRisolto: voci.filter(function (v) { return _s(v.chi) && !v.chiRisolto; }).length,
644      // un piano vuoto non e' un piano: si dichiara invece di stampare una copertina
645      vuoto: (voci.length + daAssegnare.length) === 0
646    };
647  }
648
649  win._tecAcamPiano = {
650    VERSION: VERSION, KPI_DEF: KPI_DEF, FINESTRE: FINESTRE, finestra: finestra,
651    azioniDi: azioniDi, vista: vista, allarmi: allarmi, pianoOperativo: pianoOperativo,
652    righeDocumento: righeDocumento, righeAzioni: righeAzioni, selfTest: selfTest
653  };
654})(typeof window !== 'undefined' ? window : this);
655
656/**
657 * IL DISEGNO DEL PIANO ACAM, PER IL TECNICO.
658 * Nove indicatori, ciascuno con il proprio bersaglio ACCANTO al valore: un numero senza bersaglio
659 * non dice se va bene, e un cruscotto che non dice se va bene e' decorazione. Chi non ha base lo
660 * dichiara in grigio invece di mostrare uno zero che sembra un risultato.
661 */
662(function (win) {
663  'use strict';
664  var A = win._tecAcamPiano;
665  if (!A) return;
666  function _e(s) {
667    return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
668      return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c];
669    });
670  }
671  var NAVY = '#1e3a5f', GRIGIO = '#64748b', BORDO = '#e2e8f0';
672  // helper locali del disegno: il primo IIFE non li esporta, e usarne uno che qui non esiste
673  // produce un ReferenceError ingoiato dal try/catch del chiamante — il difetto della .134.
674  function _arr(v) { return Array.isArray(v) ? v.filter(Boolean) : []; }
675  // La meta' che DISEGNA non aveva un normalizzatore di stringhe: quella che calcola si', ma
676  // vivono in due IIFE diverse e non si vedono. Presumerlo era il difetto della .134 — una
677  // funzione muta che nessuno sente tacere, finche' non esplode su una riga qualunque.
678  function _s(v) { return String(v == null ? '' : v).trim(); }
679  // stesso motivo: la meta' che disegna non aveva nemmeno un normalizzatore di numeri.
680  function _num(v) { var x = Number(v); return isNaN(x) ? 0 : x; }
681  var ESITO = {
682    centrato:       { colore: '#16a34a', sfondo: '#f0fdf4', bordo: '#bbf7d0', eti: 'bersaglio centrato' },
683    sotto:          { colore: '#dc2626', sfondo: '#fff7f7', bordo: '#fecaca', eti: 'sotto il bersaglio' },
684    non_misurabile: { colore: '#94a3b8', sfondo: '#f8fafc', bordo: '#e2e8f0', eti: 'non misurabile' }
685  };
686
687  function _cardKpi(k) {
688    var s = ESITO[k.esito] || ESITO.non_misurabile;
689    return '<div data-acam-kpi="1" style="border:1px solid ' + s.bordo + ';background:' + s.sfondo + ';border-radius:12px;padding:12px 14px">'
690      + '<div style="display:flex;align-items:baseline;gap:8px">'
691      + '<span style="font-size:26px;font-weight:900;line-height:1;color:' + s.colore + '">'
692      + (k.valore == null ? '—' : k.valore + '%') + '</span>'
693      + '<span style="font-size:10px;font-weight:800;color:' + GRIGIO + '">bersaglio ' + _e(k.targetTesto) + '</span>'
694      + (k.proposta ? '<span title="Metrica proposta da noi, derivata dalle norme: non è una prassi di mercato verificata" style="margin-left:auto;font-size:8.5px;font-weight:900;text-transform:uppercase;letter-spacing:.05em;color:#7c3aed;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:4px;padding:1px 5px">proposta</span>' : '')
695      + '</div>'
696      + '<div style="font-size:12.5px;font-weight:800;color:' + NAVY + ';margin-top:6px">' + _e(k.label) + '</div>'
697      + '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:2px;line-height:1.4">' + _e(k.definizione)
698      + (k.base ? ' · base ' + k.base : '') + '</div>'
699      + (k.valore == null
700          ? '<div style="font-size:10px;color:#94a3b8;font-style:italic;margin-top:5px">' + _e(k.nonMisurabilePerche) + '</div>'
701          : (k.esito === 'sotto'
702              ? '<div style="font-size:10.5px;color:#991b1b;margin-top:5px;line-height:1.45">' + _e(k.perche) + '</div>'
703              : ''))
704      + '<div style="font-size:9.5px;color:#94a3b8;margin-top:6px;padding-top:5px;border-top:1px solid ' + s.bordo + '">'
705      + _e(k.norma) + '</div></div>';
706  }
707
708  function htmlAllarmi(v) {
709    if (!v.allarmi.length) {
710      return '<div data-acam-blocco="carenze" style="border:1px solid #bbf7d0;background:#f0fdf4;border-radius:14px;padding:15px 17px">'
711        + '<div style="font-size:13px;font-weight:800;color:#166534">&#10003; Nessuna carenza ACAM aperta</div>'
712        + '<div style="font-size:11.5px;color:#166534;margin-top:3px">Ogni azione ha un nome, una data, un gradino della gerarchia e una prova di efficacia.</div></div>';
713    }
714    var ETI = { 1: 'Dovuto', 2: 'Da chiudere', 3: 'Qualità' };
715    var h = '<div data-acam-blocco="carenze" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;overflow:hidden">'
716      + '<div style="padding:13px 16px;border-top:3px solid ' + (v.allarmi[0].colore || '#dc2626') + ';border-bottom:1px solid ' + BORDO + '">'
717      + '<div style="font-size:14.5px;font-weight:800;color:' + NAVY + '">Che cosa manca al programma per reggere un controllo</div>'
718      + '<div style="font-size:11px;color:' + GRIGIO + ';font-style:italic;margin-top:2px">'
719      + v.allarmi.length + ' voci · ' + v.nAllarmiGravi + ' dovute per legge</div></div>';
720    v.allarmi.forEach(function (a) {
721      h += '<div style="padding:11px 16px;border-top:1px solid #f1f5f9">'
722        + '<div style="display:flex;gap:9px;align-items:baseline;flex-wrap:wrap">'
723        + '<span style="font-size:8.5px;font-weight:900;text-transform:uppercase;letter-spacing:.06em;color:#fff;background:'
724        + (a.colore || '#64748b') + ';border-radius:4px;padding:2px 7px">' + (ETI[a.gravita] || '') + '</span>'
725        + '<span style="font-size:12.5px;font-weight:800;color:' + NAVY + '">' + _e(a.titolo) + '</span></div>'
726        + '<div style="font-size:11px;color:#475569;line-height:1.5;margin-top:4px">' + _e(a.perche) + '</div>'
727        + '<div style="display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:5px">'
728        + '<div style="font-size:10px;color:' + GRIGIO + '"><b>' + _e(a.norma) + '</b> · ' + _e(a.dove) + '</div>'
729        + (a.vai
730            ? ('<button type="button" onclick="' + _e(a.vai) + '" style="margin-left:auto;cursor:pointer;border:1.5px solid '
731               + (a.colore || '#64748b') + ';background:#fff;color:' + (a.colore || '#64748b')
732               + ';border-radius:7px;font-size:10.5px;font-weight:800;padding:4px 11px;font-family:inherit">Vai &#8594;</button>')
733            : '')
734        + '</div></div>';
735    });
736    return h + '</div>';
737  }
738
739  /** Le corsie: il rigore e' proporzionato alla gravita', e si vede. */
740  function htmlCorsie(v) {
741    var c = v.corsie, tot = v.totale || 0;
742    if (!tot) return '';
743    var voci = [
744      { id: 'rapida', label: 'Rapida', n: c.rapida, colore: '#16a34a', chiede: 'azione · chi · entro quando' },
745      { id: 'standard', label: 'Standard', n: c.standard, colore: '#ca8a04', chiede: '+ causa · gradino della gerarchia · verifica di efficacia' },
746      { id: 'estesa', label: 'Estesa', n: c.estesa, colore: '#dc2626', chiede: '+ metodo RCA · escape point · propagazione · rischio introdotto' }
747    ];
748    var h = '<div data-acam-blocco="corsie" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;padding:14px 16px">'
749      + '<div style="font-size:13.5px;font-weight:800;color:' + NAVY + '">Il rigore è proporzionato alla gravità</div>'
750      + '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:2px;line-height:1.45">'
751      + 'Imporre l’indagine completa su una segnaletica mancante distrugge l’adozione; permettere un «5 perché» '
752      + 'frettoloso su un quasi-infortunio grave distrugge la difendibilità. La corsia si sceglie da sola: puoi alzarla, '
753      + 'e abbassarla richiede una motivazione che resta agli atti.</div>'
754      + '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-top:11px">';
755    voci.forEach(function (x) {
756      h += '<div style="border-left:4px solid ' + x.colore + ';background:#fbfcfe;border-radius:8px;padding:9px 12px">'
757        + '<div style="font-size:20px;font-weight:900;color:' + x.colore + ';line-height:1">' + (x.n || 0) + '</div>'
758        + '<div style="font-size:11.5px;font-weight:800;color:' + NAVY + ';margin-top:2px">Corsia ' + x.label + '</div>'
759        + '<div style="font-size:9.5px;color:' + GRIGIO + ';margin-top:2px;line-height:1.4">' + _e(x.chiede) + '</div></div>';
760    });
761    if (c.nonDeterminata) {
762      h += '<div style="border-left:4px solid #94a3b8;background:#f8fafc;border-radius:8px;padding:9px 12px">'
763        + '<div style="font-size:20px;font-weight:900;color:#94a3b8;line-height:1">' + c.nonDeterminata + '</div>'
764        + '<div style="font-size:11.5px;font-weight:800;color:' + NAVY + ';margin-top:2px">Corsia non determinata</div>'
765        + '<div style="font-size:9.5px;color:' + GRIGIO + ';margin-top:2px;line-height:1.4">Manca la gravità: il rigore non può essere scelto</div></div>';
766    }
767    return h + '</div></div>';
768  }
769
770  /** I cinque dati minimi dell'art. 28 c.2 lett. c e d, uno per colonna. */
771  // v20.373.231 — COLONNA CAUSA (Root Cause). Il dato (causa/tipo) esisteva gia' in righeAzioni
772  // ma la tabella non lo mostrava. Senza causa, l'azione agisce sull'effetto: e' una correzione
773  // travestita (ISO 45001 10.2 b). La colonna la NOMINA; il tecnico la compila (Virgilio propone).
774  function _causaCell(r) {
775    var ok = !!(r && r.causa);
776    var tipo = r && r.causaTipo;
777    var chip = tipo ? ('<span style="font-size:8px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:#fff;background:'
778      + (tipo === 'sistemica' ? '#166534' : '#b45309') + ';border-radius:4px;padding:1px 5px;margin-left:5px">' + _e(tipo) + '</span>') : '';
779    return '<td data-acam-causa="' + (ok ? '1' : '0') + '" style="padding:7px 10px;line-height:1.4;color:'
780      + (ok ? '#1e293b' : '#b45309') + '">'
781      + (ok ? (_e(r.causa) + chip)
782            : '<span style="font-weight:700">&#8212; senza causa</span>'
783              + '<div style="font-size:9px;margin-top:1px">agisce sull&#39;effetto: &#232; una correzione travestita</div>')
784      + '</td>';
785  }
786
787  // v20.373.231 — TEMPISTICA PROPOSTA. Se la scadenza manca, il tool la propone dalla gravita'
788  // (classificaUrgenza, SSOT in tec-azioni-rilievi-sync); il tecnico conferma o corregge.
789  function _tempoCell(r) {
790    if (r && r.nelTempo) return '<td style="padding:7px 10px;white-space:nowrap;color:#1e293b">' + _e(r.nelTempo) + '</td>';
791    if (r && r.nelTempoPropostoGiorni) {
792      var _d = new Date(); _d.setDate(_d.getDate() + (parseInt(r.nelTempoPropostoGiorni, 10) || 180));
793      var _iso = String(_d.getDate()).padStart(2, '0') + '/' + String(_d.getMonth() + 1).padStart(2, '0') + '/' + _d.getFullYear();
794      return '<td data-acam-tempo-proposto="1" style="padding:7px 10px;white-space:nowrap;line-height:1.35">'
795        + '<span style="font-size:8px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:#fff;background:#7c3aed;border-radius:4px;padding:1px 5px">proposta</span> '
796        + '<span style="color:#7c3aed;font-weight:700">' + _iso + '</span>'
797        + '<div style="font-size:9px;color:#94a3b8;margin-top:1px">
797' + _e(r.nelTempoPropostoMotivo || '') + ' \u2014 il tecnico conferma o corregge</div></td>';
798    }
799    return '<td style="padding:7px 10px;white-space:nowrap;color:#b45309">\u2014 da indicare</td>';
800  }
801
802  // v20.373.231 — COLONNA STATO + EFFICACIA nella tabella azioni. Lo stato dell'azione e, se
803  // e' chiusa ma il rischio non e' sceso, il segnale di REINDIRIZZAMENTO (via _efficacia).
804  function _statoCell(r) {
805    var St = win.TecAcamStato;
806    var st = (St && St.statoDi) ? St.statoDi(r) : ((r && r.stato) || 'Proposta');
807    var col = (St && St.meta) ? St.meta(st).col : '#475569';
808    return '<td data-acam-stato="1" style="padding:7px 10px;white-space:nowrap;line-height:1.35">'
809      + '<span style="font-weight:800;color:' + col + '">' + _e(st) + '</span>'
810      + _efficacia(r) + '</td>';
811  }
812
813  function htmlAzioni(d, opts) {
814    opts = opts || {};
815    var righe = opts.righe || A.righeAzioni(d);
816    if (!righe.length) {
817      return '<div data-acam-blocco="azioni" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;padding:15px 17px">'
818        + '<div style="font-size:13.5px;font-weight:800;color:' + NAVY + '">Nessuna azione nel programma</div>'
819        + '<div style="font-size:11.5px;color:' + GRIGIO + ';margin-top:3px">Non è di per sé un difetto: può non essercene bisogno. '
820        + 'Lo diventa quando ci sono rilievi aperti che nessuna misura chiude.</div></div>';
821    }
822    var h = '<div data-acam-blocco="azioni" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;overflow:hidden">'
823      + '<div style="padding:13px 16px;border-top:3px solid #4338ca;border-bottom:1px solid ' + BORDO + '">'
824      + '<div style="font-size:9.5px;font-weight:900;letter-spacing:.09em;text-transform:uppercase;color:' + GRIGIO + '">'
825      + 'art. 28 c.2 lett. c e d D.Lgs. 81/08</div>'
826      + '<div style="font-size:14.5px;font-weight:800;color:' + NAVY + ';margin-top:3px">Le azioni, con i cinque dati che la legge chiede</div>'
827      + '<div style="font-size:11px;color:' + GRIGIO + ';margin-top:2px">Cosa · Nel tempo · Come (gradino della gerarchia) · Chi · Con quale idoneità (competenze <b>e poteri</b>)</div></div>'
828      + '<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">'
829      + '<tr style="background:#f8fafc">'
830      + ['', 'Cosa', 'Causa', 'Nel tempo', 'Come', 'Chi', 'Poteri', 'Stato'].map(function (t) {
831          return '<th style="text-align:left;padding:7px 10px;font-size:9px;font-weight:900;text-transform:uppercase;letter-spacing:.06em;color:' + GRIGIO + '">' + t + '</th>';
832        }).join('') + '</tr>';
833    righe.forEach(function (r) {
834      var manca = !r.nelTempo || !r.chi;
835      var pot = r.potereDiSpesa === true ? '<span style="color:#16a34a;font-weight:800">sì</span>'
836              : r.potereDiSpesa === false ? '<span style="color:#dc2626;font-weight:800">no</span>'
837              : (r.oneroso ? '<span style="color:#b45309;font-weight:800">non dichiarato</span>'
838                           : '<span style="color:#cbd5e1">—</span>');
839      var _res = r.assegnazione;
840      var chiHtml = (_res && _res.esito === 'trovata')
841        ? _assegnazione(r)
842        : ('<span style="color:' + (r.chi ? '#1e293b' : '#b45309') + '">' + _e(r.chi || '\u2014 da indicare') + '</span>' + _assegnazione(r));
843      h += '<tr style="border-top:1px solid #f1f5f9' + (manca ? ';background:#fffbeb' : '') + '">'
844        + '<td style="padding:7px 10px;font-weight:900;color:' + GRIGIO + ';white-space:nowrap">' + _e(r.codice) + '</td>'
845        + '<td style="padding:7px 10px;color:#1e293b;line-height:1.4">' + _e(r.cosa) + '</td>'
846        + _causaCell(r)
847        + _tempoCell(r)
848        + '<td style="padding:7px 10px;color:' + (r.comeRango && r.comeRango >= 4 ? '#b45309' : '#1e293b') + '">'
849        + _e(r.come || '— non classificato') + '</td>'
850        + '<td style="padding:7px 10px">' + chiHtml + '</td>'
851        + '<td style="padding:7px 10px;white-space:nowrap">' + pot + '</td>'
852        + _statoCell(r) + '</tr>';
853    });
854    return h + '</table></div></div>';
855  }
856
857  /**
858   * IL REGISTRO DEGLI OBBLIGHI, A SCHERMO. E' la cosa che va vista per prima: prima di sapere
859   * quanto bene stiamo lavorando (gli indicatori), il tecnico deve sapere CHE COSA C'E' DA FARE.
860   * Prima questa tabella non esisteva perche' non esisteva l'elenco: l'ACAM partiva dalle azioni.
861   */
862  /**
863   * ── v20.373.140 (onda 3) — CHI CE L'HA IN CARICO, E SE QUELLA PERSONA ESISTE ────────────
864   * Il responsabile era una stringa e nessuno diceva se corrispondesse a qualcuno. Qui la
865   * colonna dice tre cose diverse con tre colori diversi: assegnata a una persona vera ·
866   * assegnata a un nome che l'azienda non conosce · ambigua fra piu' persone. E se l'azione
867   * costa e chi la porta non puo' spendere, lo scrive: e' la lett. d) resa verificabile.
868   */
869  function _assegnazione(o) {
870    var a = o && o.assegnazione;
871    var avv = _arr(o && o.avvisiAssegnazione);
872    if (!a && !avv.length) return '';
873    var h = '';
874    if (a && a.esito === 'trovata' && a.persona) {
875      h += '<div style="font-size:10px;color:#166534;margin-top:3px">&#10003; in carico a <b>' + _e(a.persona.nome)
876        + '</b> <span style="color:#94a3b8">(' + _e(a.persona.ruolo) + ')</span></div>';
877    } else if (a && a.esito === 'non_censita') {
878      h += '<div style="font-size:10px;color:#991b1b;margin-top:3px">&#9888; «' + _e(a.testo)
879        + '» non risulta in azienda</div>';
880    } else if (a && a.esito === 'ambigua') {
881      h += '<div style="font-size:10px;color:#b45309;margin-top:3px">&#9888;
881 «' + _e(a.testo)
882        + '» corrisponde a più persone: scegli tu</div>';
883    }
884    avv.filter(function (x) { return /potere/.test(x.id); }).slice(0, 1).forEach(function (x) {
885      h += '<div style="font-size:9.5px;color:#9a3412;margin-top:2px;line-height:1.4">&#9888; '
886        + _e(String(x.testo).split('.')[0]) + '.</div>';
887    });
888    avv.filter(function (x) { return /^ruolo_/.test(x.id); }).slice(0, 1).forEach(function (x) {
889      var lbl = x.id === 'ruolo_rspp' ? 'ruolo errato: l\u2019RSPP propone, non attua (artt. 32-33)'
890              : x.id === 'ruolo_rls' ? 'ruolo errato: l\u2019RLS \u00e8 consultato, non responsabile (artt. 47-50)' : 'ruolo errato';
891      h += '<div data-acam-ruolo-errore="1" style="font-size:9.5px;color:#991b1b;font-weight:700;margin-top:2px;line-height:1.4">&#9888; ' + lbl + '</div>';
892    });
893    return h;
894  }
895
896  /**
897   * ── v20.373.141 (onda 5) — «NON POSSO AVERE DISALLINEAMENTO TRA AZIONI CHIUSE E RISCHI» ──
898   * Qui si vede. Un'azione chiusa con esito efficace su un rischio rimasto dov'era non e' un
899   * successo: e' la cosa piu' importante che questa tabella possa dire, e la dice in rosso
900   * accanto allo stato «Chiuso» — perche' e' esattamente li' che il disallineamento si annida.
901   */
902  function _efficacia(o) {
903    var f = o && o.efficaciaSulRischio;
904    if (!f || !f.esito || f.esito === 'non_pertinente' || f.esito === 'nessuna_chiusa' || f.esito === 'raggiunto') return '';
905    var COL = { non_sceso: '#dc2626', sceso_parzialmente: '#b45309', atteso_non_dichiarato: '#7c3aed' };
906    var ETI = { non_sceso: 'chiusa, rischio fermo', sceso_parzialmente: 'chiusa, atteso non raggiunto',
907                atteso_non_dichiarato: 'chiusa senza un atteso' };
908    return '<div style="font-size:9.5px;font-weight:800;color:' + (COL[f.esito] || '#b45309') + ';margin-top:3px">'
909      + '&#9888; ' + _e(ETI[f.esito] || f.esito)
910      + (f.ora != null && f.atteso != null ? (' <span style="font-weight:600">(' + f.ora + ' contro ' + f.atteso + ' atteso)</span>') : '')
911      + '</div>';
912  }
913
914  /**
915   * ── CHE COSA FARE, E DOVE STA GIA' SCRITTO ────────────────────────────────────────────────
916   *
917   * Tre casi, e nessuno lascia la casella vuota:
918   *   · COPERTO — l'obbligo ha gia' una o piu' ACAM. Si NOMINANO, con il loro stato: e' il
919   *     legame che il committente ha chiesto («se quel che deve accadere e' una ACAM, perche'
920   *     non mi leghi quel dovuto a quella ACAM corrispondente?»). Erano nella riga, in `azioni`,
921   *     e questa tabella non le stampava.
922   *   · SCOPERTO CON PROPOSTA — il catalogo sa che cosa serve: si mostra, marcata come PROPOSTA
923   *     (non e' ancora una decisione di nessuno) e con il pulsante per portarla nel programma.
924   *   · SCOPERTO SENZA PROPOSTA — il catalogo non propone nulla per questo pericolo. Si DICE.
925   *     Una casella vuota accanto a «dovuto» lascia il tecnico a indovinare, ed e' esattamente
926   *     cio' di cui si e' lamentato.
927   */
928  function _cosaFare(o) {
929    var az = _arr(o.azioni);
930    if (az.length) {
931      return az.map(function (a) {
932        var t = _s(a.descrizione || a.azione || a.misura) || '(azione senza testo)';
933        var st = _s(a.stato) || 'aperta';
934        var chiusa = /^(chiusa|chiuso|attuata|efficace|assolta|assolto)$/i.test(st);
935        return '<div style="margin-bottom:3px">'
936          + '<span style="font-size:8.5px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:#fff;background:'
937          + (chiusa ? '#16a34a' : '#2563eb') + ';border-radius:4px;padding:1px 5px" data-acam-copre="1">ACAM</span> '
938          + '<span style="color:#1e293b">' + _e(t) + '</span>'
939          + '<span style="font-size:9.5px;color:' + GRIGIO + '"> — ' + _e(st) + '</span></div>';
940      }).join('');
941    }
942    var prop = _s(o.azioneProposta);
943    if (prop) {
944      return '<div><span style="font-size:8.5px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:#fff;'
945        + 'background:#7c3aed;border-radius:4px;
945padding:1px 5px" data-acam-copre="proposta">Proposta</span> '
946        + '<span style="color:#1e293b">' + _e(prop) + '</span>'
947        + (_s(o.azioneNorma) ? ('<div style="font-size:9px;color:#94a3b8;margin-top:1px">' + _e(o.azioneNorma) + '</div>') : '')
948        + '<div style="font-size:9.5px;color:' + GRIGIO + ';margin-top:2px">Non è ancora una misura del programma: '
949        + 'va accolta, modificata o rifiutata.</div></div>';
950    }
951    return '<div data-acam-copre="0" style="color:#b45309;font-size:10.5px;line-height:1.45">Il catalogo non propone una misura '
952      + 'per questo caso: la misura va scritta dal tecnico. Finché manca, l’obbligo resta scoperto.</div>';
953  }
954
955  /**
956   * LA PORTA. «Non mi porti nel campo in cui devo fare qualcosa.» Non se ne inventa una nuova:
957   * si usa quella che il prodotto ha gia' — lo Stato dei luoghi per i luoghi, la linguetta
958   * proprietaria per il resto — e quando per quell'entita' una porta non esiste, lo si DICE
959   * invece di offrire un pulsante che non porta da nessuna parte.
960   */
961  function _portaEntita(o) {
962    var t = _s(o.entitaTipo);
963    var atto = '';
964    if (t === 'luogo' && _s(o.luogoId)) {
965      atto = 'window._tecStatoLuoghi && window._tecStatoLuoghi.apri(' + JSON.stringify(_s(o.luogoId))
966        + ',\'\',' + JSON.stringify(_s(o.punto)) + ')';
967    } else if (t === 'mansione') {
968      atto = 'window._tecDashboardRischi && window._tecDashboardRischi.apri({tab:20,titolo:'
969        + JSON.stringify(_s(o.entitaNome)) + ',norma:' + JSON.stringify(_s(o.norma)) + '})';
970    } else if (t === 'attrezzo') {
971      atto = 'window._tecDashboardRischi && window._tecDashboardRischi.apri({tab:19,titolo:'
972        + JSON.stringify(_s(o.entitaNome)) + ',norma:' + JSON.stringify(_s(o.norma)) + '})';
973    } else if (t === 'ditta') {
974      atto = 'window._tecDashboardRischi && window._tecDashboardRischi.apri({tab:15,titolo:'
975        + JSON.stringify(_s(o.entitaNome)) + ',norma:' + JSON.stringify(_s(o.norma)) + '})';
976    }
977    if (!atto) {
978      return '<div data-acam-porta="0" style="font-size:9.5px;color:#94a3b8;font-style:italic;margin-top:3px">'
979        + 'nessuna schermata propria</div>';
980    }
981    return '<div style="margin-top:4px"><button type="button" data-acam-porta="1" onclick="' + _e(atto) + '" '
982      + 'title="Apri la schermata dove si lavora su questa voce" '
983      + 'style="cursor:pointer;border:1px solid #cbd5e1;background:#fff;color:#1d4ed8;border-radius:6px;'
984      + 'font-size:10px;font-weight:800;padding:2px 8px;font-family:inherit">Vai &#8594;</button></div>';
985  }
986
987
988  // v20.373.284 — portata a livello di modulo: la usano sia il Registro sia la vista «Il giro».
989  // Una funzione annidata dentro una vista non e' raggiungibile dalle altre, e duplicarla
990  // avrebbe prodotto due pulsanti che col tempo fanno cose diverse.
991    /**
992   * LA PROPOSTA DI DEFAULT DEL CLUSTER. Regola d'oro, la stessa dei Luoghi: il pulsante
993   * dichiara che cosa fa, SU QUANTE righe, e che cosa NON accade se non lo premi. E quando
994   * non c'e' nulla da proporre, non si nasconde: si dice perche'.
995   */
996  function _propostaCluster(lista, nomeCluster) {
997    lista = _arr(lista);
998    var conProposta = lista.filter(function (o) { return !_arr(o.azioni).length && _s(o.azioneProposta); });
999    var senzaNulla  = lista.filter(function (o) { return !_arr(o.azioni).length && !_s(o.azioneProposta); });
1000    var coperti     = lista.filter(function (o) { return _arr(o.azioni).length; });
1001    var idx = conProposta.map(function (o) { return _ULT_RIGHE.indexOf(o); }).filter(function (k) { return k >= 0; });
1002    var h2 = '<div data-acam-proposta-cluster="1" style="display:flex;gap:9px;flex-wrap:wrap;padding:9px 13px;background:#fbfcfe;border-bottom:1px solid ' + BORDO + '">';
1003    if (idx.length) {
1004      h2 += '<div style="flex:1 1 320px;border:1px solid #86efac;background:#f0fdf4;border-radius:10px;padding:9px 11px">'
1005        + '<button type="button" data-acam-batch="1" onclick="window._tecAcamPiano.portaProposteCluster(' + _e(JSON.stringify(idx)) + ')" '
1006        + 'title="Porta nel Programma di Miglioramento le misure che il catalogo propone per questo gruppo" '
1007        + 'style="width:100%;cursor:pointer;border:none;background:none;color:#15803d;font-weight:800;font-size:11.5px;padding:0;text-align:left;font-family:inherit">'
1008        + '&#10003; Porta nel Programma le ' + idx.length + ' misure proposte per ' + _e(nomeCluster) + '</button>'
1009        + '<div style="font-size:9.5px;color:#166534;line-height:1.45;margin-top:5px">'
1010        + 'Crea in un colpo solo ' + idx.length + ' azioni del Programma (stato «Proposta»: nessuna è ancora una decisione presa), '
1011        + 'ciascuna legata all’obbligo che la genera — così la riga passa da «Scoperto» a «In carico». '
1012        + '<b>Se non lo premi</b>: questi ' + idx.length + ' obblighi restano <u>dovuti e senza azione</u>, e il DVR dichiara un obbligo '
1013        + 'senza dire con quale misura viene assolto — art. 28 c.2 lett. c.</div></div>';
1014    } else if (lista.length && !senzaNulla.length) {
1015      h2 += '<div style="flex:1 1 320px;border:1px solid #bbf7d0;background:#f0fdf4;border-radius:10px;padding:9px 11px;'
1016        + 'font-size:10.5px;color:#166534;line-height:1.45"><b>&#10003; Nulla da proporre qui</b>: '
1017        + 'tutti i ' + lista.length + ' obblighi di questo gruppo hanno già la loro azione nel Programma.</div>';
1018    }
1019    if (senzaNulla.length) {
1020      h2 += '<div style="flex:1 1 280px;border:1px solid #fde68a;background:#fffbeb;border-radius:10px;padding:9px 11px;'
1021        + 'font-size:10.5px;color:#92400e;line-height:1.45"><b>&#9633; ' + senzaNulla.length + ' obblighi che nessun pulsante può chiudere</b>: '
1022        + 'per questi il catalogo non propone una misura, e inventarne una sarebbe peggio che tacere. '
1023        + 'Vanno scritti dal tecnico, uno per uno, dalla colonna «Che cosa fare».</div>';
1024    }
1025    if (coperti.length && idx.length) {
1026      h2 += '<div style="flex:1 1 200px;border:1px solid #c7d2fe;background:#eef2ff;border-radius:10px;padding:9px 11px;'
1027        + 'font-size:10.5px;color:#3730a3;line-height:1.45"><b>' + coperti.length + '</b> già presi in carico: '
1028        + 'il pulsante non li tocca.</div>';
1029    }
1030    return h2 + '</div>';
1031  }
1032
1033  function htmlObblighi(reg) {
1034    if (!reg || !reg.disponibile) return '';
1035    var BANDA = { non_tollerabile: '#dc2626', tollerabile_se_ridotto: '#ea580c',
1036                  da_programmare: '#ca8a04', sorvegliato: '#16a34a' };
1037    /* BL-4 — etichette da TecAcamStato.META; senza azione → «Senza azione». */
1038    var h = '<div data-acam-blocco="obblighi" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;overflow:hidden">'
1039      + '<div style="padding:13px 16px;border-top:3px solid ' + (reg.dovutiScoperti ? '#dc2626' : '#16a34a')
1040      + ';border-bottom:1px solid ' + BORDO + '">'
1041      + '<div style="font-size:9.5px;font-weight:900;letter-spacing:.09em;text-transform:uppercase;color:' + GRIGIO + '">'
1042      + _e(reg.norma) + '</div>'
1043      + '<div style="font-size:14.5px;font-weight:800;color:' + NAVY + ';margin-top:3px">Che cosa c’è da trattare</div>'
1044      + '<div style="font-size:11px;color:' + GRIGIO + ';margin-top:2px">'
1045      + reg.totale + ' obblighi di trattamento · <b>' + reg.dovuti + ' dovuti</b>'
1046      + (reg.dovutiScoperti ? ' · <b style="color:#dc2626">' + reg.dovutiScoperti + ' dovuti e ancora senza azione</b>' : ' · tutti presi in carico')
1047      + (reg.azioniAutonome ? ' · ' + reg.azioniAutonome + ' azioni di iniziativa autonoma' : '')
1048      + '</div>'
1049      + '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:4px">'
1050      + 'Mondo di questa azienda: '
1051      + Object.keys(reg.perEntita || {}).map(function (k) {
1052          return '<b>' + reg.perEntita[k].totale + '</b> su ' + _e(k);
1053        }).join(' · ')
1054      + (reg.personeCensite ? (' &nbsp;·&nbsp; <b>' + reg.personeCensite + '</b> persone censite') : '')
1055      + (reg.avvisiAssegnazione ? (' &nbsp;·&nbsp; <b style="color:#b45309">' + reg.avvisiAssegnazione + ' rilievi sull’assegnazione</b>') : '')
1056      + '</div>'
1057      + ((reg.chiuseSenzaEffetto || reg.chiuseEffettoParziale || reg.chiuseSenzaAtteso)
1058          ? ('<div style="margin-top:7px;padding:8px 11px;background:#fef2f2;border:1px solid #fecaca;border-radius:9px;'
1059             + 'font-size:11px;color:#991b1b;line-height:1.5">'
1060             + (reg.chiuseSenzaEffetto ? ('<b>' + reg.chiuseSenzaEffetto + ' azioni risultano chiuse con esito efficace e il rischio collegato è rimasto dov’era.</b> ') : '')
1061             + (reg.chiuseEffettoParziale ? (reg.chiuseEffettoParziale + ' sono scese ma non fino al livello che avevano dichiarato. ') : '')
1062             + (reg.chiuseSenzaAtteso ? (reg.chiuseSenzaAtteso + ' erano state chiuse senza dichiarare a che livello il rischio dovesse arrivare. ') : '')
1063             + 'O la misura non ha inciso, o P e D non sono stati rivalutati dopo l’attuazione: '
1064             + 'in entrambi i casi il programma dichiara un risultato che i numeri non confermano.</div>')
1065          : '');
1066    _arr(reg.sorgentiMute).forEach(function (m) {
1067      h += '<div style="font-size:10.5px;color:#b45309;font-style:italic;margin-top:5px">&#9888; La sorgente «'
1068        + _e(m.id) + '» non si è potuta leggere (' + _e(m.perche) + '): questo elenco è incompleto e non so di quanto.</div>';
1069    });
1070    // ── v20.373.150 — LA LEGENDA, PERCHE' UN COLORE SENZA LEGENDA E' UN INDOVINELLO ──────
1071    // Segnalazione del committente: «mostra le cose da fare, con colori diversi (e senza una
1072    // legenda che ne spiega il significato)». Le quattro bande hanno un significato preciso —
1073    // decidono ENTRO QUANDO l'azione e' dovuta e CHI puo' accettare il rischio residuo — e
1074    // lasciarle senza nome costringeva a dedurlo dal rosso. Un colore che va interpretato non
1075    // e' un'informazione, e' un test di memoria.
1076    h += '<div data-acam-legenda="bande" style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:0 16px 10px">'
1077      + '<span style="font-size:9px;font-weight:900;letter-spacing:.07em;text-transform:uppercase;color:' + GRIGIO + '">Bande</span>';
1078    var _SOG = (win._tecAcamObblighi && win._tecAcamObblighi.SOGLIE) || [];
1079    _arr(_SOG).forEach(function (sg) {
1080      h += '<span title="' + _e(sg.perche || '') + '" style="display:inline-flex;align-items:center;gap:5px;font-size:10.5px;color:'
1081        + GRIGIO + ';cursor:help">'
1082        + '<span style="width:10px;height:10px;border-radius:3px;background:' + (BANDA[sg.id] || '#64748b') + ';display:inline-block"></span>'
1083        + '<b style="color:#1e293b">' + _e(sg.label) + '</b> ' + _e(sg.termine) + '</span>';
1084    });
1085    h += '<span style="display:inline-flex;align-items:center;gap:5px;font-size:10.5px;color:' + GRIGIO + '">'
1086      + '<span style="width:10px;height:10px;border-radius:3px;background:#7c3aed;display:inline-block"></span>'
1087      + '<b style="color:#1e293b">Adempimento</b> dovuto per legge o non conformità rilevata</span>';
1088    h += '</div>';
1089    h += '</div>';
1090
1091    // ══ v20.373.283 — LE SOTTO-CARTELLE, I CLUSTER E LA PROPOSTA DI DEFAULT ══════════════
1092    // Segnalazione del committente sulla .282: «la tabella Programmazione e' un elenco
1093    // telefonico, occorre trasformarla come hai fatto con le tabelle Luoghi e Mansioni». E
1094    // aveva indicato lui stesso il modello: «nella tabella Luoghi, sotto-cartella "Conformita'
1095    // All. IV" e' il modello migliore: la complessita' e' gestita tramite sotto-cartelle,
1096    // ciascuna suddivisa in cluster, ciascun cluster con le proposte di default».
1097    //
1098    // Qui la gerarchia non e' inventata: e' quella che i dati hanno gia'.
1099    //   · SOTTO-CARTELLA = la NATURA dell'obbligo — adempimento dovuto per legge, non
1100    //     conformita' rilevata, rischio oltre soglia. Sono tre mestieri diversi e tre modi
1101    //     diversi di chiudere: tenerli in un elenco unico era il difetto.
1102    //   · CLUSTER = l'ENTITA' su cui l'obbligo grava — questo luogo, questa mansione, questo
1103    //     attrezzo. E' l'unita' di lavoro reale: quando si va sul posto, si chiudono insieme.
1104    //   · PROPOSTA DI DEFAULT = il gemello di «Dichiara conformi i N requisiti non esaminati».
1105    //     Dice che cosa fa, su quante righe, e — la meta' che conta — che cosa NON accade se
1106    //     non lo premi.
1107    var SORG = [
1108      { id: 'obbligo_legge', tit: 'Adempimenti dovuti per legge',
1109        sot: 'non nascono da un difetto: nascono dall’esistenza stessa dell’attività. Formazione, sorveglianza, verifiche periodiche.',
1110        col: '#7c3aed' },
1111      { id: 'non_conformita', tit: 'Non conformità rilevate',
1112        sot: 'qualcuno è andato a guardare e ha trovato uno scostamento. Finché non è chiuso, la valutazione indica un problema e non la sua misura.',
1113        col: '#dc2626' },
1114      { id: 'rischio', tit: 'Rischi oltre soglia da ridurre',
1115        sot: 'il residuo calcolato sta sopra la soglia che obbliga ad agire: la banda dice entro quando e chi può accettarlo.',
1116        col: '#ea580c' }
1117    ];
1118    var TIPO_ET = { luogo: '🏢 Luogo', mansione: '👷 Mansione', attrezzo: '🔧 Attrezzo',
1119                    ditta: '🏗️ Ditta', sede: '🏢 Sede' };
1120
1121    // l'indice serve al pulsante: la riga non ha un id proprio, e passare l'oggetto intero
1122    // dentro un onclick significherebbe serializzare mezzo progetto in un attributo HTML.
1123    var _tutte = _arr(reg.righe);
1124    _ULT_RIGHE = _tutte;
1125
1126    function _tabella(lista) {
1127      var t = '<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">'
1128        + '<tr style="background:#f8fafc">'
1129        + ['Da dove', 'Che cosa manca', 'Che cosa fare', 'Dove / chi', 'Entro', 'Chi può accettarlo', 'Stato'].map(function (t2) {
1130            return '<th style="text-align:left;padding:7px 10px;font-size:9px;font-weight:900;text-transform:uppercase;letter-spacing:.06em;color:' + GRIGIO + '">' + t2 + '</th>';
1131          }).join('') + '</tr>';
1132      lista.forEach(function (o) {
1133        var StT = win.TecAcamStato;
1134        var st = (!o.stato)
1135          ? { c: '#dc2626', t: 'Senza azione' }
1136          : (StT && StT.meta ? { c: StT.meta(o.stato).col, t: StT.normalizza(o.stato) } : { c: '#64748b', t: String(o.stato) });
1137        var col = o.soglia ? (BANDA[o.soglia.id] || '#64748b') : '#7c3aed';
1138        var dovuto = (o.soglia && o.soglia.obbliga) || o.sorgente === 'obbligo_legge' || o.sorgente === 'non_conformita';
1139        t += '<tr style="border-top:1px solid #f1f5f9' + (!o.stato && dovuto ? ';background:#fff7f7' : '') + '">'
1140          + '<td style="padding:7px 10px;white-space:nowrap">'
1141          + '<span style="font-size:8.5px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:#fff;background:'
1142          + col + ';border-radius:4px;padding:2px 6px">' + _e(dovuto ? 'dovuto' : 'proposto') + '</span>'
1143          + '<div style="font-size:9px;color:' + GRIGIO + ';margin-top:2px">' + _e(o.soglia ? o.soglia.label : (o.sorgente === 'obbligo_legge' ? 'obbligo di legge' : 'non conformità')) + '</div></td>'
1144          + '<td style="padding:7px 10px;color:#1e293b;line-height:1.4">' + _e(o.titolo)
1145          + (o.norma ? '<div style="font-size:9px;color:#94a3b8;margin-top:2px">' + _e(o.norma) + '</div>' : '') + '</td>'
1146          // ── CHE COSA FARE, E IL LEGAME CON L'ACAM ────────────────────────────────────
1147          // «Quando dici "dovuto" non mi dici cosa devo fare… e se quel che deve accadere e'
1148          // una ACAM, perche' non mi leghi quel dovuto a quella ACAM?» Le due risposte erano
1149          // gia' nella riga — `azioneProposta` e `azioni` — e questa tabella le buttava via.
1150          + '<td data-acam-cosafare="1" style="padding:7px 10px;line-height:1.45;
1150min-width:200px">' + _cosaFare(o) + '</td>'
1151          + '<td style="padding:7px 10px;font-size:10.5px;color:' + GRIGIO + '">' + _e(o.entitaTipo)
1152          + '<br><b style="color:#1e293b">' + _e(o.entitaNome || '—') + '</b>' + _portaEntita(o) + '</td>'
1153          + '<td style="padding:7px 10px;font-size:10.5px;color:#1e293b">' + _e(o.soglia ? o.soglia.termine : (o.termineDovuto || 'da programmare')) + '</td>'
1154          + '<td style="padding:7px 10px;font-size:10px;color:' + GRIGIO + ';line-height:1.4">'
1155          + _e(o.soglia ? o.soglia.accetta : '—') + _assegnazione(o) + '</td>'
1156          + '<td style="padding:7px 10px"><span style="font-size:9.5px;font-weight:800;color:' + st.c + ';white-space:nowrap">' + st.t + '</span>'
1157          + _efficacia(o) + '</td>'
1158          + '</tr>';
1159      });
1160      return t + '</table></div>';
1161    }
1162
1163
1164    SORG.forEach(function (sg) {
1165      var righeS = _tutte.filter(function (o) { return _s(o.sorgente) === sg.id; });
1166      if (!righeS.length) return;
1167      var scopertiS = righeS.filter(function (o) { return !o.stato; }).length;
1168
1169      // i cluster: una entita' = un cluster. L'ordine mette davanti chi ha piu' scoperti,
1170      // perche' e' li' che il lavoro serve.
1171      var byK = {}, ordine = [];
1172      righeS.forEach(function (o) {
1173        var k = _s(o.entitaTipo) + '|' + (_s(o.entitaNome) || '—');
1174        if (!byK[k]) { byK[k] = []; ordine.push(k); }
1175        byK[k].push(o);
1176      });
1177      ordine.sort(function (a, b) {
1178        var sa = byK[a].filter(function (o) { return !o.stato; }).length;
1179        var sb = byK[b].filter(function (o) { return !o.stato; }).length;
1180        return sb - sa || byK[b].length - byK[a].length;
1181      });
1182
1183      h += '<details data-acam-cartella="' + sg.id + '"' + (scopertiS ? ' open' : '')
1184        + ' style="border:1px solid ' + BORDO + ';border-left:4px solid ' + sg.col + ';border-radius:12px;background:#fff;overflow:hidden;margin-top:11px">'
1185        + '<summary style="cursor:pointer;padding:11px 15px;list-style:none">'
1186        + '<div style="display:flex;align-items:baseline;gap:9px;flex-wrap:wrap">'
1187        + '<span style="font-size:13.5px;font-weight:800;color:' + NAVY + '">' + _e(sg.tit) + '</span>'
1188        + '<span style="font-size:11px;font-weight:800;color:' + (scopertiS ? '#dc2626' : '#16a34a') + '">'
1189        + righeS.length + ' obblighi · ' + (scopertiS ? (scopertiS + ' ancora senza azione') : 'tutti presi in carico') + '</span>'
1190        + '<span style="font-size:10.5px;color:' + GRIGIO + '">— ' + ordine.length + ' ' + (ordine.length === 1 ? 'gruppo' : 'gruppi') + ' di lavoro · apri/chiudi</span>'
1191        + '</div>'
1192        + '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:3px;line-height:1.45">' + _e(sg.sot) + '</div>'
1193        + '</summary>';
1194
1195      ordine.forEach(function (k) {
1196        var lista = byK[k];
1197        var tipo = k.split('|')[0], nome = k.split('|').slice(1).join('|');
1198        var sc = lista.filter(function (o) { return !o.stato; }).length;
1199        h += '<div data-acam-cluster="1" style="border-top:1px solid ' + BORDO + '">'
1200          + '<div style="padding:9px 15px;background:#f8fafc;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">'
1201          + '<span style="font-size:9px;font-weight:900;letter-spacing:.06em;text-transform:uppercase;color:' + GRIGIO + '">'
1202          + _e(TIPO_ET[tipo] || tipo || 'altro') + '</span>'
1203          + '<b style="font-size:12.5px;color:' + NAVY + '">' + _e(nome || '—') + '</b>'
1204          + '<span style="font-size:10.5px;color:' + (sc ? '#dc2626' : '#16a34a') + ';font-weight:800">'
1205          + lista.length + ' obblighi · ' + (sc ? (sc + ' scoperti') : 'nessuno scoperto') + '</span>'
1206          + '</div>'
1207          + _propostaCluster(lista, nome || 'questo gruppo')
1208          + _tabella(lista)
1209          + '</div>';
1210      });
1211
1212      h += '</details>';
1213    });
1214
1215    // le righe che nessuna sotto-cartella ha raccolto non spariscono: sparire e' il difetto
1216    // che questo file combatte. Si dichiarano, con la loro sorgente in chiaro.
1217    var _resto = _tutte.filter(function (o) {
1218      return !SORG.some(function (sg) { return _s(o.sorgente) === sg.id; });
1219    });
1220    if (_resto.length) {
1221      h += '<details data-acam-cartella="altro" open style="border:1px solid ' + BORDO + ';border-radius:12px;background:#fff;overflow:hidden;margin-top:11px">'
1222        + '<summary style="cursor:pointer;padding:11px 15px;list-style:none">'
1223        + '<span style="font-size:13.5px;font-weight:800;color:' + NAVY + '">Obblighi di altra provenienza</span> '
1224        + '<span style="font-size:11px;color:' + GRIGIO + '">' + _resto.length + ' righe la cui sorgente non rientra nelle tre note: '
1225        + 'restano visibili perché nasconderle sarebbe peggio</span></summary>'
1226        + _propostaCluster(_resto, 'altra provenienza') + _tabella(_resto) + '</details>';
1227    }
1228
1229    return h + '</div>';
1230  }
1231
1232  /**
1233   * ── LA PROPOSTA DI DEFAULT, QUANDO SI PREME ──────────────────────────────────────────────
1234   * Crea le azioni del Programma per gli obblighi indicati. Tre regole, tutte e tre nate da
1235   * difetti gia' visti su questo prodotto:
1236   *   · IDEMPOTENTE — l'azione porta la chiave dell'obbligo (`_obbligoKey`): premere due volte
1237   *     non duplica nulla.
1238   *   · NON DECIDE — nasce in stato «Proposta». Il pulsante mette la misura sul tavolo, non la
1239   *     firma al posto del tecnico.
1240   *   · NON TOCCA CIO' CHE ESISTE — un obbligo gia' coperto viene saltato e contato a parte.
1241   */
1242  var _ULT_RIGHE = [];
1243  function portaProposteCluster(idx) {
1244    var lista = [];
1245    try { lista = (typeof idx === 'string') ? JSON.parse(idx) : idx; } catch (_eJ) { lista = []; }
1246    if (!Array.isArray(lista) || !lista.length) return;
1247    var d = null;
1248    try { d = (typeof win.D === 'function') ? win.D() : (win.d || null); } catch (_eD) { d = win.d || null; }
1249    if (!d) { if (win.showToast) win.showToast('Progetto non disponibile', 'warn'); return; }
1250    if (!Array.isArray(d.azioniMiglioramento)) d.azioniMiglioramento = [];
1251    var esistenti = {};
1252    d.azioniMiglioramento.forEach(function (a) { if (a && a._obbligoKey) esistenti[_s(a._obbligoKey)] = 1; });
1253    var K = win._tecAcamObblighi && win._tecAcamObblighi.chiaveObbligo;
1254    var creati = 0, saltati = 0;
1255    lista.forEach(function (i) {
1256      var o = _ULT_RIGHE[i];
1257      if (!o) return;
1258      var key = _s(o._chiave) || (K ? _s(K(o)) : '');
1259      if (!key || esistenti[key]) { saltati++; return; }
1260      var R = _num(o.residuo) || _num(o.rischioIntrinseco);
1261      d.azioniMiglioramento.push({
1262        rischioCod: _s(o.rischioCod),
1263        rischioNome: _s(o.titolo),
1264        luogoId: _s(o.luogoId), mansioneId: _s(o.mansioneId), attrezzoId: _s(o.attrezzoId),
1265        entitaTipo: _s(o.entitaTipo), entitaNome: _s(o.entitaNome),
1266        tipo: o.sorgente === 'obbligo_legge' ? 'organizzativa' : 'tecnica',
1267        descrizione: _s(o.azioneProposta),
1268        norma: _s(o.azioneNorma) || _s(o.norma),
1269        responsabile: (win._tecAzioniDaResiduo && win._tecAzioniDaResiduo.RESPONSABILE_DEFAULT) || 'Datore di Lavoro',
1270        rResiduoPre: R || null,
1271        rAtteso: R ? Math.max(1, Math.round(R * 0.6)) : null,
1272        priorita: (o.soglia && o.soglia.id === 'non_tollerabile') ? 'Alta' : (R >= 9 ? 'Alta' : (R >= 5 ? 'Media' : 'Bassa')),
1273        scadenza: '',
1274        stato: 'Proposta',
1275        fonte: 'Obbligo di trattamento — ' + _s(o.sorgente).replace(/_/g, ' ')
1276             + (_s(o.entitaNome) ? (' su ' + _s(o.entitaTipo) + ' «' + _s(o.entitaNome) + '»') : ''),
1277        _daObbligo: true,
1278        _obbligoKey: key
1279      });
1280      esistenti[key] = 1; creati++;
1281    });
1282    try {
1283      if (typeof win.setD === 'function') win.setD('azioniMiglioramento', JSON.parse(JSON.stringify(d.azioniMiglioramento)));
1284      else if (typeof win.saveAll === 'function') win.saveAll();
1285    } catch (_eS) { if (win.tecLog) win.tecLog.catch('acam:portaProposteCluster', _eS, { noThrow: true }); }
1286    if (win.showToast) {
1287      win.showToast(creati + ' azioni create nel Programma (stato «Proposta»)'
1288        + (saltati ? (' · ' + saltati + ' saltate: già presenti') : '')
1289        + ' — assegna responsabile e scadenza per renderle esigibili', creati ? 'success' : 'info');
1290    }
1291    try { if (typeof win.renderTab === 'function') win.renderTab(); } catch (_eR) {}
1292  }
1293
1294  /**
1295   * ── v20.373.150 — UNA TAB SOLA, NON QUATTRO MONDI ────────────────────────────────────
1296   * Segnalazione del committente sulla .149: «4 blocchi, invece di essere integrati sembrano
1297   * 4 mondi a parte. Un disastro». Era vero, e la causa non era grafica: i quattro blocchi
1298   * rispondevano a DUE domande diverse messe una sopra l'altra senza dirlo.
1299   *   - «che cosa c'è da fare»   → registro obblighi · carenze del programma · azioni
1300   *   - «come sto lavorando»     → i nove indicatori e le tre corsie
1301   * La seconda domanda è legittima ma non è quella che si porta in tab: si guarda quando si
1302   * vuole guardare. Da qui: una sola testata che dichiara i tre passi con i loro numeri e ci
1303   * porta sopra, i tre passi in fila numerati (la numerazione qui è vera: il passo 2 esiste
1304   * solo per gli obblighi del passo 1, il passo 3 è ciò che li chiude), e gli indicatori in
1305   * fondo, chiusi, dietro un titolo che dice a che cosa servono.
1306   */
1307  function _passo(n, titolo, sottotitolo, ancora) {
1308    return '<div id="' + ancora + '" style="display:flex;align-items:baseline;gap:9px;margin:2px 0 -5px 2px">'
1309      + '<span style="flex:none;width:20px;height:20px;border-radius:50%;background:' + NAVY + ';color:#fff;'
1310      + 'font-size:10.5px;font-weight:900;display:inline-flex;align-items:center;justify-content:center">' + n + '</span>'
1311      + '<span style="font-size:11px;font-weight:900;letter-spacing:.06em;text-transform:uppercase;color:' + NAVY + '">'
1312      + _e(titolo) + '</span>'
1313      + '<span style="font-size:10.5px;color:' + GRIGIO + ';font-style:italic">' + _e(sottotitolo) + '</span></div>';
1314  }
1315
1316  function _tappa(n, ancora, eti, cifra, colore) {
1317    return '<button type="button" onclick="var _x=document.getElementById(\'' + ancora + '\');if(_x)_x.scrollIntoView({behavior:\'smooth\',block:\'start\'})" '
1318      + 'style="cursor:pointer;text-align:left;flex:1 1 190px;border:1px solid ' + BORDO + ';border-left:4px solid ' + colore + ';'
1319      + 'background:#fbfcfe;border-radius:9px;padding:8px 12px;font-family:inherit">'
1320      + '<div style="display:flex;align-items:baseline;gap:7px">'
1321      + '<span style="font-size:9px;font-weight:900;color:' + GRIGIO + '">' + n + '</span>'
1322      + '<span style="font-size:21px;font-weight:900;line-height:1;color:' + colore + '">' + _e(cifra) + '</span></div>'
1323      + '<div style="font-size:11px;font-weight:800;color:' + NAVY + ';margin-top:3px;line-height:1.35">' + _e(eti) + '</div></button>';
1324  }
1325
1326
1327  /* ══════════════════════════════════════════════════════════════════════════════════════════
1328   * v20.373.284 — IL PASSO 1 IN CINQUE VISTE
1329   *
1330   * ── LA SEGNALAZIONE ──────────────────────────────────────────────────────────────────────
1331   * Beniamino Macchiarola, sulla .283: «il tecnico guarda quelle righe e, all'interno di
1332   * ciascun cluster, sembrano tutte uguali: non c'e' indicazione di gravita. Inoltre non
1333   * capisco a quale rischio ciascuna di quelle azioni sia riconducibile… sarebbe utile avere
1334   * un pulsante che mi colleghi alla tabella del rischio… sarebbe utile che sia indicata
1335   * l'azione, con il responsabile, la tempistica desiderata e quella reale (cosi' monitoriamo
1336   * anche l'efficacia della pianificazione)». E prima ancora: «proviamo a spezzare l'elefante
1337   * in tanti pezzi, sara' piu' facile mangiarlo».
1338   *
1339   * ── PERCHE' CINQUE VISTE E NON UNA TABELLA PIU' LARGA ────────────────────────────────────
1340   * La tabella del Registro risponde a sette domande per ogni riga — da dove nasce, che cosa
1341   * manca, che cosa fare, dove, entro quando, chi puo' accettarlo, a che punto e' — cioe' circa
1342   * diciotto fatti per riga. Su ventinove obblighi fanno cinquecento fatti a schermo. Non e'
1343   * una tabella sbagliata: e' la vista ISPETTIVA, quella che deve reggere un controllo, messa
1344   * come vista predefinita. Ma chi apre questa tab il martedi' mattina non fa un'ispezione: fa
1345   * UNA cosa, e ogni cosa vuole campi diversi.
1346   *   1 Oggi         — «che cosa faccio adesso?»            ordinata per scadenza e gravita'
1347   *   2 Il giro      — «vado sul posto: cosa chiudo li'?»   raggruppata per entita'
1348   *   3 Avanzamento  — «dove si e' fermato il lavoro?»      quattro colonne di stato
1349   *   4 Scadenze     — «previsto contro reale»              quattro bande di legge
1350   *   5 Registro     — «regge un controllo?»                la tabella di prima, intera
1351   *
1352   * ── COME SONO FATTE ──────────────────────────────────────────────────────────────────────
1353   * Un solo dato — `reg.righe` — e cinque proiezioni. Nessuna vista ricalcola nulla e nessuna
1354   * possiede righe che le altre non hanno: e' la stessa regola SSOT del Q1, applicata alla
1355   * vista invece che al motore. Tutte e cinque vengono disegnate insieme e si alternano
1356   * nascondendo un contenitore: cosi' cambiare vista non ridisegna la tab, non perde lo stato
1357   * aperto delle cartelle del Registro e non costa una seconda lettura dei motori.
1358   * ══════════════════════════════════════════════════════════════════════════════════════════ */
1359
1360  /** Legge una data italiana (gg/mm/aaaa) o ISO. null se non e' una data: non si indovina. */
1361  function _dataIt(v) {
1362    var s = _s(v); if (!s) return null;
1363    var m = s.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/);
1364    if (m) { var a = +m[3]; if (a < 100) a += 2000; return new Date(a, +m[2] - 1, +m[1]); }
1365    m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
1366    if (m) return new Date(+m[1], +m[2] - 1, +m[3]);
1367    return null;
1368  }
1369  function _oggiD() { var x = new Date(); return new Date(x.getFullYear(), x.getMonth(), x.getDate()); }
1370  function _ggTra(a, b) { return Math.round((a - b) / 864e5); }
1371
1372  /**
1373   * LO SCOSTAMENTO — previsto contro reale, sulla singola riga.
1374   * «Cosi' monitoriamo anche l'efficacia della pianificazione.» E' l'indicatore «puntualita'»
1375   * di tec-programma-misura, che oggi si calcola sull'intero programma e non si vede su nessuna
1376   * riga. Qui non si inventa nulla: si mettono le due date una accanto all'altra e si dice la
1377   * differenza. Quando la scadenza e' solo PROPOSTA dal catalogo e nessuno l'ha presa, lo si
1378   * DICHIARA invece di misurare un ritardo contro una data che non esiste — sarebbe un
1379   * indicatore inventato, e questo file esiste per non produrne.
1380   */
1381  function _scost(o) {
1382    var pi = (o && o.piano) || {};
1383    var pv = _dataIt(pi.scadenza), rl = _dataIt(pi.dataAttuazione);
1384    if (!pv) {
1385      if (_num(pi.giorniProposti)) {
1386        return { cls: 'prop', txt: _num(pi.giorniProposti) + ' gg proposti — data non decisa',
1387                 col: '#64748b', bg: '#f1f5f9', bd: '#e2e8f0' };
1388      }
1389      return { cls: 'none', txt: 'da pianificare', col: '#64748b', bg: '#f1f5f9', bd: '#e2e8f0' };
1390    }
1391    if (rl) {
1392      var d = _ggTra(rl, pv);
1393      return d > 0
1394        ? { cls: 'late', txt: 'chiusa con ' + d + ' gg di ritardo', col: '#b91c1c', bg: '#fef2f2', bd: '#fecaca' }
1395        : { cls: 'ok', txt: (d === 0 ? 'chiusa in data' : ('chiusa ' + (-d) + ' gg prima')), col: '#15803d', bg: '#f0fdf4', bd: '#bbf7d0' };
1396    }
1397    var r = _ggTra(pv, _oggiD());
1398    if (r < 0) return { cls: 'late', txt: 'scaduta da ' + (-r) + ' gg', col: '#b91c1c', bg: '#fef2f2', bd: '#fecaca' };
1399    if (r <= 45) return { cls: 'soon', txt: 'mancano ' + r + ' gg', col: '#a16207', bg: '#fffbeb', bd: '#fde68a' };
1400    return { cls: 'far', txt: 'mancano ' + r + ' gg', col: '#475569', bg: '#f8fafc', bd: '#e2e8f0' };
1401  }
1402
1403  /**
1404   * LA GRAVITA', CON LA SUA SCALA.
1405   * Non esiste UNA gravita' per tutti e tre i tipi di obbligo, e fingere che esista sarebbe un
1406   * numero inventato: un residuo P x D e una sanzione penale non sono lo stesso numero. Ogni
1407   * peso porta quindi il nome della scala da cui viene, e chi legge sa che cosa sta
1408   * confrontando. Il valore arriva gia' calcolato dal registro: qui non si ricalcola, si
1409   * traduce in colore e in ordine.
1410   */
1411  var _RANGO = { critico: 4, alto: 3, medio: 2, basso: 1 };
1412  function _gravOf(o) {
1413    var lab = _s(o && o.gravita) || '—';
1414    var col = '#' + (_s(o && o.gravitaColore) || '64748B');
1415    var peso = _num(o && o.residuo) || _num(o && o.rischioIntrinseco);
1416    return {
1417      lab: lab, col: col, peso: peso, rango: _RANGO[lab.toLowerCase()] || 0,
1418      scala: _s(o && o.gravitaScala) || 'scala non dichiarata',
1419      cifra: peso ? String(peso) : '●'
1420    };
1421  }
1422  /** Stessa tinta del peso, quasi bianca: un fondo che si accorda senza urlare. */
1423  /**
1424   * v20.373.284 — IL TITOLO E' UN TITOLO, NON UN PARAGRAFO.
1425   * Su un adempimento di legge `titolo` e `azioneProposta` sono LO STESSO TESTO — la KB scrive
1426   * l'obbligo come frase d'azione — e una riga a due piani lo stampava due volte di seguito,
1427   * per intero: quattrocento caratteri ripetuti. Qui si taglia, si mette il testo completo nel
1428   * title (niente si perde: il taglio e' visivo) e piu' sotto si smette di ripetere.
1429   */
1430  function _tronca(v, n) {
1431    var t = _s(v);
1432    return (t.length <= n) ? t : (t.slice(0, n - 1).replace(/[\s,;:.]+$/, '') + '…');
1433  }
1434  function _stessoTesto(a, b) {
1435    function k(x) { return _s(x).toLowerCase().replace(/[\s.,;:]+/g, ' ').trim(); }
1436    return !!k(a) && k(a) === k(b);
1437  }
1438
1439  function _tenue(hex) {
1440    try {
1441      var h = String(hex).replace('#', '');
1442      return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',.08)';
1443    } catch (_e) { return '#f8fafc'; }
1444  }
1445
1446  /**
1447   * LA PORTA VERSO LA TABELLA DEL RISCHIO.
1448   * «Sarebbe utile avere un pulsante che mi colleghi alla tabella del rischio (luoghi,
1449   * mansioni, attrezzi, trasversale).» Le prime tre le sapeva gia' fare `_portaEntita`; la
1450   * quarta no. Un rischio TRASVERSALE d'impresa — stress, differenze di genere, gravidanza —
1451   * non si valuta sulla mansione: si valuta una volta sola, nella tab dei Rischi. Mandare li'
1452   * il tecnico sulla mansione sarebbe mandarlo nel posto sbagliato con un pulsante che
1453   * funziona, che e' peggio di un pulsante rotto.
1454   * La classe non la decidiamo qui: si legge dal CATALOGO del Motore, che ne e' il proprietario.
1455   */
1456  function _eTrasversale(cod) {
1457    if (!_s(cod)) return false;
1458    try {
1459      var CAT = win._tecMotore && win._tecMotore.CATALOGO_PERICOLI;
1460      if (!CAT) return false;
1461      for (var k in CAT) {
1462        if (CAT[k] && _s(CAT[k].cod).toUpperCase() === _s(cod).toUpperCase()) {
1463          return /trasversal/i.test(_s(CAT[k].categoria));
1464        }
1465      }
1466    } catch (_e) {}
1467    return false;
1468  }
1469  function _dest(o) {
1470    var t = _s(o.entitaTipo);
1471    if (_eTrasversale(o.rischioCod)) {
1472      return { lab: 'Rischi trasversali', atto: 'window._tecVaiTab && window._tecVaiTab(5)',
1473               tit: 'Questo rischio è trasversale d’impresa: si valuta una volta sola, nella tab dei Rischi' };
1474    }
1475    if (t === 'luogo' && _s(o.luogoId)) {
1476      return { lab: 'Luoghi',
1477               atto: 'window._tecStatoLuoghi && window._tecStatoLuoghi.apri(' + JSON.stringify(_s(o.luogoId)) + ',\'\',' + JSON.stringify(_s(o.punto)) + ')',
1478               tit: 'Apre la scheda del luogo, sul requisito ' + (_s(o.punto) || 'interessato') };
1479    }
1480    var TAB = { mansione: [20, 'Mansioni'], attrezzo: [19, 'Attrezzi'], ditta: [15, 'Ditte'], luogo: [3, 'Luoghi'] };
1481    var x = TAB[t];
1482    if (!x) return null;
1483    return { lab: x[1],
1484             atto: 'window._tecDashboardRischi && window._tecDashboardRischi.apri({tab:' + x[0] + ',titolo:'
1485                   + JSON.stringify(_s(o.entitaNome)) + ',norma:' + JSON.stringify(_s(o.norma)) + '})',
1486             tit: 'Apre la tabella in cui questo rischio è stato valutato: è lì che si cambiano P e D' };
1487  }
1488  function _btnVai(o, largo) {
1489    var D = _dest(o);
1490    if (!D) return '<span data-acam-porta="0" style="font-size:9.5px;color:#94a3b8;font-style:italic">nessuna schermata propria</span>';
1491    return '<button type="button" data-acam-porta="1" onclick="event.stopPropagation();' + _e(D.atto) + '" title="' + _e(D.tit) + '" '
1492      + 'style="cursor:pointer;border:1px solid ' + BORDO + ';background:#fff;color:#1d4ed8;border-radius:7px;'
1493      + 'font-family:inherit;font-size:11px;font-weight:750;padding:5px 9px;white-space:nowrap'
1494      + (largo ? ';width:100%' : '') + '">&#8594; ' + _e(D.lab) + '</button>';
1495  }
1496
1497  /* BL-4 — UI stato da TecAcamStato.META; chiave '' = senza azione collegata. */
1498  function _statoUi(k) {
1499    if (!k) return { l: 'Senza azione', c: '#c2410c', b: '#fff3ec' };
1500    var St = win.TecAcamStato;
1501    if (St && St.meta) {
1502      var m = St.meta(k);
1503      return { l: St.normalizza(k), c: m.col, b: m.bg };
1504    }
1505    return { l: String(k), c: '#64748b', b: '#f8fafc' };
1506  }
1507  var _SORG_UI = {
1508    rischio:        { l: 'Rischio valutato', c: '#c2410c', b: '#fff3ec' },
1509    non_conformita: { l: 'Non conformità', c: '#a16207', b: '#fffbeb' },
1510    obbligo_legge:  { l: 'Adempimento',      c: '#6d28d9', b: '#f5f2ff' }
1511  };
1512  var _TIPO_ET = { luogo: 'Luogo', mansione: 'Mansione', attrezzo: 'Attrezzo', ditta: 'Ditta' };
1513
1514  /**
1515   * LA RIGA. Tre piani, e la gravita' a sinistra perche' si legga senza leggere:
1516   *   1 · codice del rischio · di quale rischio si tratta · su quale entita'
1517   *   2 · l'azione — o la dichiarazione che il catalogo non ne propone
1518   *   3 · responsabile · prevista · reale · scostamento
1519   * `piani:false` toglie i piani 2 e 3 dove non servono (il Giro, dove conta l'inventario).
1520   */
1521  function _rigaObbligo(o, i, opz) {
1522    opz = opz || {};
1523    var G = _gravOf(o), S = _scost(o), St = _statoUi(o.stato);
1524    var pi = o.piano || {};
1525    var h = '<button type="button" data-acam-riga="1" onclick="window._tecAcamDettaglio&&window._tecAcamDettaglio(' + i + ')" '
1526      + 'style="display:grid;grid-template-columns:6px 62px 1fr auto;align-items:stretch;width:100%;text-align:left;'
1527      + 'font-family:inherit;background:#fff;border:0;border-top:1px solid #f1f5f9;padding:0;cursor:pointer">'
1528      + '<span style="background:' + G.col + '"></span>'
1529      + '<span style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:9px 4px;'
1530      +   'background:' + _tenue(G.col) + ';border-right:1px solid #f1f5f9">'
1531      +   '<span style="font-size:19px;font-weight:800;line-height:1;color:' + G.col + '">' + G.cifra + '</span>'
1532      +   '<span style="font-size:8px;font-weight:900;letter-spacing:.05em;text-transform:uppercase;margin-top:3px;color:' + G.col + '">' + _e(G.lab) + '</span>'
1533      +   '<span style="font-size:7.5px;color:' + GRIGIO + ';margin-top:1px">' + _e(G.scala.split(' (')[0]) + '</span>'
1534      + '</span>'
1535      + '<span style="padding:9px 12px;min-width:0">'
1536      +   '<span style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">'
1537      +     (_s(o.rischioCod)
1538            ? '<span style="font-size:10.5px;font-weight:800;padding:1px 5px;border-radius:4px;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe">' + _e(o.rischioCod) + '</span>'
1539            : '<span title="Un adempimento di legge non discende da un rischio valutato" style="font-size:10.5px;font-weight:800;padding:1px 5px;border-radius:4px;background:#f5f2ff;color:#6d28d9;border:1px solid #ddd6fe">LEGGE</span>')
1540      +     '<span title="' + _e(o.titolo) + '" style="font-size:13.2px;font-weight:650;color:#1e293b">' + _e(_tronca(o.titolo, 96)) + '</span>'
1541      +     '<span style="font-size:11.5px;color:' + GRIGIO + '">' + _e(_TIPO_ET[o.entitaTipo] || o.entitaTipo) + ' · ' + _e(o.entitaNome || '—') + '</span>'
1542      +   '</span>';
1543    if (opz.piani !== false) {
1544      h += '<span data-acam-cosafare="1" style="display:flex;gap:6px;margin-top:5px;font-size:11.8px;color:#334155;line-height:1.45">'
1545        + '<span style="flex:none;color:' + GRIGIO + '">&#9874;</span><span>'
1546        + (_arr(o.azioni).length
1547            ? (_arr(o.azioni).map(function (a) { return _e(_tronca(_s(a.descrizione || a.azione || a.misura) || '(azione senza testo)', 150)); }).join(' · ')
1548               + ' <span data-acam-copre="1" style="font-size:8.5px;font-weight:900;text-transform:uppercase;color:#fff;background:#2563eb;border-radius:4px;padding:1px 5px">ACAM</span>')
1549            : (_s(o.azioneProposta)
1550                ? ((_stessoTesto(o.azioneProposta, o.titolo)
1551                     ? '<span style="color:#64748b">l’adempimento stesso è la misura: non c’è altro da progettare, va eseguito</span>'
1552                     : _e(_tronca(o.azioneProposta, 150)))
1553                   + ' <span data-acam-copre="proposta" style="font-size:8.5px;font-weight:900;text-transform:uppercase;color:#fff;background:#7c3aed;border-radius:4px;padding:1px 5px">Proposta</span>')
1554                : '<span data-acam-copre="0" style="color:#b45309">il catalogo non propone la misura: va scritta dal tecnico</span>'))
1555        + '</span></span>'
1556        + '<span style="display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:5px;font-size:11px;color:' + GRIGIO + '">'
1557        +   '<span>Resp. <b style="color:#334155">' + _e(_s(pi.responsabile) || 'da assegnare') + '</b></span><span>·</span>'
1558        +   '<span>prevista <b style="color:#334155">' + _e(_s(pi.scadenza) || '—') + '</b></span><span>·</span>'
1559        +   '<span>reale <b style="color:#334155">' + _e(_s(pi.dataAttuazione) || '—') + '</b></span>'
1560        +   '<span style="font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;color:' + S.col + ';background:' + S.bg + ';border:1px solid ' + S.bd + '">' + _e(S.txt) + '</span>'
1561        + '</span>';
1562    } else {
1563      h += '<span data-acam-cosafare="1" style="display:block;margin-top:3px;font-size:10.5px;color:' + GRIGIO + '">'
1564        + _e(St.l) + ' · ' + _e(_s(pi.responsabile) || 'responsabile da assegnare')
1565        + (_s(pi.scadenza) ? (' · prevista ' + _e(pi.scadenza)) : '') + '</span>';
1566    }
1567    h += '</span>'
1568      + '<span style="display:flex;flex-direction:column;gap:6px;align-items:flex-end;justify-content:center;padding:9px 12px 9px 6px">'
1569      +   '<span style="font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:5px;color:' + St.c + ';background:' + St.b + '">' + St.l + '</span>'
1570      +   _btnVai(o)
1571      + '</span></button>';
1572    return h;
1573  }
1574
1575  function _cornice(l) {
1576    return '<div style="border:1px solid ' + BORDO + ';border-radius:10px;overflow:hidden;background:#fff">' + l + '</div>';
1577  }
1578
1579  /** L'istogramma delle gravita': quattro barre che dicono di che cosa e' fatto l'insieme. */
1580  function _istogramma(righe) {
1581    var K = [['critico', '#b91c1c'], ['alto', '#c2410c'], ['medio', '#a16207'], ['basso', '#15803d']];
1582    var n = K.map(function (k) { return _arr(righe).filter(function (o) { return _s(o.gravita).toLowerCase() === k[0]; }).length; });
1583    var mx = Math.max.apply(null, n.concat([1]));
1584    return '<div data-acam-istogramma="1" title="Di che cosa è fatto questo insieme, per gravità" '
1585      + 'style="flex:none;display:flex;gap:5px;align-items:flex-end;border:1px solid ' + BORDO + ';background:#fbfcfe;border-radius:9px;padding:8px 11px">'
1586      + K.map(function (k, i) {
1587          return '<div style="display:flex;flex-direction:column;align-items:center;gap:4px;min-width:34px">'
1588            + '<div style="font-size:11px;font-weight:700;color:' + k[1] + '">' + n[i] + '</div>'
1589            + '<div style="width:100%;border-radius:3px 3px 0 0;height:' + Math.round(5 + n[i] / mx * 28) + 'px;background:' + k[1] + ';opacity:' + (n[i] ? 1 : 0.2) + '"></div>'
1590            + '<div style="font-size:8.5px;font-weight:900;letter-spacing:.05em;text-transform:uppercase;color:' + GRIGIO + '">' + k[0].slice(0, 3) + '</div></div>';
1591        }).join('') + '</div>';
1592  }
1593  function _testataVista(tit, sotto, righe) {
1594    return '<div style="display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:13px">'
1595      + '<div style="flex:1;min-width:250px"><div style="font-size:14.5px;font-weight:800;color:' + NAVY + '">' + tit + '</div>'
1596      + '<div style="font-size:12px;color:' + GRIGIO + ';margin-top:3px;line-height:1.5;max-width:76ch">' + sotto + '</div></div>'
1597      + _istogramma(righe) + '</div>';
1598  }
1599
1600  /* ── 1 · OGGI ───────────────────────────────────────────────────────────────────────────
1601   * L'ordine e' quello del mestiere, non quello dell'archivio: prima chi e' gia' scaduto, poi
1602   * chi scade presto, e a parita' di tempo il piu' grave. E' l'unica vista che mette insieme le
1603   * tre sorgenti senza distinguerle: alle otto del martedi' non importa da dove nasce
1604   * l'obbligo, importa quale sia il prossimo. */
1605  function _vOggi(righe) {
1606    var TETTO = 12;
1607    var ord = { late: 0, soon: 1, prop: 2, far: 3, none: 4, ok: 5 };
1608    var aperti = _arr(righe).filter(function (o) { return o.stato !== 'chiuso'; }).slice().sort(function (a, b) {
1609      var sa = _scost(a), sb = _scost(b);
1610      return (ord[sa.cls] - ord[sb.cls]) || (_gravOf(b).rango - _gravOf(a).rango) || (_gravOf(b).peso - _gravOf(a).peso);
1611    });
1612    if (!aperti.length) {
1613      return _testataVista('Nessun obbligo aperto',
1614        'Tutti gli obblighi del registro sono chiusi. Non è un traguardo definitivo: la valutazione è dinamica e il registro si riapre al primo dato nuovo.', righe);
1615    }
1616    var vis = aperti.slice(0, TETTO);
1617    return _testataVista('Le ' + vis.length + ' che pesano di più, in ordine di scadenza e gravità',
1618        'Prima ciò che è già scaduto, poi ciò che scade presto; a parità di tempo, il più grave. La colonna di sinistra è il peso: '
1619      + 'sotto il numero c\'è la <b>scala</b> da cui viene, perché un residuo P×D e una sanzione penale non sono lo stesso numero.', aperti)
1620      + _cornice(vis.map(function (o) { return _rigaObbligo(o, _ULT_RIGHE.indexOf(o)); }).join(''))
1621      + (aperti.length > TETTO
1622          ? ('<div style="font-size:11.5px;color:' + GRIGIO + ';margin-top:9px">Altri <b>' + (aperti.length - TETTO)
1623             + '</b> obblighi aperti oltre questi: non sono nascosti, sono in coda — e per intero nella vista <b>Registro</b>.</div>')
1624          : '');
1625  }
1626
1627  /* ── 2 · IL GIRO ────────────────────────────────────────────────────────────────────────
1628   * Come si lavora davvero: quando vai in Archivio chiudi tutto quel che riguarda l'Archivio,
1629   * non «tutti gli scivolamenti dell'azienda». Qui la riga si accorcia — l'entita' e' nella
1630   * testata e ripeterla su ogni riga era rumore — e il pulsante «Vai» sta nella testata una
1631   * volta sola. I riquadri sono ordinati per il caso peggiore che contengono. */
1632  function _vGiro(righe) {
1633    var by = {}, ord = [];
1634    _arr(righe).forEach(function (o) {
1635      var k = _s(o.entitaTipo) + '|' + (_s(o.entitaNome) || '—');
1636      if (!by[k]) { by[k] = []; ord.push(k); }
1637      by[k].push(o);
1638    });
1639    function peggiore(l) {
1640      var StG = win.TecAcamStato;
1641      return Math.max.apply(null, l.map(function (o) {
1642        return (StG && StG.chiusa({ stato: o.stato })) ? 0 : _gravOf(o).rango;
1643      }).concat([0]));
1644    }
1645    ord.sort(function (a, b) { return peggiore(by[b]) - peggiore(by[a]) || by[b].length - by[a].length; });
1646    return _testataVista('Un riquadro per posto — ordinati per la gravità peggiore che contengono',
1647        'L\'unità di lavoro reale: quando si va sul posto si chiude tutto quel che c\'è lì. Dentro ogni riquadro le righe sono ordinate per peso, '
1648      + 'e la proposta di default agisce sulle sole righe di quel posto.', righe)
1649      + '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:13px">'
1650      + ord.map(function (k) {
1651          var l = by[k].slice().sort(function (a, b) { return _gravOf(b).rango - _gravOf(a).rango || _gravOf(b).peso - _gravOf(a).peso; });
1652          var sc = l.filter(function (o) { return !o.stato; }).length;
1653          var StG2 = win.TecAcamStato;
1654          var w = l.filter(function (o) { return !(StG2 && StG2.chiusa({ stato: o.stato })); })[0];
1655          var G = w ? _gravOf(w) : null;
1656          var nome = k.split('|').slice(1).join('|');
1657          return '<div data-acam-cluster="1" style="border:1px solid ' + BORDO + ';border-radius:11px;background:#fff;overflow:hidden;display:flex;flex-direction:column">'
1658            + '<div style="padding:10px 12px;background:#f8fafc;border-bottom:1px solid ' + BORDO + ';display:flex;gap:10px;align-items:flex-start">'
1659            +   '<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:750;color:' + NAVY + '">' + _e(nome) + '</div>'
1660            +   '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:2px">' + _e(_TIPO_ET[k.split('|')[0]] || k.split('|')[0]) + ' · ' + l.length + ' obblighi · '
1661            +   (sc ? ('<b style="color:#c2410c">' + sc + ' scoperti</b>') : 'nessuno scoperto')
1662            +   (G ? (' · peggiore: <b style="color:' + G.col + '">' + _e(G.lab) + '</b>') : '') + '</div></div>'
1663            +   '<div style="flex:none;max-width:152px">' + _btnVai(l[0], true) + '</div>'
1664            + '</div>'
1665            + '<div style="flex:1">' + l.map(function (o) { return _rigaObbligo(o, _ULT_RIGHE.indexOf(o), { piani: false }); }).join('') + '</div>'
1666            + _propostaCluster(l, nome || 'questo gruppo')
1667            + '</div>';
1668        }).join('') + '</div>';
1669  }
1670
1671  /* ── 3 · AVANZAMENTO ────────────────────────────────────────────────────────────────────
1672   * La domanda non e' «che cosa manca» ma «dove si e' fermato il lavoro». Una colonna «In
1673   * carico» piena di bordi rossi che non si svuota e' un'informazione che nessun elenco
1674   * ordinato per urgenza restituisce. Il bordo sinistro di ogni scheda e' la gravita', la
1675   * pastiglia e' lo scostamento fra data prevista e data reale. */
1676  function _vAvanz(righe) {
1677    var StA = win.TecAcamStato;
1678    var C = [''].concat((StA && StA.STATI) ? StA.STATI.slice() : []);
1679    return _testataVista('Il Programma come flusso, non come elenco',
1680        'Le stesse righe disposte per stato. Una colonna che non si svuota, o che si svuota in ritardo, si vede da qui e da nessun\'altra parte.', righe)
1681      + '<div style="display:grid;grid-template-columns:repeat(' + Math.min(C.length, 5) + ',minmax(196px,1fr));gap:11px;overflow-x:auto">'
1682      + C.map(function (k) {
1683          var l = _arr(righe).filter(function (o) { return k ? o.stato === k : !o.stato; })
1684                             .sort(function (a, b) { return _gravOf(b).rango - _gravOf(a).rango; });
1685          if (!l.length && k) return '';
1686          var St = _statoUi(k);
1687          return '<div data-acam-colonna="' + k + '" style="background:#f8fafc;border:1px solid ' + BORDO + ';border-radius:11px;padding:10px;min-width:196px">'
1688            + '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:9px;font-size:11px;font-weight:900;'
1689            +   'letter-spacing:.05em;text-transform:uppercase;color:' + St.c + '">' + St.l + ' <span style="font-size:13px">' + l.length + '</span></div>'
1690            + (l.length ? l.map(function (o) {
1691                var G = _gravOf(o), S = _scost(o);
1692                return '<button type="button" data-acam-riga="1" onclick="window._tecAcamDettaglio&&window._tecAcamDettaglio(' + _ULT_RIGHE.indexOf(o) + ')" '
1693                  + 'style="display:block;width:100%;text-align:left;font-family:inherit;background:#fff;border:1px solid #f1f5f9;'
1694                  + 'border-left:3px solid ' + G.col + ';border-radius:8px;padding:8px 10px;margin-bottom:7px;cursor:pointer">'
1695                  + '<span style="display:block;font-size:12.2px;font-weight:650;color:#1e293b;line-height:1.35">'
1696                  + (_s(o.rischioCod) ? ('<span style="font-size:10px;font-weight:800;color:#1d4ed8">' + _e(o.rischioCod) + '</span> ') : '')
1697                  + _e(_tronca(o.titolo, 72)) + '</span>'
1698                  + '<span style="display:flex;justify-content:space-between;gap:6px;align-items:center;margin-top:4px;font-size:10.5px;color:' + GRIGIO + '">'
1699                  + '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + _e(o.entitaNome || '—') + '</span>'
1700                  + '<span style="flex:none;font-weight:700;padding:1px 7px;border-radius:999px;color:' + S.col + ';background:' + S.bg + ';border:1px solid ' + S.bd + '">' + _e(S.txt) + '</span>'
1701                  + '</span></button>';
1702              }).join('')
1703              : ('<div style="font-size:11.5px;color:' + GRIGIO + ';font-style:italic;padding:8px 2px;line-height:1.45">Nessun obbligo in questo stato.</div>'))
1704            + '</div>';
1705        }).join('') + '</div>';
1706  }
1707
1708  /* ── 4 · SCADENZE ───────────────────────────────────────────────────────────────────────
1709   * L'unica vista in cui «chi puo' accettare il rischio residuo» conta davvero: e' la banda a
1710   * deciderlo, ed e' la riga piu' pesante del DVR sul piano della responsabilita'. Sta nel
1711   * sottotitolo della banda — una volta — invece che ripetuta in ogni riga. */
1712  function _vScad(righe) {
1713    var aperti = _arr(righe).filter(function (o) { return o.stato !== 'chiuso'; });
1714    // ── LA BANDA SI CHIEDE AL REGISTRO, NON SI INDOVINA DALL'ETICHETTA ────────────────────
1715    // La prima stesura ordinava le bande con una regex sul TESTO del termine («urgente»,
1716    // «senza indugio»). Il cricchetto delle regole duplicate l'ha bocciata, e aveva ragione:
1717    // e' la classe di difetti in cui una regex sul nome decide al posto della struttura che
1718    // lo dichiara, e basta riformulare un termine perche' l'ordine di legge si scomponga in
1719    // silenzio. La struttura c'e': `soglia.rango` (1 = non tollerabile … 4 = sorvegliato) e la
1720    // `sorgente`. L'etichetta resta solo cio' che si legge a schermo.
1721    function _chiaveBanda(o) {
1722      if (_s(o.sorgente) === 'obbligo_legge') return 'L';          // gia' dovuto: nessuna soglia
1723      if (o.soglia && _num(o.soglia.rango)) return 'S' + _num(o.soglia.rango);
1724      if (_s(o.sorgente) === 'non_conformita') return 'N';          // il termine lo fissa il DL
1725      return 'Z';
1726    }
1727    // L'ordine di legge: cio' che e' gia' dovuto, poi le bande del residuo dalla piu' stretta,
1728    // poi cio' il cui termine e' ancora da concordare.
1729    var ORDINE = { L: 0, S1: 1, S2: 2, N: 3, S3: 4, S4: 5, Z: 9 };
1730    var by = {}, ord = [];
1731    aperti.forEach(function (o) {
1732      var k = _chiaveBanda(o);
1733      if (!by[k]) { by[k] = []; ord.push(k); }
1734      by[k].push(o);
1735    });
1736    ord.sort(function (a, b) { return (ORDINE[a] == null ? 9 : ORDINE[a]) - (ORDINE[b] == null ? 9 : ORDINE[b]); });
1737    var late = aperti.filter(function (o) { return _scost(o).cls === 'late'; }).length;
1738    return _testataVista('Quattro bande di legge — e in ognuna, previsto contro reale',
1739        'Chi può accettare il rischio residuo lo decide la banda: sta scritto qui una volta, invece che ripetuto su ogni riga. '
1740      + (late ? ('In questo progetto <b style="color:#b91c1c">' + late + ' obblighi aperti sono già oltre la data prevista</b>: è la misura di quanto la pianificazione sta reggendo.')
1741              : 'Nessun obbligo aperto è oltre la data prevista.'), aperti)
1742      + '<div style="position:relative;padding-left:22px">'
1743      + '<div style="position:absolute;
1743left:6px;top:8px;bottom:8px;width:2px;background:' + BORDO + '"></div>'
1744      + ord.map(function (k) {
1745          var l = by[k].slice().sort(function (a, b) { return _gravOf(b).rango - _gravOf(a).rango; });
1746          var acc = _s(l[0].soglia ? l[0].soglia.accetta : '') || 'lo fissa il Datore di Lavoro alla presa d’atto';
1747          var eti = _s(l[0].soglia ? l[0].soglia.termine : (l[0].termineDovuto || 'da programmare'));
1748          return '<div data-acam-banda="' + k + '" style="position:relative;margin-bottom:16px">'
1749            + '<div style="position:absolute;left:-20px;top:6px;width:10px;height:10px;border-radius:50%;background:#fff;border:2.5px solid ' + NAVY + '"></div>'
1750            + '<div style="font-size:12.5px;font-weight:750;color:' + NAVY + '">' + _e(eti) + ' <span style="font-size:11.5px;font-weight:600;color:' + GRIGIO + '">· ' + l.length + '</span></div>'
1751            + '<div style="font-size:11px;color:' + GRIGIO + ';margin:2px 0 8px;line-height:1.45">Chi può accettarlo: <b>' + _e(acc) + '</b></div>'
1752            + _cornice(l.map(function (o) { return _rigaObbligo(o, _ULT_RIGHE.indexOf(o)); }).join(''))
1753            + '</div>';
1754        }).join('') + '</div>';
1755  }
1756
1757  /* ── LA BARRA DELLE VISTE ──────────────────────────────────────────────────────────────
1758   * Cinque linguette, ciascuna con la DOMANDA a cui risponde: e' l'etichetta che fa scegliere,
1759   * non il nome. */
1760  var VISTE = [
1761    { id: 'oggi',  n: '1', t: 'Oggi',        q: 'Che cosa faccio adesso?' },
1762    { id: 'giro',  n: '2', t: 'Il giro',     q: 'Vado sul posto: cosa chiudo lì?' },
1763    { id: 'avanz', n: '3', t: 'Avanzamento', q: 'Dove si è fermato il lavoro?' },
1764    { id: 'scad',  n: '4', t: 'Scadenze',    q: 'Previsto contro reale' },
1765    { id: 'reg',   n: '5', t: 'Registro',    q: 'Regge un controllo?' }
1766  ];
1767  var VISTA_DEFAULT = 'oggi';
1768
1769  function htmlPasso1(reg) {
1770    if (!reg || !reg.disponibile) return '';
1771    var righe = _arr(reg.righe);
1772    _ULT_RIGHE = righe;
1773    var att = _vistaCorrente();
1774    var CORPO = { oggi: _vOggi, giro: _vGiro, avanz: _vAvanz, scad: _vScad };
1775
1776    var h = '<div data-acam-blocco="passo1" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;overflow:hidden">';
1777    h += '<div style="display:flex;border-bottom:1px solid ' + BORDO + ';overflow-x:auto">';
1778    VISTE.forEach(function (v) {
1779      var on = (v.id === att);
1780      h += '<button type="button" data-acam-vista-btn="' + v.id + '" onclick="window._tecAcamVista&&window._tecAcamVista(\'' + v.id + '\')" '
1781        + 'style="flex:1 0 auto;min-width:150px;text-align:left;font-family:inherit;cursor:pointer;border:0;'
1782        + 'border-bottom:2.5px solid ' + (on ? NAVY : 'transparent') + ';background:' + (on ? '#f4f8fd' : '#fff') + ';padding:11px 15px 9px">'
1783        + '<span style="display:block;font-size:9.5px;font-weight:800;letter-spacing:.08em;color:' + (on ? '#2E5FA1' : '#94a3b8') + '">VISTA ' + v.n + '</span>'
1784        + '<span style="display:block;font-size:13px;font-weight:750;margin-top:2px;color:' + (on ? NAVY : '#475569') + '">' + _e(v.t) + '</span>'
1785        + '<span style="display:block;font-size:10.5px;margin-top:1px;color:' + GRIGIO + '">' + _e(v.q) + '</span>'
1786        + '</button>';
1787    });
1788    h += '</div>';
1789
1790    VISTE.forEach(function (v) {
1791      var on = (v.id === att);
1792      h += '<div data-acam-vista="' + v.id + '" style="padding:16px 16px 18px' + (on ? '' : ';display:none') + '">';
1793      try {
1794        h += (v.id === 'reg')
1795          ? ('<div style="font-size:14.5px;font-weight:800;color:' + NAVY + '">La vista completa — sette colonne, tutte le sotto-cartelle</div>'
1796             + '<div style="font-size:12px;color:' + GRIGIO + ';margin:3px 0 13px;line-height:1.5;max-width:76ch">'
1797             + 'È la vista che porti a un controllo e che allega il DVR. Non è cambiata: cambia solo che non è più la prima che vedi.</div>'
1798             + htmlObblighi(reg))
1799          : CORPO[v.id](righe);
1800      } catch (_eV) {
1801        if (win.tecLog) win.tecLog.catch('acam:vista:' + v.id, _eV, { noThrow: true });
1802        h += '<div style="font-size:11.5px;color:#b45309">Questa vista non si è potuta disegnare ('
1803           + _e(_s(_eV && _eV.message)) + '): le altre restano leggibili, e il Registro ha comunque tutte le righe.</div>';
1804      }
1805      h += '</div>';
1806    });
1807    return h + '</div>';
1808  }
1809
1810  /* ── LO STATO DELLA VISTA: per progetto, e senza sporcare il progetto ──────────────────
1811   * «La vista scelta si ricorda per progetto.» Si ricorda in localStorage: scriverla dentro `d`
1812   * marcherebbe il progetto come modificato ogni volta che si cambia linguetta, e una
1813   * preferenza di lettura non e' una modifica al DVR. */
1814  function _chiaveVista() {
1815    var pid = '';
1816    try { pid = _s(win.currentProjectId) || _s(win.CP && win.CP() && win.CP().id); } catch (_e) {}
1817    return 'tecdvr:acam:vista:' + (pid || 'default');
1818  }
1819  function _vistaCorrente() {
1820    if (_s(win._acamVista)) return win._acamVista;
1821    try {
1822      var v = win.localStorage && win.localStorage.getItem(_chiaveVista());
1823      if (v && VISTE.some(function (x) { return x.id === v; })) { win._acamVista = v; return v; }
1824    } catch (_e) {}
1825    return VISTA_DEFAULT;
1826  }
1827  win._tecAcamVista = function (k) {
1828    if (!VISTE.some(function (x) { return x.id === k; })) return;
1829    win._acamVista = k;
1830    try { if (win.localStorage) win.localStorage.setItem(_chiaveVista(), k); } catch (_e) {}
1831    // si alternano i contenitori: nessun ridisegno, nessuno stato perso
1832    try {
1833      var i, els = document.querySelectorAll('[data-acam-vista]');
1834      for (i = 0; i < els.length; i++) els[i].style.display = (els[i].getAttribute('data-acam-vista') === k) ? '' : 'none';
1835      var bs = document.querySelectorAll('[data-acam-vista-btn]');
1836      for (i = 0; i < bs.length; i++) {
1837        var on = bs[i].getAttribute('data-acam-vista-btn') === k;
1838        bs[i].style.borderBottomColor = on ? NAVY : 'transparent';
1839        bs[i].style.background = on ? '#f4f8fd' : '#fff';
1840      }
1841    } catch (_e2) {}
1842  };
1843
1844  /* ── IL DETTAGLIO, DI LATO ─────────────────────────────────────────────────────────────
1845   * E' il patto che rende possibile la poverta' di colonne nelle quattro viste operative:
1846   * niente e' nascosto, e' solo SU RICHIESTA. Qui stanno tutti e sette i campi del Registro,
1847   * piu' la spiegazione della gravita' e il previsto-contro-reale per esteso. */
1848  win._tecAcamDettaglio = function (i) {
1849    var o = _ULT_RIGHE[i]; if (!o) return;
1850    var G = _gravOf(o), S = _scost(o), St = _statoUi(o.stato);
1851    var Sg = _SORG_UI[_s(o.sorgente)] || { l: _s(o.sorgente), c: GRIGIO, b: '#f8fafc' };
1852    var pi = o.piano || {};
1853    var id = 'tecAcamDrawer';
1854    var old = document.getElementById(id); if (old && old.parentNode) old.parentNode.removeChild(old);
1855    function rg(k, v) {
1856      return '<div style="display:grid;grid-template-columns:118px 1fr;gap:10px;align-items:baseline;margin-bottom:9px">'
1857        + '<div style="font-size:10px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:' + GRIGIO + '">' + k + '</div>'
1858        + '<div style="font-size:12.3px;color:#334155;line-height:1.5">' + v + '</div></div>';
1859    }
1860    var chiudi = 'var m=document.getElementById(\'' + id + '\');if(m&&m.parentNode)m.parentNode.removeChild(m)';
1861    var fonteTxt = _s(pi.fonte) === 'azione' ? 'quella dell’azione assunta'
1862                 : _s(pi.fonte) === 'decisa' ? 'una scadenza decisa dal tecnico'
1863                 : _s(pi.fonte) === 'proposta' ? 'solo una proposta del catalogo, non ancora presa da nessuno'
1864                 : 'assente';
1865    var box = document.createElement('div');
1866    box.id = id;
1867    box.style.cssText = 'position:fixed;inset:0;z-index:9998;display:flex;justify-content:flex-end';
1868    box.innerHTML =
1869        '<div onclick="' + chiudi + '" style="position:absolute;inset:0;background:rgba(15,23,42,.45)"></div>'
1870      + '<div style="position:relative;width:min(460px,94vw);height:100%;background:#fff;border-left:1px solid ' + BORDO + ';'
1871      +   'box-shadow:-16px 0 44px -22px rgba(15,23,42,.5);display:flex;flex-direction:column;font-family:inherit">'
1872      +   '<div style="padding:15px 18px;border-bottom:1px solid ' + BORDO + ';display:flex;gap:12px;align-items:flex-start">'
1873      +     '<div style="flex:1;font-size:14px;font-weight:800;color:' + NAVY + ';line-height:1.35">' + _e(o.titolo) + '</div>'
1874      +     '<button type="button" onclick="' + chiudi + '" style="cursor:pointer;border:1px solid ' + BORDO + ';background:#fff;color:' + GRIGIO + ';border-radius:7px;width:28px;height:28px;font:inherit;font-size:15px">&times;</button>'
1875      +   '</div>'
1876      +   '<div style="padding:16px 18px;overflow:auto;flex:1">'
1877      +     '<div style="display:flex;gap:7px;flex-wrap:wrap;margin-bottom:15px">'
1878      +       '<span style="font-size:9.5px;font-weight:800;text-transform:uppercase;padding:2px 8px;border-radius:5px;color:' + G.col + ';background:' + _tenue(G.col) + '">' + _e(G.lab) + (G.peso ? (' · ' + G.peso) : '') + '</span>'
1879      +       '<span style="font-size:9.5px;font-weight:800;text-transform:uppercase;padding:2px 8px;border-radius:5px;color:' + Sg.c + ';background:' + Sg.b + '">' + _e(Sg.l) + '</span>'
1880      +       '<span style="font-size:9.5px;font-weight:800;text-transform:uppercase;padding:2px 8px;border-radius:5px;color:' + St.c + ';background:' + St.b + '">' + St.l + '</span>'
1881      +     '</div>'
1882      +     rg('Rischio', _s(o.rischioCod)
1883              ? ('<b>' + _e(o.rischioCod) + '</b>' + (_s(o.rischioNome) ? (' — ' + _e(o.rischioNome)) : ''))
1884              : '<i>nessuno: è un adempimento di legge, non discende da un P × D</i>')
1885      +     rg('Gravità', '<b style="color:' + G.col + '">' + _e(G.lab) + '</b>'
1886              + (G.peso ? (' — ' + _num(o.P) + ' × ' + _num(o.D) + ' = ' + G.peso) : '')
1887              + '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:2px">scala: ' + _e(G.scala) + '</div>')
1888      +     rg('Dove / chi', _e(_TIPO_ET[o.entitaTipo] || o.entitaTipo) + ' — <b>' + _e(o.entitaNome || '—') + '</b>')
1889      +     rg('Norma', _e(o.norma))
1890      +     rg('Entro', _e(_s(o.soglia ? o.soglia.termine : (o.termineDovuto || 'da programmare'))))
1891      +     rg('Chi accetta', _e(_s(o.soglia ? o.soglia.accetta : '') || 'lo fissa il Datore di Lavoro alla presa d’atto'))
1892      +     '<div style="margin-top:16px;padding-top:14px;border-top:1px solid #f1f5f9">'
1893      +       '<div style="font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:' + GRIGIO + ';margin-bottom:8px">L’azione</div>'
1894      +       _cosaFare(o)
1895      +       '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:11px">'
1896      +         '<div style="border:1px solid ' + BORDO + ';border-radius:9px;padding:8px 10px;background:#fbfcfe">'
1897      +           '<div style="font-size:9px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:' + GRIGIO + '">Responsabile</div>'
1898      +           '<div style="font-size:12px;font-weight:650;color:#1e293b;margin-top:3px">' + _e(_s(pi.responsabile) || 'da assegnare') + '</div></div>'
1899      +         '<div style="border:1px solid ' + BORDO + ';border-radius:9px;padding:8px 10px;background:#fbfcfe">'
1900      +           '<div style="font-size:9px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:' + GRIGIO + '">Prevista</div>'
1901      +           '<div style="font-size:12px;font-weight:650;color:#1e293b;margin-top:3px">' + _e(_s(pi.scadenza) || '—') + '</div></div>'
1902      +         '<div style="border:1px solid ' + BORDO + ';border-radius:9px;padding:8px 10px;background:#fbfcfe">'
1903      +           '<div style="font-size:9px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:' + GRIGIO + '">Reale</div>'
1904      +           '<div style="font-size:12px;font-weight:650;color:#1e293b;margin-top:3px">' + _e(_s(pi.dataAttuazione) || '—') + '</div></div>'
1905      +       '</div>'
1906      +       '<div style="margin-top:9px"><span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;color:' + S.col + ';background:' + S.bg + ';border:1px solid ' + S.bd + '">' + _e(S.txt) + '</span>'
1907      +       '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:6px;line-height:1.45">Lo scostamento fra prevista e reale è l’indicatore <b>puntualità</b> del programma, portato sulla singola riga. '
1908      +       'La data qui è <b>' + _e(fonteTxt) + '</b>.</div></div>'
1909      +     '</div>'
1910      +     '<div style="margin-top:16px;padding-top:14px;border-top:1px solid #f1f5f9">'
1911      +       '<div style="font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:' + GRIGIO + ';margin-bottom:8px">Da qui si va</div>'
1912      +       _btnVai(o)
1913      +       '<div style="font-size:10.5px;color:' + GRIGIO + ';margin-top:8px;line-height:1.45">Apre la tabella in cui questo rischio è stato valutato: è lì che si cambiano P e D, non qui.</div>'
1914      +     '</div>'
1915      +   '</div></div>';
1916    document.body.appendChild(box);
1917  };
1918
1919  function html(d, opts) {
1920    opts = opts || {};
1921    var v = opts.vista || A.vista(d);
1922    var reg = v.obblighi || {};
1923    var nAzioni = v.totale || 0;
1924    var nScoperti = reg.disponibile ? (reg.dovutiScoperti || 0) : null;
1925    var nCarenze = (v.allarmi || []).length;
1926
1927    var h = '<div style="display:grid;gap:14px">';
1928
1929    // ── LA TESTATA: una sola, e dice dove si va ────────────────────────────────────────
1930    h += '<div data-acam-testata="1" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;padding:14px 17px">'
1931      + '<div style="font-size:9.5px;font-weight:900;letter-spacing:.09em;text-transform:uppercase;color:' + GRIGIO + '">'
1932      + _e(v.norma) + '</div>'
1933      + '<div style="font-size:16px;font-weight:900;color:' + NAVY + ';margin-top:3px">Che cosa c’è da fare, e con che cosa lo chiudiamo</div>'
1934      + '<div style="font-size:11.5px;color:' + GRIGIO + ';margin-top:3px;line-height:1.5">'
1935      + 'Tre passi, uno dentro l’altro: gli obblighi che questa azienda ha, che cosa manca al programma '
1936      + 'per reggerli, e le azioni che li chiudono. Gli indicatori di come stiamo lavorando stanno in fondo, '
1937      + 'chiusi: rispondono a un’altra domanda.</div>'
1938      + '<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:11px">'
1939      + (reg.disponibile
1940          ? _tappa('1', 'acam-passo-1', (nScoperti ? 'obblighi dovuti e ancora senza azione' : 'obblighi dovuti, tutti presi in carico'),
1941                   (nScoperti || reg.dovuti || 0), (nScoperti ? '#dc2626' : '#16a34a'))
1942          : '')
1943      + _tappa('2', 'acam-passo-2', (nCarenze ? 'cose che il programma non regge a un controllo' : 'il programma non ha carenze aperte'),
1944               (nCarenze || '✓'), (nCarenze ? '#ea580c' : '#16a34a'))
1945      + _tappa('3', 'acam-passo-3', (nAzioni ? 'azioni in programma' : 'nessuna azione nel programma'),
1946               (nAzioni || '—'), (nAzioni ? '#4338ca' : '#94a3b8'))
1947      + '</div></div>';
1948
1949    // ── PASSO 1 — gli obblighi ─────────────────────────────────────────────────────────
1950    if (reg.disponibile) {
1951      h += _passo('1', 'Gli obblighi', 'che cosa la legge e i dati censiti chiedono a questa azienda', 'acam-passo-1');
1952      // v20.373.284 — cinque viste sullo stesso registro; la quinta E' htmlObblighi, intatta.
1953      h += htmlPasso1(reg);
1954    }
1955
1956    // ── PASSO 2 — che cosa manca al programma ──────────────────────────────────────────
1957    h += _passo('2', 'Le carenze del programma', 'perché quegli obblighi, così come sono scritti, non reggono', 'acam-passo-2');
1958    h += htmlAllarmi(v);
1959
1960    // ── PASSO 3 — le azioni ────────────────────────────────────────────────────────────
1961    h += _passo('3', 'Le azioni', 'che cosa li chiude, con i cinque dati che l’art. 28 c.2 lett. c e d chiede', 'acam-passo-3');
1962    h += htmlAzioni(d, opts);
1963
1964    // ── IN FONDO, CHIUSO: come stiamo lavorando ────────────────────────────────────────
1965    h += '<details data-acam-blocco="indicatori" style="border:1px solid ' + BORDO + ';border-radius:14px;background:#fff;overflow:hidden">'
1966      + '<summary style="cursor:pointer;padding:13px 16px;list-style:none">'
1967      + '<div style="font-size:13.5px;font-weight:800;color:' + NAVY + '">Come stiamo lavorando su queste azioni '
1968      + '<span style="font-size:11px;font-weight:700;color:' + GRIGIO + '">— apri</span></div>'
1969      + '<div style="font-size:11px;color:' + GRIGIO + ';margin-top:2px;line-height:1.45">'
1970      + v.kpi.length + ' indicatori · <b style="color:' + (v.nSotto ? '#dc2626' : '#16a34a') + '">'
1971      + v.nCentrati + ' bersagli centrati</b>'
1972      + (v.nNonMisurabili ? ' · ' + v.nNonMisurabili + ' non ancora misurabili' : '')
1973      + ' · e il rigore richiesto a ciascuna corsia. Non è che cosa c’è da fare: è come lo stiamo facendo.</div>'
1974      + '</summary>'
1975      + '<div style="padding:0 16px 15px;display:grid;gap:12px">'
1976      + '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:11px">';
1977    v.kpi.forEach(function (k) { h += _cardKpi(k); });
1978    h += '</div>' + htmlCorsie(v) + '</div></details>';
1979
1980    return h + '</div>';
1981  }
1982
1983  A.html = html;
1984  A.htmlObblighi = htmlObblighi;
1985  A.htmlPasso1 = htmlPasso1;
1986  A.VISTE = VISTE;
1987  A.portaProposteCluster = portaProposteCluster;
1988  A.htmlAllarmi = htmlAllarmi;
1989  A.htmlAzioni = htmlAzioni;
1990})(typeof window !== 'undefined' ? window : this);

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.