Differenze

Queste sono le differenze tra la revisione selezionata e la versione attuale della pagina.

Link a questa pagina di confronto

Prossima revisione
Revisione precedente
gestione_turni_med [2025/08/12 09:45] – creata neoadmingestione_turni_med [2025/08/12 10:16] (versione attuale) neoadmin
Linea 7: Linea 7:
         body {         body {
             font-family: Arial, sans-serif;             font-family: Arial, sans-serif;
-            margin: 20px;+            margin: 10px;
             background: #f5f5f5;             background: #f5f5f5;
 +            line-height: 1.4;
         }         }
                  
Linea 15: Linea 16:
             margin: 0 auto;             margin: 0 auto;
             background: white;             background: white;
-            padding: 20px+            padding: 15px
-            border-radius: 10px;+            border-radius: 8px;
             box-shadow: 0 2px 10px rgba(0,0,0,0.1);             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
         }         }
Linea 24: Linea 25:
             background: linear-gradient(135deg, #2c3e50, #3498db);             background: linear-gradient(135deg, #2c3e50, #3498db);
             color: white;             color: white;
-            padding: 20px+            padding: 15px
-            border-radius: 10px+            border-radius: 8px
-            margin-bottom: 20px;+            margin-bottom: 15px; 
 +        } 
 +         
 +        .header h1 { 
 +            margin: 0 0 8px 0; 
 +            font-size: 22px; 
 +            font-weight: bold; 
 +        } 
 +         
 +        .header h2 { 
 +            margin: 0 0 5px 0; 
 +            font-size: 16px; 
 +            font-weight: normal; 
 +        } 
 +         
 +        .header p { 
 +            margin: 0; 
 +            font-size: 13px; 
 +            opacity: 0.9;
         }         }
                  
         .controls {         .controls {
             display: flex;             display: flex;
-            gap: 20px+            gap: 15px
-            margin-bottom: 20px;+            margin-bottom: 15px;
             flex-wrap: wrap;             flex-wrap: wrap;
 +            align-items: flex-end;
         }         }
                  
Linea 39: Linea 59:
             display: flex;             display: flex;
             flex-direction: column;             flex-direction: column;
-            gap: 5px;+            gap: 4px; 
 +        } 
 +         
 +        .control-group label { 
 +            font-size: 12px; 
 +            font-weight: bold; 
 +            color: #2c3e50;
         }         }
                  
         select, input, button {         select, input, button {
-            padding: 8px 12px;+            padding: 6px 10px;
             border: 1px solid #ddd;             border: 1px solid #ddd;
-            border-radius: 5px+            border-radius: 4px
-            font-size: 14px;+            font-size: 13px;
         }         }
                  
Linea 54: Linea 80:
             cursor: pointer;             cursor: pointer;
             font-weight: bold;             font-weight: bold;
 +            transition: background 0.3s;
         }         }
                  
Linea 253: Linea 280:
                 <label>Configurazione</label>                 <label>Configurazione</label>
                 <button onclick="mostraConfiguratoreCopertura()">⚙️ Configura Copertura</button>                 <button onclick="mostraConfiguratoreCopertura()">⚙️ Configura Copertura</button>
 +            </div>
 +            
 +            <div class="control-group">
 +                <label>Gestione Medici</label>
 +                <button onclick="mostraGestioneMedici()">👥 Gestisci Medici</button>
             </div>             </div>
                          
Linea 278: Linea 310:
     <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>     <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
     <script>     <script>
-        // Database medici con competenze vincoli perfezionati +        // Database medici con caratteristiche complete configurabili 
-        const medici = {+        let medici = {
             'MC':             'MC':
-                nome: 'MC - Direttore SS',  +                nome: 'M', cognome: 'C', nomeCompleto: 'MC - Direttore SS', 
-                reperibilitafalse,  +                ruolo'Direttore SS', posizione: 'rTIN', reperibile: true, privilege: '',  
-                guardiaGiorno: false,  +                anzianita: '>15', dataAssunzione: '', fte: 100, 
-                guardiaNotte: false,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                // Competenze turni 
-                followUp: true,  +                reperibilita: false, guardiaGiorno: false, guardiaNotte: false,  
-                urgenzeSalaParto: true, +                ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, 
-                maxOreSettimanali: 42, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 7, 
-                minOreSettimanali: 38, +                maxNottiConsecutive: 0, maxWeekendMese:
-                prioritaRiposo: 7, // Alta priorità riposo +            }, 
-                maxNottiConsecutive: 0, +            'MR': 
-                maxWeekendMese: 1+                nome: 'M', cognome: 'R', nomeCompleto: 'MR - Dirigente', 
 +                ruolo: 'Dirigente Medico', posizione: 'Turnista', reperibile: false, privilege: '',  
 +                anzianita: '<5', dataAssunzione: '', fte: 100, 
 +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
 +                decCalabria: '5', ferieResidue: 0, oreEccedenza: 0, 
 +                // Competenze turni 
 +                reperibilita: false, guardiaGiorno: true, guardiaNotte: false,  
 +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
 +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
 +                maxNottiConsecutive: 0, maxWeekendMese: 4
             },             },
             'CM':             'CM':
-                nome: 'CM - Aiuto Senior',  +                nome: 'C', cognome: 'M', nomeCompleto: 'CM - Aiuto Senior', 
-                reperibilita: true,  +                ruolo: 'AS', posizione: 'rFUN', reperibile: true, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'>15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                // Competenze turni 
-                followUp: true,  +                reperibilita: true, guardiaGiorno: true, guardiaNotte: true,  
-                urgenzeSalaParto: true, +                ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, 
-                maxOreSettimanali: 42, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 8, 
-                minOreSettimanali: 38, +                maxNottiConsecutive: 1, maxWeekendMese: 3
-                prioritaRiposo: 8, // Minimo guardie per CM +
-                maxNottiConsecutive: 1, +
-                maxWeekendMese: 3+
             },             },
             'FM':             'FM':
-                nome: 'FM - Aiuto Senior',  +                nome: 'F', cognome: 'M', nomeCompleto: 'FM - Aiuto Senior', 
-                reperibilita: true,  +                ruolo: 'AS', posizione: 'rTIN', reperibile: true, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'>15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: true, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'FCa':             'FCa':
-                nome: 'FCa - Dirigente',  +                nome: 'F', cognome: 'Ca', nomeCompleto: 'FCa - Dirigente', 
-                reperibilita: true,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'5-15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: true, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: true,  +                ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'EF':             'EF':
-                nome: 'EF - Dirigente',  +                nome: 'E', cognome: 'F', nomeCompleto: 'EF - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'5-15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'FF':             'FF':
-                nome: 'FF - Dirigente',  +                nome: 'F', cognome: 'F', nomeCompleto: 'FF - Dirigente', 
-                reperibilita: true,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'5-15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: true, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'LF':             'LF':
-                nome: 'LF - Dirigente',  +                nome: 'L', cognome: 'F', nomeCompleto: 'LF - Dirigente', 
-                reperibilita: true,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'5-15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: true, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'MF':             'MF':
-                nome: 'MF - Dirigente PT',  +                nome: 'M', cognome: 'F', nomeCompleto: 'MF - Dirigente PT', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rNido', reperibile: false, privilege: '',  
-                guardiaGiorno: false,  +                anzianita: '5-15', dataAssunzione: '', fte: 50, 
-                guardiaNotte: false,  +                limitazioniGravidanza: false, altreLimitazioni: 'Part-time 50%', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: false, guardiaNotte: false,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: false, 
-                urgenzeSalaParto: false, +                maxOreSettimanali: 20, minOreSettimanali: 18, prioritaRiposo: 9, 
-                maxOreSettimanali: 20, // Part-time 50% +                maxNottiConsecutive: 0, maxWeekendMese: 2
-                minOreSettimanali: 18, +
-                prioritaRiposo: 9, +
-                maxNottiConsecutive: 0, +
-                maxWeekendMese: 2+
             },             },
             'FC':             'FC':
