diff --git a/db/migrations/20260904095755_add_category_to_pause_reasons.php b/db/migrations/20260904095755_add_category_to_pause_reasons.php new file mode 100644 index 00000000..82f8b1c2 --- /dev/null +++ b/db/migrations/20260904095755_add_category_to_pause_reasons.php @@ -0,0 +1,21 @@ +table('pause_reasons') + ->addColumn('category', 'enum', [ + 'values' => ['planned', 'unplanned'], + 'null' => false, + 'default' => 'unplanned', + 'after' => 'is_problem', + 'comment' => 'planned = fermo pianificato (cambio, pulizia, mensa); unplanned = guasto, mancanza materiale, qualità', + ]) + ->update(); + } +} diff --git a/db/migrations/20260904095814_add_daily_capacity_to_production_lines.php b/db/migrations/20260904095814_add_daily_capacity_to_production_lines.php new file mode 100644 index 00000000..784befdc --- /dev/null +++ b/db/migrations/20260904095814_add_daily_capacity_to_production_lines.php @@ -0,0 +1,22 @@ +table('production_lines') + ->addColumn('max_hours_per_day', 'decimal', [ + 'precision' => 4, + 'scale' => 2, + 'null' => false, + 'default' => 24.00, + 'after' => 'color', + 'comment' => 'Ore massime di disponibilita giornaliera della linea (default 24)', + ]) + ->update(); + } +} diff --git a/db/migrations/20260907130003_add_preferred_line_to_auth_users.php b/db/migrations/20260907130003_add_preferred_line_to_auth_users.php new file mode 100644 index 00000000..1e25d13d --- /dev/null +++ b/db/migrations/20260907130003_add_preferred_line_to_auth_users.php @@ -0,0 +1,26 @@ +table('auth_users') + ->addColumn('preferred_line_id', 'integer', [ + 'signed' => true, // production_lines.id è int(11) SIGNED → deve combaciare + 'limit' => 11, + 'null' => true, + 'default' => null, + 'after' => 'notify_webapp', + 'comment' => 'Linea di produzione preferita per la Line View (NULL = Tutte)', + ]) + ->addForeignKey('preferred_line_id', 'production_lines', 'id', [ + 'delete' => 'SET_NULL', + 'update' => 'NO_ACTION', + ]) + ->update(); + } +} diff --git a/db/seeds/PauseReasonsPlannedSeeder.php b/db/seeds/PauseReasonsPlannedSeeder.php new file mode 100644 index 00000000..18e3cf5d --- /dev/null +++ b/db/seeds/PauseReasonsPlannedSeeder.php @@ -0,0 +1,22 @@ + 'Cambio turno', 'is_problem' => 0, 'category' => 'planned'], + ['name' => 'Aggiunta / cambio mescola', 'is_problem' => 0, 'category' => 'planned'], + ['name' => 'Cambio produzione', 'is_problem' => 0, 'category' => 'planned'], + ['name' => 'Attrezzaggio / cambio stampo', 'is_problem' => 0, 'category' => 'planned'], + ['name' => 'Pulizia impianto', 'is_problem' => 0, 'category' => 'planned'], + ['name' => 'Manutenzione programmata', 'is_problem' => 0, 'category' => 'planned'], + ]; + + $this->table('pause_reasons')->insert($data)->saveData(); + } +} diff --git a/public/userarea/edit_linea.php b/public/userarea/edit_linea.php index cae8e68e..0a47889a 100644 --- a/public/userarea/edit_linea.php +++ b/public/userarea/edit_linea.php @@ -121,6 +121,13 @@ value=""> +
+ + + Capacità teorica giornaliera. Default 24 (h24). +
+
+ Capacità teorica giornaliera. Default 24 (h24). +
+
diff --git a/public/userarea/pause_stats.php b/public/userarea/pause_stats.php new file mode 100644 index 00000000..0e0ec27e --- /dev/null +++ b/public/userarea/pause_stats.php @@ -0,0 +1,831 @@ +getConnection(); + +$linee = $pdo->query("SELECT id, name, color FROM production_lines ORDER BY line_number")->fetchAll(); +$matrici = $pdo->query("SELECT id, nome FROM matrice ORDER BY nome")->fetchAll(); +$mescole = $pdo->query("SELECT id, nome FROM mescole ORDER BY nome")->fetchAll(); + +if (!empty($_GET['ajax'])) { + header('Content-Type: application/json'); + + $dateFrom = $_GET['date_from'] ?? ''; + $dateTo = $_GET['date_to'] ?? ''; + $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; + $mescId = isset($_GET['mescola_id']) && $_GET['mescola_id'] !== '' ? (int)$_GET['mescola_id'] : null; + + $where = ["pp.end_pause IS NOT NULL"]; + $params = []; + if ($dateFrom !== '') { + $where[] = "p.Data >= :date_from"; + $params['date_from'] = $dateFrom; + } + if ($dateTo !== '') { + $where[] = "p.Data <= :date_to"; + $params['date_to'] = $dateTo; + } + if (!empty($lineArr)) { + $where[] = "p.id_linea IN (" . implode(',', $lineArr) . ")"; + } + if ($matId !== null) { + $where[] = "p.idmatrice = :mat_id"; + $params['mat_id'] = $matId; + } + if ($mescId !== null) { + $where[] = "EXISTS (SELECT 1 FROM productiondata_mescole pm + WHERE pm.id_productiondata = p.id AND pm.id_mescola = :mesc_id)"; + $params['mesc_id'] = $mescId; + } + $whereSql = implode(' AND ', $where); + + try { + $sqlKpi = "SELECT + COUNT(*) AS n_pause, COUNT(DISTINCT pp.id_production) AS n_prod_con_pause, + COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS tot_s, + COALESCE(AVG(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS media_s, + COALESCE(MAX(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS max_s, + COALESCE(SUM(CASE WHEN pr.is_problem=1 THEN 1 ELSE 0 END),0) AS n_problemi, + COALESCE(SUM(CASE WHEN pr.category='planned' THEN TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause) ELSE 0 END),0) AS planned_s, + COALESCE(SUM(CASE WHEN pr.category='unplanned' THEN TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause) ELSE 0 END),0) AS unplanned_s + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE $whereSql"; + $stmt = $pdo->prepare($sqlKpi); + $stmt->execute($params); + $kpi = $stmt->fetch(PDO::FETCH_ASSOC); + + $sqlMotivo = "SELECT pr.id, pr.name AS motivo, pr.is_problem, pr.category, + COUNT(pp.id) AS n_pause, + COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s, + COALESCE(AVG(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS media_s + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE $whereSql GROUP BY pr.id ORDER BY durata_s DESC"; + $stmt = $pdo->prepare($sqlMotivo); + $stmt->execute($params); + $perMotivo = $stmt->fetchAll(PDO::FETCH_ASSOC); + $totDurata = array_sum(array_map(fn($x) => (int)$x['durata_s'], $perMotivo)); + $cum = 0; + foreach ($perMotivo as &$m) { + $cum += (int)$m['durata_s']; + $m['cum_perc'] = ($totDurata > 0) ? round($cum / $totDurata * 100, 1) : 0; + $m['perc'] = ($totDurata > 0) ? round($m['durata_s'] / $totDurata * 100, 1) : 0; + } + unset($m); + + $sqlLinea = "SELECT l.name AS linea, l.color, + COUNT(pp.id) AS n_pause, + COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s, + COALESCE(SUM(CASE WHEN pr.is_problem=1 THEN 1 ELSE 0 END),0) AS n_problemi + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + JOIN production_lines l ON l.id = p.id_linea + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE $whereSql GROUP BY l.id ORDER BY durata_s DESC"; + $stmt = $pdo->prepare($sqlLinea); + $stmt->execute($params); + $perLinea = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $sqlGiorno = "SELECT p.Data AS giorno, + COUNT(pp.id) AS n_pause, + COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE $whereSql GROUP BY p.Data ORDER BY p.Data"; + $stmt = $pdo->prepare($sqlGiorno); + $stmt->execute($params); + $perGiorno = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $sqlDett = "SELECT pp.id, pp.start_pause, pp.end_pause, pp.note, + TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause) AS durata_s, + pr.name AS motivo, pr.is_problem, pr.category, + l.name AS linea, m.nome AS matrice, p.id AS prod_id + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + LEFT JOIN production_lines l ON l.id = p.id_linea + LEFT JOIN matrice m ON m.id = p.idmatrice + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE $whereSql ORDER BY pp.start_pause DESC LIMIT 500"; + $stmt = $pdo->prepare($sqlDett); + $stmt->execute($params); + $dettaglio = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode([ + 'success' => true, + 'kpi' => [ + 'n_pause' => (int)$kpi['n_pause'], + 'n_prod_con_pause' => (int)$kpi['n_prod_con_pause'], + 'tot_s' => (int)$kpi['tot_s'], + 'media_s' => (int)$kpi['media_s'], + 'max_s' => (int)$kpi['max_s'], + 'n_problemi' => (int)$kpi['n_problemi'], + 'planned_s' => (int)$kpi['planned_s'], + 'unplanned_s' => (int)$kpi['unplanned_s'], + ], + 'per_motivo' => $perMotivo, + 'per_linea' => $perLinea, + 'per_giorno' => $perGiorno, + 'dettaglio' => $dettaglio, + ]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'msg' => $e->getMessage()]); + } + exit; +} +?> + + + + + + + Statistiche Pause & Fermi + + + + + + + +
+ + + +
+
+ +
+

Statistiche Pause & Fermi

+ +
+ +
+
+
+
+ +
+ + + +
+
+
+
+
+ + +
+
+ +
+
Caricamento…
+
+ + + + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/public/userarea/photos/14-18-1788520038.jpg b/public/userarea/photos/14-18-1788520038.jpg new file mode 100644 index 00000000..5a6d80f7 Binary files /dev/null and b/public/userarea/photos/14-18-1788520038.jpg differ diff --git a/public/userarea/production_dashboard.php b/public/userarea/production_dashboard.php index 4c376596..ec3dde58 100644 --- a/public/userarea/production_dashboard.php +++ b/public/userarea/production_dashboard.php @@ -23,12 +23,42 @@ $dashboardSections = [ 'permission' => 'production.line_view.view', ], [ - 'label' => 'Statistiche', + 'label' => 'Magazzino', + 'icon' => '📦', + 'class' => 'btn-magazzino', + 'url' => 'warehouse_dashboard.php', + 'permission' => 'warehouse.dashboard.view', + ], + [ + 'label' => 'Smart-Alert', + 'icon' => '⏰', + 'class' => 'btn-scadenziario', + 'url' => 'scadenzario/index.php', + 'permission' => 'deadlines.view', + ], + ], + ], + [ + 'id' => 'secDirezione', + 'title' => 'Direzione', + 'subtitle' => 'Statistiche, analisi fermi e strumenti di supervisione', + 'icon' => '📊', + 'open' => false, + 'buttons' => [ + [ + 'label' => 'Statistiche Produzione', 'icon' => '📈', 'class' => 'btn-statistiche', 'url' => 'production_stats.php', 'permission' => 'production.stats.view', ], + [ + 'label' => 'Pause & Fermi', + 'icon' => '🛑', + 'class' => 'btn-pause-stats', + 'url' => 'pause_stats.php', + 'permission' => 'production.stats.view', + ], [ 'label' => 'Manager', 'icon' => '👔', @@ -43,20 +73,6 @@ $dashboardSections = [ 'url' => 'manager_stats.php', 'permission' => 'production.manager_stats.view', ], - [ - 'label' => 'Magazzino', - 'icon' => '📦', - 'class' => 'btn-magazzino', - 'url' => 'warehouse_dashboard.php', - 'permission' => 'warehouse.dashboard.view', - ], - [ - 'label' => 'Smart-Alert', - 'icon' => '⏰', - 'class' => 'btn-scadenziario', - 'url' => 'scadenzario/index.php', - 'permission' => 'deadlines.view', - ], ], ], [ @@ -421,6 +437,11 @@ $dashboardSections = [ background: linear-gradient(135deg, #d2f7d9, #e7ffea); } + .btn-pause-stats { + background: linear-gradient(135deg, #fca5a5, #fecaca); + color: #7f1d1d; + } + .btn-magazzino { background: linear-gradient(135deg, #b9e3ffff, #d7f1ffff); } diff --git a/public/userarea/production_line_view2.php b/public/userarea/production_line_view2.php index 142759c2..51c91434 100644 --- a/public/userarea/production_line_view2.php +++ b/public/userarea/production_line_view2.php @@ -26,6 +26,10 @@ $statusPausaId = $statusPausa['id']; // --- PAUSE REASONS --- $pauseReasons = $pdo->query("SELECT id, name, is_problem FROM pause_reasons ORDER BY name")->fetchAll(); +// --- DATI PER MODALE "NUOVO" (riga libera) --- +$allMatrici = $pdo->query("SELECT id, nome FROM matrice ORDER BY nome")->fetchAll(); +$allMescole = $pdo->query("SELECT id, nome FROM mescole WHERE is_active = 1 ORDER BY nome")->fetchAll(); + // --- AVVIO / RIPRESA PRODUZIONE --- if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['start_production'])) { @@ -62,6 +66,15 @@ 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 + $sqlClose = "UPDATE production_pauses + SET end_pause = UTC_TIMESTAMP() + WHERE id_production = :id AND end_pause IS NULL + ORDER BY start_pause DESC + LIMIT 1"; + $stmtClose = $pdo->prepare($sqlClose); + $stmtClose->execute(['id' => $id]); + $sqlUpdate = "UPDATE productiondata SET id_status = :status, end_time = NULL @@ -162,36 +175,61 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['stop_production'])) $reason = $_POST['reason'] ?? 'fine'; $note = trim($_POST['note'] ?? ''); try { + $pdo->beginTransaction(); + + // Se sto stoppando mentre è in pausa, chiudo la pausa aperta + $sqlClose = "UPDATE production_pauses + SET end_pause = UTC_TIMESTAMP() + WHERE id_production = :id AND end_pause IS NULL"; + $pdo->prepare($sqlClose)->execute(['id' => $id]); + + // Leggo dati produzione $sql = "SELECT start_time, id_status FROM productiondata WHERE id = :id"; $stmt = $pdo->prepare($sql); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); - $seconds = 0; - if ($row && $row['start_time'] && $row['id_status'] == $statusProduzioneId) { + + // Tempo di occupazione linea (LORDO, pause incluse): start -> ora + $occupatoSec = 0; + if ($row && $row['start_time']) { $start = new DateTime($row['start_time'], new DateTimeZone('UTC')); $now = new DateTime('now', new DateTimeZone('UTC')); - $seconds = $now->getTimestamp() - $start->getTimestamp(); + $occupatoSec = max(0, $now->getTimestamp() - $start->getTimestamp()); } + // Somma totale delle pause di questa produzione (in secondi) + $sqlPause = "SELECT COALESCE(SUM(TIMESTAMPDIFF(SECOND, start_pause, end_pause)), 0) + FROM production_pauses + WHERE id_production = :id AND end_pause IS NOT NULL"; + $stmtP = $pdo->prepare($sqlPause); + $stmtP->execute(['id' => $id]); + $pauseSec = (int)$stmtP->fetchColumn(); + // Stato di destinazione $nextStatus = ($reason === 'problema') ? 8 : $statusQualita; // 8 = Problema in produzione $sql = "UPDATE productiondata - SET id_status = :status, - end_time = UTC_TIMESTAMP(), - note_operatore = :note - WHERE id = :id AND id_status IN ($statusProduzioneId, $statusPausaId)"; + SET id_status = :status, + end_time = UTC_TIMESTAMP(), + note_operatore = :note, + tempo_totale_produzione = :occupato, + seconds_pause = :pause, + seconds_produzione = :netto + WHERE id = :id AND id_status IN ($statusProduzioneId, $statusPausaId)"; $stmt = $pdo->prepare($sql); $stmt->execute([ - 'status' => $nextStatus, - 'seconds' => $seconds, - 'note' => $note ?: null, - 'id' => $id, - 'produzione' => $statusProduzioneId, - 'pausa' => $statusPausaId + 'status' => $nextStatus, + 'note' => $note ?: null, + 'occupato' => $occupatoSec, + 'pause' => $pauseSec, + 'netto' => max(0, $occupatoSec - $pauseSec), + 'id' => $id, ]); + + $pdo->commit(); echo json_encode(['success' => true]); } catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); echo json_encode(['success' => false, 'msg' => $e->getMessage()]); } exit; @@ -208,11 +246,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data'])) $reason = $_POST['reason'] ?? 'fine'; try { - // Recupero tempo totale già calcolato - $sql = "SELECT tempo_totale_produzione FROM productiondata WHERE id = :id"; - $stmt = $pdo->prepare($sql); + $pdo->beginTransaction(); + + // Chiudo eventuali pause ancora aperte + $pdo->prepare("UPDATE production_pauses SET end_pause = UTC_TIMESTAMP() + WHERE id_production = :id AND end_pause IS NULL") + ->execute(['id' => $id]); + + // Leggo start_time per calcolare l'occupazione + $stmt = $pdo->prepare("SELECT start_time FROM productiondata WHERE id = :id"); $stmt->execute(['id' => $id]); - $seconds = (int)$stmt->fetchColumn(); + $startTime = $stmt->fetchColumn(); + + // Occupazione LORDA (start -> ora), pause incluse + $occupatoSec = 0; + if ($startTime) { + $start = new DateTime($startTime, new DateTimeZone('UTC')); + $now = new DateTime('now', new DateTimeZone('UTC')); + $occupatoSec = max(0, $now->getTimestamp() - $start->getTimestamp()); + } + + // Somma pause chiuse + $stmtP = $pdo->prepare("SELECT COALESCE(SUM(TIMESTAMPDIFF(SECOND, start_pause, end_pause)), 0) + FROM production_pauses + WHERE id_production = :id AND end_pause IS NOT NULL"); + $stmtP->execute(['id' => $id]); + $pauseSec = (int)$stmtP->fetchColumn(); + + $seconds = $occupatoSec; // hourprod = tempo di occupazione (come da tua logica) // Calcolo scarto % $totaleKg = $kgprod + $scarto; @@ -227,14 +288,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data'])) // Update finale $sql = "UPDATE productiondata - SET kgprod = :kgprod, - mtprod = :mtprod, - scarto = :scarto, - scarto_perc = :scarto_perc, - note_operatore = :note, - hourprod = SEC_TO_TIME(:seconds), - id_status = :status, - end_time = COALESCE(end_time, UTC_TIMESTAMP()) + SET kgprod = :kgprod, + mtprod = :mtprod, + scarto = :scarto, + scarto_perc = :scarto_perc, + note_operatore = :note, + hourprod = SEC_TO_TIME(:seconds), + tempo_totale_produzione = :occupato, + seconds_pause = :pause, + seconds_produzione = :netto, + id_status = :status, + end_time = COALESCE(end_time, UTC_TIMESTAMP()) WHERE id = :id"; $stmt = $pdo->prepare($sql); @@ -245,12 +309,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data'])) 'scarto_perc' => $scartoPerc, 'note' => $note, 'seconds' => $seconds, + 'occupato' => $occupatoSec, + 'pause' => $pauseSec, + 'netto' => max(0, $occupatoSec - $pauseSec), 'status' => $nextStatus, 'id' => $id ]); + $pdo->commit(); echo json_encode(['success' => true]); } catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); echo json_encode(['success' => false, 'msg' => $e->getMessage()]); } exit; @@ -258,12 +327,91 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data'])) +// --- CREAZIONE RIGA LIBERA (Nuovo) + avvio immediato --- +if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['create_quick'])) { + $idMatrice = (int)($_POST['id_matrice'] ?? 0); + $idLinea = (int)($_POST['id_linea'] ?? 0); + $mescRaw = $_POST['id_mescole'] ?? ''; + $mescIds = $mescRaw !== '' ? array_values(array_unique(array_filter(array_map('intval', explode(',', $mescRaw))))) : []; + $descr = trim($_POST['descrizione'] ?? ''); + + try { + if ($idMatrice <= 0) { + echo json_encode(['success' => false, 'msg' => 'Seleziona una matrice.']); + exit; + } + if ($idLinea <= 0) { + echo json_encode(['success' => false, 'msg' => 'Seleziona una linea.']); + exit; + } + + $avvia = !empty($_POST['avvia']); + + $pdo->beginTransaction(); + + // Priorità: in fondo alla coda della linea (max+1) + $stmtPri = $pdo->prepare("SELECT COALESCE(MAX(priority),0)+1 + FROM productiondata + WHERE id_linea = :linea AND id_status = :programmato"); + $stmtPri->execute(['linea' => $idLinea, 'programmato' => $statusProgrammatoId]); + $nextPriority = (int)$stmtPri->fetchColumn(); + + // Se "avvia": nasce in PRODUZIONE (2) con start_time. + // Altrimenti: PROGRAMMATO (6), pronto per essere avviato dopo. + $statusIns = $avvia ? $statusProduzioneId : $statusProgrammatoId; + $startExpr = $avvia ? 'UTC_TIMESTAMP()' : 'NULL'; + + $sqlIns = "INSERT INTO productiondata + (Data, idmatrice, id_linea, note_extra, priority, + id_status, start_time, end_time, + fermomacchina, hour, hourprod, kgprod, mtprod, scarto) + VALUES + (CURDATE(), :idmatrice, :id_linea, :note_extra, :priority, + :status, $startExpr, NULL, + 0, '00:00:00', '00:00:00', 0, 0, 0)"; + $stmtIns = $pdo->prepare($sqlIns); + $stmtIns->execute([ + 'idmatrice' => $idMatrice, + 'id_linea' => $idLinea, + 'note_extra' => $descr ?: null, + 'priority' => $nextPriority, + 'status' => $statusIns, + ]); + + $newId = (int)$pdo->lastInsertId(); + + if (!empty($mescIds)) { + $stmtM = $pdo->prepare("INSERT INTO productiondata_mescole (id_productiondata, id_mescola) VALUES (:pid, :mid)"); + foreach ($mescIds as $mid) { + if ($mid > 0) { + $stmtM->execute(['pid' => $newId, 'mid' => $mid]); + } + } + } + + $pdo->commit(); + echo json_encode(['success' => true, 'id' => $newId]); + } catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'msg' => $e->getMessage()]); + } + exit; +} + + // --- AJAX PRINCIPALE: carica i record della data selezionata --- if (!empty($_GET['ajax'])) { - $lineRaw = $_GET['line'] ?? ''; $lineArray = $lineRaw !== '' ? explode(',', $lineRaw) : []; + // Linee già occupate da una produzione attiva (in produzione=2 o in pausa=7). + // Usato per bloccare l'avvio di una seconda produzione sulla stessa linea. + $lineeOccupate = []; + $stmtOcc = $pdo->query("SELECT DISTINCT id_linea FROM productiondata WHERE id_status IN (2, 7)"); + foreach ($stmtOcc->fetchAll(PDO::FETCH_COLUMN) as $lid) { + $lineeOccupate[(int)$lid] = true; + } + // --- RECORD IN PRODUZIONE (2,7,8) // --- RECORD IN PRODUZIONE (2,7,8) $sql = "SELECT @@ -396,6 +544,53 @@ if (!empty($_GET['ajax'])) { Linea Produzione - Tablet + + + + + +
+ + + +
+
+ +
+

Statistiche Produzione

+ +
+ +
+
+
+ +
+ +
+ + + +
+
+ +
+
+ +
+ +
+ + +
+
+ +
+ + +
+
+ +
+
Caricamento…
+
+ + + + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/public/userarea/render_production_card.php b/public/userarea/render_production_card.php index 8e567bce..a8346654 100644 --- a/public/userarea/render_production_card.php +++ b/public/userarea/render_production_card.php @@ -81,6 +81,13 @@ if ($matricePhoto) { + +
+ Descrizione + +
+ +
Linea
Matrice
@@ -258,9 +265,22 @@ if ($matricePhoto) {
- + + + + + + 24) $maxHours = 24; if (!$lineNumber || !$name) { echo json_encode(['success' => false, 'message' => 'Numero linea e nome sono obbligatori.']); @@ -19,10 +22,10 @@ try { } $stmt = $pdo->prepare(" - INSERT INTO production_lines (line_number, name, model, brand, status, color) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO production_lines (line_number, name, model, brand, status, color, max_hours_per_day) + VALUES (?, ?, ?, ?, ?, ?, ?) "); - $stmt->execute([$lineNumber, $name, $model, $brand, $status, $color]); + $stmt->execute([$lineNumber, $name, $model, $brand, $status, $color, $maxHours]); echo json_encode(['success' => true]); } catch (Exception $e) { diff --git a/public/userarea/stats.php b/public/userarea/stats.php new file mode 100644 index 00000000..9dedcbc2 --- /dev/null +++ b/public/userarea/stats.php @@ -0,0 +1,300 @@ +getConnection(); + +/* 2=produzione, 3=qualità(fine ok), 7=pausa, 8=problema. "Concluse"=3,8 */ +$STATI_CONCLUSI = [3, 8]; + +$linee = $pdo->query("SELECT id, name FROM production_lines ORDER BY line_number")->fetchAll(); +$matrici = $pdo->query("SELECT id, nome FROM matrice ORDER BY nome")->fetchAll(); +$mescole = $pdo->query("SELECT id, nome FROM mescole ORDER BY nome")->fetchAll(); + +if (!empty($_GET['ajax'])) { + header('Content-Type: application/json'); + + $dateFrom = $_GET['date_from'] ?? ''; + $dateTo = $_GET['date_to'] ?? ''; + $lineId = isset($_GET['line_id']) && $_GET['line_id'] !== '' ? (int)$_GET['line_id'] : null; + $matId = isset($_GET['matrice_id']) && $_GET['matrice_id'] !== '' ? (int)$_GET['matrice_id'] : null; + $mescId = isset($_GET['mescola_id']) && $_GET['mescola_id'] !== '' ? (int)$_GET['mescola_id'] : null; + + $where = ["p.id_status IN (" . implode(',', $STATI_CONCLUSI) . ")"]; + $params = []; + if ($dateFrom !== '') { $where[] = "p.Data >= :date_from"; $params['date_from'] = $dateFrom; } + if ($dateTo !== '') { $where[] = "p.Data <= :date_to"; $params['date_to'] = $dateTo; } + if ($lineId !== null) { $where[] = "p.id_linea = :line_id"; $params['line_id'] = $lineId; } + if ($matId !== null) { $where[] = "p.idmatrice = :mat_id"; $params['mat_id'] = $matId; } + if ($mescId !== null) { + $where[] = "EXISTS (SELECT 1 FROM productiondata_mescole pm + WHERE pm.id_productiondata = p.id AND pm.id_mescola = :mesc_id)"; + $params['mesc_id'] = $mescId; + } + $whereSql = implode(' AND ', $where); + + try { + $sqlKpi = "SELECT + COUNT(*) AS n_produzioni, + COALESCE(SUM(p.tempo_totale_produzione),0) AS tot_occupato_s, + COALESCE(SUM(p.seconds_pause),0) AS tot_pause_s, + COALESCE(SUM(p.seconds_produzione),0) AS tot_netto_s, + COALESCE(SUM(p.kgprod),0) AS tot_kg, + COALESCE(SUM(p.mtprod),0) AS tot_mt, + COALESCE(SUM(p.scarto),0) AS tot_scarto + FROM productiondata p WHERE $whereSql"; + $stmt = $pdo->prepare($sqlKpi); $stmt->execute($params); + $kpi = $stmt->fetch(PDO::FETCH_ASSOC); + + $effTempo = ($kpi['tot_occupato_s'] > 0) ? round($kpi['tot_netto_s'] / $kpi['tot_occupato_s'] * 100, 1) : 0; + $denomScarto = $kpi['tot_kg'] + $kpi['tot_scarto']; + $scartoPercTot = ($denomScarto > 0) ? round($kpi['tot_scarto'] / $denomScarto * 100, 2) : 0; + + $sqlLinea = "SELECT l.id, l.name AS linea, 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, + 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 JOIN production_lines l ON l.id = p.id_linea + WHERE $whereSql GROUP BY l.id ORDER BY l.line_number"; + $stmt = $pdo->prepare($sqlLinea); $stmt->execute($params); + $perLinea = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($perLinea as &$row) { + $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; + } unset($row); + + $sqlGiorno = "SELECT p.Data AS giorno, + COALESCE(SUM(p.tempo_totale_produzione),0) AS occupato_s, + 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 p.Data ORDER BY p.Data"; + $stmt = $pdo->prepare($sqlGiorno); $stmt->execute($params); + $perGiorno = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($perGiorno as &$g) { + $g['eff_tempo'] = ($g['occupato_s'] > 0) ? round($g['netto_s'] / $g['occupato_s'] * 100, 1) : 0; + $den = $g['kg'] + $g['scarto']; + $g['scarto_perc'] = ($den > 0) ? round($g['scarto'] / $den * 100, 2) : 0; + } unset($g); + + $sqlCat = "SELECT pr.category AS category, + COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)),0) AS durata_s + FROM production_pauses pp + JOIN productiondata p ON p.id = pp.id_production + JOIN pause_reasons pr ON pr.id = pp.reason_id + WHERE pp.end_pause IS NOT NULL AND $whereSql GROUP BY pr.category"; + $stmt = $pdo->prepare($sqlCat); $stmt->execute($params); + $plannedS = 0; $unplannedS = 0; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $c) { + if ($c['category'] === 'planned') $plannedS = (int)$c['durata_s']; else $unplannedS += (int)$c['durata_s']; + } + + $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, + p.kgprod, p.mtprod, p.scarto, p.scarto_perc, p.id_status, + (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 + FROM productiondata p + LEFT JOIN production_lines l ON l.id = p.id_linea + LEFT JOIN matrice m ON m.id = p.idmatrice + LEFT JOIN clients c ON c.id = p.id_cliente + WHERE $whereSql ORDER BY p.Data DESC, p.id DESC LIMIT 500"; + $stmt = $pdo->prepare($sqlDett); $stmt->execute($params); + $dettaglio = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode([ + 'success' => true, + 'kpi' => [ + 'n_produzioni'=>(int)$kpi['n_produzioni'],'tot_occupato_s'=>(int)$kpi['tot_occupato_s'], + 'tot_pause_s'=>(int)$kpi['tot_pause_s'],'tot_netto_s'=>(int)$kpi['tot_netto_s'], + 'tot_kg'=>(float)$kpi['tot_kg'],'tot_mt'=>(float)$kpi['tot_mt'],'tot_scarto'=>(float)$kpi['tot_scarto'], + 'eff_tempo'=>$effTempo,'scarto_perc_tot'=>$scartoPercTot,'planned_s'=>$plannedS,'unplanned_s'=>$unplannedS, + ], + 'per_linea'=>$perLinea,'per_giorno'=>$perGiorno,'dettaglio'=>$dettaglio, + ]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'msg' => $e->getMessage()]); + } + exit; +} +?> + + + + + + Statistiche Produzione + + + + + + + + +
+

Statistiche Produzione

+ +
+ +
+
+
+
+
+
+
+ + +
+
+ +
Caricamento…
+ + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/update_linea.php b/public/userarea/update_linea.php index f01835de..fa7be282 100644 --- a/public/userarea/update_linea.php +++ b/public/userarea/update_linea.php @@ -18,6 +18,9 @@ try { $brand = trim($_POST['brand'] ?? ''); $status = trim($_POST['status'] ?? ''); $color = trim($_POST['color'] ?? '#dc2626'); + $maxHours = isset($_POST['max_hours_per_day']) ? (float)$_POST['max_hours_per_day'] : 24; + if ($maxHours < 0) $maxHours = 0; + if ($maxHours > 24) $maxHours = 24; if ($lineNumber === '' || $lineName === '') { echo json_encode(['success' => false, 'message' => 'Numero linea e nome sono obbligatori.']); @@ -26,24 +29,26 @@ try { $sql = " UPDATE production_lines - SET line_number = :line_number, - name = :name, - model = :model, - brand = :brand, - status = :status, - color = :color + SET line_number = :line_number, + name = :name, + model = :model, + brand = :brand, + status = :status, + color = :color, + max_hours_per_day = :max_hours_per_day WHERE id = :id "; $stmt = $pdo->prepare($sql); $stmt->execute([ - ':line_number' => $lineNumber, - ':name' => $lineName, - ':model' => $model, - ':brand' => $brand, - ':status' => $status, - ':color' => $color, - ':id' => $id + ':line_number' => $lineNumber, + ':name' => $lineName, + ':model' => $model, + ':brand' => $brand, + ':status' => $status, + ':color' => $color, + ':max_hours_per_day' => $maxHours, + ':id' => $id ]); echo json_encode(['success' => true]);