producstion pause
This commit is contained in:
@@ -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,'"')}">?</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>
|
||||
|
||||
Reference in New Issue
Block a user