-                nome: 'FC - Dirigente',  +                nome: 'F', cognome: 'C', nomeCompleto: 'FC - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'5-15', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'VE':             'VE':
-                nome: 'VE - Dirigente',  +                nome: 'V', cognome: 'E', nomeCompleto: 'VE - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rNido', reperibile: false, privilege: '',  
-                guardiaGiorno: false,  +                anzianita: '5-15', dataAssunzione: '', fte: 100, 
-                guardiaNotte: false,  +                limitazioniGravidanza: false, altreLimitazioni: 'Specialista Nido', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: false, guardiaNotte: false,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: false, 
-                urgenzeSalaParto: false, +                maxOreSettimanali: 38, minOreSettimanali: 30, prioritaRiposo: 8, 
-                maxOreSettimanali: 38, +                maxNottiConsecutive: 0, maxWeekendMese: 2
-                minOreSettimanali: 30, +
-                prioritaRiposo: 8, // Specialista Nido +
-                maxNottiConsecutive: 0, +
-                maxWeekendMese: 2+
             },             },
             'CC':             'CC':
-                nome: 'CC - Dirigente',  +                nome: 'C', cognome: 'C', nomeCompleto: 'CC - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'<5', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             },             },
             'FT':             'FT':
-                nome: 'FT - Dirigente',  +                nome: 'F', cognome'T'nomeCompleto'FT - Dirigente'
-                reperibilitafalse +                ruolo'Dirigente Medico'posizione'rTIN'reperibile: false, privilege'',  
-                guardiaGiornotrue,  +                anzianita'<5'dataAssunzione''fte100
-                guardiaNottetrue +                limitazioniGravidanzafalsealtreLimitazioni: '', assentefalse, 
-                ptntrue +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaPartotrue+                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali38, +
-                prioritaRiposo5+
-                maxNottiConsecutive2, +
-                maxWeekendMese+
-            }, +
-            'MR':  +
-                nome: 'MR - Dirigente',  +
-                reperibilita: false,  +
-                guardiaGiorno: true,  +
-                guardiaNotte: false,  +
-                ptn: true,  +
-                nido: true,  +
-                followUp: false,  +
-                urgenzeSalaParto: true, +
-                maxOreSettimanali: 42, +
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 0, +
-                maxWeekendMese: 4+
             },             },
             'GA':             'GA':
-                nome: 'GA - Dirigente',  +                nome: 'G', cognome: 'A', nomeCompleto: 'GA - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'<5', dataAssunzione: '', fte: 100
-                guardiaNotte: false,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptntrue,  +                decCalabria'', ferieResidue: 0, oreEccedenza: 0
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: false,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 0, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 0, +
-                maxWeekendMese: 4+
             },             },
             'SM':             'SM':
-                nome: 'SM - Dirigente',  +                nome: 'S', cognome: 'M', nomeCompleto: 'SM - Dirigente', 
-                reperibilita: false,  +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
-                guardiaGiornotrue,  +                anzianita'<5', dataAssunzione: '', fte: 100
-                guardiaNotte: true,  +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
-                ptn: true,  +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
-                nido: true,  +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
-                followUp: false,  +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
-                urgenzeSalaParto: true, +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
-                maxOreSettimanali: 42, +                maxNottiConsecutive: 2, maxWeekendMese: 4
-                minOreSettimanali: 38, +
-                prioritaRiposo: 5, +
-                maxNottiConsecutive: 2, +
-                maxWeekendMese: 4+
             }             }
         };         };
  
