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>