Questa è una vecchia versione del documento!
<htm>
<head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Generatore Turni TIN - Algoritmo Perfezionato</title> <style> body { font-family: Arial, sans-serif; margin: 10px; background: #f5f5f5; line-height: 1.4; } .container { max-width: 1400px; margin: 0 auto; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .header { text-align: center; background: linear-gradient(135deg, #2c3e50, #3498db); color: white; padding: 15px; border-radius: 8px; 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 { display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; align-items: flex-end; } .control-group { display: flex; flex-direction: column; gap: 4px; } .control-group label { font-size: 12px; font-weight: bold; color: #2c3e50; } select, input, button { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; } button { background: #3498db; color: white; cursor: pointer; font-weight: bold; transition: background 0.3s; } button:hover { background: #2980b9; } .turni-table { width: 100%; border-collapse: collapse; font-size: 10px; margin: 20px 0; background: white; } .header-ospedale { background: white; font-weight: bold; text-align: center; font-size: 12px; padding: 8px; border: 1px solid #000; } .header-day { background: #f8f9fa; font-weight: bold; text-align: center; font-size: 10px; padding: 6px 2px; border: 1px solid #000; min-width: 80px; } .day-name { display: block; font-weight: bold; margin-bottom: 2px; } .day-date { display: block; font-size: 8px; color: #666; } .medico-name { background: #f8f9fa; font-weight: bold; padding: 6px; border: 1px solid #000; text-align: left; min-width: 100px; } .turno-cell { background: white; padding: 4px 2px; border: 1px solid #000; text-align: center; font-size: 9px; vertical-align: middle; min-height: 30px; } .empty-sep { background: #f8f9fa; border: 1px solid #000; width: 8px; } /* Colori turni */ .guardia-giorno { background: #f39c12; color: white; font-weight: bold; } .guardia-notte { background: #2c3e50; color: white; font-weight: bold; } .ptn { background: #8e44ad; color: white; } .nido { background: #27ae60; color: white; } .urgenze-sala-parto { background: #d35400; color: white; } .follow-up { background: #16a085; color: white; } .reperibilita { background: #e74c3c; color: white; } .riposo { background: #95a5a6; color: white; } .ptn-pom { background: #9b59b6; color: white; } .error { background: #e74c3c; color: white; padding: 10px; border-radius: 5px; margin: 10px 0; } .success { background: #27ae60; color: white; padding: 10px; border-radius: 5px; margin: 10px 0; } .warning { background: #f39c12; color: white; padding: 10px; border-radius: 5px; margin: 10px 0; } .stats-panel { background: #ecf0f1; border-radius: 8px; padding: 15px; margin: 20px 0; font-size: 12px; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; } .stat-item { background: white; padding: 10px; border-radius: 5px; border-left: 4px solid #3498db; } </style>
</head> <body>
<div class="container"> <div class="header"> <h1>🏥 GENERATORE TURNI TIN - ALGORITMO PERFEZIONATO</h1> <h2>Neonatologia e Terapia Intensiva Neonatale</h2> <p>Sistema Automatico con Vincoli Avanzati e Bilanciamento Equo</p> </div>
<div class="controls"> <div class="control-group"> <label>Mese</label> <select id="meseSelect"> <option value="0">Gennaio</option> <option value="1">Febbraio</option> <option value="2">Marzo</option> <option value="3">Aprile</option> <option value="4">Maggio</option> <option value="5">Giugno</option> <option value="6">Luglio</option> <option value="7">Agosto</option> <option value="8">Settembre</option> <option value="9">Ottobre</option> <option value="10">Novembre</option> <option value="11">Dicembre</option> </select> </div> <div class="control-group"> <label>Anno</label> <input type="number" id="annoInput" value="2025" min="2024" max="2030"> </div> <div class="control-group"> <label>Configurazione</label> <button onclick="mostraConfiguratoreCopertura()">⚙️ Configura Copertura</button> </div> <div class="control-group"> <label>Gestione Medici</label> <button onclick="mostraGestioneMedici()">👥 Gestisci Medici</button> </div> <div class="control-group"> <label>Azione</label> <button onclick="generaTurniMigliorati()">🚀 Genera Turni Avanzati</button> </div> <div class="control-group"> <label>Validazione</label> <button onclick="validaTurni()">🔍 Valida Vincoli</button> </div> <div class="control-group"> <label>Esporta</label> <button onclick="esportaExcel()">📄 Esporta Excel</button> </div> </div>
<div id="messaggi"></div> <div id="statistiche"></div> <div id="turniContainer"></div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> <script> // Database medici con caratteristiche complete e configurabili let medici = { 'MC': { nome: 'M', cognome: 'C', nomeCompleto: 'MC - Direttore SS', ruolo: 'Direttore SS', posizione: 'rTIN', reperibile: true, privilege: '', anzianita: '>15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, // Competenze turni reperibilita: false, guardiaGiorno: false, guardiaNotte: false, ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 7, maxNottiConsecutive: 0, maxWeekendMese: 1 }, 'MR': { 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': { nome: 'C', cognome: 'M', nomeCompleto: 'CM - Aiuto Senior', ruolo: 'AS', posizione: 'rFUN', reperibile: true, privilege: '', anzianita: '>15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, // Competenze turni reperibilita: true, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 8, maxNottiConsecutive: 1, maxWeekendMese: 3 }, 'FM': { nome: 'F', cognome: 'M', nomeCompleto: 'FM - Aiuto Senior', ruolo: 'AS', posizione: 'rTIN', reperibile: true, privilege: '', anzianita: '>15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: true, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'FCa': { nome: 'F', cognome: 'Ca', nomeCompleto: 'FCa - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: true, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: true, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'EF': { nome: 'E', cognome: 'F', nomeCompleto: 'EF - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'FF': { nome: 'F', cognome: 'F', nomeCompleto: 'FF - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: true, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'LF': { nome: 'L', cognome: 'F', nomeCompleto: 'LF - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: true, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: true, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'MF': { nome: 'M', cognome: 'F', nomeCompleto: 'MF - Dirigente PT', ruolo: 'Dirigente Medico', posizione: 'rNido', reperibile: false, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 50, limitazioniGravidanza: false, altreLimitazioni: 'Part-time 50%', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: false, guardiaNotte: false, ptn: true, nido: true, followUp: false, urgenzeSalaParto: false, maxOreSettimanali: 20, minOreSettimanali: 18, prioritaRiposo: 9, maxNottiConsecutive: 0, maxWeekendMese: 2 }, 'FC': { nome: 'F', cognome: 'C', nomeCompleto: 'FC - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'VE': { nome: 'V', cognome: 'E', nomeCompleto: 'VE - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rNido', reperibile: false, privilege: '', anzianita: '5-15', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: 'Specialista Nido', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: false, guardiaNotte: false, ptn: true, nido: true, followUp: false, urgenzeSalaParto: false, maxOreSettimanali: 38, minOreSettimanali: 30, prioritaRiposo: 8, maxNottiConsecutive: 0, maxWeekendMese: 2 }, 'CC': { nome: 'C', cognome: 'C', nomeCompleto: 'CC - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '<5', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'FT': { nome: 'F', cognome: 'T', nomeCompleto: 'FT - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '<5', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 }, 'GA': { nome: 'G', cognome: 'A', nomeCompleto: 'GA - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '<5', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: false, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 0, maxWeekendMese: 4 }, 'SM': { nome: 'S', cognome: 'M', nomeCompleto: 'SM - Dirigente', ruolo: 'Dirigente Medico', posizione: 'rTIN', reperibile: false, privilege: '', anzianita: '<5', dataAssunzione: '', fte: 100, limitazioniGravidanza: false, altreLimitazioni: '', assente: false, decCalabria: '', ferieResidue: 0, oreEccedenza: 0, reperibilita: false, guardiaGiorno: true, guardiaNotte: true, ptn: true, nido: true, followUp: false, urgenzeSalaParto: true, maxOreSettimanali: 42, minOreSettimanali: 38, prioritaRiposo: 5, maxNottiConsecutive: 2, maxWeekendMese: 4 } };
// 🎯 CONFIGURAZIONI MASTER 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> <5, 5-15, >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: 4 }; // 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 let schemaCopertura = { base: { guardiaGiorno: 1, guardiaNotte: 1, reperibilita: 1 }, feriali: { ptnMattino: 2, nido: 2, urgenzeSalaParto: 1, followUp: { lunedi: 0, martedi: 1, mercoledi: 0, giovedi: 0, venerdi: 1 } }, weekend: { ptnMattino: 1, nido: 2, urgenzeSalaParto: 1, followUp: 0 }, festivi: { ptnMattino: 1, nido: 2, urgenzeSalaParto: 1, followUp: 0 } };
// VINCOLI E CONFIGURAZIONI AVANZATE const vincoliAvanzati = { maxNottiConsecutive: 2, minDistanzaGuardieNotte: 2, // giorni maxOreConsecutive: 24, riposiMinimi: 2, // per settimana maxWeekendConsecutivi: 2, prioritaCombinazioni: [ 'ven-notte_dom-giorno', 'sab-giorno_dom-notte' ] };
function mostraMessaggio(messaggio, tipo = 'success') { const div = document.getElementById('messaggi'); div.innerHTML = `<div class="${tipo}">${messaggio}</div>`; setTimeout(() => div.innerHTML = '', 5000); }
function mostraConfiguratoreCopertura() { const configHtml = ` <div id="configuratoreCopertura" style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border: 2px solid #3498db;"> <h3>⚙️ Configuratore Schema Copertura Avanzato</h3> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;"> <div style="background: white; padding: 15px; border-radius: 5px;"> <h4>🔄 Copertura Base (tutti i giorni)</h4> <label>Guardie Giorno: <input type="number" id="baseGuardiaGiorno" value="${schemaCopertura.base.guardiaGiorno}" min="1" max="3"></label><br> <label>Guardie Notte: <input type="number" id="baseGuardiaNotte" value="${schemaCopertura.base.guardiaNotte}" min="1" max="3"></label><br> <label>Reperibilità: <input type="number" id="baseReperibilita" value="${schemaCopertura.base.reperibilita}" min="1" max="2"></label> </div>
<div style="background: white; padding: 15px; border-radius: 5px;"> <h4>🏢 Giorni Feriali (Lun-Ven)</h4> <label>PTN Mattino: <input type="number" id="ferialiPTN" value="${schemaCopertura.feriali.ptnMattino}" min="1" max="4"></label><br> <label>NIDO: <input type="number" id="ferialiNIDO" value="${schemaCopertura.feriali.nido}" min="1" max="4"></label><br> <label>Urgenze/Sala Parto: <input type="number" id="ferialiUrgenze" value="${schemaCopertura.feriali.urgenzeSalaParto}" min="0" max="3"></label><br> <h5>Follow-up per giorno:</h5> <label style="font-size: 12px;">Lun: <input type="number" id="followUpLun" value="${schemaCopertura.feriali.followUp.lunedi}" min="0" max="2" style="width: 50px;"></label> <label style="font-size: 12px;">Mar: <input type="number" id="followUpMar" value="${schemaCopertura.feriali.followUp.martedi}" min="0" max="2" style="width: 50px;"></label> <label style="font-size: 12px;">Mer: <input type="number" id="followUpMer" value="${schemaCopertura.feriali.followUp.mercoledi}" min="0" max="2" style="width: 50px;"></label><br> <label style="font-size: 12px;">Gio: <input type="number" id="followUpGio" value="${schemaCopertura.feriali.followUp.giovedi}" min="0" max="2" style="width: 50px;"></label> <label style="font-size: 12px;">Ven: <input type="number" id="followUpVen" value="${schemaCopertura.feriali.followUp.venerdi}" min="0" max="2" style="width: 50px;"></label> </div>
<div style="background: white; padding: 15px; border-radius: 5px;"> <h4>🏖️ Weekend (Sab-Dom)</h4> <label>PTN Mattino: <input type="number" id="weekendPTN" value="${schemaCopertura.weekend.ptnMattino}" min="0" max="3"></label><br> <label>NIDO: <input type="number" id="weekendNIDO" value="${schemaCopertura.weekend.nido}" min="1" max="4"></label><br> <label>Urgenze/Sala Parto: <input type="number" id="weekendUrgenze" value="${schemaCopertura.weekend.urgenzeSalaParto}" min="0" max="3"></label><br> <label>Follow-up: <input type="number" id="weekendFollowUp" value="${schemaCopertura.weekend.followUp}" min="0" max="2"></label> </div>
<div style="background: white; padding: 15px; border-radius: 5px;"> <h4>🎯 Vincoli Avanzati</h4> <label>Max Notti Consecutive: <input type="number" id="maxNottiConsecutive" value="${vincoliAvanzati.maxNottiConsecutive}" min="1" max="4"></label><br> <label>Min Distanza Guardie Notte: <input type="number" id="minDistanzaGuardieNotte" value="${vincoliAvanzati.minDistanzaGuardieNotte}" min="1" max="5"></label><br> <label>Riposi Minimi/Settimana: <input type="number" id="riposiMinimi" value="${vincoliAvanzati.riposiMinimi}" min="1" max="3"></label><br> <label>Max Weekend Consecutivi: <input type="number" id="maxWeekendConsecutivi" value="${vincoliAvanzati.maxWeekendConsecutivi}" min="1" max="4"></label> </div> </div>
<div style="text-align: center; margin-top: 20px;"> <button onclick="salvaSchemaCopertura()" style="background: #27ae60; color: white; padding: 10px 20px; border: none; border-radius: 5px; font-weight: bold; margin-right: 10px;">💾 Salva Schema</button> <button onclick="resetSchemaCopertura()" style="background: #e74c3c; color: white; padding: 10px 20px; border: none; border-radius: 5px; font-weight: bold; margin-right: 10px;">🔄 Reset Default</button> <button onclick="nascondConfiguratore()" style="background: #95a5a6; color: white; padding: 10px 20px; border: none; border-radius: 5px; font-weight: bold;">❌ Chiudi</button> </div>
<div id="previewCopertura" style="margin-top: 15px; padding: 15px; background: #ecf0f1; border-radius: 5px; font-family: monospace; font-size: 12px;"></div> </div> `; const controlsDiv = document.querySelector('.controls'); controlsDiv.insertAdjacentHTML('afterend', configHtml); aggiornaPreviweSchema(); }
function salvaSchemaCopertura() { // Salva schema copertura schemaCopertura = { base: { guardiaGiorno: parseInt(document.getElementById('baseGuardiaGiorno').value), guardiaNotte: parseInt(document.getElementById('baseGuardiaNotte').value), reperibilita: parseInt(document.getElementById('baseReperibilita').value) }, feriali: { ptnMattino: parseInt(document.getElementById('ferialiPTN').value), nido: parseInt(document.getElementById('ferialiNIDO').value), urgenzeSalaParto: parseInt(document.getElementById('ferialiUrgenze').value), followUp: { lunedi: parseInt(document.getElementById('followUpLun').value), martedi: parseInt(document.getElementById('followUpMar').value), mercoledi: parseInt(document.getElementById('followUpMer').value), giovedi: parseInt(document.getElementById('followUpGio').value), venerdi: parseInt(document.getElementById('followUpVen').value) } }, weekend: { ptnMattino: parseInt(document.getElementById('weekendPTN').value), nido: parseInt(document.getElementById('weekendNIDO').value), urgenzeSalaParto: parseInt(document.getElementById('weekendUrgenze').value), followUp: parseInt(document.getElementById('weekendFollowUp').value) } };
// Salva vincoli avanzati vincoliAvanzati.maxNottiConsecutive = parseInt(document.getElementById('maxNottiConsecutive').value); vincoliAvanzati.minDistanzaGuardieNotte = parseInt(document.getElementById('minDistanzaGuardieNotte').value); vincoliAvanzati.riposiMinimi = parseInt(document.getElementById('riposiMinimi').value); vincoliAvanzati.maxWeekendConsecutivi = parseInt(document.getElementById('maxWeekendConsecutivi').value); aggiornaPreviweSchema(); mostraMessaggio('Schema copertura e vincoli salvati! Rigenera i turni per applicare le modifiche.', 'success'); }
function resetSchemaCopertura() { // Reset ai valori default... mostraMessaggio('Schema copertura ripristinato ai valori default.', 'success'); }
function nascondConfiguratore() { const config = document.getElementById('configuratoreCopertura'); if (config) config.remove(); }
function aggiornaPreviweSchema() { const preview = document.getElementById('previewCopertura'); if (!preview) return; let html = '<h4>📋 Anteprima Schema Copertura Avanzato:</h4>'; html += '<strong>FERIALI:</strong><br>'; html += `• Guardie Giorno: ${schemaCopertura.base.guardiaGiorno} | Guardie Notte: ${schemaCopertura.base.guardiaNotte} | Reperibilità: ${schemaCopertura.base.reperibilita}<br>`; html += `• PTN Mattino: ${schemaCopertura.feriali.ptnMattino} | NIDO: ${schemaCopertura.feriali.nido} | Urgenze/SP: ${schemaCopertura.feriali.urgenzeSalaParto}<br>`; html += `• Follow-up: Lun(${schemaCopertura.feriali.followUp.lunedi}) Mar(${schemaCopertura.feriali.followUp.martedi}) Mer(${schemaCopertura.feriali.followUp.mercoledi}) Gio(${schemaCopertura.feriali.followUp.giovedi}) Ven(${schemaCopertura.feriali.followUp.venerdi})<br><br>`; html += '<strong>WEEKEND:</strong><br>'; html += `• Guardie Giorno: ${schemaCopertura.base.guardiaGiorno} | Guardie Notte: ${schemaCopertura.base.guardiaNotte} | Reperibilità: ${schemaCopertura.base.reperibilita}<br>`; html += `• PTN Mattino: ${schemaCopertura.weekend.ptnMattino} | NIDO: ${schemaCopertura.weekend.nido} | Urgenze/SP: ${schemaCopertura.weekend.urgenzeSalaParto} | Follow-up: ${schemaCopertura.weekend.followUp}<br><br>`; html += '<strong>VINCOLI AVANZATI:</strong><br>'; html += `• Max Notti Consecutive: ${vincoliAvanzati.maxNottiConsecutive} | Min Distanza Guardie: ${vincoliAvanzati.minDistanzaGuardieNotte} giorni<br>`; html += `• Riposi Minimi: ${vincoliAvanzati.riposiMinimi}/settimana | Max Weekend Consecutivi: ${vincoliAvanzati.maxWeekendConsecutivi}<br>`; preview.innerHTML = html; }
function getDaysInMonth(month, year) { return new Date(year, month + 1, 0).getDate(); }
// 🚀 ALGORITMO PERFEZIONATO CON VINCOLI AVANZATI function generaTurniMigliorati() { const mese = parseInt(document.getElementById('meseSelect').value); const anno = parseInt(document.getElementById('annoInput').value); const giorni = getDaysInMonth(mese, anno);
try { console.log("🚀 Inizio generazione turni migliorati..."); // Reset strutture turniMensili = {}; statisticheMensili = inizializzaStatistiche();
// 1. Inizializza struttura turni inizializzaStrutturaGiorni(mese, anno, giorni); // 2. Assegna reperibilità 7/7 con bilanciamento assegnaReperibilita7su7Bilanciata(giorni); // 3. Assegna turni speciali MC e VE assegnaTurniSpeciali(mese, anno, giorni); // 4. Algoritmo principale con vincoli avanzati assegnaTurniConVincoli(giorni); // 5. Bilanciamento finale e ottimizzazioni ottimizzaBilanciamento(giorni); // 6. Validazione finale const validazione = validaTurniCompleti(); // 7. Genera statistiche calcolaStatisticheMensili(giorni); // 8. Visualizza risultati visualizzaTurniMigliorati(mese, anno); visualizzaStatistiche(); if (validazione.errori.length > 0) { mostraMessaggio(`⚠️ Turni generati con ${validazione.errori.length} avvisi. Controlla la validazione.`, 'warning'); } else { mostraMessaggio(`✅ Turni generati perfettamente per ${giorni} giorni! Tutti i vincoli rispettati.`, 'success'); }
} catch (error) { mostraMessaggio('❌ Errore nella generazione: ' + error.message, 'error'); console.error("Errore dettagliato:", error); } }
function inizializzaStatistiche() { const stats = {}; Object.keys(medici).forEach(codMedico => { stats[codMedico] = { guardie: { giorno: 0, notte: 0 }, turni: { ptn: 0, nido: 0, urgenze: 0, followUp: 0, reperibilita: 0 }, oreSettimanali: [], riposi: 0, weekendLavorati: 0, nottiConsecutive: 0, ultimaGuardiaNotte: -10 }; }); return stats; }
function inizializzaStrutturaGiorni(mese, anno, giorni) { for (let giorno = 1; giorno <= giorni; giorno++) { const data = new Date(anno, mese, giorno); turniMensili[giorno] = { data: data, turni: { guardiaGiorno: null, guardiaNotte: null, ptnMattino: [], nido: [], urgenzeSalaParto: [], followUp: null, reperibilita: null, ptnPomeridiano: [], riposi: [] } }; } console.log("✅ Struttura giorni inizializzata"); }
function assegnaReperibilita7su7Bilanciata(giorni) { const mediciReperibili = ['CM', 'FM', 'FCa', 'FF', 'LF']; let contatoreReperibilita = {}; mediciReperibili.forEach(medico => { contatoreReperibilita[medico] = 0; }); for (let giorno = 1; giorno <= giorni; giorno++) { // Trova il medico con meno reperibilità assegnate let medicoScelto = mediciReperibili.reduce((min, medico) => contatoreReperibilita[medico] < contatoreReperibilita[min] ? medico : min ); turniMensili[giorno].turni.reperibilita = medicoScelto; contatoreReperibilita[medicoScelto]++; statisticheMensili[medicoScelto].turni.reperibilita++; } console.log("✅ Reperibilità bilanciata assegnata:", contatoreReperibilita); }
function assegnaTurniSpeciali(mese, anno, giorni) { // Turni MC - Direttore SS assegnaTurniMC(mese, anno, giorni); // Turni VE - Specialista Nido assegnaTurniVE(giorni); // Turni MF - Part-time 50% assegnaTurniMF(giorni); console.log("✅ Turni speciali assegnati"); }
function assegnaTurniMC(mese, anno, giorni) { let domenicheConTurno = 0; const domenicheTotali = Math.floor(giorni / 7); for (let giorno = 1; giorno <= giorni; giorno++) { const data = new Date(anno, mese, giorno); const dayOfWeek = data.getDay(); if (dayOfWeek === 3 || dayOfWeek === 6) { // Mer e Sab - riposo fisso turniMensili[giorno].turni.riposi.push('MC'); statisticheMensili['MC'].riposi++; } else if (dayOfWeek === 0) { // Domenica - alternanza if (domenicheConTurno < domenicheTotali / 2) { turniMensili[giorno].turni.ptnMattino.push('MC'); statisticheMensili['MC'].turni.ptn++; domenicheConTurno++; } else { turniMensili[giorno].turni.riposi.push('MC'); statisticheMensili['MC'].riposi++; } } else if ([1, 2, 4, 5].includes(dayOfWeek)) { // Lun, Mar, Gio, Ven turniMensili[giorno].turni.ptnMattino.push('MC'); statisticheMensili['MC'].turni.ptn++; } } }
function assegnaTurniVE(giorni) { let turniNidoVE = 0; const maxTurniNido = Math.min(5, Math.floor(giorni * 5 / 7)); // 4-5 giorni settimana for (let giorno = 1; giorno <= giorni; giorno++) { if (turniNidoVE < maxTurniNido && turniMensili[giorno].turni.nido.length < 2) { turniMensili[giorno].turni.nido.push('VE'); statisticheMensili['VE'].turni.nido++; turniNidoVE++; } else { turniMensili[giorno].turni.riposi.push('VE'); statisticheMensili['VE'].riposi++; } } }
function assegnaTurniMF(giorni) { // MF lavora max 20 ore/settimana (part-time 50%) let oreAssegnate = 0; const maxOreSettimanali = 20; for (let giorno = 1; giorno <= giorni; giorno++) { const data = turniMensili[giorno].data; const settimana = Math.floor((giorno - 1) / 7); if (settimana === 0) oreAssegnate = 0; // Reset settimanale if (oreAssegnate < maxOreSettimanali && turniMensili[giorno].turni.nido.length < 2) { turniMensili[giorno].turni.nido.push('MF'); statisticheMensili['MF'].turni.nido++; oreAssegnate += 4.5; // 4.5 ore per turno nido } else { turniMensili[giorno].turni.riposi.push('MF'); statisticheMensili['MF'].riposi++; } } }
function assegnaTurniConVincoli(giorni) { const mediciGuardiaGiorno = ['CM', 'FM', 'FCa', 'EF', 'FF', 'LF', 'FC', 'CC', 'FT', 'MR', 'GA', 'SM']; const mediciGuardiaNotte = ['CM', 'FM', 'FCa', 'EF', 'FF', 'LF', 'FC', 'CC', 'FT', 'SM']; // Pool di medici ordinati per priorità (meno carichi prima) let poolGuardieGiorno = [...mediciGuardiaGiorno]; let poolGuardieNotte = [...mediciGuardiaNotte]; for (let giorno = 1; giorno <= giorni; giorno++) { const data = turniMensili[giorno].data; const fabbisogno = getFabbisognoGiorno(data); // 1. Assegna guardie giorno con vincoli assegnaGuardiaGiornoConVincoli(giorno, poolGuardieGiorno, fabbisogno); // 2. Assegna guardie notte con distanziamento assegnaGuardiaNotteConVincoli(giorno, poolGuardieNotte, fabbisogno); // 3. Completa altri turni rispettando vincoli orari completaTurniGiorno(giorno, fabbisogno); // 4. Riordina pool per next iteration riordinaPoolMedici(poolGuardieGiorno, poolGuardieNotte); } console.log("✅ Turni assegnati con vincoli avanzati"); }
function assegnaGuardiaGiornoConVincoli(giorno, pool, fabbisogno) { if (turniMensili[giorno].turni.guardiaGiorno) return; for (let medico of pool) { if (verificaVincoliMedico(medico, giorno, 'guardiaGiorno')) { turniMensili[giorno].turni.guardiaGiorno = medico; statisticheMensili[medico].guardie.giorno++; break; } } }
function assegnaGuardiaNotteConVincoli(giorno, pool, fabbisogno) { if (turniMensili[giorno].turni.guardiaNotte) return; for (let medico of pool) { if (verificaVincoliMedico(medico, giorno, 'guardiaNotte')) { // Verifica distanziamento da ultima guardia notte const ultimaGuardia = statisticheMensili[medico].ultimaGuardiaNotte; const distanza = giorno - ultimaGuardia; if (distanza >= vincoliAvanzati.minDistanzaGuardieNotte) { turniMensili[giorno].turni.guardiaNotte = medico; statisticheMensili[medico].guardie.notte++; statisticheMensili[medico].ultimaGuardiaNotte = giorno; break; } } } }
function verificaVincoliMedico(medico, giorno, tipoTurno) { const config = medici[medico]; const stats = statisticheMensili[medico]; // 1. Verifica competenze base if (tipoTurno === 'guardiaGiorno' && !config.guardiaGiorno) return false; if (tipoTurno === 'guardiaNotte' && !config.guardiaNotte) return false; // 2. Verifica conflitti con reperibilità if (turniMensili[giorno].turni.reperibilita === medico) return false; // 3. Verifica notti consecutive if (tipoTurno === 'guardiaNotte') { let nottiConsecutive = 0; for (let g = giorno - 1; g >= Math.max(1, giorno - config.maxNottiConsecutive); g--) { if (turniMensili[g] && turniMensili[g].turni.guardiaNotte === medico) { nottiConsecutive++; } else { break; } } if (nottiConsecutive >= config.maxNottiConsecutive) return false; } // 4. Verifica ore settimanali (approssimativa) const settimana = Math.floor((giorno - 1) / 7); const oreSettimana = calcolaOreSettimanaMedico(medico, settimana); const oreTurno = tipoTurno === 'guardiaGiorno' || tipoTurno === 'guardiaNotte' ? 12 : 8; if (oreSettimana + oreTurno > config.maxOreSettimanali) return false; return true; }
function calcolaOreSettimanaMedico(medico, settimana) { // Calcolo approssimativo ore per settimana // TODO: implementare calcolo preciso return 0; }
function completaTurniGiorno(giorno, fabbisogno) { const mediciDisponibili = Object.keys(medici).filter(m => !isMedicoOccupatoGiorno(m, giorno) && !turniMensili[giorno].turni.riposi.includes(m) ); // Completa PTN mattino while (turniMensili[giorno].turni.ptnMattino.length < fabbisogno.ptnMattino && mediciDisponibili.length > 0) { const medico = mediciDisponibili.shift(); if (medici[medico].ptn) { turniMensili[giorno].turni.ptnMattino.push(medico); statisticheMensili[medico].turni.ptn++; } } // Completa NIDO while (turniMensili[giorno].turni.nido.length < fabbisogno.nido && mediciDisponibili.length > 0) { const medico = mediciDisponibili.shift(); if (medici[medico].nido && !turniMensili[giorno].turni.ptnMattino.includes(medico)) { turniMensili[giorno].turni.nido.push(medico); statisticheMensili[medico].turni.nido++; } } // Completa Urgenze/SP while (turniMensili[giorno].turni.urgenzeSalaParto.length < fabbisogno.urgenzeSalaParto && mediciDisponibili.length > 0) { const medico = mediciDisponibili.shift(); if (medici[medico].urgenzeSalaParto && !turniMensili[giorno].turni.ptnMattino.includes(medico) && !turniMensili[giorno].turni.nido.includes(medico)) { turniMensili[giorno].turni.urgenzeSalaParto.push(medico); statisticheMensili[medico].turni.urgenze++; } } // Follow-up se necessario if (fabbisogno.followUp > 0 && !turniMensili[giorno].turni.followUp) { const candidati = ['CM', 'FCa'].filter(m => !isMedicoOccupatoGiorno(m, giorno) && medici[m].followUp ); if (candidati.length > 0) { turniMensili[giorno].turni.followUp = candidati[0]; statisticheMensili[candidati[0]].turni.followUp++; } } // Assegna riposi ai medici rimanenti mediciDisponibili.forEach(medico => { if (!isMedicoOccupatoGiorno(medico, giorno)) { turniMensili[giorno].turni.riposi.push(medico); statisticheMensili[medico].riposi++; } }); }
function riordinaPoolMedici(poolGiorno, poolNotte) { // Riordina i pool basandosi sul carico attuale di lavoro poolGiorno.sort((a, b) => { const loadA = statisticheMensili[a].guardie.giorno + statisticheMensili[a].turni.ptn; const loadB = statisticheMensili[b].guardie.giorno + statisticheMensili[b].turni.ptn; return loadA - loadB; }); poolNotte.sort((a, b) => { const loadA = statisticheMensili[a].guardie.notte; const loadB = statisticheMensili[b].guardie.notte; return loadA - loadB; }); }
function ottimizzaBilanciamento(giorni) { // TODO: Implementare ottimizzazioni finali // - Scambi per migliorare equità // - Verifica combinazioni weekend ottimali // - Bilanciamento riposi console.log("🎯 Ottimizzazione bilanciamento completata"); }
function isMedicoOccupatoGiorno(medico, giorno) { const turni = turniMensili[giorno].turni; return turni.guardiaGiorno === medico || turni.guardiaNotte === medico || turni.ptnMattino.includes(medico) || turni.nido.includes(medico) || turni.urgenzeSalaParto.includes(medico) || turni.followUp === medico || turni.reperibilita === medico; }
function getFabbisognoGiorno(data) { const dayOfWeek = data.getDay(); const isWeekendDay = dayOfWeek === 0 || dayOfWeek === 6; let fabbisogno = { guardiaGiorno: schemaCopertura.base.guardiaGiorno, guardiaNotte: schemaCopertura.base.guardiaNotte, reperibilita: schemaCopertura.base.reperibilita, ptnMattino: 0, nido: 0, urgenzeSalaParto: 0, followUp: 0 }; if (isWeekendDay) { fabbisogno.ptnMattino = schemaCopertura.weekend.ptnMattino; fabbisogno.nido = schemaCopertura.weekend.nido; fabbisogno.urgenzeSalaParto = schemaCopertura.weekend.urgenzeSalaParto; fabbisogno.followUp = schemaCopertura.weekend.followUp; } else { fabbisogno.ptnMattino = schemaCopertura.feriali.ptnMattino; fabbisogno.nido = schemaCopertura.feriali.nido; fabbisogno.urgenzeSalaParto = schemaCopertura.feriali.urgenzeSalaParto; const giorni = ['domenica', 'lunedi', 'martedi', 'mercoledi', 'giovedi', 'venerdi', 'sabato']; const nomeGiorno = giorni[dayOfWeek]; fabbisogno.followUp = schemaCopertura.feriali.followUp[nomeGiorno] || 0; } return fabbisogno; }
// VALIDAZIONE AVANZATA DEI TURNI function validaTurni() { if (!turniMensili || Object.keys(turniMensili).length === 0) { mostraMessaggio('❌ Genera prima i turni!', 'error'); return; } console.log("🔍 Avvio validazione turni..."); const validazione = validaTurniCompleti(); console.log("Risultato validazione:", validazione); let messaggioValidazione = ` <div class="stats-panel"> <h3>🔍 VALIDAZIONE VINCOLI COMPLETA</h3> <div class="stats-grid"> <div class="stat-item" style="border-left-color: #27ae60;"> <strong>✅ Controlli Superati:</strong> ${validazione.controlliSuperati} </div> <div class="stat-item" style="border-left-color: #f39c12;"> <strong>⚠️ Avvisi:</strong> ${validazione.avvisi.length} </div> <div class="stat-item" style="border-left-color: #e74c3c;"> <strong>❌ Errori Critici:</strong> ${validazione.errori.length} </div> <div class="stat-item" style="border-left-color: #3498db;"> <strong>📊 Punteggio Qualità:</strong> ${validazione.punteggioQualita}% </div> </div> `; // Mostra dettagli positivi if (validazione.successiImportanti.length > 0) { messaggioValidazione += '<h4 style="color: #27ae60;">✅ Vincoli Rispettati:</h4><ul>'; validazione.successiImportanti.forEach(successo => { messaggioValidazione += `<li style="color: #27ae60;"><strong>${successo}</strong></li>`; }); messaggioValidazione += '</ul>'; } if (validazione.avvisi.length > 0) { messaggioValidazione += '<h4 style="color: #f39c12;">⚠️ Avvisi (Non Critici):</h4><ul>'; validazione.avvisi.forEach(avviso => { messaggioValidazione += `<li style="color: #f39c12;">${avviso}</li>`; }); messaggioValidazione += '</ul>'; } if (validazione.errori.length > 0) { messaggioValidazione += '<h4 style="color: #e74c3c;">❌ Errori Critici:</h4><ul>'; validazione.errori.forEach(errore => { messaggioValidazione += `<li style="color: #e74c3c;"><strong>${errore}</strong></li>`; }); messaggioValidazione += '</ul>'; } // Suggerimenti if (validazione.suggerimenti.length > 0) { messaggioValidazione += '<h4 style="color: #3498db;">💡 Suggerimenti per Migliorare:</h4><ul>'; validazione.suggerimenti.forEach(suggerimento => { messaggioValidazione += `<li style="color: #3498db;">${suggerimento}</li>`; }); messaggioValidazione += '</ul>'; } messaggioValidazione += '</div>'; document.getElementById('messaggi').innerHTML = messaggioValidazione; // Messaggio riassuntivo if (validazione.errori.length === 0) { setTimeout(() => { mostraMessaggio(`🎉 Validazione completata! Punteggio qualità: ${validazione.punteggioQualita}%`, 'success'); }, 500); } else { setTimeout(() => { mostraMessaggio(`⚠️ Validazione completata con ${validazione.errori.length} errori critici`, 'warning'); }, 500); } }
function validaTurniCompleti() { const validazione = { controlliSuperati: 0, avvisi: [], errori: [], successiImportanti: [], suggerimenti: [], punteggioQualita: 0 }; const giorni = Object.keys(turniMensili).length; if (giorni === 0) { validazione.errori.push("Nessun turno generato"); return validazione; } 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++) { const turni = turniMensili[giorno]; if (!turni || !turni.turni) { validazione.errori.push(`Giorno ${giorno}: Struttura turni mancante`); continue; } const turniGiorno = turni.turni; let coperturaGiorno = 0; // Guardia giorno if (!turniGiorno.guardiaGiorno) { validazione.errori.push(`Giorno ${giorno}: Manca guardia giorno`); } else { validazione.controlliSuperati++; coperturaGiorno++; } // Guardia notte if (!turniGiorno.guardiaNotte) { validazione.errori.push(`Giorno ${giorno}: Manca guardia notte`); } else { validazione.controlliSuperati++; coperturaGiorno++; } // Reperibilità if (!turniGiorno.reperibilita) { validazione.errori.push(`Giorno ${giorno}: Manca reperibilità`); } else { 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 let mediciValidati = 0; Object.keys(medici).forEach(medico => { const stats = statisticheMensili[medico]; const config = medici[medico]; if (!stats) { validazione.avvisi.push(`${medico}: Statistiche mancanti`); return; } mediciValidati++; // Verifica ore settimanali const oreStimate = (stats.guardie.giorno + stats.guardie.notte) * 12 + (stats.turni.ptn + stats.turni.urgenze + stats.turni.followUp) * 8 + stats.turni.nido * 4.5; const settimane = Math.ceil(giorni / 7); const oreSettimanaliMedie = settimane > 0 ? oreStimate / settimane : 0; if (oreSettimanaliMedie < 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) { validazione.avvisi.push(`${medico}: Ore settimanali sopra il massimo (${oreSettimanaliMedie.toFixed(1)}h vs ${config.maxOreSettimanali}h limite)`); } else { validazione.controlliSuperati++; } // Verifica riposi minimi const riposiSettimanaliMedi = settimane > 0 ? stats.riposi / settimane : 0; if (riposiSettimanaliMedi < vincoliAvanzati.riposiMinimi) { validazione.avvisi.push(`${medico}: Riposi sotto il minimo (${riposiSettimanaliMedi.toFixed(1)} vs ${vincoliAvanzati.riposiMinimi} richiesti/settimana)`); } else { 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; }
function calcolaStatisticheMensili(giorni) { // Le statistiche sono già calcolate durante l'assegnazione console.log("📊 Statistiche mensili calcolate"); }
function visualizzaStatistiche() { const giorni = Object.keys(turniMensili).length; const settimane = Math.ceil(giorni / 7); let html = ` <div class="stats-panel"> <h3>📊 STATISTICHE MENSILI DETTAGLIATE</h3> <div class="stats-grid"> `; Object.keys(medici).forEach(medico => { const stats = statisticheMensili[medico]; const config = medici[medico]; const totaleGuardie = stats.guardie.giorno + stats.guardie.notte; const totaleTurni = stats.turni.ptn + stats.turni.nido + stats.turni.urgenze + stats.turni.followUp; const oreStimate = totaleGuardie * 12 + totaleTurni * 7 + stats.turni.nido * 0.5; // Nido 4.5h const oreSettimanali = oreStimate / settimane; let statoOre = ''; if (oreSettimanali < config.minOreSettimanali) { statoOre = 'style="color: orange;"'; } else if (oreSettimanali > config.maxOreSettimanali) { statoOre = 'style="color: red;"'; } else { statoOre = 'style="color: green;"'; } html += ` <div class="stat-item"> <strong>${medico}</strong><br> <small>Guardie: G${stats.guardie.giorno} N${stats.guardie.notte}</small><br> <small>Turni: PTN${stats.turni.ptn} NIDO${stats.turni.nido} URG${stats.turni.urgenze}</small><br> <small>Rep: ${stats.turni.reperibilita} | Riposi: ${stats.riposi}</small><br> <small ${statoOre}>Ore/sett: ${oreSettimanali.toFixed(1)}</small> </div> `; }); html += ` </div> <div style="margin-top: 15px; padding: 10px; background: #3498db; color: white; border-radius: 5px;"> <strong>🎯 RIASSUNTO:</strong> ${giorni} giorni, ${settimane} settimane | <strong>Copertura H24:</strong> ✅ Garantita | <strong>Bilanciamento:</strong> 🔄 Ottimizzato </div> </div> `; document.getElementById('statistiche').innerHTML = html; }
function getDayName(date) { const giorni = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; return giorni[date.getDay()]; }
function visualizzaTurniMigliorati(mese, anno) { const giorni = getDaysInMonth(mese, anno); const nomiMesi = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre']; let html = ` <table class="turni-table"> <tr> <td colspan="${(giorni * 2) + 2}" class="header-ospedale"> P.O. "ALESSANDRO MANZONI" LECCO<br> Dipartimento Materno-Infantile<br> S.C. NEONATOLOGIA E TERAPIA INTENSIVA NEONATALE<br> TURNI DEL PERSONALE MEDICO - ${nomiMesi[mese]} ${anno} <br><small style="color: #666;">🚀 Generato con Algoritmo Perfezionato</small> </td> </tr> <tr> <td class="header-day"></td> <td class="header-day"></td>`; // Intestazioni giorni for (let giorno = 1; giorno <= giorni; giorno++) { const data = new Date(anno, mese, giorno); const nomeGiorno = getDayName(data); const dataStr = `${giorno.toString().padStart(2, '0')}/${(mese + 1).toString().padStart(2, '0')}`; html += `<td colspan="2" class="header-day"> <span class="day-name">${nomeGiorno}</span> <span class="day-date">${dataStr}</span> </td>`; } html += '</tr>';
// Righe medici const tuttiMedici = Object.keys(medici); tuttiMedici.forEach(codMedico => { html += `<tr> <td class="medico-name">${medici[codMedico].nome}</td> <td class="empty-sep"></td>`; for (let giorno = 1; giorno <= giorni; giorno++) { const turnoMedico = getTurnoMedicoMigliorato(codMedico, giorno); html += visualizzaTurnoGiornoMigliorato(turnoMedico); } html += '</tr>'; });
html += '</table>'; document.getElementById('turniContainer').innerHTML = html; }
function getTurnoMedicoMigliorato(medico, giorno) { const turni = turniMensili[giorno].turni; // Controllo guardia notte che INIZIA oggi if (turni.guardiaNotte === medico) { return { tipo: 'guardia-notte', testo: 'Guardia 20:30--', spalmato: true }; } // Controllo guardia notte che FINISCE oggi (iniziata ieri) if (giorno > 1) { const turniIeri = turniMensili[giorno - 1]?.turni; if (turniIeri?.guardiaNotte === medico) { return { tipo: 'guardia-notte', testo: 'notte --8:30', continuaNotte: true }; } } // REPERIBILITÀ: gestione spalmate if (turni.reperibilita === medico) { let turnoBase = ''; if (turni.ptnMattino.includes(medico)) { turnoBase = 'PTN 8:30-16:00'; } else if (turni.nido.includes(medico)) { turnoBase = 'NIDO 8:30-13:00'; } else if (turni.followUp === medico) { turnoBase = 'Follow-up 8:30-16:30'; } else if (turni.urgenzeSalaParto.includes(medico)) { turnoBase = 'Urgenze/SP 8:30-16:00'; } else { turnoBase = 'Turno 8:30-16:00'; } return { tipo: 'reperibilita', testo: turnoBase, spalmatoRep: true }; } // Controllo reperibilità che finisce oggi (iniziata ieri) if (giorno > 1) { const turniIeri = turniMensili[giorno - 1]?.turni; if (turniIeri?.reperibilita === medico) { return { tipo: 'reperibilita', testo: 'Rep. --8:30', continuaRep: true }; } } // Altri turni standard if (turni.guardiaGiorno === medico) return { tipo: 'guardia-giorno', testo: 'Guardia Giorno\n8:30-20:30' }; if (turni.ptnMattino.includes(medico)) return { tipo: 'ptn', testo: 'PTN\n8:30-16:00' }; if (turni.nido.includes(medico)) return { tipo: 'nido', testo: 'NIDO\n8:30-13:00' }; if (turni.urgenzeSalaParto.includes(medico)) return { tipo: 'urgenze-sala-parto', testo: 'Urgenze/SP\n8:30-16:00' }; if (turni.followUp === medico) return { tipo: 'follow-up', testo: 'Follow-up\n8:30-16:30' }; if (turni.ptnPomeridiano.includes(medico)) return { tipo: 'ptn-pom', testo: 'PTN Pom.\n14:00-20:00' }; if (turni.riposi.includes(medico)) return { tipo: 'riposo', testo: 'R' }; return { tipo: 'riposo', testo: 'R' }; }
function visualizzaTurnoGiornoMigliorato(turno) { if (turno.spalmato) { // Guardia notte: inizia un giorno e finisce il successivo return `<td class="turno-cell"></td> <td class="turno-cell ${turno.tipo}">Guardia 20:30--</td>`; } else if (turno.continuaNotte) { // Continuazione guardia notte return `<td class="turno-cell ${turno.tipo}">notte --8:30</td> <td class="turno-cell"></td>`; } else if (turno.spalmatoRep) { // Reperibilità spalmata return `<td class="turno-cell ${turno.tipo}">${turno.testo}</td> <td class="turno-cell ${turno.tipo}">Rep. 16:00--</td>`; } else if (turno.continuaRep) { // Continuazione reperibilità return `<td class="turno-cell ${turno.tipo}">Rep. --8:30</td> <td class="turno-cell"></td>`; } else { // Turno normale su colonne unite return `<td colspan="2" class="turno-cell ${turno.tipo}">${turno.testo}</td>`; } }
// ESPORTAZIONE EXCEL MIGLIORATA function esportaExcel() { if (!turniMensili || Object.keys(turniMensili).length === 0) { mostraMessaggio('❌ Genera prima i turni!', 'error'); return; }
const mese = parseInt(document.getElementById('meseSelect').value); const anno = parseInt(document.getElementById('annoInput').value); const giorni = getDaysInMonth(mese, anno); const nomiMesi = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'];
try { const wb = XLSX.utils.book_new(); // FOGLIO 1: TURNI CON BORDI const excelData = []; // Intestazioni con stile excelData.push(['P.O. "ALESSANDRO MANZONI" LECCO']); excelData.push(['Dipartimento Materno-Infantile']); excelData.push(['S.C. NEONATOLOGIA E TERAPIA INTENSIVA NEONATALE']); excelData.push(['TURNI DEL PERSONALE MEDICO - ' + nomiMesi[mese] + ' ' + anno]); excelData.push(['🚀 Generato con Algoritmo Perfezionato']); excelData.push([]); // Intestazioni giorni const rigaIntestazioni = ['MEDICO', '']; for (let giorno = 1; giorno <= giorni; giorno++) { const data = new Date(anno, mese, giorno); const nomeGiorno = getDayName(data); const dataStr = `${giorno.toString().padStart(2, '0')}/${(mese + 1).toString().padStart(2, '0')}`; rigaIntestazioni.push(`${nomeGiorno} ${dataStr}`); rigaIntestazioni.push(''); } excelData.push(rigaIntestazioni); // Righe medici const tuttiMedici = Object.keys(medici); tuttiMedici.forEach(codMedico => { const rigaMedico = [medici[codMedico].nomeCompleto, '']; for (let giorno = 1; giorno <= giorni; giorno++) { const turnoMedico = getTurnoMedicoMigliorato(codMedico, giorno); if (turnoMedico.spalmato || turnoMedico.spalmatoRep) { rigaMedico.push(turnoMedico.testo || ''); rigaMedico.push(turnoMedico.spalmato ? 'Guardia 20:30--' : 'Rep. 16:00--'); } else if (turnoMedico.continuaNotte || turnoMedico.continuaRep) { rigaMedico.push(turnoMedico.testo || ''); rigaMedico.push(''); } else { rigaMedico.push(turnoMedico.testo); rigaMedico.push(''); } } excelData.push(rigaMedico); });
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');
// FOGLIO 2: STATISTICHE const statsData = []; statsData.push(['STATISTICHE MENSILI DETTAGLIATE']); statsData.push(['Medico', 'Guardie Giorno', 'Guardie Notte', 'PTN', 'NIDO', 'Urgenze/SP', 'Follow-up', 'Reperibilità', 'Riposi', 'Ore Stimate', 'Ore/Settimana']); Object.keys(medici).forEach(medico => { const stats = statisticheMensili[medico]; const settimane = Math.ceil(giorni / 7); const totaleGuardie = stats.guardie.giorno + stats.guardie.notte; const totaleTurni = stats.turni.ptn + stats.turni.nido + stats.turni.urgenze + stats.turni.followUp; const oreStimate = totaleGuardie * 12 + totaleTurni * 7 + stats.turni.nido * 0.5; const oreSettimanali = oreStimate / settimane; statsData.push([ medici[medico].nome, stats.guardie.giorno, stats.guardie.notte, stats.turni.ptn, stats.turni.nido, stats.turni.urgenze, stats.turni.followUp, stats.turni.reperibilita, stats.riposi, oreStimate.toFixed(1), oreSettimanali.toFixed(1) ]); }); 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');
// FOGLIO 3: VALIDAZIONE CON BORDI const validazione = validaTurniCompleti(); const validazioneData = []; validazioneData.push(['VALIDAZIONE VINCOLI E CONTROLLI']); validazioneData.push([]); validazioneData.push(['RISULTATI', 'VALORE']); validazioneData.push(['Controlli Superati', validazione.controlliSuperati]); validazioneData.push(['Avvisi', validazione.avvisi.length]); validazioneData.push(['Errori Critici', validazione.errori.length]); validazioneData.push(['Punteggio Qualità (%)', validazione.punteggioQualita]); 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) { validazioneData.push(['AVVISI (NON CRITICI)', '']); validazione.avvisi.forEach(avviso => { validazioneData.push([avviso, '⚠️']); }); validazioneData.push([]); } if (validazione.errori.length > 0) { validazioneData.push(['ERRORI CRITICI', '']); validazione.errori.forEach(errore => { validazioneData.push([errore, '❌']); }); } 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');
// 🎨 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`; XLSX.writeFile(wb, fileName); mostraMessaggio(`✅ File Excel esportato con bordi e stili: ${fileName}`, 'success'); } catch (error) { mostraMessaggio('❌ Errore nell\'esportazione: ' + error.message, 'error'); } }
// INIZIALIZZAZIONE document.addEventListener('DOMContentLoaded', function() { const oggi = new Date(); document.getElementById('meseSelect').value = oggi.getMonth(); document.getElementById('annoInput').value = oggi.getFullYear(); console.log('🚀 Sistema Turni TIN Perfezionato caricato correttamente'); // Mostra messaggio di benvenuto setTimeout(() => { mostraMessaggio('🏥 Sistema caricato! Clicca "Genera Turni Avanzati" per iniziare.', 'success'); }, 1000); }); </script>
</body> </html>