-        let turniMensili = {}; +        // 🎯 CONFIGURAZIONI MASTER 
-        let statisticheMensili = {};+        const configurazioniMaster = { 
 +            ruoli: ['Direttore SC', 'Direttore SS', 'AS', 'Dirigente Medico'], 
 +            posizioni: ['rTIN', 'rFUN', 'rNido', 'cNido', 'Turnista'], 
 +            anzianita: ['<5', '5-15', '>15'], 
 +            fte: [25, 50, 75, 100], 
 +            privilege: [1, 2, 3, 4, 5] // Scala da definire 
 +        }; 
 + 
 +        // 👥 GESTIONE MEDICI DINAMICA 
 +        function mostraGestioneMedici() { 
 +            const gestioneHtml = ` 
 +                <div id="gestioneMedici" style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border: 2px solid #3498db; max-height: 80vh; overflow-y: auto;"> 
 +                    <h3>👥 GESTIONE MEDICI E CARATTERISTICHE</h3> 
 +                     
 +                    <div style="display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap;"> 
 +                        <button onclick="aggiungiNuovoMedico()" style="background: #27ae60; color: white; padding: 10px 15px; border: none; border-radius: 5px; font-weight: bold;">➕ Aggiungi Medico</button> 
 +                        <button onclick="importaCaratteristiche()" style="background: #3498db; color: white; padding: 10px 15px; border: none; border-radius: 5px; font-weight: bold;">📂 Importa da File</button> 
 +                        <button onclick="esportaCaratteristiche()" style="background: #9b59b6; color: white; padding: 10px 15px; border: none; border-radius: 5px; font-weight: bold;">💾 Esporta</button> 
 +                        <button onclick="resetMediciDefault()" style="background: #e74c3c; color: white; padding: 10px 15px; border: none; border-radius: 5px; font-weight: bold;">🔄 Reset Default</button> 
 +                    </div> 
 + 
 +                    <div style="overflow-x: auto;"> 
 +                        <table id="tabellaMedici" style="width: 100%; border-collapse: collapse; font-size: 11px; background: white; border-radius: 5px;"> 
 +                            <thead> 
 +                                <tr style="background: #3498db; color: white;"> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 40px;">Nome</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Cognome</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 80px;">Ruolo</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Posizione</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Reperibile</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 50px;">Privilege</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Anzianità</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 40px;">FTE%</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Limit. Grav.</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 80px;">Altre Limit.</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Assente</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Dec. Cal.</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Ferie Res.</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Ore Ecc.</th> 
 +                                    <th style="padding: 8px; border: 1px solid #ddd; min-width: 60px;">Azioni</th> 
 +                                </tr> 
 +                            </thead> 
 +                            <tbody id="corpotabellaMedici"> 
 +                                ${generaRigheMedici()} 
 +                            </tbody> 
 +                        </table> 
 +                    </div> 
 + 
 +                    <div style="margin-top: 20px; padding: 15px; background: #ecf0f1; border-radius: 5px; font-size: 12px;"> 
 +                        <h4>📋 LEGENDA CAMPI:</h4> 
 +                        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;"> 
 +                            <div><strong>Ruoli:</strong> Direttore SC/SS, AS, Dirigente Medico</div> 
 +                            <div><strong>Posizioni:</strong> rTIN, rFUN, rNido, cNido, Turnista</div> 
 +                            <div><strong>Anzianità:</strong> &lt;5, 5-15, &gt;15 anni</div> 
 +                            <div><strong>FTE:</strong> Tempo pieno 100%, part-time, ecc.</div> 
 +                            <div><strong>Privilege:</strong> Scala 1-5 (da definire)</div> 
 +                            <div><strong>Dec. Calabria:</strong> Anno specialità, assenza = vuoto</div> 
 +                        </div> 
 +                    </div> 
 + 
 +                    <div style="text-align: center; margin-top: 20px;"> 
 +                        <button onclick="salvaMedici()" style="background: #27ae60; color: white; padding: 12px 20px; border: none; border-radius: 5px; font-weight: bold; margin-right: 10px;">💾 Salva Modifiche</button> 
 +                        <button onclick="applicaCaratteristicheATurni()" style="background: #f39c12; color: white; padding: 12px 20px; border: none; border-radius: 5px; font-weight: bold; margin-right: 10px;">🔄 Applica ai Turni</button> 
 +                        <button onclick="chiudiGestioneMedici()" style="background: #95a5a6; color: white; padding: 12px 20px; border: none; border-radius: 5px; font-weight: bold;">❌ Chiudi</button> 
 +                    </div> 
 +                </div> 
 +            `; 
 +             
 +            const controlsDiv = document.querySelector('.controls'); 
 +            controlsDiv.insertAdjacentHTML('afterend', gestioneHtml); 
 +        } 
 + 
 +        function generaRigheMedici() { 
 +            let html ''; 
 +            Object.keys(medici).forEach(codice => { 
 +                const medico = medici[codice]; 
 +                html += ` 
 +                    <tr id="riga_${codice}"> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="text" value="${medico.nome}" id="nome_${codice}" style="width: 100%; border: none; font-size: 11px;"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="text" value="${medico.cognome}" id="cognome_${codice}" style="width: 100%; border: none; font-size: 11px;"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"> 
 +                            <select id="ruolo_${codice}" style="width: 100%; border: none; font-size: 11px;"> 
 +                                ${configurazioniMaster.ruoli.map(r => `<option value="${r}" ${medico.ruolo === r ? 'selected' : ''}>${r}</option>`).join('')} 
 +                            </select> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"> 
 +                            <select id="posizione_${codice}" style="width: 100%; border: none; font-size: 11px;"> 
 +                                ${configurazioniMaster.posizioni.map(p => `<option value="${p}" ${medico.posizione === p ? 'selected' : ''}>${p}</option>`).join('')} 
 +                            </select> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd; text-align: center;"> 
 +                            <input type="checkbox" id="reperibile_${codice}" ${medico.reperibile ? 'checked' : ''}> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"> 
 +                            <select id="privilege_${codice}" style="width: 100%; border: none; font-size: 11px;"> 
 +                                <option value="">-</option> 
 +                                ${configurazioniMaster.privilege.map(p => `<option value="${p}" ${medico.privilege == p ? 'selected' : ''}>${p}</option>`).join('')} 
 +                            </select> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"> 
 +                            <select id="anzianita_${codice}" style="width: 100%; border: none; font-size: 11px;"> 
 +                                ${configurazioniMaster.anzianita.map(a => `<option value="${a}" ${medico.anzianita === a ? 'selected' : ''}>${a}</option>`).join('')} 
 +                            </select> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"> 
 +                            <select id="fte_${codice}" style="width: 100%; border: none; font-size: 11px;"> 
 +                                ${configurazioniMaster.fte.map(f => `<option value="${f}" ${medico.fte == f ? 'selected' : ''}>${f}%</option>`).join('')} 
 +                            </select> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd; text-align: center;"> 
 +                            <input type="checkbox" id="limitGrav_${codice}" ${medico.limitazioniGravidanza ? 'checked' : ''}> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="text" value="${medico.altreLimitazioni}" id="altreLim_${codice}" style="width: 100%; border: none; font-size: 11px;"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd; text-align: center;"> 
 +                            <input type="checkbox" id="assente_${codice}" ${medico.assente ? 'checked' : ''}> 
 +                        </td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="text" value="${medico.decCalabria}" id="decCal_${codice}" style="width: 100%; border: none; font-size: 11px;"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="number" value="${medico.ferieResidue}" id="ferie_${codice}" style="width: 100%; border: none; font-size: 11px;" min="0"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd;"><input type="number" value="${medico.oreEccedenza}" id="oreEcc_${codice}" style="width: 100%; border: none; font-size: 11px;" min="0"></td> 
 +                        <td style="padding: 5px; border: 1px solid #ddd; text-align: center;"> 
 +                            <button onclick="modificaCompetenze('${codice}')" style="background: #3498db; color: white; border: none; border-radius: 3px; padding: 3px 6px; font-size: 10px; margin-right: 3px;">⚙️</button> 
 +                            <button onclick="eliminaMedico('${codice}')" style="background: #e74c3c; color: white; border: none; border-radius: 3px; padding: 3px 6px; font-size: 10px;">🗑️</button> 
 +                        </td> 
 +                    </tr> 
 +                `; 
 +            }); 
 +            return html
 +        
 + 
 +        function aggiungiNuovoMedico() { 
 +            const nome = prompt("Nome del nuovo medico:"); 
 +            const cognome = prompt("Cognome del nuovo medico:"); 
 +             
 +            if (!nome || !cognome) { 
 +                mostraMessaggio('❌ Nome e cognome sono obbligatori', 'error'); 
 +                return; 
 +            } 
 +             
 +            const codice = (nome.charAt(0) + cognome.charAt(0)).toUpperCase(); 
 +            let codiceFinal codice; 
 +            let counter = 1; 
 +             
 +            // Evita duplicati 
 +            while (medici[codiceFinal]) { 
 +                codiceFinal = codice + counter; 
 +                counter++; 
 +            } 
 +             
 +            // Nuovo medico con valori default 
 +            medici[codiceFinal] = { 
 +                nome: nome, cognome: cognome, nomeCompleto: `${codiceFinal} - Nuovo`, 
 +                ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '',  
 +                anzianita: '<5', dataAssunzione: '', fte: 100, 
 +                limitazioniGravidanza: false, altreLimitazioni: '', assente: false, 
 +                decCalabria: '', ferieResidue: 0, oreEccedenza: 0, 
 +                // Competenze turni default 
 +                reperibilita: false, guardiaGiorno: true, guardiaNotte: true,  
 +                ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, 
 +                maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, 
 +                maxNottiConsecutive: 2, maxWeekendMese:
 +            }; 
 +             
 +            // Aggiorna tabella 
 +            document.getElementById('corpotabellaMedici').innerHTML = generaRigheMedici(); 
 +            mostraMessaggio(`✅ Medico ${codiceFinal} aggiunto con successo`, 'success'); 
 +        } 
 + 
 +        function eliminaMedico(codice) { 
 +            if (confirm(`Sei sicuro di voler eliminare il medico ${medici[codice].nomeCompleto}?`)) { 
 +                delete medici[codice]; 
 +                document.getElementById('corpotabellaMedici').innerHTML = generaRigheMedici(); 
 +                mostraMessaggio(`✅ Medico ${codice} eliminato`, 'success'); 
 +            } 
 +        } 
 + 
 +        function modificaCompetenze(codice) { 
 +            const medico = medici[codice]; 
 +            const competenzeHtml = ` 
 +                <div id="modificaCompetenze_${codice}" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 1000; max-width: 500px;"> 
 +                    <h4>⚙️ Competenze Turni - ${medico.nomeCompleto}</h4> 
 +                     
 +                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 15px 0;"> 
 +                        <label><input type="checkbox" id="comp_reperibilita_${codice}" ${medico.reperibilita ? 'checked' : ''}> Reperibilità</label> 
 +                        <label><input type="checkbox" id="comp_guardiaGiorno_${codice}" ${medico.guardiaGiorno ? 'checked' : ''}> Guardia Giorno</label> 
 +                        <label><input type="checkbox" id="comp_guardiaNotte_${codice}" ${medico.guardiaNotte ? 'checked' : ''}> Guardia Notte</label> 
 +                        <label><input type="checkbox" id="comp_ptn_${codice}" ${medico.ptn ? 'checked' : ''}> PTN</label> 
 +                        <label><input type="checkbox" id="comp_nido_${codice}" ${medico.nido ? 'checked' : ''}> NIDO</label> 
 +                        <label><input type="checkbox" id="comp_followUp_${codice}" ${medico.followUp ? 'checked' : ''}> Follow-up</label> 
 +                        <label><input type="checkbox" id="comp_urgenze_${codice}" ${medico.urgenzeSalaParto ? 'checked' : ''}> Urgenze/SP</label> 
 +                    </div> 
 +                     
 +                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 15px 0;"> 
 +                        <label>Min Ore/Sett: <input type="number" id="comp_minOre_${codice}" value="${medico.minOreSettimanali}" min="0" max="60" style="width: 60px;"></label> 
 +                        <label>Max Ore/Sett: <input type="number" id="comp_maxOre_${codice}" value="${medico.maxOreSettimanali}" min="0" max="60" style="width: 60px;"></label> 
 +                        <label>Max Notti Consec: <input type="number" id="comp_maxNotti_${codice}" value="${medico.maxNottiConsecutive}" min="0" max="5" style="width: 60px;"></label> 
 +                        <label>Max Weekend/Mese: <input type="number" id="comp_maxWeekend_${codice}" value="${medico.maxWeekendMese}" min="0" max="8" style="width: 60px;"></label> 
 +                    </div> 
 +                     
 +                    <div style="text-align: center; margin-top: 20px;"> 
 +                        <button onclick="salvaCompetenze('${codice}')" style="background: #27ae60; color: white; padding: 8px 15px; border: none; border-radius: 5px; margin-right: 10px;">💾 Salva</button> 
 +                        <button onclick="chiudiCompetenze('${codice}')" style="background: #95a5a6; color: white; padding: 8px 15px; border: none; border-radius: 5px;">❌ Annulla</button> 
 +                    </div> 
 +                </div> 
 +                <div id="overlay_${codice}" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999;" onclick="chiudiCompetenze('${codice}')"></div> 
 +            `; 
 +             
 +            document.body.insertAdjacentHTML('beforeend', competenzeHtml); 
 +        } 
 + 
 +        function salvaCompetenze(codice) { 
 +            const medico = medici[codice]; 
 +             
 +            medico.reperibilita = document.getElementById(`comp_reperibilita_${codice}`).checked; 
 +            medico.guardiaGiorno = document.getElementById(`comp_guardiaGiorno_${codice}`).checked; 
 +            medico.guardiaNotte = document.getElementById(`comp_guardiaNotte_${codice}`).checked; 
 +            medico.ptn = document.getElementById(`comp_ptn_${codice}`).checked; 
 +            medico.nido = document.getElementById(`comp_nido_${codice}`).checked; 
 +            medico.followUp = document.getElementById(`comp_followUp_${codice}`).checked; 
 +            medico.urgenzeSalaParto = document.getElementById(`comp_urgenze_${codice}`).checked; 
 +             
 +            medico.minOreSettimanali = parseInt(document.getElementById(`comp_minOre_${codice}`).value); 
 +            medico.maxOreSettimanali = parseInt(document.getElementById(`comp_maxOre_${codice}`).value); 
 +            medico.maxNottiConsecutive = parseInt(document.getElementById(`comp_maxNotti_${codice}`).value); 
 +            medico.maxWeekendMese = parseInt(document.getElementById(`comp_maxWeekend_${codice}`).value); 
 +             
 +            chiudiCompetenze(codice); 
 +            mostraMessaggio(`✅ Competenze salvate per ${medico.nomeCompleto}`, 'success'); 
 +        } 
 + 
 +        function chiudiCompetenze(codice) { 
 +            const overlay = document.getElementById(`overlay_${codice}`); 
 +            const modal = document.getElementById(`modificaCompetenze_${codice}`); 
 +            if (overlay) overlay.remove(); 
 +            if (modal) modal.remove(); 
 +        } 
 + 
 +        function salvaMedici() { 
 +            // Salva tutte le modifiche dalla tabella 
 +            Object.keys(medici).forEach(codice => { 
 +                const medico = medici[codice]; 
 +                 
 +                medico.nome = document.getElementById(`nome_${codice}`).value; 
 +                medico.cognome = document.getElementById(`cognome_${codice}`).value; 
 +                medico.ruolo = document.getElementById(`ruolo_${codice}`).value; 
 +                medico.posizione = document.getElementById(`posizione_${codice}`).value; 
 +                medico.reperibile = document.getElementById(`reperibile_${codice}`).checked; 
 +                medico.privilege = document.getElementById(`privilege_${codice}`).value; 
 +                medico.anzianita = document.getElementById(`anzianita_${codice}`).value; 
 +                medico.fte = parseInt(document.getElementById(`fte_${codice}`).value); 
 +                medico.limitazioniGravidanza = document.getElementById(`limitGrav_${codice}`).checked; 
 +                medico.altreLimitazioni = document.getElementById(`altreLim_${codice}`).value; 
 +                medico.assente = document.getElementById(`assente_${codice}`).checked; 
 +                medico.decCalabria = document.getElementById(`decCal_${codice}`).value; 
 +                medico.ferieResidue = parseInt(document.getElementById(`ferie_${codice}`).value) || 0; 
 +                medico.oreEccedenza = parseInt(document.getElementById(`oreEcc_${codice}`).value) || 0; 
 +                 
 +                // Aggiorna nome completo 
 +                medico.nomeCompleto = `${codice} - ${medico.ruolo}`; 
 +            }); 
 +             
 +            mostraMessaggio('💾 Caratteristiche medici salvate con successo!', 'success'); 
 +        } 
 + 
 +        function applicaCaratteristicheATurni() { 
 +            // Applica le limitazioni e caratteristiche all'algoritmo dei turni 
 +            Object.keys(medici).forEach(codice => { 
 +                const medico = medici[codice]; 
 +                 
 +                // Regole automatiche basate su caratteristiche 
 +                if (medico.fte < 100) { 
 +                    medico.maxOreSettimanali = Math.floor(42 * medico.fte / 100); 
 +                    medico.minOreSettimanali = Math.floor(38 * medico.fte / 100); 
 +                } 
 +                 
 +                if (medico.limitazioniGravidanza || medico.altreLimitazioni.includes('gravidanza')) { 
 +                    medico.guardiaNotte = false; 
 +                    medico.reperibilita = false; 
 +                    medico.maxOreSettimanali = Math.min(medico.maxOreSettimanali, 36); 
 +                } 
 +                 
 +                if (medico.assente) { 
 +                    medico.guardiaGiorno = false; 
 +                    medico.guardiaNotte = false; 
 +                    medico.reperibilita = false; 
 +                    medico.ptn = false; 
 +                    medico.nido = false; 
 +                    medico.urgenzeSalaParto = false; 
 +                } 
 +                 
 +                if (medico.anzianita === '<5' && medico.ruolo === 'Dirigente Medico') { 
 +                    medico.maxWeekendMese = Math.max(medico.maxWeekendMese, 4); 
 +                } 
 +            }); 
 +             
 +            mostraMessaggio('🔄 Caratteristiche applicate all\'algoritmo turni!', 'success'); 
 +        } 
 + 
 +        function resetMediciDefault() { 
 +            if (confirm('Sei sicuro di voler ripristinare tutti i medici ai valori default? Tutte le modifiche andranno perse.')) { 
 +                // Ripristina database originale 
 +                location.reload(); // Ricarica la pagina per reset completo 
 +            } 
 +        } 
 + 
 +        function applicaStiliCaratteristiche(ws, numRighe, numColonne) { 
 +            const range = XLSX.utils.decode_range(ws['!ref']); 
 +             
 +            const borderStyle = { 
 +                border: { 
 +                    top: { style: 'thin', color: { rgb: '000000' } }, 
 +                    bottom: { style: 'thin', color: { rgb: '000000' } }, 
 +                    left: { style: 'thin', color: { rgb: '000000' } }, 
 +                    right: { style: 'thin', color: { rgb: '000000' } } 
 +                } 
 +            }; 
 +             
 +            // Intestazione (riga 0) 
 +            for (let col = 0; col < numColonne; col++) { 
 +                const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col }); 
 +                if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                ws[cellAddress].s = { 
 +                    font: { bold: true, color: { rgb: 'FFFFFF' } }, 
 +                    fill: { fgColor: { rgb: '9b59b6' } }, 
 +                    alignment: { horizontal: 'center', vertical: 'center' }, 
 +                    border: borderStyle.border 
 +                }; 
 +            } 
 +             
 +            // Trova dove inizia la legenda 
 +            let inizioLegenda = -1; 
 +            for (let row = 0; row <= range.e.r; row++) { 
 +                const cellAddress = XLSX.utils.encode_cell({ r: row, c: 0 }); 
 +                if (ws[cellAddress] && ws[cellAddress].v === 'LEGENDA') { 
 +                    inizioLegenda = row; 
 +                    break; 
 +                } 
 +            } 
 +             
 +            // Applica stili ai dati medici 
 +            for (let row = 1; row < (inizioLegenda > 0 ? inizioLegenda - 1 : range.e.r + 1); row++) { 
 +                for (let col = 0; col < numColonne; col++) { 
 +                    const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); 
 +                    if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                    ws[cellAddress].s = { 
 +                        alignment: { horizontal: 'center', vertical: 'center' }, 
 +                        border: borderStyle.border 
 +                    }; 
 +                } 
 +            } 
 +             
 +            // Applica stili alla legenda 
 +            if (inizioLegenda > 0) { 
 +                for (let row = inizioLegenda; row <= range.e.r; row++) { 
 +                    for (let col = 0; col < numColonne; col++) { 
 +                        const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); 
 +                        if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                         
 +                        if (row === inizioLegenda) { 
 +                            // Titolo legenda 
 +                            ws[cellAddress].s = { 
 +                                font: { bold: true, size: 12 }, 
 +                                fill: { fgColor: { rgb: 'f39c12' } }, 
 +                                alignment: { horizontal: 'center', vertical: 'center' }, 
 +                                border: borderStyle.border 
 +                            }; 
 +                        } else { 
 +                            // Contenuto legenda 
 +                            ws[cellAddress].s = { 
 +                                fill: { fgColor: { rgb: 'fef9e7' } }, 
 +                                alignment: { horizontal: 'center', vertical: 'center' }, 
 +                                border: borderStyle.border 
 +                            }; 
 +                        } 
 +                    } 
 +                } 
 +            } 
 +             
 +            // Larghezza colonne ottimizzata 
 +            ws['!cols'] = [ 
 +                { width: 8 }, { width: 10 }, { width: 15 }, { width: 10 }, { width: 10 }, 
 +                { width: 8 }, { width: 10 }, { width: 15 }, { width: 8 }, { width: 10 }, 
 +                { width: 15 }, { width: 8 }, { width: 10 }, { width: 10 }, { width: 10 } 
 +            ]; 
 +        } 
 + 
 +        function esportaCaratteristiche() { 
 +            try { 
 +                const wb = XLSX.utils.book_new(); 
 +                 
 +                // Prepara dati per export 
 +                const exportData = [ 
 +                    ['Nome', 'Cognome', 'Ruolo', 'Posizione', 'Reperibile', 'Privilege', 'Anzianità', 'Data assunzione', 'FTE', 'Limitazioni Gravidanza', 'Altre limitazioni', 'Assente', 'Dec. Calabria', 'Ferie residue', 'Ore eccedenza'
 +                ]; 
 +                 
 +                Object.keys(medici).forEach(codice => { 
 +                    const m = medici[codice]; 
 +                    exportData.push([ 
 +                        m.nome, m.cognome, m.ruolo, m.posizione, m.reperibile ? 'Sì' : 'No', 
 +                        m.privilege, m.anzianita, m.dataAssunzione, m.fte, 
 +                        m.limitazioniGravidanza ? 'Sì' : 'No', m.altreLimitazioni, 
 +                        m.assente ? 'Sì' : 'No', m.decCalabria, m.ferieResidue, m.oreEccedenza 
 +                    ]); 
 +                }); 
 +                 
 +                // Aggiungi legenda 
 +                exportData.push([]); 
 +                exportData.push(['LEGENDA']); 
 +                exportData.push(['', '', 'Direttore SC', 'rTIN', 'Sì/No', 'Vedere Scala', '<5', 'Per definizione anzianità di Reparto', 'Tempo pieno 100%, ecc.', 'Sì/No', 'Qualitativo – da vedere', 'Sì/No', 'Anno di specialità, assenza = No', 'N° giorni', 'N° ore']); 
 +                exportData.push(['', '', 'Direttore SS', 'rFUN', '', '', '5-15', '', '', '', '', '', '', '', '']); 
 +                exportData.push(['', '', 'AS', 'rNido', '', '', '>15', '', '', '', '', '', '', '', '']); 
 +                exportData.push(['', '', 'Dirigente Medico', 'cNido', '', '', '', '', '', '', '', '', '', '', '']); 
 +                exportData.push(['', '', '', 'Turnista', '', '', '', '', '', '', '', '', '', '', '']); 
 +                 
 +                const ws = XLSX.utils.aoa_to_sheet(exportData); 
 +                 
 +                // Applica stili anche al foglio esportazione caratteristiche 
 +                applicaStiliCaratteristiche(ws, exportData.length, 15); 
 +                 
 +                XLSX.utils.book_append_sheet(wb, ws, 'Caratteristiche Medici'); 
 +                 
 +                const fileName = `Caratteristiche_Medici_${new Date().toISOString().split('T')[0]}.xlsx`; 
 +                XLSX.writeFile(wb, fileName); 
 +                 
 +                mostraMessaggio(`📄 File esportato: ${fileName}`, 'success'); 
 +                 
 +            } catch (error) { 
 +                mostraMessaggio('❌ Errore nell\'esportazione: ' + error.message, 'error'); 
 +            } 
 +        }
  
         // SCHEMA COPERTURA CONFIGURABILE         // SCHEMA COPERTURA CONFIGURABILE
Linea 1185: Linea 1588:
                 controlliSuperati: 0,                 controlliSuperati: 0,
                 avvisi: [],                 avvisi: [],
-                errori: []+                errori: []
 +                successiImportanti: [], 
 +                suggerimenti: [], 
 +                punteggioQualita: 0
             };             };
                          
             const giorni = Object.keys(turniMensili).length;             const giorni = Object.keys(turniMensili).length;
 +            if (giorni === 0) {
 +                validazione.errori.push("Nessun turno generato");
 +                return validazione;
 +            }
                          
