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