diff --git a/db/migrations/20260915073019_make_pause_reason_nullable.php b/db/migrations/20260915073019_make_pause_reason_nullable.php
new file mode 100644
index 00000000..8ecbbece
--- /dev/null
+++ b/db/migrations/20260915073019_make_pause_reason_nullable.php
@@ -0,0 +1,24 @@
+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();
+ }
+}
diff --git a/db/migrations/20260915100659_add_stato_da_compilare_dati.php b/db/migrations/20260915100659_add_stato_da_compilare_dati.php
new file mode 100644
index 00000000..3f2dad73
--- /dev/null
+++ b/db/migrations/20260915100659_add_stato_da_compilare_dati.php
@@ -0,0 +1,22 @@
+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");
+ }
+}
diff --git a/public/userarea/pause_stats.php b/public/userarea/pause_stats.php
index 0e0ec27e..2d985c60 100644
--- a/public/userarea/pause_stats.php
+++ b/public/userarea/pause_stats.php
@@ -286,6 +286,7 @@ if (!empty($_GET['ajax'])) {
}
.kpi-card {
+ position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -339,6 +340,7 @@ if (!empty($_GET['ajax'])) {
}
.chart-card {
+ position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -354,6 +356,7 @@ if (!empty($_GET['ajax'])) {
color: #1e293b;
margin: 0 0 .8rem;
font-size: .95rem;
+ padding-right: 1.8rem;
}
.chart-wrap {
@@ -362,6 +365,7 @@ if (!empty($_GET['ajax'])) {
}
.table-card {
+ position: relative;
background: #fff;
border-radius: 1rem;
padding: 1.1rem 1.2rem;
@@ -374,6 +378,7 @@ if (!empty($_GET['ajax'])) {
font-weight: 700;
color: #1e293b;
margin: 0 0 .8rem;
+ padding-right: 1.8rem;
}
table.stats-table {
@@ -442,6 +447,69 @@ 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: #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;
+ }
@@ -496,28 +564,34 @@ if (!empty($_GET['ajax'])) {
+
?
Pareto causali — tempo perso e cumulata
+
?
Ripartizione tempo: pianificato vs non pianificato
+
?
Tempo perso per linea (ore)
+
?
Numero fermi per causale
+
?
Andamento fermi giornaliero (ore)
+
?
Dettaglio eventi di pausa
@@ -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 `? `;
+ }
+
function renderKpi(k) {
const pctProblemi = k.n_pause > 0 ? Math.round(k.n_problemi / k.n_pause * 100) : 0;
$('#kpiGrid').html(`
- Fermi totali
${k.n_pause}
${k.n_prod_con_pause} produzioni coinvolte
- Tempo perso
${secToHms(k.tot_s)}
totale fermi
- Durata media
${secToHms(k.media_s)}
max ${secToHms(k.max_s)}
- Problemi tecnici
${k.n_problemi}
${pctProblemi}% dei fermi
- Pianificato
${secToHms(k.planned_s)}
fermi previsti
- Non pianificato
${secToHms(k.unplanned_s)}
da ridurre
+ ${infoBtn('n_pause')}
Fermi totali
${k.n_pause}
${k.n_prod_con_pause} produzioni coinvolte
+ ${infoBtn('tot')}
Tempo perso
${secToHms(k.tot_s)}
totale fermi
+ ${infoBtn('media')}
Durata media
${secToHms(k.media_s)}
max ${secToHms(k.max_s)}
+ ${infoBtn('problemi')}
Problemi tecnici
${k.n_problemi}
${pctProblemi}% dei fermi
+ ${infoBtn('planned')}
Pianificato
${secToHms(k.planned_s)}
fermi previsti
+ ${infoBtn('unplanned')}
Non pianificato
${secToHms(k.unplanned_s)}
da ridurre
`);
}
@@ -802,6 +910,7 @@ if (!empty($_GET['ajax'])) {
$('#kpiGrid').html('Errore di comunicazione con il server.
');
});
}
+
$(function() {
const today = new Date(),
past = new Date();
@@ -823,6 +932,24 @@ if (!empty($_GET['ajax'])) {
$('#fDateFrom').val(past.toISOString().slice(0, 10));
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 = $('
');
+ $pop.html('' + title + ' ' + text);
+ $btn.after($pop);
+ });
+ $(document).on('click', function() {
+ $('.info-pop').remove();
+ });
+
loadStats();
});
diff --git a/public/userarea/production_da_compilare.php b/public/userarea/production_da_compilare.php
new file mode 100644
index 00000000..93dcd64e
--- /dev/null
+++ b/public/userarea/production_da_compilare.php
@@ -0,0 +1,620 @@
+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 ' Nessuna produzione da compilare.
';
+ 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 '';
+
+ echo '
';
+ echo '
' . htmlspecialchars($r['matrice'] ?? '—') . '
';
+ echo '
Da compilare
';
+ echo '
';
+
+ echo '
';
+ echo '
Linea ' . htmlspecialchars($r['linea'] ?? '—') . '
';
+ echo '
Cliente ' . htmlspecialchars($r['cliente'] ?? '—') . '
';
+ echo '
Fine produzione ' . $endTime . '
';
+ echo '
Tempo occupato ' . $occupato . '
';
+ echo '
Tempo pause ' . $pause . '
';
+ echo '
Tempo netto ' . $netto . '
';
+ echo '
';
+
+ if (!empty($r['mescole_list'])) {
+ echo '
Mescole: '
+ . htmlspecialchars($r['mescole_list']) . '
';
+ }
+ if (!empty($r['note_extra'])) {
+ echo '
Descrizione: '
+ . htmlspecialchars($r['note_extra']) . '
';
+ }
+
+ echo '
';
+ echo 'Compila dati ';
+ echo '
';
+
+ echo '
'; // .compile-card
+ }
+ exit;
+}
+?>
+
+
+
+
+
+
+
+ Dati da compilare - Produzione
+
+
+
+
+
+
+
+
+
+
+
+
Dati di produzione
+
Confronta i valori teorici e inserisci quelli reali:
+
+
+
+
+ Annulla
+ Conferma e invia a Qualità
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/userarea/production_line_view2.php b/public/userarea/production_line_view2.php
index 320bbdfc..2ff0cc7d 100644
--- a/public/userarea/production_line_view2.php
+++ b/public/userarea/production_line_view2.php
@@ -77,14 +77,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['start_production']))
// 3) Ripresa: da PAUSA (7) → PRODUZIONE (2) **senza toccare start_time**
} 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
- 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
ORDER BY start_pause DESC
LIMIT 1";
$stmtClose = $pdo->prepare($sqlClose);
- $stmtClose->execute(['id' => $id]);
+ $paramsClose = ['id' => $id];
+ if ($resumeReasonId > 0) $paramsClose['reason_id'] = $resumeReasonId;
+ $stmtClose->execute($paramsClose);
$sqlUpdate = "UPDATE productiondata
SET id_status = :status,
@@ -123,23 +142,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['pause_production']))
try {
- if ($reasonId <= 0) {
- echo json_encode(['success' => false, 'msg' => 'Motivo pausa non valido.']);
- exit;
+ // Motivo pausa ora OPZIONALE: se non lo sanno, salvano NULL e lo mettono al riprendi
+ if ($reasonId > 0) {
+ $sql = "SELECT is_problem FROM pause_reasons WHERE id = :id LIMIT 1";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute(['id' => $reasonId]);
+ $isProblem = $stmt->fetchColumn();
+
+ if ($isProblem === false) {
+ echo json_encode(['success' => false, 'msg' => 'Motivo pausa non trovato.']);
+ exit;
+ }
+ $isProblem = (int)$isProblem;
+ } else {
+ $reasonId = null; // nessun motivo scelto ora
+ $isProblem = 0;
}
-
- // Leggo se il motivo è "problema" oppure pausa normale
- $sql = "SELECT is_problem FROM pause_reasons WHERE id = :id LIMIT 1";
- $stmt = $pdo->prepare($sql);
- $stmt->execute(['id' => $reasonId]);
- $isProblem = $stmt->fetchColumn();
-
- if ($isProblem === false) {
- echo json_encode(['success' => false, 'msg' => 'Motivo pausa non trovato.']);
- exit;
- }
-
- $isProblem = (int)$isProblem;
// ❌ PRIMA – sbagliato per la tua logica:
// $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);
}
- // Stato destinazione
- $nextStatus = ($reason === 'problema') ? 8 : $statusQualita;
+ // "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;
+ }
// Update finale
$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 ---
if (!empty($_GET['mescole_by_matrice'])) {
header('Content-Type: application/json; charset=utf-8');
@@ -1497,17 +1542,32 @@ if (!empty($_GET['ajax'])) {
-
Motivo pausa:
-
- Seleziona un motivo
-
-
- = htmlspecialchars($pr['name']) ?>
-
-
-
+
+
+
+
+
+ Inserisco il motivo dopo (al riprendi)
+
+
+
+
+ Motivo pausa:
+
+ Seleziona un motivo
+
+
+ = htmlspecialchars($pr['name']) ?>
+
+
+
+
@@ -1579,8 +1639,10 @@ if (!empty($_GET['ajax'])) {
-
@@ -2183,7 +2245,20 @@ if (!empty($_GET['ajax'])) {
e.stopPropagation();
playBeep();
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 ---
@@ -2216,6 +2291,17 @@ if (!empty($_GET['ajax'])) {
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 ---
function showReasonModal(action, id) {
@@ -2226,6 +2312,10 @@ if (!empty($_GET['ajax'])) {
$('#noteContainer').hide();
$('#pauseReasonContainer').hide();
$('#pauseReason').val('');
+ // reset toggle "motivo dopo"
+ $('#pauseLater').prop('checked', false);
+ $('#pauseReasonSelectWrap').show();
+ $('#pauseReason').prop('disabled', false);
// Titolo e testo dinamici
switch (action) {
@@ -2235,8 +2325,22 @@ if (!empty($_GET['ajax'])) {
break;
case 'resume':
+ // Motivo già presente in pausa → solo conferma
$('#modalTitle').text('Ripresa 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;
case 'pause':
@@ -2244,6 +2348,7 @@ if (!empty($_GET['ajax'])) {
$('#modalQuestion').text('Seleziona il motivo della pausa:');
$('#modalOptions').hide(); // niente radio
$('#pauseReasonContainer').show(); // mostra tendina
+ $('#laterToggleWrap').show(); // toggle "motivo dopo" visibile
$('#noteContainer').show(); // nota opzionale / obbligatoria se problema
break;
@@ -2266,15 +2371,21 @@ if (!empty($_GET['ajax'])) {
// --- LOGICA PAUSA ---
if (action === 'pause') {
- const pauseReasonId = $('#pauseReason').val();
- if (!pauseReasonId) {
- alert('Seleziona un motivo di pausa.');
+ const later = $('#pauseLater').is(':checked');
+
+ // 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;
}
+ // Se "motivo dopo" → NULL (0), altrimenti l'id scelto
+ const pauseReasonId = later ? 0 : ($('#pauseReason').val() || 0);
+
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.');
return;
}
@@ -2304,10 +2415,24 @@ if (!empty($_GET['ajax'])) {
// Azioni specifiche
switch (action) {
case 'start':
- case 'resume':
startProduction(id);
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':
// Sia "fine" che "problema" aprono il modale dati finali.
// Il reason viene passato e conservato per il salvataggio.
@@ -2375,6 +2500,28 @@ if (!empty($_GET['ajax'])) {
$('#finalCancel').off('click').on('click', function() {
$('#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() {
@@ -2394,10 +2541,11 @@ if (!empty($_GET['ajax'])) {
// --- START PRODUZIONE ---
- function startProduction(id) {
+ function startProduction(id, reasonId) {
$.post('', {
start_production: 1,
- id: id
+ id: id,
+ reason_id: reasonId || 0
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore durante l\'avvio.');
diff --git a/public/userarea/production_stats.php b/public/userarea/production_stats.php
index 7fa65a7d..92562101 100644
--- a/public/userarea/production_stats.php
+++ b/public/userarea/production_stats.php
@@ -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;
+ }
@@ -532,20 +587,17 @@ if (!empty($_GET['ajax'])) {
Da (data)
A (data)
-
Linee
-
= htmlspecialchars($l['name']) ?>
-
Matrice
Tutte
= htmlspecialchars($m['nome']) ?>
@@ -554,7 +606,6 @@ if (!empty($_GET['ajax'])) {
Tutte
= htmlspecialchars($me['nome']) ?>
-
Vista temporale
@@ -562,7 +613,6 @@ if (!empty($_GET['ajax'])) {
Mese
-
Applica
Reset
@@ -575,28 +625,34 @@ if (!empty($_GET['ajax'])) {
+
?
Efficienza tempo nel periodo (%)
+
?
Efficienza tempo per linea (%)
+
?
Saturazione linea (% su capacità)
+
?
Kg prodotti vs scarto per linea
+
?
Dettaglio produzioni
@@ -628,7 +684,6 @@ if (!empty($_GET['ajax'])) {