-            // 1. Verifica copertura minima giornaliera+            console.log(`Validando ${giorni} giorni di turni...`); 
 +             
 +            let coperturaTotale = 0; 
 +            let coperturaCompleta = 0; 
 +             
 +            // 1. ✅ VERIFICA COPERTURA MINIMA GIORNALIERA
             for (let giorno = 1; giorno <= giorni; giorno++) {             for (let giorno = 1; giorno <= giorni; giorno++) {
-                const turni = turniMensili[giorno].turni;+                const turni = turniMensili[giorno]
 +                if (!turni || !turni.turni) { 
 +                    validazione.errori.push(`Giorno ${giorno}: Struttura turni mancante`); 
 +                    continue; 
 +                }
                                  
-                if (!turni.guardiaGiorno) {+                const turniGiorno = turni.turni; 
 +                let coperturaGiorno = 0; 
 +                 
 +                // Guardia giorno 
 +                if (!turniGiorno.guardiaGiorno) {
                     validazione.errori.push(`Giorno ${giorno}: Manca guardia giorno`);                     validazione.errori.push(`Giorno ${giorno}: Manca guardia giorno`);
                 } else {                 } else {
                     validazione.controlliSuperati++;                     validazione.controlliSuperati++;
 +                    coperturaGiorno++;
                 }                 }
                                  
-                if (!turni.guardiaNotte) {+                // Guardia notte   
 +                if (!turniGiorno.guardiaNotte) {
                     validazione.errori.push(`Giorno ${giorno}: Manca guardia notte`);                     validazione.errori.push(`Giorno ${giorno}: Manca guardia notte`);
                 } else {                 } else {
                     validazione.controlliSuperati++;                     validazione.controlliSuperati++;
 +                    coperturaGiorno++;
                 }                 }
                                  
-                if (!turni.reperibilita) {+                // Reperibilità 
 +                if (!turniGiorno.reperibilita) {
                     validazione.errori.push(`Giorno ${giorno}: Manca reperibilità`);                     validazione.errori.push(`Giorno ${giorno}: Manca reperibilità`);
                 } else {                 } else {
                     validazione.controlliSuperati++;                     validazione.controlliSuperati++;
 +                    coperturaGiorno++;
                 }                 }
 +                
 +                coperturaTotale += 3; // 3 controlli per giorno
 +                coperturaCompleta += coperturaGiorno;
 +            }
 +            
 +            // Aggiungi successo per copertura H24
 +            if (coperturaCompleta === coperturaTotale) {
 +                validazione.successiImportanti.push("Copertura H24 garantita tutti i giorni");
             }             }
                          
