producstion pause

This commit is contained in:
2026-09-15 15:46:32 +02:00
parent 4241d8e62b
commit cc24202937
6 changed files with 1125 additions and 82 deletions
+137 -35
View File
@@ -18,7 +18,6 @@ if (!empty($_GET['ajax'])) {
$dateFrom = $_GET['date_from'] ?? '';
$dateTo = $_GET['date_to'] ?? '';
// linee multiple (bottoni): CSV di id. Vuoto = tutte.
$lineRaw = $_GET['lines'] ?? '';
$lineArr = $lineRaw !== '' ? array_values(array_filter(array_map('intval', explode(',', $lineRaw)))) : [];
$matId = isset($_GET['matrice_id']) && $_GET['matrice_id'] !== '' ? (int)$_GET['matrice_id'] : null;
@@ -50,7 +49,6 @@ if (!empty($_GET['ajax'])) {
$whereSql = implode(' AND ', $where);
try {
/* ---------- KPI ---------- */
$sqlKpi = "SELECT
COUNT(*) AS n_produzioni,
COALESCE(SUM(p.tempo_totale_produzione),0) AS tot_occupato_s,
@@ -68,26 +66,19 @@ if (!empty($_GET['ajax'])) {
$denomScarto = $kpi['tot_kg'] + $kpi['tot_scarto'];
$scartoPercTot = ($denomScarto > 0) ? round($kpi['tot_scarto'] / $denomScarto * 100, 2) : 0;
/* ---------- SATURAZIONE (uso di max_hours_per_day) ----------
Capacità teorica = somma, per ogni linea nel perimetro,
di max_hours_per_day * numero giorni di calendario del range.
*/
$giorniRange = 1;
if ($dateFrom !== '' && $dateTo !== '') {
$d1 = new DateTime($dateFrom);
$d2 = new DateTime($dateTo);
$giorniRange = max(1, $d1->diff($d2)->days + 1);
}
// Linee da considerare per la capacità: quelle filtrate, o tutte
$capSql = "SELECT COALESCE(SUM(max_hours_per_day),0) AS somma_ore_giorno
FROM production_lines"
. (!empty($lineArr) ? " WHERE id IN (" . implode(',', $lineArr) . ")" : "");
$capOreGiorno = (float)$pdo->query($capSql)->fetchColumn();
$capacitaTeoricaS = $capOreGiorno * 3600 * $giorniRange;
$saturazione = ($capacitaTeoricaS > 0)
? round($kpi['tot_occupato_s'] / $capacitaTeoricaS * 100, 1) : 0;
$saturazione = ($capacitaTeoricaS > 0) ? round($kpi['tot_occupato_s'] / $capacitaTeoricaS * 100, 1) : 0;
/* ---------- PER LINEA ---------- */
$sqlLinea = "SELECT l.id, l.name AS linea, l.color, l.max_hours_per_day, COUNT(*) AS n_prod,
COALESCE(SUM(p.tempo_totale_produzione),0) AS occupato_s,
COALESCE(SUM(p.seconds_pause),0) AS pause_s,
@@ -102,13 +93,11 @@ if (!empty($_GET['ajax'])) {
$row['eff_tempo'] = ($row['occupato_s'] > 0) ? round($row['netto_s'] / $row['occupato_s'] * 100, 1) : 0;
$den = $row['kg'] + $row['scarto'];
$row['scarto_perc'] = ($den > 0) ? round($row['scarto'] / $den * 100, 2) : 0;
// saturazione della singola linea
$capLineaS = (float)$row['max_hours_per_day'] * 3600 * $giorniRange;
$row['saturazione'] = ($capLineaS > 0) ? round($row['occupato_s'] / $capLineaS * 100, 1) : 0;
}
unset($row);
/* ---------- SERIE TEMPORALE (granularità) ---------- */
if ($granul === 'month') {
$periodExpr = "DATE_FORMAT(p.Data, '%Y-%m')";
} else {
@@ -119,8 +108,7 @@ if (!empty($_GET['ajax'])) {
COALESCE(SUM(p.seconds_pause),0) AS pause_s,
COALESCE(SUM(p.seconds_produzione),0) AS netto_s,
COALESCE(SUM(p.kgprod),0) AS kg, COALESCE(SUM(p.scarto),0) AS scarto
FROM productiondata p WHERE $whereSql
GROUP BY periodo ORDER BY periodo";
FROM productiondata p WHERE $whereSql GROUP BY periodo ORDER BY periodo";
$stmt = $pdo->prepare($sqlSerie);
$stmt->execute($params);
$serie = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -131,7 +119,6 @@ if (!empty($_GET['ajax'])) {
}
unset($s);
/* ---------- PAUSE CATEGORIA ---------- */
$sqlCat = "SELECT pr.category AS category,
COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s
FROM production_pauses pp
@@ -147,7 +134,6 @@ if (!empty($_GET['ajax'])) {
else $unplannedS += (int)$c['durata_s'];
}
/* ---------- DETTAGLIO ---------- */
$sqlDett = "SELECT p.id, p.Data, p.conferma_ordine, p.note_extra,
l.name AS linea, m.nome AS matrice, c.nome AS cliente,
p.tempo_totale_produzione AS occupato_s, p.seconds_pause AS pause_s, p.seconds_produzione AS netto_s,
@@ -359,6 +345,7 @@ if (!empty($_GET['ajax'])) {
}
.kpi-card {
position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -416,6 +403,7 @@ if (!empty($_GET['ajax'])) {
}
.chart-card {
position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -431,6 +419,7 @@ if (!empty($_GET['ajax'])) {
color: #1e293b;
margin: 0 0 .8rem;
font-size: .95rem;
padding-right: 1.8rem;
}
.chart-wrap {
@@ -439,6 +428,7 @@ if (!empty($_GET['ajax'])) {
}
.table-card {
position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -451,6 +441,7 @@ if (!empty($_GET['ajax'])) {
font-weight: 700;
color: #1e293b;
margin: 0 0 .8rem;
padding-right: 1.8rem;
}
table.stats-table {
@@ -509,6 +500,70 @@ if (!empty($_GET['ajax'])) {
padding: 3rem;
color: #94a3b8;
}
/* --- PULSANTINO "?" INFO --- */
.info-btn {
position: absolute;
top: .6rem;
right: .6rem;
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
cursor: pointer;
background: #e2e8f0;
color: #475569;
font-size: .72rem;
font-weight: 800;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: .15s;
z-index: 5;
}
.info-btn:hover {
background: #2563eb;
color: #fff;
}
/* box spiegazione */
.info-pop {
position: absolute;
top: 2.1rem;
right: .6rem;
width: 250px;
max-width: 80vw;
background: #0f172a;
color: #f1f5f9;
border-radius: .6rem;
padding: .7rem .8rem;
font-size: .8rem;
line-height: 1.4;
font-weight: 400;
text-transform: none;
letter-spacing: normal;
box-shadow: 0 8px 24px rgba(0, 0, 0, .25);
z-index: 50;
display: none;
}
.info-pop.show {
display: block;
}
.info-pop strong {
color: #fff;
}
.info-pop .info-pop-title {
display: block;
font-weight: 800;
margin-bottom: .25rem;
color: #fff;
}
</style>
</head>
@@ -532,20 +587,17 @@ if (!empty($_GET['ajax'])) {
<div class="filters-bar">
<div><label>Da (data)</label><input type="date" id="fDateFrom"></div>
<div><label>A (data)</label><input type="date" id="fDateTo"></div>
<div style="grid-column:span 2;">
<label>Linee</label>
<div class="line-buttons" id="lineButtons">
<?php foreach ($linee as $l): ?>
<button type="button" class="line-btn2 active"
data-line="<?= (int)$l['id'] ?>"
<button type="button" class="line-btn2 active" data-line="<?= (int)$l['id'] ?>"
style="background:<?= htmlspecialchars($l['color'] ?: '#3b82f6') ?>;">
<?= htmlspecialchars($l['name']) ?>
</button>
<?php endforeach; ?>
</div>
</div>
<div><label>Matrice</label><select id="fMatrice">
<option value="">Tutte</option>
<?php foreach ($matrici as $m): ?><option value="<?= (int)$m['id'] ?>"><?= htmlspecialchars($m['nome']) ?></option><?php endforeach; ?>
@@ -554,7 +606,6 @@ if (!empty($_GET['ajax'])) {
<option value="">Tutte</option>
<?php foreach ($mescole as $me): ?><option value="<?= (int)$me['id'] ?>"><?= htmlspecialchars($me['nome']) ?></option><?php endforeach; ?>
</select></div>
<div>
<label>Vista temporale</label>
<div class="gran-toggle">
@@ -562,7 +613,6 @@ if (!empty($_GET['ajax'])) {
<button type="button" class="gran-btn" data-gran="month">Mese</button>
</div>
</div>
<div class="filters-actions">
<button class="btn-apply" id="btnApply"><i class="bi bi-funnel"></i> Applica</button>
<button class="btn-reset" id="btnReset">Reset</button>
@@ -575,28 +625,34 @@ if (!empty($_GET['ajax'])) {
<div class="charts-grid" id="chartsGrid" style="display:none;">
<div class="chart-card wide">
<button type="button" class="info-btn" data-info="Mostra come l'efficienza tempo cambia nel periodo scelto. In vista Giorno un punto per giorno, in vista Mese un punto per mese. Guarda la tendenza: sta salendo o scendendo? I cali indicano giornate con molti fermi." data-title="Efficienza nel tempo">?</button>
<h6 id="titSerie">Efficienza tempo nel periodo (%)</h6>
<div class="chart-wrap"><canvas id="chartSerie"></canvas></div>
</div>
<div class="chart-card">
<button type="button" class="info-btn" data-info="Confronta l'efficienza tempo delle diverse linee. Individua a colpo d'occhio la linea che perde più tempo in pause rispetto alle altre." data-title="Efficienza per linea">?</button>
<h6>Efficienza tempo per linea (%)</h6>
<div class="chart-wrap"><canvas id="chartEffLinea"></canvas></div>
</div>
<div class="chart-card">
<button type="button" class="info-btn" data-info="Quanto è stata sfruttata ogni linea rispetto alla sua capacità massima. Barre basse = linee spesso ferme o scariche di lavoro; barre alte = linee molto impegnate." data-title="Saturazione linea">?</button>
<h6>Saturazione linea (% su capacità)</h6>
<div class="chart-wrap"><canvas id="chartSat"></canvas></div>
</div>
<div class="chart-card">
<button type="button" class="info-btn" data-info="Per ogni linea, il confronto tra chili buoni prodotti (viola) e chili scartati (rosso). Una barra rossa alta rispetto alla produzione segnala un problema di qualità." data-title="Kg prodotti vs scarto">?</button>
<h6>Kg prodotti vs scarto per linea</h6>
<div class="chart-wrap"><canvas id="chartKgLinea"></canvas></div>
</div>
<div class="chart-card">
<button type="button" class="info-btn" data-info="La percentuale di scarto di ciascuna linea. Confronta le linee tra loro: quale scarta di più in proporzione a quanto produce?" data-title="Scarto per linea">?</button>
<h6>Scarto % per linea</h6>
<div class="chart-wrap"><canvas id="chartScartoLinea"></canvas></div>
</div>
</div>
<div class="table-card" id="tableCard" style="display:none;">
<button type="button" class="info-btn" data-info="Ogni riga è una lavorazione conclusa: data, linea, matrice, cliente, mescole, tempi (occupato / pause / netto), efficienza, chili, metri, scarto ed esito. Esito 'Completata' = chiusura normale; 'Problema' = interrotta per un guasto." data-title="Dettaglio produzioni">?</button>
<h6>Dettaglio produzioni</h6>
<table class="stats-table">
<thead>
@@ -628,7 +684,6 @@ if (!empty($_GET['ajax'])) {
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
// il codice sotto usa jQuery (già caricato nel <head>) e Chart.js
function secToHms(s) {
s = Math.max(0, parseInt(s || 0, 10));
const h = String(Math.floor(s / 3600)).padStart(2, '0'),
@@ -665,17 +720,50 @@ if (!empty($_GET['ajax'])) {
charts[k] = null;
}
}
let currentGran = 'day';
// Testi spiegazione per i KPI (chiave = data-kpi)
const KPI_INFO = {
n_produzioni: {
t: "Produzioni",
d: "Quante lavorazioni sono state completate nel periodo e con i filtri scelti. Dà il contesto agli altri numeri."
},
eff_tempo: {
t: "Efficienza tempo",
d: "Di tutto il tempo in cui la linea è stata impegnata, quanta parte è stata davvero produttiva (non in pausa). Più è alta, meglio è. 100% = nessuna pausa."
},
saturazione: {
t: "Saturazione",
d: "Quanto è stata sfruttata la linea rispetto a quanto poteva lavorare al massimo (capacità giornaliera). Bassa = linea rimasta ferma o scarica di lavoro."
},
pause: {
t: "Tempo in pausa",
d: "Il tempo totale in cui le linee sono rimaste ferme. Diviso tra pianificato (previsto, es. cambio turno) e non pianificato (imprevisto, es. guasti). È il non pianificato da ridurre."
},
kg: {
t: "Kg prodotti",
d: "I chili totali prodotti nel periodo (sotto, anche i metri). È il volume di produzione."
},
scarto: {
t: "Scarto",
d: "La percentuale di materiale scartato sul totale lavorato. Più è bassa, meglio è. In salita segnala un problema di qualità."
},
};
function infoBtn(key) {
const i = KPI_INFO[key];
if (!i) return '';
return `<button type="button" class="info-btn" data-title="${i.t}" data-info="${i.d.replace(/"/g,'&quot;')}">?</button>`;
}
function renderKpi(k) {
$('#kpiGrid').html(`
<div class="kpi-card"><div class="kpi-label">Produzioni</div><div class="kpi-value">${k.n_produzioni}</div><div class="kpi-sub">record conclusi</div></div>
<div class="kpi-card green"><div class="kpi-label">Efficienza tempo</div><div class="kpi-value">${k.eff_tempo}%</div><div class="kpi-sub">netto / occupato</div></div>
<div class="kpi-card teal"><div class="kpi-label">Saturazione</div><div class="kpi-value">${k.saturazione}%</div><div class="kpi-sub">su ${k.giorni_range} gg di capacità</div></div>
<div class="kpi-card amber"><div class="kpi-label">Tempo in pausa</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.tot_pause_s)}</div><div class="kpi-sub">pian. ${secToHms(k.planned_s)} · non ${secToHms(k.unplanned_s)}</div></div>
<div class="kpi-card violet"><div class="kpi-label">Kg prodotti</div><div class="kpi-value" style="font-size:1.4rem;">${num(k.tot_kg)}</div><div class="kpi-sub">${num(k.tot_mt)} metri</div></div>
<div class="kpi-card red"><div class="kpi-label">Scarto</div><div class="kpi-value" style="font-size:1.4rem;">${k.scarto_perc_tot}%</div><div class="kpi-sub">${num(k.tot_scarto)} kg scartati</div></div>
<div class="kpi-card">${infoBtn('n_produzioni')}<div class="kpi-label">Produzioni</div><div class="kpi-value">${k.n_produzioni}</div><div class="kpi-sub">record conclusi</div></div>
<div class="kpi-card green">${infoBtn('eff_tempo')}<div class="kpi-label">Efficienza tempo</div><div class="kpi-value">${k.eff_tempo}%</div><div class="kpi-sub">netto / occupato</div></div>
<div class="kpi-card teal">${infoBtn('saturazione')}<div class="kpi-label">Saturazione</div><div class="kpi-value">${k.saturazione}%</div><div class="kpi-sub">su ${k.giorni_range} gg di capacità</div></div>
<div class="kpi-card amber">${infoBtn('pause')}<div class="kpi-label">Tempo in pausa</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.tot_pause_s)}</div><div class="kpi-sub">pian. ${secToHms(k.planned_s)} · non ${secToHms(k.unplanned_s)}</div></div>
<div class="kpi-card violet">${infoBtn('kg')}<div class="kpi-label">Kg prodotti</div><div class="kpi-value" style="font-size:1.4rem;">${num(k.tot_kg)}</div><div class="kpi-sub">${num(k.tot_mt)} metri</div></div>
<div class="kpi-card red">${infoBtn('scarto')}<div class="kpi-label">Scarto</div><div class="kpi-value" style="font-size:1.4rem;">${k.scarto_perc_tot}%</div><div class="kpi-sub">${num(k.tot_scarto)} kg scartati</div></div>
`);
}
@@ -684,7 +772,6 @@ if (!empty($_GET['ajax'])) {
serie = d.serie || [],
gran = d.granularity;
const labels = serie.map(x => gran === 'month' ? fmtMonth(x.periodo) : fmtDateIt(x.periodo));
$('#titSerie').text(gran === 'month' ? 'Efficienza tempo per mese (%)' : 'Efficienza tempo per giorno (%)');
destroyChart('serie');
@@ -892,7 +979,6 @@ if (!empty($_GET['ajax'])) {
$('#fDateTo').val(today.toISOString().slice(0, 10));
$('#fDateFrom').val(past.toISOString().slice(0, 10));
// Bottoni linea: toggle, mai zero (se azzeri torni a tutte)
$('#lineButtons').on('click', '.line-btn2', function() {
$(this).toggleClass('active');
if ($('#lineButtons .line-btn2.active').length === 0) {
@@ -900,15 +986,12 @@ if (!empty($_GET['ajax'])) {
}
loadStats();
});
// Granularità
$('.gran-btn').on('click', function() {
$('.gran-btn').removeClass('active');
$(this).addClass('active');
currentGran = $(this).data('gran');
loadStats();
});
$('#btnApply').on('click', loadStats);
$('#btnReset').on('click', function() {
$('#lineButtons .line-btn2').addClass('active');
@@ -921,6 +1004,25 @@ if (!empty($_GET['ajax'])) {
loadStats();
});
// --- POPOVER INFO (delegato: funziona anche sui box creati via JS) ---
$(document).on('click', '.info-btn', function(e) {
e.stopPropagation();
const $btn = $(this);
// se questo popover è già aperto, chiudilo
const alreadyOpen = $btn.next('.info-pop').length && $btn.next('.info-pop').hasClass('show');
$('.info-pop').remove(); // chiudi tutti gli altri
if (alreadyOpen) return;
const title = $btn.data('title') || '';
const text = $btn.data('info') || '';
const $pop = $('<div class="info-pop show"></div>');
$pop.html('<span class="info-pop-title">' + title + '</span>' + text);
$btn.after($pop);
});
// chiudi cliccando fuori
$(document).on('click', function() {
$('.info-pop').remove();
});
loadStats();
});
</script>