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
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class MakePauseReasonNullable extends AbstractMigration
{
public function up(): void
{
// La FK su reason_id è già stata rimossa e la colonna è già nullable.
// Garantisco lo stato finale in modo idempotente, senza ricreare la FK.
$this->table('production_pauses')
->changeColumn('reason_id', 'integer', ['null' => true])
->update();
}
public function down(): void
{
$this->table('production_pauses')
->changeColumn('reason_id', 'integer', ['null' => false])
->update();
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddStatoDaCompilareDati extends AbstractMigration
{
public function up(): void
{
// Stato 11: produzione terminata ma dati (kg/metri/scarto) da inserire dopo
$this->execute("
INSERT INTO production_status (id, nome, ordinamento, badge_color, line_color)
VALUES (11, 'Da compilare dati', 48, '#7c3aed', '#ede9fe')
");
}
public function down(): void
{
$this->execute("DELETE FROM production_status WHERE id = 11");
}
}
+133 -6
View File
@@ -286,6 +286,7 @@ if (!empty($_GET['ajax'])) {
} }
.kpi-card { .kpi-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -339,6 +340,7 @@ if (!empty($_GET['ajax'])) {
} }
.chart-card { .chart-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -354,6 +356,7 @@ if (!empty($_GET['ajax'])) {
color: #1e293b; color: #1e293b;
margin: 0 0 .8rem; margin: 0 0 .8rem;
font-size: .95rem; font-size: .95rem;
padding-right: 1.8rem;
} }
.chart-wrap { .chart-wrap {
@@ -362,6 +365,7 @@ if (!empty($_GET['ajax'])) {
} }
.table-card { .table-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -374,6 +378,7 @@ if (!empty($_GET['ajax'])) {
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
margin: 0 0 .8rem; margin: 0 0 .8rem;
padding-right: 1.8rem;
} }
table.stats-table { table.stats-table {
@@ -442,6 +447,69 @@ if (!empty($_GET['ajax'])) {
padding: 3rem; padding: 3rem;
color: #94a3b8; 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: #dc2626;
color: #fff;
}
.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> </style>
</head> </head>
@@ -496,28 +564,34 @@ if (!empty($_GET['ajax'])) {
<div class="charts-grid" id="chartsGrid" style="display:none;"> <div class="charts-grid" id="chartsGrid" style="display:none;">
<div class="chart-card wide"> <div class="chart-card wide">
<button type="button" class="info-btn" data-title="Pareto delle causali" data-info="Il grafico più utile. Le barre sono le ore perse per ogni causa, ordinate dalla peggiore. La linea sale e mostra la somma progressiva in percentuale. Regola pratica: poche cause fanno la maggior parte del tempo perso. Concentrati sulle prime 2-3 barre a sinistra.">?</button>
<h6>Pareto causali — tempo perso e cumulata</h6> <h6>Pareto causali — tempo perso e cumulata</h6>
<div class="chart-wrap"><canvas id="chartPareto"></canvas></div> <div class="chart-wrap"><canvas id="chartPareto"></canvas></div>
</div> </div>
<div class="chart-card"> <div class="chart-card">
<button type="button" class="info-btn" data-title="Pianificato vs non pianificato" data-info="La fetta di tempo perso previsto (arancione, es. cambio turno, pulizia) contro quello imprevisto (rosso, es. guasti). Una torta sbilanciata verso il rosso significa molte perdite evitabili.">?</button>
<h6>Ripartizione tempo: pianificato vs non pianificato</h6> <h6>Ripartizione tempo: pianificato vs non pianificato</h6>
<div class="chart-wrap"><canvas id="chartCat"></canvas></div> <div class="chart-wrap"><canvas id="chartCat"></canvas></div>
</div> </div>
<div class="chart-card"> <div class="chart-card">
<button type="button" class="info-btn" data-title="Tempo perso per linea" data-info="Quale linea perde più ore di produzione a causa dei fermi. Individua la linea più problematica su cui concentrare le attenzioni.">?</button>
<h6>Tempo perso per linea (ore)</h6> <h6>Tempo perso per linea (ore)</h6>
<div class="chart-wrap"><canvas id="chartLinea"></canvas></div> <div class="chart-wrap"><canvas id="chartLinea"></canvas></div>
</div> </div>
<div class="chart-card"> <div class="chart-card">
<button type="button" class="info-btn" data-title="Numero fermi per causale" data-info="Quante volte si verifica ogni causa (la frequenza, non la durata). Una causa che compare tante volte anche se breve indica un problema ricorrente da eliminare alla radice.">?</button>
<h6>Numero fermi per causale</h6> <h6>Numero fermi per causale</h6>
<div class="chart-wrap"><canvas id="chartCount"></canvas></div> <div class="chart-wrap"><canvas id="chartCount"></canvas></div>
</div> </div>
<div class="chart-card"> <div class="chart-card">
<button type="button" class="info-btn" data-title="Andamento giornaliero" data-info="Le ore perse giorno per giorno. Individua le giornate nere e cerca cosa è successo (un guasto grave, una fornitura mancata).">?</button>
<h6>Andamento fermi giornaliero (ore)</h6> <h6>Andamento fermi giornaliero (ore)</h6>
<div class="chart-wrap"><canvas id="chartGiorno"></canvas></div> <div class="chart-wrap"><canvas id="chartGiorno"></canvas></div>
</div> </div>
</div> </div>
<div class="table-card" id="tableCard" style="display:none;"> <div class="table-card" id="tableCard" style="display:none;">
<button type="button" class="info-btn" data-title="Dettaglio eventi" data-info="Ogni riga è un singolo fermo: inizio, fine, durata, motivo, tipo (Problema o Normale), categoria (Pianificato o Non pianificato), linea, matrice e la nota dell'operatore. Il massimo dettaglio per ricostruire cosa è successo in un momento preciso.">?</button>
<h6>Dettaglio eventi di pausa</h6> <h6>Dettaglio eventi di pausa</h6>
<table class="stats-table"> <table class="stats-table">
<thead> <thead>
@@ -579,15 +653,49 @@ if (!empty($_GET['ajax'])) {
} }
} }
// Testi spiegazione per i KPI
const KPI_INFO = {
n_pause: {
t: "Fermi totali",
d: "Quante volte le linee si sono fermate in pausa nel periodo. Sotto, quante lavorazioni sono state coinvolte."
},
tot: {
t: "Tempo perso",
d: "Il totale di tutte le pause sommate: il costo in tempo complessivo dei fermi. Confrontalo tra periodi per vedere se migliora."
},
media: {
t: "Durata media",
d: "Quanto dura in media un singolo fermo (accanto, il fermo più lungo). Se il massimo è molto più alto della media, c'è stato un episodio eccezionale da approfondire."
},
problemi: {
t: "Problemi tecnici",
d: "Quanti fermi erano dovuti a un guasto o anomalia vera, e la loro percentuale sul totale. È la spia della salute degli impianti."
},
planned: {
t: "Pianificato",
d: "Il tempo dei fermi previsti (cambio turno, pulizia, manutenzione programmata...). È tempo normale, in parte inevitabile."
},
unplanned: {
t: "Non pianificato",
d: "Il tempo dei fermi imprevisti (guasti, mancanza materiale, qualità...). È QUESTO il tempo da ridurre: sono le perdite evitabili."
},
};
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) { function renderKpi(k) {
const pctProblemi = k.n_pause > 0 ? Math.round(k.n_problemi / k.n_pause * 100) : 0; const pctProblemi = k.n_pause > 0 ? Math.round(k.n_problemi / k.n_pause * 100) : 0;
$('#kpiGrid').html(` $('#kpiGrid').html(`
<div class="kpi-card"><div class="kpi-label">Fermi totali</div><div class="kpi-value">${k.n_pause}</div><div class="kpi-sub">${k.n_prod_con_pause} produzioni coinvolte</div></div> <div class="kpi-card">${infoBtn('n_pause')}<div class="kpi-label">Fermi totali</div><div class="kpi-value">${k.n_pause}</div><div class="kpi-sub">${k.n_prod_con_pause} produzioni coinvolte</div></div>
<div class="kpi-card"><div class="kpi-label">Tempo perso</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.tot_s)}</div><div class="kpi-sub">totale fermi</div></div> <div class="kpi-card">${infoBtn('tot')}<div class="kpi-label">Tempo perso</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.tot_s)}</div><div class="kpi-sub">totale fermi</div></div>
<div class="kpi-card slate"><div class="kpi-label">Durata media</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.media_s)}</div><div class="kpi-sub">max ${secToHms(k.max_s)}</div></div> <div class="kpi-card slate">${infoBtn('media')}<div class="kpi-label">Durata media</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.media_s)}</div><div class="kpi-sub">max ${secToHms(k.max_s)}</div></div>
<div class="kpi-card red"><div class="kpi-label">Problemi tecnici</div><div class="kpi-value">${k.n_problemi}</div><div class="kpi-sub">${pctProblemi}% dei fermi</div></div> <div class="kpi-card red">${infoBtn('problemi')}<div class="kpi-label">Problemi tecnici</div><div class="kpi-value">${k.n_problemi}</div><div class="kpi-sub">${pctProblemi}% dei fermi</div></div>
<div class="kpi-card green"><div class="kpi-label">Pianificato</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.planned_s)}</div><div class="kpi-sub">fermi previsti</div></div> <div class="kpi-card green">${infoBtn('planned')}<div class="kpi-label">Pianificato</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.planned_s)}</div><div class="kpi-sub">fermi previsti</div></div>
<div class="kpi-card amber"><div class="kpi-label">Non pianificato</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.unplanned_s)}</div><div class="kpi-sub">da ridurre</div></div> <div class="kpi-card amber">${infoBtn('unplanned')}<div class="kpi-label">Non pianificato</div><div class="kpi-value" style="font-size:1.25rem;">${secToHms(k.unplanned_s)}</div><div class="kpi-sub">da ridurre</div></div>
`); `);
} }
@@ -802,6 +910,7 @@ if (!empty($_GET['ajax'])) {
$('#kpiGrid').html('<div class="empty">Errore di comunicazione con il server.</div>'); $('#kpiGrid').html('<div class="empty">Errore di comunicazione con il server.</div>');
}); });
} }
$(function() { $(function() {
const today = new Date(), const today = new Date(),
past = new Date(); past = new Date();
@@ -823,6 +932,24 @@ if (!empty($_GET['ajax'])) {
$('#fDateFrom').val(past.toISOString().slice(0, 10)); $('#fDateFrom').val(past.toISOString().slice(0, 10));
loadStats(); loadStats();
}); });
// --- POPOVER INFO (delegato: funziona anche sui box creati via JS) ---
$(document).on('click', '.info-btn', function(e) {
e.stopPropagation();
const $btn = $(this);
const alreadyOpen = $btn.next('.info-pop').length && $btn.next('.info-pop').hasClass('show');
$('.info-pop').remove();
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);
});
$(document).on('click', function() {
$('.info-pop').remove();
});
loadStats(); loadStats();
}); });
</script> </script>
+620
View File
@@ -0,0 +1,620 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Stato Qualità di destinazione dopo la compilazione
$statusQualita = (int)$pdo->query("SELECT id FROM production_status WHERE id = 3 LIMIT 1")->fetchColumn();
if (!$statusQualita) {
die("Errore: stato Controllo qualità (3) non trovato in production_status");
}
// --- SALVATAGGIO DATI DI PRODUZIONE (da stato 11 → Qualità 3) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_compile'])) {
header('Content-Type: application/json; charset=utf-8');
$id = (int)($_POST['id'] ?? 0);
$kgprod = (float)($_POST['kgprod'] ?? 0);
$mtprod = (float)($_POST['mtprod'] ?? 0);
$scarto = (float)($_POST['scarto'] ?? 0);
$note = trim($_POST['note'] ?? '');
try {
if ($id <= 0) {
echo json_encode(['success' => false, 'msg' => 'ID non valido.']);
exit;
}
if ($kgprod <= 0 && $mtprod <= 0) {
echo json_encode(['success' => false, 'msg' => 'Inserisci almeno uno tra Kg o Metri prodotti.']);
exit;
}
// Verifico che la riga sia davvero in stato 11 (da compilare)
$stmtChk = $pdo->prepare("SELECT id_status FROM productiondata WHERE id = :id LIMIT 1");
$stmtChk->execute(['id' => $id]);
$cur = $stmtChk->fetchColumn();
if ($cur === false) {
echo json_encode(['success' => false, 'msg' => 'Produzione non trovata.']);
exit;
}
if ((int)$cur !== 11) {
echo json_encode(['success' => false, 'msg' => 'Questa produzione non è più in stato "Da compilare".']);
exit;
}
// Calcolo scarto %
$totaleKg = $kgprod + $scarto;
$scartoPerc = $totaleKg > 0 ? round(($scarto / $totaleKg) * 100, 2) : 0.00;
// Update: riempio i dati e passo a Qualità (3)
$sql = "UPDATE productiondata
SET kgprod = :kgprod,
mtprod = :mtprod,
scarto = :scarto,
scarto_perc = :scarto_perc,
note_operatore = :note,
id_status = :status
WHERE id = :id AND id_status = 11";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'kgprod' => $kgprod,
'mtprod' => $mtprod,
'scarto' => $scarto,
'scarto_perc' => $scartoPerc,
'note' => $note ?: null,
'status' => $statusQualita,
'id' => $id
]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'msg' => $e->getMessage()]);
}
exit;
}
// --- AJAX: lista produzioni in stato 11 ---
if (!empty($_GET['ajax'])) {
$sql = "SELECT
p.*,
m.nome AS matrice,
l.name AS linea,
c.nome AS cliente,
(
SELECT GROUP_CONCAT(mes.nome ORDER BY mes.nome SEPARATOR ' | ')
FROM productiondata_mescole pm
JOIN mescole mes ON mes.id = pm.id_mescola
WHERE pm.id_productiondata = p.id
) AS mescole_list
FROM productiondata p
LEFT JOIN matrice m ON p.idmatrice = m.id
LEFT JOIN production_lines l ON p.id_linea = l.id
LEFT JOIN clients c ON p.id_cliente = c.id
WHERE p.id_status = 11
ORDER BY p.end_time ASC, p.id ASC";
$records = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
if (empty($records)) {
echo '<div class="no-records"><i class="bi bi-check2-circle"></i><br>Nessuna produzione da compilare.</div>';
exit;
}
// Helper: secondi -> HH:MM:SS
$fmt = function ($sec) {
$sec = (int)$sec;
$h = floor($sec / 3600);
$m = floor(($sec % 3600) / 60);
$s = $sec % 60;
return sprintf('%02d:%02d:%02d', $h, $m, $s);
};
foreach ($records as $r) {
$occupato = $fmt($r['tempo_totale_produzione'] ?? 0);
$pause = $fmt($r['seconds_pause'] ?? 0);
$netto = $fmt($r['seconds_produzione'] ?? 0);
$endTime = $r['end_time'] ? date('d/m/Y H:i', strtotime($r['end_time'])) : '—';
$teoKg = number_format((float)($r['kg_sp'] ?? 0), 2, '.', '');
$teoMt = number_format((float)($r['metri'] ?? 0), 2, '.', '');
echo '<div class="compile-card" data-id="' . (int)$r['id'] . '"
data-teokg="' . htmlspecialchars($teoKg) . '"
data-teomt="' . htmlspecialchars($teoMt) . '">';
echo '<div class="compile-head">';
echo '<div class="compile-title">' . htmlspecialchars($r['matrice'] ?? '—') . '</div>';
echo '<div class="compile-badge">Da compilare</div>';
echo '</div>';
echo '<div class="compile-grid">';
echo '<div><strong>Linea</strong><span>' . htmlspecialchars($r['linea'] ?? '—') . '</span></div>';
echo '<div><strong>Cliente</strong><span>' . htmlspecialchars($r['cliente'] ?? '—') . '</span></div>';
echo '<div><strong>Fine produzione</strong><span>' . $endTime . '</span></div>';
echo '<div><strong>Tempo occupato</strong><span>' . $occupato . '</span></div>';
echo '<div><strong>Tempo pause</strong><span>' . $pause . '</span></div>';
echo '<div><strong>Tempo netto</strong><span>' . $netto . '</span></div>';
echo '</div>';
if (!empty($r['mescole_list'])) {
echo '<div class="compile-mescole"><strong>Mescole:</strong> '
. htmlspecialchars($r['mescole_list']) . '</div>';
}
if (!empty($r['note_extra'])) {
echo '<div class="compile-mescole"><strong>Descrizione:</strong> '
. htmlspecialchars($r['note_extra']) . '</div>';
}
echo '<div class="compile-actions">';
echo '<button class="compile-btn btn-fill" data-id="' . (int)$r['id'] . '">Compila dati</button>';
echo '</div>';
echo '</div>'; // .compile-card
}
exit;
}
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Dati da compilare - Produzione</title>
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Segoe UI', sans-serif;
}
body {
background: #f1f5f9;
-webkit-tap-highlight-color: transparent;
}
.wrapper,
.page-wrapper,
.page-content {
height: 100%;
display: flex;
flex-direction: column;
}
.page-content {
padding: 0;
}
.main-card {
flex: 1;
display: flex;
flex-direction: column;
margin: 0.8rem;
border-radius: 1.2rem;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
background: #fff;
}
.card-header {
background: linear-gradient(135deg, #6d28d9, #7c3aed);
color: white;
padding: 1.2rem 1.5rem;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h5 {
margin: 0;
font-weight: 600;
font-size: 1.35rem;
color: #fff;
}
.back-btn {
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.4);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.8rem;
font-weight: 500;
cursor: pointer;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.35);
}
#recordsContainer {
flex: 1;
overflow-y: auto;
padding: 1.2rem 1.5rem 5rem 1.5rem;
background: #f8fafc;
}
.compile-card {
background: #fff;
border-radius: 1rem;
border-left: 6px solid #7c3aed;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06);
padding: 1.1rem 1.3rem;
margin-bottom: 1rem;
}
.compile-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
}
.compile-title {
font-size: 1.3rem;
font-weight: 800;
color: #1e293b;
}
.compile-badge {
background: #ede9fe;
color: #6d28d9;
padding: 0.35rem 0.85rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.compile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.7rem 1rem;
margin-bottom: 0.6rem;
}
.compile-grid strong {
display: block;
color: #64748b;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.15rem;
}
.compile-grid span {
color: #1e293b;
font-weight: 600;
font-size: 1.05rem;
}
.compile-mescole {
color: #475569;
font-size: 0.95rem;
margin-top: 0.3rem;
}
.compile-actions {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
.compile-btn {
padding: 0.9rem 1.6rem;
font-size: 1.05rem;
font-weight: 700;
border: none;
border-radius: 0.8rem;
cursor: pointer;
color: white;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.btn-fill {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
}
.btn-fill:hover {
background: linear-gradient(135deg, #6d28d9, #5b21b6);
}
.no-records {
text-align: center;
padding: 4rem 1.5rem;
color: #64748b;
font-size: 1.15rem;
}
.no-records i {
font-size: 3.5rem;
color: #a78bfa;
}
/* --- MODALE --- */
.custom-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
justify-content: center;
align-items: center;
}
.custom-modal.active {
display: flex !important;
}
.custom-modal .modal-content {
background: white;
padding: 2rem;
border-radius: 1.2rem;
width: 95%;
max-width: 780px;
max-height: 92vh;
overflow-y: auto;
text-align: left;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
}
.final-flex {
display: flex;
gap: 2rem;
margin-top: 1.2rem;
flex-wrap: wrap;
}
.final-col {
flex: 1;
min-width: 260px;
}
.teo-box {
background: #eef6ff;
padding: 1rem;
border-radius: 0.8rem;
}
.teo-row {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.teo-row label {
font-weight: 600;
color: #1e3a8a;
}
.teo-row span {
font-weight: 700;
color: #0f172a;
font-size: 1.25rem;
display: block;
}
.real-box label {
margin-top: 0.6rem;
font-weight: 600;
display: block;
}
.input-big {
width: 100%;
padding: 0.9rem;
font-size: 1.25rem;
border-radius: 0.6rem;
border: 1px solid #cbd5e1;
margin-bottom: 1rem;
}
.modal-buttons {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
.modal-btn {
flex: 1;
padding: 0.9rem;
font-weight: 600;
border: none;
border-radius: 0.8rem;
cursor: pointer;
font-size: 1.05rem;
}
.modal-confirm {
background: #7c3aed;
color: white;
}
.modal-cancel {
background: #e2e8f0;
color: #475569;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="main-card">
<div class="card-header">
<h5>Dati di produzione da compilare</h5>
<button class="back-btn" onclick="location.href='production_line.php'">Torna</button>
</div>
<div id="recordsContainer">
<!-- Caricato via JS -->
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE COMPILAZIONE DATI -->
<div id="compileModal" class="custom-modal">
<div class="modal-content">
<h3>Dati di produzione</h3>
<p>Confronta i valori teorici e inserisci quelli reali:</p>
<div class="final-flex">
<div class="final-col teo-box">
<h4>Teorici</h4>
<div class="teo-row">
<label>Kg teorici:</label>
<span id="teoKg">0.00</span>
</div>
<div class="teo-row">
<label>Metri teorici:</label>
<span id="teoMt">0.00</span>
</div>
</div>
<div class="final-col real-box">
<h4>Reali</h4>
<label>Kg prodotti:</label>
<input type="number" id="kgprod" step="0.01" min="0" placeholder="0.00" class="input-big">
<label>Metri prodotti:</label>
<input type="number" id="mtprod" step="0.01" min="0" placeholder="0.00" class="input-big">
<label>Scarto (kg):</label>
<input type="number" id="scarto" step="0.01" min="0" placeholder="0.00" class="input-big">
<label>Scarto (%):</label>
<input type="text" id="scartoPerc" class="input-big" readonly>
<label>Note operatore:</label>
<textarea id="compileNote" rows="3" class="input-big"></textarea>
</div>
</div>
<div class="modal-buttons">
<button id="compileCancel" class="modal-btn modal-cancel">Annulla</button>
<button id="compileConfirm" class="modal-btn modal-confirm">Conferma e invia a Qualità</button>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<?php include('jsinclude.php'); ?>
<script>
$(function() {
function loadRecords() {
$('#recordsContainer').html(
'<div class="text-center p-5"><i class="bi bi-hourglass-split" style="font-size:2.5rem;color:#a78bfa;"></i></div>'
);
$.get('', {
ajax: 1
}, function(html) {
$('#recordsContainer').html(html);
}).fail(function() {
$('#recordsContainer').html('<div class="no-records">Errore di connessione</div>');
});
}
function recalcScrapPerc() {
const kg = parseFloat($('#kgprod').val()) || 0;
const sc = parseFloat($('#scarto').val()) || 0;
const tot = kg + sc;
const perc = tot > 0 ? (sc / tot) * 100 : 0;
$('#scartoPerc').val(perc.toFixed(2) + ' %');
}
// Apri modale compilazione
$(document).on('click', '.compile-btn', function(e) {
e.stopPropagation();
const card = $(this).closest('.compile-card');
const id = card.data('id');
$('#kgprod, #mtprod, #scarto').val('');
$('#scartoPerc').val('');
$('#compileNote').val('');
$('#teoKg').text(card.data('teokg') || '0.00');
$('#teoMt').text(card.data('teomt') || '0.00');
$('#kgprod, #scarto').off('input').on('input', recalcScrapPerc);
// Conferma
$('#compileConfirm').off('click').on('click', function() {
const kg = parseFloat($('#kgprod').val()) || 0;
const mt = parseFloat($('#mtprod').val()) || 0;
const scarto = parseFloat($('#scarto').val()) || 0;
const note = $('#compileNote').val().trim();
if (kg <= 0 && mt <= 0) {
alert('Inserisci almeno uno tra Kg o Metri prodotti.');
return;
}
$('#compileConfirm').prop('disabled', true);
$.post('', {
save_compile: 1,
id: id,
kgprod: kg,
mtprod: mt,
scarto: scarto,
note: note
}, function(r) {
$('#compileConfirm').prop('disabled', false);
if (r.success) {
$('#compileModal').removeClass('active');
loadRecords();
} else {
alert(r.msg || 'Errore nel salvataggio.');
}
}, 'json').fail(function() {
$('#compileConfirm').prop('disabled', false);
alert('Errore di comunicazione con il server.');
});
});
$('#compileModal').addClass('active');
});
// Annulla → non salva, resta in stato 11
$('#compileCancel').on('click', function() {
$('#compileModal').removeClass('active');
});
$(document).on('click', '#compileModal', function(e) {
if ($(e.target).is('#compileModal')) {
$('#compileModal').removeClass('active');
}
});
loadRecords();
});
</script>
</body>
</html>
+168 -20
View File
@@ -77,14 +77,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['start_production']))
// 3) Ripresa: da PAUSA (7) → PRODUZIONE (2) **senza toccare start_time** // 3) Ripresa: da PAUSA (7) → PRODUZIONE (2) **senza toccare start_time**
} elseif ((int)$currentStatus === (int)$statusPausaId) { } elseif ((int)$currentStatus === (int)$statusPausaId) {
// Chiudo l'ultima pausa aperta di questa produzione // Il motivo è obbligatorio alla ripresa: lo ricevo dal form
$resumeReasonId = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
// Verifico se la pausa aperta ha già un motivo
$stmtChk = $pdo->prepare("SELECT reason_id FROM production_pauses
WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC LIMIT 1");
$stmtChk->execute(['id' => $id]);
$existingReason = $stmtChk->fetchColumn();
// Se manca il motivo (sia sulla pausa che nel form) → blocco
if (($existingReason === null || $existingReason === false) && $resumeReasonId <= 0) {
echo json_encode(['success' => false, 'msg' => 'Seleziona il motivo della pausa prima di riprendere.']);
exit;
}
// Chiudo l'ultima pausa aperta + scrivo il motivo se fornito ora
$sqlClose = "UPDATE production_pauses $sqlClose = "UPDATE production_pauses
SET end_pause = UTC_TIMESTAMP() SET end_pause = UTC_TIMESTAMP()"
. ($resumeReasonId > 0 ? ", reason_id = :reason_id" : "") . "
WHERE id_production = :id AND end_pause IS NULL WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC ORDER BY start_pause DESC
LIMIT 1"; LIMIT 1";
$stmtClose = $pdo->prepare($sqlClose); $stmtClose = $pdo->prepare($sqlClose);
$stmtClose->execute(['id' => $id]); $paramsClose = ['id' => $id];
if ($resumeReasonId > 0) $paramsClose['reason_id'] = $resumeReasonId;
$stmtClose->execute($paramsClose);
$sqlUpdate = "UPDATE productiondata $sqlUpdate = "UPDATE productiondata
SET id_status = :status, SET id_status = :status,
@@ -123,12 +142,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['pause_production']))
try { try {
if ($reasonId <= 0) { // Motivo pausa ora OPZIONALE: se non lo sanno, salvano NULL e lo mettono al riprendi
echo json_encode(['success' => false, 'msg' => 'Motivo pausa non valido.']); if ($reasonId > 0) {
exit;
}
// Leggo se il motivo è "problema" oppure pausa normale
$sql = "SELECT is_problem FROM pause_reasons WHERE id = :id LIMIT 1"; $sql = "SELECT is_problem FROM pause_reasons WHERE id = :id LIMIT 1";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $reasonId]); $stmt->execute(['id' => $reasonId]);
@@ -138,8 +153,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['pause_production']))
echo json_encode(['success' => false, 'msg' => 'Motivo pausa non trovato.']); echo json_encode(['success' => false, 'msg' => 'Motivo pausa non trovato.']);
exit; exit;
} }
$isProblem = (int)$isProblem; $isProblem = (int)$isProblem;
} else {
$reasonId = null; // nessun motivo scelto ora
$isProblem = 0;
}
// ❌ PRIMA – sbagliato per la tua logica: // ❌ PRIMA – sbagliato per la tua logica:
// $nextStatus = $isProblem ? 8 : $statusPausaId; // 8 = Problema in produzione // $nextStatus = $isProblem ? 8 : $statusPausaId; // 8 = Problema in produzione
@@ -294,8 +312,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data']))
$scartoPerc = round(($scarto / $totaleKg) * 100, 2); $scartoPerc = round(($scarto / $totaleKg) * 100, 2);
} }
// Stato destinazione // "Compila dopo" → stato 11 (dati da inserire), altrimenti flusso normale
$compilaDopo = !empty($_POST['compila_dopo']);
if ($compilaDopo) {
$nextStatus = 11; // Da compilare dati
} else {
$nextStatus = ($reason === 'problema') ? 8 : $statusQualita; $nextStatus = ($reason === 'problema') ? 8 : $statusQualita;
}
// Update finale // Update finale
$sql = "UPDATE productiondata $sql = "UPDATE productiondata
@@ -410,6 +434,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_quick'])) {
} }
// --- AJAX: la pausa aperta di questa produzione ha già un motivo? ---
if (!empty($_GET['check_pause_reason'])) {
header('Content-Type: application/json; charset=utf-8');
$pid = (int)($_GET['id'] ?? 0);
if ($pid <= 0) {
echo json_encode(['success' => false]);
exit;
}
$stmt = $pdo->prepare("SELECT reason_id FROM production_pauses
WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC LIMIT 1");
$stmt->execute(['id' => $pid]);
$rid = $stmt->fetchColumn();
// has_reason = true se esiste una pausa aperta con reason_id non NULL
echo json_encode([
'success' => true,
'has_reason' => ($rid !== false && $rid !== null)
]);
exit;
}
// --- AJAX: mescole collegate a una matrice --- // --- AJAX: mescole collegate a una matrice ---
if (!empty($_GET['mescole_by_matrice'])) { if (!empty($_GET['mescole_by_matrice'])) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
@@ -1497,6 +1542,20 @@ if (!empty($_GET['ajax'])) {
</div> </div>
<!-- Motivo pausa (dropdown, solo per PAUSA) --> <!-- Motivo pausa (dropdown, solo per PAUSA) -->
<div id="pauseReasonContainer" style="margin-top:1rem; display:none;"> <div id="pauseReasonContainer" style="margin-top:1rem; display:none;">
<!-- Toggle "motivo dopo" (comodo per tablet) -->
<label id="laterToggleWrap" for="pauseLater"
style="display:flex; align-items:center; gap:0.8rem; padding:0.9rem 1rem;
background:#f1f5f9; border:2px solid #cbd5e1; border-radius:0.8rem;
cursor:pointer; margin-bottom:1rem; user-select:none;">
<input type="checkbox" id="pauseLater"
style="width:1.6rem; height:1.6rem; cursor:pointer; flex:0 0 auto;">
<span style="font-weight:700; font-size:1.1rem; color:#1e3a8a;">
Inserisco il motivo dopo (al riprendi)
</span>
</label>
<div id="pauseReasonSelectWrap">
<label for="pauseReason"><strong>Motivo pausa:</strong></label> <label for="pauseReason"><strong>Motivo pausa:</strong></label>
<select id="pauseReason" <select id="pauseReason"
style="width:100%;padding:0.5rem;border-radius:0.5rem;border:1px solid #ccc;"> style="width:100%;padding:0.5rem;border-radius:0.5rem;border:1px solid #ccc;">
@@ -1509,6 +1568,7 @@ if (!empty($_GET['ajax'])) {
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
</div>
<!-- Campo nota --> <!-- Campo nota -->
@@ -1579,8 +1639,10 @@ if (!empty($_GET['ajax'])) {
</div> </div>
<div class="modal-buttons" style="margin-top:1.5rem;"> <div class="modal-buttons" style="margin-top:1.5rem; flex-wrap:wrap;">
<button id="finalCancel" class="modal-btn modal-cancel">Annulla</button> <button id="finalCancel" class="modal-btn modal-cancel">Annulla</button>
<button id="finalLater" class="modal-btn modal-confirm"
style="background:#7c3aed;">Compila dopo</button>
<button id="finalConfirm" class="modal-btn modal-confirm">Conferma</button> <button id="finalConfirm" class="modal-btn modal-confirm">Conferma</button>
</div> </div>
</div> </div>
@@ -2183,7 +2245,20 @@ if (!empty($_GET['ajax'])) {
e.stopPropagation(); e.stopPropagation();
playBeep(); playBeep();
const id = $(this).data('id'); const id = $(this).data('id');
showReasonModal('resume', id);
// Chiedo al server se la pausa ha già un motivo
$.getJSON('', {
check_pause_reason: 1,
id: id
}, function(r) {
const hasReason = r && r.success && r.has_reason;
// 'resume' → motivo già presente: solo conferma
// 'resume_ask' → motivo mancante: chiedi il motivo
showReasonModal(hasReason ? 'resume' : 'resume_ask', id);
}).fail(function() {
// In caso di errore, per sicurezza chiedo il motivo
showReasonModal('resume_ask', id);
});
}); });
// --- PAUSA --- // --- PAUSA ---
@@ -2216,6 +2291,17 @@ if (!empty($_GET['ajax'])) {
return `${day} ${month} ${year}`; return `${day} ${month} ${year}`;
} }
// Toggle "motivo dopo": nasconde/disabilita la tendina
$(document).off('change', '#pauseLater').on('change', '#pauseLater', function() {
const later = $(this).is(':checked');
if (later) {
$('#pauseReason').val('').prop('disabled', true);
$('#pauseReasonSelectWrap').slideUp(120);
} else {
$('#pauseReason').prop('disabled', false);
$('#pauseReasonSelectWrap').slideDown(120);
}
});
// --- MODALE UNIFICATA PER TUTTE LE AZIONI --- // --- MODALE UNIFICATA PER TUTTE LE AZIONI ---
function showReasonModal(action, id) { function showReasonModal(action, id) {
@@ -2226,6 +2312,10 @@ if (!empty($_GET['ajax'])) {
$('#noteContainer').hide(); $('#noteContainer').hide();
$('#pauseReasonContainer').hide(); $('#pauseReasonContainer').hide();
$('#pauseReason').val(''); $('#pauseReason').val('');
// reset toggle "motivo dopo"
$('#pauseLater').prop('checked', false);
$('#pauseReasonSelectWrap').show();
$('#pauseReason').prop('disabled', false);
// Titolo e testo dinamici // Titolo e testo dinamici
switch (action) { switch (action) {
@@ -2235,8 +2325,22 @@ if (!empty($_GET['ajax'])) {
break; break;
case 'resume': case 'resume':
// Motivo già presente in pausa → solo conferma
$('#modalTitle').text('Ripresa produzione'); $('#modalTitle').text('Ripresa produzione');
$('#modalQuestion').text('Vuoi riprendere la produzione?'); $('#modalQuestion').text('Vuoi riprendere la produzione?');
$('#pauseReasonContainer').hide();
$('#noteContainer').hide();
break;
case 'resume_ask':
// Pausa senza motivo → lo chiedo ora (obbligatorio)
$('#modalTitle').text('Ripresa produzione');
$('#modalQuestion').text('Indica il motivo della pausa per riprendere:');
$('#pauseReasonContainer').show();
$('#laterToggleWrap').hide(); // niente "motivo dopo" al riprendi
$('#pauseReasonSelectWrap').show();
$('#pauseReason').prop('disabled', false);
$('#noteContainer').show();
break; break;
case 'pause': case 'pause':
@@ -2244,6 +2348,7 @@ if (!empty($_GET['ajax'])) {
$('#modalQuestion').text('Seleziona il motivo della pausa:'); $('#modalQuestion').text('Seleziona il motivo della pausa:');
$('#modalOptions').hide(); // niente radio $('#modalOptions').hide(); // niente radio
$('#pauseReasonContainer').show(); // mostra tendina $('#pauseReasonContainer').show(); // mostra tendina
$('#laterToggleWrap').show(); // toggle "motivo dopo" visibile
$('#noteContainer').show(); // nota opzionale / obbligatoria se problema $('#noteContainer').show(); // nota opzionale / obbligatoria se problema
break; break;
@@ -2266,15 +2371,21 @@ if (!empty($_GET['ajax'])) {
// --- LOGICA PAUSA --- // --- LOGICA PAUSA ---
if (action === 'pause') { if (action === 'pause') {
const pauseReasonId = $('#pauseReason').val(); const later = $('#pauseLater').is(':checked');
if (!pauseReasonId) {
alert('Seleziona un motivo di pausa.'); // Se NON ha scelto "motivo dopo", il motivo è obbligatorio ora
if (!later && !$('#pauseReason').val()) {
alert('Seleziona un motivo, oppure attiva "Inserisco il motivo dopo".');
return; return;
} }
// Se "motivo dopo" → NULL (0), altrimenti l'id scelto
const pauseReasonId = later ? 0 : ($('#pauseReason').val() || 0);
const isProblem = $('#pauseReason option:selected').data('problem') == 1; const isProblem = $('#pauseReason option:selected').data('problem') == 1;
if (isProblem && note === '') { // "Problema" richiede la nota (solo se il motivo è stato scelto ora)
if (!later && isProblem && note === '') {
alert('Inserisci una nota per una pausa con problema tecnico.'); alert('Inserisci una nota per una pausa con problema tecnico.');
return; return;
} }
@@ -2304,10 +2415,24 @@ if (!empty($_GET['ajax'])) {
// Azioni specifiche // Azioni specifiche
switch (action) { switch (action) {
case 'start': case 'start':
case 'resume':
startProduction(id); startProduction(id);
break; break;
case 'resume':
// Motivo già presente: riprendo senza inviare reason_id
startProduction(id);
break;
case 'resume_ask':
// Motivo mancante: obbligatorio sceglierlo ora
const resumeReasonId = $('#pauseReason').val();
if (!resumeReasonId) {
alert('Seleziona il motivo della pausa prima di riprendere.');
return;
}
startProduction(id, resumeReasonId);
break;
case 'stop': case 'stop':
// Sia "fine" che "problema" aprono il modale dati finali. // Sia "fine" che "problema" aprono il modale dati finali.
// Il reason viene passato e conservato per il salvataggio. // Il reason viene passato e conservato per il salvataggio.
@@ -2375,6 +2500,28 @@ if (!empty($_GET['ajax'])) {
$('#finalCancel').off('click').on('click', function() { $('#finalCancel').off('click').on('click', function() {
$('#finalDataModal').removeClass('active'); $('#finalDataModal').removeClass('active');
}); });
// "Compila dopo": chiude la produzione senza kg/metri, va in stato 11
$('#finalLater').off('click').on('click', function() {
if (!confirm('La produzione verrà terminata e i dati (kg, metri, scarto) resteranno da inserire nella pagina dedicata. Confermi?')) {
return;
}
$('#finalDataModal').removeClass('active');
$.post('', {
save_final_data: 1,
id: id,
reason: reason,
compila_dopo: 1, // flag: rimanda la compilazione
kgprod: 0,
mtprod: 0,
scarto: 0,
note: $('#finalNote').val().trim()
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore nel salvataggio.');
}, 'json');
});
} }
function recalcScrapPerc() { function recalcScrapPerc() {
@@ -2394,10 +2541,11 @@ if (!empty($_GET['ajax'])) {
// --- START PRODUZIONE --- // --- START PRODUZIONE ---
function startProduction(id) { function startProduction(id, reasonId) {
$.post('', { $.post('', {
start_production: 1, start_production: 1,
id: id id: id,
reason_id: reasonId || 0
}, function(r) { }, function(r) {
if (r.success) loadRecords(); if (r.success) loadRecords();
else alert(r.msg || 'Errore durante l\'avvio.'); else alert(r.msg || 'Errore durante l\'avvio.');
+137 -35
View File
@@ -18,7 +18,6 @@ if (!empty($_GET['ajax'])) {
$dateFrom = $_GET['date_from'] ?? ''; $dateFrom = $_GET['date_from'] ?? '';
$dateTo = $_GET['date_to'] ?? ''; $dateTo = $_GET['date_to'] ?? '';
// linee multiple (bottoni): CSV di id. Vuoto = tutte.
$lineRaw = $_GET['lines'] ?? ''; $lineRaw = $_GET['lines'] ?? '';
$lineArr = $lineRaw !== '' ? array_values(array_filter(array_map('intval', explode(',', $lineRaw)))) : []; $lineArr = $lineRaw !== '' ? array_values(array_filter(array_map('intval', explode(',', $lineRaw)))) : [];
$matId = isset($_GET['matrice_id']) && $_GET['matrice_id'] !== '' ? (int)$_GET['matrice_id'] : null; $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); $whereSql = implode(' AND ', $where);
try { try {
/* ---------- KPI ---------- */
$sqlKpi = "SELECT $sqlKpi = "SELECT
COUNT(*) AS n_produzioni, COUNT(*) AS n_produzioni,
COALESCE(SUM(p.tempo_totale_produzione),0) AS tot_occupato_s, 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']; $denomScarto = $kpi['tot_kg'] + $kpi['tot_scarto'];
$scartoPercTot = ($denomScarto > 0) ? round($kpi['tot_scarto'] / $denomScarto * 100, 2) : 0; $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; $giorniRange = 1;
if ($dateFrom !== '' && $dateTo !== '') { if ($dateFrom !== '' && $dateTo !== '') {
$d1 = new DateTime($dateFrom); $d1 = new DateTime($dateFrom);
$d2 = new DateTime($dateTo); $d2 = new DateTime($dateTo);
$giorniRange = max(1, $d1->diff($d2)->days + 1); $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 $capSql = "SELECT COALESCE(SUM(max_hours_per_day),0) AS somma_ore_giorno
FROM production_lines" FROM production_lines"
. (!empty($lineArr) ? " WHERE id IN (" . implode(',', $lineArr) . ")" : ""); . (!empty($lineArr) ? " WHERE id IN (" . implode(',', $lineArr) . ")" : "");
$capOreGiorno = (float)$pdo->query($capSql)->fetchColumn(); $capOreGiorno = (float)$pdo->query($capSql)->fetchColumn();
$capacitaTeoricaS = $capOreGiorno * 3600 * $giorniRange; $capacitaTeoricaS = $capOreGiorno * 3600 * $giorniRange;
$saturazione = ($capacitaTeoricaS > 0) $saturazione = ($capacitaTeoricaS > 0) ? round($kpi['tot_occupato_s'] / $capacitaTeoricaS * 100, 1) : 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, $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.tempo_totale_produzione),0) AS occupato_s,
COALESCE(SUM(p.seconds_pause),0) AS pause_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; $row['eff_tempo'] = ($row['occupato_s'] > 0) ? round($row['netto_s'] / $row['occupato_s'] * 100, 1) : 0;
$den = $row['kg'] + $row['scarto']; $den = $row['kg'] + $row['scarto'];
$row['scarto_perc'] = ($den > 0) ? round($row['scarto'] / $den * 100, 2) : 0; $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; $capLineaS = (float)$row['max_hours_per_day'] * 3600 * $giorniRange;
$row['saturazione'] = ($capLineaS > 0) ? round($row['occupato_s'] / $capLineaS * 100, 1) : 0; $row['saturazione'] = ($capLineaS > 0) ? round($row['occupato_s'] / $capLineaS * 100, 1) : 0;
} }
unset($row); unset($row);
/* ---------- SERIE TEMPORALE (granularità) ---------- */
if ($granul === 'month') { if ($granul === 'month') {
$periodExpr = "DATE_FORMAT(p.Data, '%Y-%m')"; $periodExpr = "DATE_FORMAT(p.Data, '%Y-%m')";
} else { } else {
@@ -119,8 +108,7 @@ if (!empty($_GET['ajax'])) {
COALESCE(SUM(p.seconds_pause),0) AS pause_s, COALESCE(SUM(p.seconds_pause),0) AS pause_s,
COALESCE(SUM(p.seconds_produzione),0) AS netto_s, COALESCE(SUM(p.seconds_produzione),0) AS netto_s,
COALESCE(SUM(p.kgprod),0) AS kg, COALESCE(SUM(p.scarto),0) AS scarto COALESCE(SUM(p.kgprod),0) AS kg, COALESCE(SUM(p.scarto),0) AS scarto
FROM productiondata p WHERE $whereSql FROM productiondata p WHERE $whereSql GROUP BY periodo ORDER BY periodo";
GROUP BY periodo ORDER BY periodo";
$stmt = $pdo->prepare($sqlSerie); $stmt = $pdo->prepare($sqlSerie);
$stmt->execute($params); $stmt->execute($params);
$serie = $stmt->fetchAll(PDO::FETCH_ASSOC); $serie = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -131,7 +119,6 @@ if (!empty($_GET['ajax'])) {
} }
unset($s); unset($s);
/* ---------- PAUSE CATEGORIA ---------- */
$sqlCat = "SELECT pr.category AS category, $sqlCat = "SELECT pr.category AS category,
COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s
FROM production_pauses pp FROM production_pauses pp
@@ -147,7 +134,6 @@ if (!empty($_GET['ajax'])) {
else $unplannedS += (int)$c['durata_s']; else $unplannedS += (int)$c['durata_s'];
} }
/* ---------- DETTAGLIO ---------- */
$sqlDett = "SELECT p.id, p.Data, p.conferma_ordine, p.note_extra, $sqlDett = "SELECT p.id, p.Data, p.conferma_ordine, p.note_extra,
l.name AS linea, m.nome AS matrice, c.nome AS cliente, 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, 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 { .kpi-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -416,6 +403,7 @@ if (!empty($_GET['ajax'])) {
} }
.chart-card { .chart-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -431,6 +419,7 @@ if (!empty($_GET['ajax'])) {
color: #1e293b; color: #1e293b;
margin: 0 0 .8rem; margin: 0 0 .8rem;
font-size: .95rem; font-size: .95rem;
padding-right: 1.8rem;
} }
.chart-wrap { .chart-wrap {
@@ -439,6 +428,7 @@ if (!empty($_GET['ajax'])) {
} }
.table-card { .table-card {
position: relative;
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
padding: 1.1rem 1.2rem; padding: 1.1rem 1.2rem;
@@ -451,6 +441,7 @@ if (!empty($_GET['ajax'])) {
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
margin: 0 0 .8rem; margin: 0 0 .8rem;
padding-right: 1.8rem;
} }
table.stats-table { table.stats-table {
@@ -509,6 +500,70 @@ if (!empty($_GET['ajax'])) {
padding: 3rem; padding: 3rem;
color: #94a3b8; 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> </style>
</head> </head>
@@ -532,20 +587,17 @@ if (!empty($_GET['ajax'])) {
<div class="filters-bar"> <div class="filters-bar">
<div><label>Da (data)</label><input type="date" id="fDateFrom"></div> <div><label>Da (data)</label><input type="date" id="fDateFrom"></div>
<div><label>A (data)</label><input type="date" id="fDateTo"></div> <div><label>A (data)</label><input type="date" id="fDateTo"></div>
<div style="grid-column:span 2;"> <div style="grid-column:span 2;">
<label>Linee</label> <label>Linee</label>
<div class="line-buttons" id="lineButtons"> <div class="line-buttons" id="lineButtons">
<?php foreach ($linee as $l): ?> <?php foreach ($linee as $l): ?>
<button type="button" class="line-btn2 active" <button type="button" class="line-btn2 active" data-line="<?= (int)$l['id'] ?>"
data-line="<?= (int)$l['id'] ?>"
style="background:<?= htmlspecialchars($l['color'] ?: '#3b82f6') ?>;"> style="background:<?= htmlspecialchars($l['color'] ?: '#3b82f6') ?>;">
<?= htmlspecialchars($l['name']) ?> <?= htmlspecialchars($l['name']) ?>
</button> </button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div> </div>
<div><label>Matrice</label><select id="fMatrice"> <div><label>Matrice</label><select id="fMatrice">
<option value="">Tutte</option> <option value="">Tutte</option>
<?php foreach ($matrici as $m): ?><option value="<?= (int)$m['id'] ?>"><?= htmlspecialchars($m['nome']) ?></option><?php endforeach; ?> <?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> <option value="">Tutte</option>
<?php foreach ($mescole as $me): ?><option value="<?= (int)$me['id'] ?>"><?= htmlspecialchars($me['nome']) ?></option><?php endforeach; ?> <?php foreach ($mescole as $me): ?><option value="<?= (int)$me['id'] ?>"><?= htmlspecialchars($me['nome']) ?></option><?php endforeach; ?>
</select></div> </select></div>
<div> <div>
<label>Vista temporale</label> <label>Vista temporale</label>
<div class="gran-toggle"> <div class="gran-toggle">
@@ -562,7 +613,6 @@ if (!empty($_GET['ajax'])) {
<button type="button" class="gran-btn" data-gran="month">Mese</button> <button type="button" class="gran-btn" data-gran="month">Mese</button>
</div> </div>
</div> </div>
<div class="filters-actions"> <div class="filters-actions">
<button class="btn-apply" id="btnApply"><i class="bi bi-funnel"></i> Applica</button> <button class="btn-apply" id="btnApply"><i class="bi bi-funnel"></i> Applica</button>
<button class="btn-reset" id="btnReset">Reset</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="charts-grid" id="chartsGrid" style="display:none;">
<div class="chart-card wide"> <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> <h6 id="titSerie">Efficienza tempo nel periodo (%)</h6>
<div class="chart-wrap"><canvas id="chartSerie"></canvas></div> <div class="chart-wrap"><canvas id="chartSerie"></canvas></div>
</div> </div>
<div class="chart-card"> <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> <h6>Efficienza tempo per linea (%)</h6>
<div class="chart-wrap"><canvas id="chartEffLinea"></canvas></div> <div class="chart-wrap"><canvas id="chartEffLinea"></canvas></div>
</div> </div>
<div class="chart-card"> <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> <h6>Saturazione linea (% su capacità)</h6>
<div class="chart-wrap"><canvas id="chartSat"></canvas></div> <div class="chart-wrap"><canvas id="chartSat"></canvas></div>
</div> </div>
<div class="chart-card"> <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> <h6>Kg prodotti vs scarto per linea</h6>
<div class="chart-wrap"><canvas id="chartKgLinea"></canvas></div> <div class="chart-wrap"><canvas id="chartKgLinea"></canvas></div>
</div> </div>
<div class="chart-card"> <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> <h6>Scarto % per linea</h6>
<div class="chart-wrap"><canvas id="chartScartoLinea"></canvas></div> <div class="chart-wrap"><canvas id="chartScartoLinea"></canvas></div>
</div> </div>
</div> </div>
<div class="table-card" id="tableCard" style="display:none;"> <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> <h6>Dettaglio produzioni</h6>
<table class="stats-table"> <table class="stats-table">
<thead> <thead>
@@ -628,7 +684,6 @@ if (!empty($_GET['ajax'])) {
<?php include('jsinclude.php'); ?> <?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script> <script>
// il codice sotto usa jQuery (già caricato nel <head>) e Chart.js
function secToHms(s) { function secToHms(s) {
s = Math.max(0, parseInt(s || 0, 10)); s = Math.max(0, parseInt(s || 0, 10));
const h = String(Math.floor(s / 3600)).padStart(2, '0'), const h = String(Math.floor(s / 3600)).padStart(2, '0'),
@@ -665,17 +720,50 @@ if (!empty($_GET['ajax'])) {
charts[k] = null; charts[k] = null;
} }
} }
let currentGran = 'day'; 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) { function renderKpi(k) {
$('#kpiGrid').html(` $('#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">${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"><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 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"><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 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"><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 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"><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 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"><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 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 || [], serie = d.serie || [],
gran = d.granularity; gran = d.granularity;
const labels = serie.map(x => gran === 'month' ? fmtMonth(x.periodo) : fmtDateIt(x.periodo)); 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 (%)'); $('#titSerie').text(gran === 'month' ? 'Efficienza tempo per mese (%)' : 'Efficienza tempo per giorno (%)');
destroyChart('serie'); destroyChart('serie');
@@ -892,7 +979,6 @@ if (!empty($_GET['ajax'])) {
$('#fDateTo').val(today.toISOString().slice(0, 10)); $('#fDateTo').val(today.toISOString().slice(0, 10));
$('#fDateFrom').val(past.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() { $('#lineButtons').on('click', '.line-btn2', function() {
$(this).toggleClass('active'); $(this).toggleClass('active');
if ($('#lineButtons .line-btn2.active').length === 0) { if ($('#lineButtons .line-btn2.active').length === 0) {
@@ -900,15 +986,12 @@ if (!empty($_GET['ajax'])) {
} }
loadStats(); loadStats();
}); });
// Granularità
$('.gran-btn').on('click', function() { $('.gran-btn').on('click', function() {
$('.gran-btn').removeClass('active'); $('.gran-btn').removeClass('active');
$(this).addClass('active'); $(this).addClass('active');
currentGran = $(this).data('gran'); currentGran = $(this).data('gran');
loadStats(); loadStats();
}); });
$('#btnApply').on('click', loadStats); $('#btnApply').on('click', loadStats);
$('#btnReset').on('click', function() { $('#btnReset').on('click', function() {
$('#lineButtons .line-btn2').addClass('active'); $('#lineButtons .line-btn2').addClass('active');
@@ -921,6 +1004,25 @@ if (!empty($_GET['ajax'])) {
loadStats(); 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(); loadStats();
}); });
</script> </script>