-            // 2. Verifica vincoli per medico+            // 2. ✅ VERIFICA VINCOLI PER MEDICO 
 +            let mediciValidati = 0;
             Object.keys(medici).forEach(medico => {             Object.keys(medici).forEach(medico => {
                 const stats = statisticheMensili[medico];                 const stats = statisticheMensili[medico];
                 const config = medici[medico];                 const config = medici[medico];
                                  
-                // Verifica ore settimanali approssimative+                if (!stats) { 
 +                    validazione.avvisi.push(`${medico}: Statistiche mancanti`); 
 +                    return; 
 +                } 
 +                 
 +                mediciValidati++; 
 +                 
 +                // Verifica ore settimanali
                 const oreStimate = (stats.guardie.giorno + stats.guardie.notte) * 12 +                  const oreStimate = (stats.guardie.giorno + stats.guardie.notte) * 12 + 
-                                  (stats.turni.ptn + stats.turni.urgenze) * 8 + +                                  (stats.turni.ptn + stats.turni.urgenze + stats.turni.followUp) * 8 + 
                                   stats.turni.nido * 4.5;                                   stats.turni.nido * 4.5;
                 const settimane = Math.ceil(giorni / 7);                 const settimane = Math.ceil(giorni / 7);
-                const oreSettimanaliMedie = oreStimate / settimane;+                const oreSettimanaliMedie = settimane > 0 ? oreStimate / settimane : 0;
                                  
                 if (oreSettimanaliMedie < config.minOreSettimanali) {                 if (oreSettimanaliMedie < config.minOreSettimanali) {
-                    validazione.avvisi.push(`${medico}: Ore settimanali sotto il minimo (${oreSettimanaliMedie.toFixed(1)}/${config.minOreSettimanali})`);+                    validazione.avvisi.push(`${medico}: Ore settimanali sotto il minimo (${oreSettimanaliMedie.toFixed(1)}h vs ${config.minOreSettimanali}h richieste)`);
                 } else if (oreSettimanaliMedie > config.maxOreSettimanali) {                 } else if (oreSettimanaliMedie > config.maxOreSettimanali) {
-                    validazione.avvisi.push(`${medico}: Ore settimanali sopra il massimo (${oreSettimanaliMedie.toFixed(1)}/${config.maxOreSettimanali})`);+                    validazione.avvisi.push(`${medico}: Ore settimanali sopra il massimo (${oreSettimanaliMedie.toFixed(1)}h vs ${config.maxOreSettimanali}h limite)`);
                 } else {                 } else {
                     validazione.controlliSuperati++;                     validazione.controlliSuperati++;
Linea 1234: Linea 1678:
                                  
                 // Verifica riposi minimi                 // Verifica riposi minimi
-                const riposiSettimanaliMedi = stats.riposi / settimane;+                const riposiSettimanaliMedi = settimane > 0 ? stats.riposi / settimane : 0;
                 if (riposiSettimanaliMedi < vincoliAvanzati.riposiMinimi) {                 if (riposiSettimanaliMedi < vincoliAvanzati.riposiMinimi) {
-                    validazione.avvisi.push(`${medico}: Riposi sotto il minimo settimanale (${riposiSettimanaliMedi.toFixed(1)}/${vincoliAvanzati.riposiMinimi})`);+                    validazione.avvisi.push(`${medico}: Riposi sotto il minimo (${riposiSettimanaliMedi.toFixed(1)} vs ${vincoliAvanzati.riposiMinimi} richiesti/settimana)`);
                 } else {                 } else {
                     validazione.controlliSuperati++;                     validazione.controlliSuperati++;
 +                }
 +                
 +                // Verifica competenze utilizzate correttamente
 +                if (stats.guardie.notte > 0 && !config.guardiaNotte) {
 +                    validazione.errori.push(`${medico}: Assegnate guardie notte ma non abilitato`);
 +                }
 +                if (stats.turni.reperibilita > 0 && !config.reperibilita) {
 +                    validazione.errori.push(`${medico}: Assegnata reperibilità ma non abilitato`);
                 }                 }
             });             });
 +            
 +            // 3. ✅ VERIFICA BILANCIAMENTO
 +            const guardieDiurne = Object.values(statisticheMensili).map(s => s.guardie.giorno).filter(g => g > 0);
 +            const guardieNotturne = Object.values(statisticheMensili).map(s => s.guardie.notte).filter(g => g > 0);
 +            
 +            if (guardieDiurne.length > 0) {
 +                const maxGD = Math.max(...guardieDiurne);
 +                const minGD = Math.min(...guardieDiurne);
 +                const differenzaGD = maxGD - minGD;
 +                
 +                if (differenzaGD <= 2) {
 +                    validazione.successiImportanti.push("Guardie diurne ben bilanciate (differenza max: " + differenzaGD + ")");
 +                    validazione.controlliSuperati++;
 +                } else {
 +                    validazione.avvisi.push(`Guardie diurne sbilanciate: differenza di ${differenzaGD} tra max (${maxGD}) e min (${minGD})`);
 +                }
 +            }
 +            
 +            if (guardieNotturne.length > 0) {
 +                const maxGN = Math.max(...guardieNotturne);
 +                const minGN = Math.min(...guardieNotturne);
 +                const differenzaGN = maxGN - minGN;
 +                
 +                if (differenzaGN <= 2) {
 +                    validazione.successiImportanti.push("Guardie notturne ben bilanciate (differenza max: " + differenzaGN + ")");
 +                    validazione.controlliSuperati++;
 +                } else {
 +                    validazione.avvisi.push(`Guardie notturne sbilanciate: differenza di ${differenzaGN} tra max (${maxGN}) e min (${minGN})`);
 +                }
 +            }
 +            
 +            // 4. ✅ VERIFICA REPERIBILITÀ 7/7
 +            let reperibilita7su7 = true;
 +            for (let giorno = 1; giorno <= giorni; giorno++) {
 +                if (!turniMensili[giorno].turni.reperibilita) {
 +                    reperibilita7su7 = false;
 +                    break;
 +                }
 +            }
 +            
 +            if (reperibilita7su7) {
 +                validazione.successiImportanti.push("Reperibilità 7/7 garantita per tutto il mese");
 +                validazione.controlliSuperati++;
 +            }
 +            
 +            // 5. ✅ CONTROLLI SPECIALI MC e VE
 +            const mcStats = statisticheMensili['MC'];
 +            const veStats = statisticheMensili['VE'];
 +            
 +            if (mcStats && mcStats.guardie.giorno === 0 && mcStats.guardie.notte === 0) {
 +                validazione.successiImportanti.push("MC (Direttore): Correttamente escluso da guardie");
 +                validazione.controlliSuperati++;
 +            }
 +            
 +            if (veStats && veStats.turni.nido >= 4) {
 +                validazione.successiImportanti.push("VE (Specialista Nido): Correttamente assegnato prevalentemente al NIDO");
 +                validazione.controlliSuperati++;
 +            }
 +            
 +            // 6. 💡 SUGGERIMENTI
 +            if (validazione.avvisi.length > validazione.errori.length * 2) {
 +                validazione.suggerimenti.push("Molti avvisi rilevati: considera di ricalcolare i turni con vincoli più flessibili");
 +            }
 +            
 +            if (giorni >= 28 && validazione.controlliSuperati < giorni * 2) {
 +                validazione.suggerimenti.push("Per mesi completi, considera di aumentare la flessibilità dei vincoli orari");
 +            }
 +            
 +            if (Object.keys(statisticheMensili).length < Object.keys(medici).length) {
 +                validazione.suggerimenti.push("Alcuni medici non hanno statistiche: potrebbero non essere stati utilizzati ottimalmente");
 +            }
 +            
 +            // 7. 📊 CALCOLA PUNTEGGIO QUALITÀ
 +            const controlliTotali = coperturaTotale + mediciValidati * 2 + 5; // Stima controlli totali
 +            const percentualeSuccesso = controlliTotali > 0 ? Math.round((validazione.controlliSuperati / controlliTotali) * 100) : 0;
 +            
 +            // Penalità per errori critici
 +            const penalitaErrori = validazione.errori.length * 10;
 +            const penalitaAvvisi = validazione.avvisi.length * 2;
 +            
 +            validazione.punteggioQualita = Math.max(0, Math.min(100, percentualeSuccesso - penalitaErrori - penalitaAvvisi));
 +            
 +            console.log(`Validazione completata: ${validazione.controlliSuperati} controlli superati, ${validazione.errori.length} errori, ${validazione.avvisi.length} avvisi`);
 +            console.log(`Punteggio qualità: ${validazione.punteggioQualita}%`);
                          
             return validazione;             return validazione;
Linea 1459: Linea 1995:
                 const wb = XLSX.utils.book_new();                 const wb = XLSX.utils.book_new();
                                  
-                // FOGLIO 1: TURNI+                // FOGLIO 1: TURNI CON BORDI
                 const excelData = [];                 const excelData = [];
                                  
-                // Intestazioni+                // Intestazioni con stile
                 excelData.push(['P.O. "ALESSANDRO MANZONI" LECCO']);                 excelData.push(['P.O. "ALESSANDRO MANZONI" LECCO']);
                 excelData.push(['Dipartimento Materno-Infantile']);                 excelData.push(['Dipartimento Materno-Infantile']);
Linea 1471: Linea 2007:
                                  
                 // Intestazioni giorni                 // Intestazioni giorni
-                const rigaIntestazioni = ['', ''];+                const rigaIntestazioni = ['MEDICO', ''];
                 for (let giorno = 1; giorno <= giorni; giorno++) {                 for (let giorno = 1; giorno <= giorni; giorno++) {
                     const data = new Date(anno, mese, giorno);                     const data = new Date(anno, mese, giorno);
Linea 1484: Linea 2020:
                 const tuttiMedici = Object.keys(medici);                 const tuttiMedici = Object.keys(medici);
                 tuttiMedici.forEach(codMedico => {                 tuttiMedici.forEach(codMedico => {
-                    const rigaMedico = [medici[codMedico].nome, ''];+                    const rigaMedico = [medici[codMedico].nomeCompleto, ''];
                                          
                     for (let giorno = 1; giorno <= giorni; giorno++) {                     for (let giorno = 1; giorno <= giorni; giorno++) {
Linea 1505: Linea 2041:
  
                 const ws1 = XLSX.utils.aoa_to_sheet(excelData);                 const ws1 = XLSX.utils.aoa_to_sheet(excelData);
 +                
 +                // Applica stili e bordi al foglio turni
 +                applicaStiliExcel(ws1, excelData.length, rigaIntestazioni.length);
 +                
                 XLSX.utils.book_append_sheet(wb, ws1, 'Turni');                 XLSX.utils.book_append_sheet(wb, ws1, 'Turni');
  
Linea 1536: Linea 2076:
                                  
                 const ws2 = XLSX.utils.aoa_to_sheet(statsData);                 const ws2 = XLSX.utils.aoa_to_sheet(statsData);
 +                
 +                // Applica stili al foglio statistiche
 +                applicaStiliStatistiche(ws2, statsData.length, 11);
 +                
                 XLSX.utils.book_append_sheet(wb, ws2, 'Statistiche');                 XLSX.utils.book_append_sheet(wb, ws2, 'Statistiche');
  
-                // FOGLIO 3: VALIDAZIONE+                // FOGLIO 3: VALIDAZIONE CON BORDI
                 const validazione = validaTurniCompleti();                 const validazione = validaTurniCompleti();
                 const validazioneData = [];                 const validazioneData = [];
                 validazioneData.push(['VALIDAZIONE VINCOLI E CONTROLLI']);                 validazioneData.push(['VALIDAZIONE VINCOLI E CONTROLLI']);
 +                validazioneData.push([]);
 +                validazioneData.push(['RISULTATI', 'VALORE']);
                 validazioneData.push(['Controlli Superati', validazione.controlliSuperati]);                 validazioneData.push(['Controlli Superati', validazione.controlliSuperati]);
                 validazioneData.push(['Avvisi', validazione.avvisi.length]);                 validazioneData.push(['Avvisi', validazione.avvisi.length]);
-                validazioneData.push(['Errori', validazione.errori.length]);+                validazioneData.push(['Errori Critici', validazione.errori.length]); 
 +                validazioneData.push(['Punteggio Qualità (%)', validazione.punteggioQualita]);
                 validazioneData.push([]);                 validazioneData.push([]);
 +                
 +                if (validazione.successiImportanti.length > 0) {
 +                    validazioneData.push(['SUCCESSI IMPORTANTI', '']);
 +                    validazione.successiImportanti.forEach(successo => {
 +                        validazioneData.push([successo, '✅']);
 +                    });
 +                    validazioneData.push([]);
 +                }
                                  
                 if (validazione.avvisi.length > 0) {                 if (validazione.avvisi.length > 0) {
-                    validazioneData.push(['AVVISI:']);+                    validazioneData.push(['AVVISI (NON CRITICI)', '']);
                     validazione.avvisi.forEach(avviso => {                     validazione.avvisi.forEach(avviso => {
-                        validazioneData.push([avviso]);+                        validazioneData.push([avviso, '⚠️']);
                     });                     });
                     validazioneData.push([]);                     validazioneData.push([]);
Linea 1556: Linea 2111:
                                  
                 if (validazione.errori.length > 0) {                 if (validazione.errori.length > 0) {
-                    validazioneData.push(['ERRORI:']);+                    validazioneData.push(['ERRORI CRITICI', '']);
                     validazione.errori.forEach(errore => {                     validazione.errori.forEach(errore => {
-                        validazioneData.push([errore]);+                        validazioneData.push([errore, '❌']);
                     });                     });
                 }                 }
                                  
                 const ws3 = XLSX.utils.aoa_to_sheet(validazioneData);                 const ws3 = XLSX.utils.aoa_to_sheet(validazioneData);
 +                
 +                // Applica stili al foglio validazione
 +                applicaStiliValidazione(ws3, validazioneData.length, 2);
 +                
                 XLSX.utils.book_append_sheet(wb, ws3, 'Validazione');                 XLSX.utils.book_append_sheet(wb, ws3, 'Validazione');
  
-                // Esporta file+        // 🎨 FUNZIONI PER STILI EXCEL CON BORDI 
 +        function applicaStiliExcel(ws, numRighe, numColonne) { 
 +            const range = XLSX.utils.decode_range(ws['!ref']); 
 +             
 +            // Stili per bordi 
 +            const borderStyle = { 
 +                border: { 
 +                    top: { style: 'thin', color: { rgb: '000000' } }, 
 +                    bottom: { style: 'thin', color: { rgb: '000000' } }, 
 +                    left: { style: 'thin', color: { rgb: '000000' } }, 
 +                    right: { style: 'thin', color: { rgb: '000000' } } 
 +                } 
 +            }; 
 +             
 +            const headerStyle = { 
 +                font: { bold: true, color: { rgb: 'FFFFFF' } }, 
 +                fill: { fgColor: { rgb: '3498db' } }, 
 +                alignment: { horizontal: 'center', vertical: 'center' }, 
 +                border: { 
 +                    top: { style: 'medium', color: { rgb: '000000' } }, 
 +                    bottom: { style: 'medium', color: { rgb: '000000' } }, 
 +                    left: { style: 'medium', color: { rgb: '000000' } }, 
 +                    right: { style: 'medium', color: { rgb: '000000' } } 
 +                } 
 +            }; 
 +             
 +            const titleStyle = { 
 +                font: { bold: true, size: 14 }, 
 +                alignment: { horizontal: 'center' }, 
 +                border: borderStyle.border 
 +            }; 
 +             
 +            // Applica stili alle intestazioni (riga 7 - giorni) 
 +            for (let col = 0; col <= range.e.c; col++) { 
 +                const cellAddress = XLSX.utils.encode_cell({ r: 6, c: col }); 
 +                if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                ws[cellAddress].s = headerStyle; 
 +            } 
 +             
 +            // Applica bordi a tutte le celle dei dati 
 +            for (let row = 7; row <= range.e.r; row++) { 
 +                for (let col = 0; col <= range.e.c; col++) { 
 +                    const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); 
 +                    if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                     
 +                    if (col === 0) { 
 +                        // Prima colonna (nomi medici) - stile speciale 
 +                        ws[cellAddress].s = { 
 +                            font: { bold: true }, 
 +                            fill: { fgColor: { rgb: 'f8f9fa' } }, 
 +                            alignment: { horizontal: 'left', vertical: 'center' }, 
 +                            border: borderStyle.border 
 +                        }; 
 +                    } else { 
 +                        // Altre celle - bordi normali 
 +                        ws[cellAddress].s = { 
 +                            alignment: { horizontal: 'center', vertical: 'center' }, 
 +                            border: borderStyle.border 
 +                        }; 
 +                    } 
 +                } 
 +            } 
 +             
 +            // Applica stili ai titoli (prime 5 righe) 
 +            for (let row = 0; row < 5; row++) { 
 +                const cellAddress = XLSX.utils.encode_cell({ r: row, c: 0 }); 
 +                if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                ws[cellAddress].s = titleStyle; 
 +            } 
 +             
 +            // Imposta larghezza colonne 
 +            const colWidths = [{ width: 20 }, { width: 3 }]; 
 +            for (let i = 2; i < numColonne; i++) { 
 +                colWidths.push({ width: 12 }); 
 +            } 
 +            ws['!cols'] = colWidths; 
 +        } 
 +         
 +        function applicaStiliStatistiche(ws, numRighe, numColonne) { 
 +            const range = XLSX.utils.decode_range(ws['!ref']); 
 +             
 +            const borderStyle = { 
 +                border: { 
 +                    top: { style: 'thin', color: { rgb: '000000' } }, 
 +                    bottom: { style: 'thin', color: { rgb: '000000' } }, 
 +                    left: { style: 'thin', color: { rgb: '000000' } }, 
 +                    right: { style: 'thin', color: { rgb: '000000' } } 
 +                } 
 +            }; 
 +             
 +            // Intestazione (riga 1) 
 +            for (let col = 0; col < numColonne; col++) { 
 +                const cellAddress = XLSX.utils.encode_cell({ r: 1, c: col }); 
 +                if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                ws[cellAddress].s = { 
 +                    font: { bold: true, color: { rgb: 'FFFFFF' } }, 
 +                    fill: { fgColor: { rgb: '27ae60' } }, 
 +                    alignment: { horizontal: 'center', vertical: 'center' }, 
 +                    border: borderStyle.border 
 +                }; 
 +            } 
 +             
 +            // Dati 
 +            for (let row = 2; row <= range.e.r; row++) { 
 +                for (let col = 0; col < numColonne; col++) { 
 +                    const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); 
 +                    if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                    ws[cellAddress].s = { 
 +                        alignment: { horizontal: 'center', vertical: 'center' }, 
 +                        border: borderStyle.border 
 +                    }; 
 +                } 
 +            } 
 +             
 +            // Larghezza colonne 
 +            ws['!cols'] = [ 
 +                { width: 25 }, { width: 12 }, { width: 12 }, { width: 8 }, { width: 8 }, 
 +                { width: 10 }, { width: 10 }, { width: 12 }, { width: 8 }, { width: 12 }, { width: 12 } 
 +            ]; 
 +        } 
 +         
 +        function applicaStiliValidazione(ws, numRighe, numColonne) { 
 +            const range = XLSX.utils.decode_range(ws['!ref']); 
 +             
 +            const borderStyle = { 
 +                border: { 
 +                    top: { style: 'thin', color: { rgb: '000000' } }, 
 +                    bottom: { style: 'thin', color: { rgb: '000000' } }, 
 +                    left: { style: 'thin', color: { rgb: '000000' } }, 
 +                    right: { style: 'thin', color: { rgb: '000000' } } 
 +                } 
 +            }; 
 +             
 +            // Applica bordi a tutte le celle 
 +            for (let row = 0; row <= range.e.r; row++) { 
 +                for (let col = 0; col < numColonne; col++) { 
 +                    const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); 
 +                    if (!ws[cellAddress]) ws[cellAddress] = { v: '', s: {} }; 
 +                     
 +                    // Titoli sezioni in grassetto 
 +                    if (ws[cellAddress].v && typeof ws[cellAddress].v === 'string') { 
 +                        if (ws[cellAddress].v.includes('VALIDAZIONE') ||  
 +                            ws[cellAddress].v.includes('SUCCESSI') ||  
 +                            ws[cellAddress].v.includes('AVVISI') ||  
 +                            ws[cellAddress].v.includes('ERRORI')) { 
 +                            ws[cellAddress].s = { 
 +                                font: { bold: true, size: 12 }, 
 +                                fill: { fgColor: { rgb: 'e8f4fd' } }, 
 +                                alignment: { horizontal: 'left', vertical: 'center' }, 
 +                                border: borderStyle.border 
 +                            }; 
 +                        } else { 
 +                            ws[cellAddress].s = { 
 +                                alignment: { horizontal: 'left', vertical: 'center' }, 
 +                                border: borderStyle.border 
 +                            }; 
 +                        } 
 +                    } else { 
 +                        ws[cellAddress].s = { 
 +                            border: borderStyle.border 
 +                        }; 
 +                    } 
 +                } 
 +            } 
 +             
 +            // Larghezza colonne 
 +            ws['!cols'] = [{ width: 50 }, { width: 8 }]; 
 +        } 
 +                 
 +                // Esporta file con stili applicati
                 const fileName = `Turni_TIN_Perfezionati_${nomiMesi[mese]}_${anno}.xlsx`;                 const fileName = `Turni_TIN_Perfezionati_${nomiMesi[mese]}_${anno}.xlsx`;
                 XLSX.writeFile(wb, fileName);                 XLSX.writeFile(wb, fileName);
                                  
-                mostraMessaggio(`✅ File Excel esportato con successo: ${fileName}`, 'success');+                mostraMessaggio(`✅ File Excel esportato con bordi e stili: ${fileName}`, 'success');
                                  
             } catch (error) {             } catch (error) {
gestione_turni_med.1754984718.txt.gz · Ultima modifica: da neoadmin