Files
zibo-dashboard/public/userarea/production_line_view2.php
T
2026-10-21 10:03:59 +02:00

3018 lines
109 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// --- LISTE LINEE ---
$linee = $pdo->query("SELECT id, name, color FROM production_lines ORDER BY line_number")->fetchAll();
// --- LINEA PREFERITA DELL'UTENTE (NULL = Tutte) ---
$preferredLineId = null;
if (!empty($iduserlogin)) {
$stmtPref = $pdo->prepare("SELECT preferred_line_id FROM auth_users WHERE id = :uid LIMIT 1");
$stmtPref->execute(['uid' => (int)$iduserlogin]);
$val = $stmtPref->fetchColumn();
if ($val !== false && $val !== null) {
$preferredLineId = (int)$val;
}
}
// --- STATUS DINAMICI ---
$statusProgrammato = $pdo->query("SELECT id, nome, badge_color, line_color FROM production_status WHERE id = 6 LIMIT 1")->fetch();
$statusProduzione = $pdo->query("SELECT id, nome, badge_color, line_color FROM production_status WHERE id = 2 LIMIT 1")->fetch();
$statusPausa = $pdo->query("SELECT id, nome, badge_color, line_color FROM production_status WHERE id = 7 LIMIT 1")->fetch();
$statusQualita = $pdo->query("SELECT id FROM production_status WHERE id = 3 LIMIT 1")->fetchColumn();
if (!$statusProgrammato || !$statusProduzione || !$statusPausa || !$statusQualita) {
die("Errore: uno o più status non trovati in production_status");
}
$statusProgrammatoId = $statusProgrammato['id'];
$statusProduzioneId = $statusProduzione['id'];
$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'])) {
$id = (int)$_POST['id'];
try {
// 1) Leggo stato attuale
$sql = "SELECT id_status FROM productiondata WHERE id = :id LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $id]);
$currentStatus = $stmt->fetchColumn();
if ($currentStatus === false) {
echo json_encode(['success' => false, 'msg' => 'Record produzione non trovato.']);
exit;
}
// 2) Primo avvio: da PROGRAMMATO (6) → PRODUZIONE (2) + set start_time
if ((int)$currentStatus === (int)$statusProgrammatoId) {
$sqlUpdate = "UPDATE productiondata
SET id_status = :status,
start_time = UTC_TIMESTAMP(),
end_time = NULL
WHERE id = :id AND id_status = :programmato";
$stmtUpdate = $pdo->prepare($sqlUpdate);
$stmtUpdate->execute([
'status' => $statusProduzioneId,
'id' => $id,
'programmato' => $statusProgrammatoId
]);
// 3) Ripresa: da PAUSA (7) → PRODUZIONE (2) **senza toccare start_time**
} elseif ((int)$currentStatus === (int)$statusPausaId) {
// Il motivo è obbligatorio alla ripresa: lo ricevo dal form
$resumeReasonId = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
// Verifico se la pausa aperta ha già un motivo
$stmtChk = $pdo->prepare("SELECT reason_id FROM production_pauses
WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC LIMIT 1");
$stmtChk->execute(['id' => $id]);
$existingReason = $stmtChk->fetchColumn();
// Se manca il motivo (sia sulla pausa che nel form) → blocco
if (($existingReason === null || $existingReason === false) && $resumeReasonId <= 0) {
echo json_encode(['success' => false, 'msg' => 'Seleziona il motivo della pausa prima di riprendere.']);
exit;
}
// Chiudo l'ultima pausa aperta + scrivo il motivo se fornito ora
$sqlClose = "UPDATE production_pauses
SET end_pause = UTC_TIMESTAMP()"
. ($resumeReasonId > 0 ? ", reason_id = :reason_id" : "") . "
WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC
LIMIT 1";
$stmtClose = $pdo->prepare($sqlClose);
$paramsClose = ['id' => $id];
if ($resumeReasonId > 0) $paramsClose['reason_id'] = $resumeReasonId;
$stmtClose->execute($paramsClose);
$sqlUpdate = "UPDATE productiondata
SET id_status = :status,
end_time = NULL
WHERE id = :id AND id_status = :pausa";
$stmtUpdate = $pdo->prepare($sqlUpdate);
$stmtUpdate->execute([
'status' => $statusProduzioneId,
'id' => $id,
'pausa' => $statusPausaId
]);
} else {
echo json_encode([
'success' => false,
'msg' => 'Lo stato corrente non consente avvio/ripresa (stato id: ' . $currentStatus . ')'
]);
exit;
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'msg' => $e->getMessage()]);
}
exit;
}
// --- PAUSA PRODUZIONE (crea record in production_pauses + cambia stato) ---
// --- PAUSA PRODUZIONE (crea record in production_pauses + cambia stato) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['pause_production'])) {
$id = (int)$_POST['id'];
$reasonId = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
$note = trim($_POST['note'] ?? '');
try {
// Motivo pausa ora OPZIONALE: se non lo sanno, salvano NULL e lo mettono al riprendi
if ($reasonId > 0) {
$sql = "SELECT is_problem FROM pause_reasons WHERE id = :id LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $reasonId]);
$isProblem = $stmt->fetchColumn();
if ($isProblem === false) {
echo json_encode(['success' => false, 'msg' => 'Motivo pausa non trovato.']);
exit;
}
$isProblem = (int)$isProblem;
} else {
$reasonId = null; // nessun motivo scelto ora
$isProblem = 0;
}
// ❌ PRIMA – sbagliato per la tua logica:
// $nextStatus = $isProblem ? 8 : $statusPausaId; // 8 = Problema in produzione
// ✅ ORA – in PAUSA lo stato è SEMPRE pausa (7)
$nextStatus = $statusPausaId;
// Aggiorno lo stato sulla produzione SOLO se è in produzione (id_status = produzione)
$sql = "UPDATE productiondata
SET id_status = :status,
note_operatore = :note
WHERE id = :id AND id_status = :produzione";
$stmtUpdate = $pdo->prepare($sql);
$stmtUpdate->execute([
'status' => $nextStatus,
'note' => $note ?: null,
'id' => $id,
'produzione' => $statusProduzioneId
]);
// Se effettivamente ho cambiato lo stato, registro la pausa
if ($stmtUpdate->rowCount() > 0) {
$sqlPause = "INSERT INTO production_pauses
(id_production, reason_id, start_pause, note)
VALUES (:id_production, :reason_id, UTC_TIMESTAMP(), :note)";
$stmtPause = $pdo->prepare($sqlPause);
$stmtPause->execute([
'id_production' => $id,
'reason_id' => $reasonId,
'note' => $note ?: null
]);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'msg' => $e->getMessage()]);
}
exit;
}
// --- STOP PRODUZIONE (vai a Qualità + salva tempo finale) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['stop_production'])) {
$id = (int)$_POST['id'];
$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();
// 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'));
$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,
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,
'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;
}
// --- SALVATAGGIO DATI FINE PRODUZIONE ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['save_final_data'])) {
$id = (int)$_POST['id'];
$kgprod = (float)$_POST['kgprod'];
$mtprod = (float)$_POST['mtprod'];
$scarto = (float)$_POST['scarto'];
$note = trim($_POST['note'] ?? '');
$reason = $_POST['reason'] ?? 'fine';
try {
$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]);
$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;
$scartoPerc = 0.00;
if ($totaleKg > 0) {
$scartoPerc = round(($scarto / $totaleKg) * 100, 2);
}
// "Compila dopo" → stato 11 (dati da inserire), altrimenti flusso normale
$compilaDopo = !empty($_POST['compila_dopo']);
if ($compilaDopo) {
$nextStatus = 11; // Da compilare dati
} else {
$nextStatus = ($reason === 'problema') ? 8 : $statusQualita;
}
// Update finale
$sql = "UPDATE productiondata
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);
$stmt->execute([
'kgprod' => $kgprod,
'mtprod' => $mtprod,
'scarto' => $scarto,
'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;
}
// --- 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: la pausa aperta di questa produzione ha già un motivo? ---
if (!empty($_GET['check_pause_reason'])) {
header('Content-Type: application/json; charset=utf-8');
$pid = (int)($_GET['id'] ?? 0);
if ($pid <= 0) {
echo json_encode(['success' => false]);
exit;
}
$stmt = $pdo->prepare("SELECT reason_id FROM production_pauses
WHERE id_production = :id AND end_pause IS NULL
ORDER BY start_pause DESC LIMIT 1");
$stmt->execute(['id' => $pid]);
$rid = $stmt->fetchColumn();
// has_reason = true se esiste una pausa aperta con reason_id non NULL
echo json_encode([
'success' => true,
'has_reason' => ($rid !== false && $rid !== null)
]);
exit;
}
// --- AJAX: mescole collegate a una matrice ---
if (!empty($_GET['mescole_by_matrice'])) {
header('Content-Type: application/json; charset=utf-8');
$idMatrice = (int)($_GET['id_matrice'] ?? 0);
if ($idMatrice <= 0) {
echo json_encode(['success' => false, 'rows' => []]);
exit;
}
$stmt = $pdo->prepare("
SELECT me.id, me.nome
FROM matrici_mescole mm
JOIN mescole me ON me.id = mm.idmescola
WHERE mm.idmatrice = :idm AND me.is_active = 1
ORDER BY me.nome
");
$stmt->execute(['idm' => $idMatrice]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Linee associate a questa matrice
$stmtL = $pdo->prepare("
SELECT ml.idlinea AS id
FROM matrici_lines ml
WHERE ml.idmatrice = :idm
");
$stmtL->execute(['idm' => $idMatrice]);
$lineIds = array_map('intval', $stmtL->fetchAll(PDO::FETCH_COLUMN));
echo json_encode(['success' => true, 'rows' => $rows, 'lines' => $lineIds]);
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
p.*,
m.id AS matrice_id,
m.nome AS matrice,
m.photo AS matrice_photo,
(
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,
l.name AS linea,
c.nome AS cliente,
s.nome AS status_nome,
s.badge_color,
s.line_color,
p.tempo_totale_produzione,
-- 🔧 ADD: tools list & count
(
SELECT GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ' | ')
FROM productiondata_tools pt
JOIN inv_equipment t ON t.id = pt.tool_id
WHERE pt.productiondata_id = p.id
) AS tools_list,
(
SELECT COUNT(*)
FROM productiondata_tools pt
WHERE pt.productiondata_id = p.id
) AS tools_count
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
LEFT JOIN production_status s ON p.id_status = s.id
WHERE p.id_status IN (2, 7, 8)
" . (!empty($lineArray) ? " AND p.id_linea IN (" . implode(',', array_map('intval', $lineArray)) . ")" : "") . "
ORDER BY l.line_number, p.Data";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$records = $stmt->fetchAll();
foreach ($records as $r) {
// --- PARAMETRI LINEA (slot dinamici)
$sqlParams = "SELECT position, short_label, icon
FROM production_line_params
WHERE line_id = :line
ORDER BY position ASC";
$stmtP = $pdo->prepare($sqlParams);
$stmtP->execute(['line' => $r['id_linea']]);
$paramsLinea = $stmtP->fetchAll();
$r['param_slots'] = $paramsLinea;
include __DIR__ . "/render_production_card.php";
}
// --- RECORD IN STATO 6 ORDINATI PER PRIORITY
$sql2 = "SELECT
p.*,
m.id AS matrice_id,
m.nome AS matrice,
m.photo AS matrice_photo,
(
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,
l.name AS linea,
c.nome AS cliente,
s.nome AS status_nome,
s.badge_color,
s.line_color,
p.tempo_totale_produzione,
-- 🔧 ADD: tools list & count
(
SELECT GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ' | ')
FROM productiondata_tools pt
JOIN inv_equipment t ON t.id = pt.tool_id
WHERE pt.productiondata_id = p.id
) AS tools_list,
(
SELECT COUNT(*)
FROM productiondata_tools pt
WHERE pt.productiondata_id = p.id
) AS tools_count
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
LEFT JOIN production_status s ON p.id_status = s.id
WHERE p.id_status = 6
" . (!empty($lineArray) ? " AND p.id_linea IN (" . implode(',', array_map('intval', $lineArray)) . ")" : "") . "
ORDER BY p.priority ASC
";
$stmt2 = $pdo->prepare($sql2);
$stmt2->execute();
$recordsPriority = $stmt2->fetchAll();
if (!empty($recordsPriority)) {
echo '<div style="margin:20px 0; font-size:1.3rem; font-weight:700; color:#1e40af;">Programmato (in ordine di priorità)</div>';
foreach ($recordsPriority as $r) {
include __DIR__ . "/render_production_card.php";
}
}
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>Linea Produzione - Tablet</title>
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
/* Select2 dentro il modale + z-index sopra l'overlay */
.select2-container {
width: 100% !important;
}
.select2-container--default .select2-selection--single {
height: auto;
padding: 0.6rem 0.6rem;
border: 1px solid #cbd5e1;
border-radius: 0.6rem;
font-size: 1.15rem;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 100%;
}
.select2-dropdown {
z-index: 2000;
}
.select2-container--open {
z-index: 2000;
}
/* Select2 MULTIPLO: chip grandi e comodi per tablet */
.select2-container--default .select2-selection--multiple {
min-height: 3.2rem;
padding: 0.3rem 0.5rem;
border: 1px solid #cbd5e1;
border-radius: 0.6rem;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
font-size: 1.05rem;
padding: 0.35rem 0.7rem;
border-radius: 0.5rem;
margin-top: 0.35rem;
}
.select2-container--default .select2-results__option {
font-size: 1.1rem;
padding: 0.7rem 0.9rem;
}
</style>
<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, #1d4ed8, #3b82f6);
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;
}
.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;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.35);
}
.filters {
display: flex;
gap: 1rem;
padding: 1rem 1.5rem;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 160px;
}
.filter-group label {
display: block;
font-weight: 600;
margin-bottom: 0.4rem;
color: #1e40af;
font-size: 0.9rem;
}
.filter-group input,
.filter-group select {
width: 100%;
padding: 0.7rem 1rem;
border: 1px solid #cbd5e1;
border-radius: 0.8rem;
font-size: 1rem;
background: white;
}
.filter-group input:focus,
.filter-group select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
#recordsContainer {
flex: 1;
overflow-y: auto;
/* top right bottom left */
padding: 1rem 1.5rem 5rem 1.5rem;
background: #f8fafc;
}
.record-card {
margin-bottom: 1rem;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06);
background: white;
transition: all 0.3s ease;
cursor: pointer;
}
.record-card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.record-card.in-production {
background: var(--line-bg);
border-left: 6px solid var(--badge-bg);
}
.record-summary {
padding: 1rem 1.2rem;
position: relative;
background: transparent;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.8rem;
font-size: 0.9rem;
}
.summary-grid strong {
display: block;
color: #1e293b;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 0.2rem;
font-weight: 700;
}
.summary-grid span {
color: #475569;
font-weight: 500;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1.2rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: white;
}
.expand-arrow {
display: inline-block;
margin-left: 0.5rem;
font-size: 1rem;
transition: transform 0.3s ease;
font-weight: bold;
}
.record-card.expanded .expand-arrow {
transform: rotate(180deg);
}
.record-expanded {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), padding 0.4s ease;
padding: 0 1.2rem;
background: #f8fafc;
}
.record-card.expanded .record-expanded {
max-height: 400px;
padding: 1.2rem;
}
.timer-display {
text-align: center;
font-size: 2.4rem;
font-weight: 700;
color: #059669;
margin: 1rem 0;
font-family: 'Courier New', monospace;
letter-spacing: 2px;
}
.action-buttons {
display: flex;
gap: 0.8rem;
}
.action-btn {
flex: 1;
padding: 1rem;
font-size: 1.2rem;
font-weight: 700;
border: none;
color: white;
text-transform: uppercase;
letter-spacing: 1.2px;
cursor: pointer;
border-radius: 0.8rem;
transition: all 0.2s;
}
.start-btn {
background: linear-gradient(135deg, #10b981, #059669);
}
.start-btn:hover {
background: linear-gradient(135deg, #059669, #047857);
}
.pause-btn {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.pause-btn:hover {
background: linear-gradient(135deg, #d97706, #c05621);
}
.resume-btn {
background: linear-gradient(135deg, #10b981, #059669);
}
.resume-btn:hover {
background: linear-gradient(135deg, #059669, #047857);
}
.stop-btn {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.stop-btn:hover {
background: linear-gradient(135deg, #dc2626, #b91c1c);
}
.no-records {
text-align: center;
padding: 4rem 1.5rem;
color: #64748b;
font-size: 1.15rem;
}
.no-records i {
font-size: 3.5rem;
margin-bottom: 1rem;
color: #cbd5e1;
}
.custom-modal {
display: none;
/* di default invisibile */
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;
}
/* quando la modale è attiva, la centriamo */
.custom-modal.active {
display: flex !important;
}
.custom-modal .modal-content {
background: white;
padding: 2rem;
border-radius: 1.2rem;
width: 90%;
max-width: 400px;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
animation: slideUp 0.25s ease-out;
}
/* MODALE GRANDE PER PREVIEW FOTO */
.preview-large {
width: 95% !important;
max-width: 1200px !important;
max-height: 90vh !important;
padding: 0 !important;
background: black !important;
position: relative;
border-radius: 12px !important;
display: flex;
justify-content: center;
align-items: center;
}
/* animazione dolce */
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.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;
}
.modal-confirm {
background: #10b981;
color: white;
}
.modal-cancel {
background: #e2e8f0;
color: #475569;
}
.line-btn {
flex: 0 0 150px;
padding: 0.8rem 0;
font-size: 1.4rem;
font-weight: 700;
border-radius: 0.8rem;
border: none;
cursor: pointer;
transition: 0.2s;
color: white;
}
.line-btn {
flex: 0 0 150px;
padding: 0.8rem 0;
font-size: 1.4rem;
font-weight: 700;
border-radius: 0.8rem;
border: none;
cursor: pointer;
transition: 0.2s;
color: white;
}
.line-btn.active {
background: var(--line-color);
}
.line-btn.inactive {
background: #cbd5e1 !important;
color: #475569 !important;
}
/* --- TAB LINEE (selezione singola) --- */
.line-tab {
flex: 1 1 auto;
min-width: 130px;
padding: 0.9rem 0.5rem;
font-size: 1.3rem;
font-weight: 800;
border: 2px solid #e2e8f0;
border-radius: 0.9rem 0.9rem 0 0;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
transition: 0.15s;
letter-spacing: 0.5px;
}
.line-tab:hover {
background: #e2e8f0;
}
.line-tab.active {
background: var(--line-color, #1d4ed8);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
/* il tab "TUTTI" attivo usa un blu neutro */
.line-tab[data-line="all"].active {
background: #1d4ed8;
}
/* Modale più grande e ottimizzata */
.custom-modal .modal-content.final-wide {
width: 95%;
max-width: 780px;
}
.final-flex {
display: flex;
gap: 2rem;
margin-top: 1.2rem;
}
.final-col {
flex: 1;
}
.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;
}
.real-box label {
margin-top: 0.6rem;
font-weight: 600;
}
.input-big {
width: 100%;
padding: 0.9rem;
font-size: 1.25rem;
border-radius: 0.6rem;
border: 1px solid #cbd5e1;
margin-bottom: 1rem;
}
#imagePreviewModal .modal-content {
max-width: 1200px !important;
width: 95% !important;
}
/* Mantiene stile identico ai bottoni delle foto */
/* Stesso stile dei photo-btn */
.qc-btn {
width: 42px;
height: 42px;
border-radius: 10px;
border: 1px solid #000000;
/* bordo nero */
background: #c8f3df;
/* verde pastello interno */
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s ease;
}
.qc-btn i {
font-size: 1.8rem;
color: #000000;
/* icona nera */
}
.qc-btn:hover {
background: #b3e9d3;
/* leggero scurimento */
transform: scale(1.07);
}
.param-grid {
display: grid;
gap: 10px;
margin-bottom: 15px;
}
@media (max-width: 900px) {
.param-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.param-slot {
background: #ffffff;
border-radius: 12px;
padding: 10px;
text-align: center;
border: 2px solid #e2e8f0;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
transition: 0.2s ease;
}
.param-slot:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.thumb {
width: 100%;
height: 75px;
background: #f1f5f9;
border-radius: 8px;
overflow: hidden;
}
.thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.param-label {
margin-top: 6px;
font-weight: 700;
font-size: 1rem;
}
.photo-tools-row .qc-right {
margin-left: auto;
/* la porta tutta a destra */
}
.show-matrice-files i {
font-size: 1.5rem;
}
.modal-backdrop.show {
z-index: 1050 !important;
}
.modal.show {
z-index: 1055 !important;
}
.modal-worksheet-list {
max-width: 1320px;
width: 97vw;
}
.modal-worksheet-view {
max-width: 1500px;
width: 97vw;
}
.worksheet-badge-fl {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 68px;
padding: 6px 12px;
border-radius: 999px;
background: #e8f7ee;
color: #198754;
font-weight: 800;
font-size: .85rem;
}
.worksheet-badge-rev {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 54px;
padding: 6px 10px;
border-radius: 999px;
background: #e7f1ff;
color: #0d6efd;
font-weight: 700;
font-size: .84rem;
}
.worksheet-badge-status-active {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
padding: 6px 10px;
border-radius: 999px;
background: #d1e7dd;
color: #0f5132;
font-weight: 700;
font-size: .84rem;
}
.worksheet-badge-status-inactive {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
padding: 6px 10px;
border-radius: 999px;
background: #f8d7da;
color: #842029;
font-weight: 700;
font-size: .84rem;
}
.worksheet-list-table th {
background: #e7f1ff !important;
color: #0b3d91;
}
.worksheet-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: #e7f1ff;
color: #0d6efd;
font-weight: 700;
font-size: .82rem;
}
.readonly-card {
border: 1px solid #e9ecef;
border-radius: 16px;
background: #fff;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, .04);
height: 100%;
}
.readonly-card-header {
background: linear-gradient(135deg, #e7f1ff, #d6e9ff);
color: #0b3d91;
font-weight: 800;
padding: 12px 16px;
border-bottom: 1px solid #cfe2ff;
}
.readonly-card-body {
padding: 16px;
text-align: left;
}
.readonly-grid {
display: grid;
grid-template-columns: 220px 1fr;
gap: 8px 16px;
}
.readonly-label {
font-weight: 700;
color: #495057;
}
.readonly-value {
color: #1f2d3d;
word-break: break-word;
}
.worksheet-open-link {
background: #fff;
border: 1px solid #b6d4fe;
color: #0d6efd;
font-weight: 700;
border-radius: 10px;
padding: 9px 14px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.worksheet-open-link:hover {
color: #0a58ca;
background: #f4f9ff;
}
.worksheet-title-box {
background: linear-gradient(135deg, #0d6efd, #3b82f6);
color: #fff;
border-radius: 18px;
padding: 18px 20px;
box-shadow: 0 8px 24px rgba(13, 110, 253, 0.2);
}
.worksheet-title-box small {
color: rgba(255, 255, 255, .9);
}
.mix-readonly-table thead th {
background: #dbeafe !important;
color: #0b3d91;
}
.btn-view-worksheet {
background: linear-gradient(135deg, #0d6efd, #3b82f6);
color: #fff;
border: 0;
border-radius: 8px;
padding: 8px 14px;
font-weight: 700;
}
.btn-view-worksheet:hover {
color: #fff;
opacity: .95;
}
.descr-btn {
display: flex;
align-items: center;
gap: .5rem;
width: 100%;
min-height: 44px;
padding: .55rem .8rem;
border: 1px solid #cbd5e1;
border-radius: .7rem;
background: #f8fafc;
color: #334155;
font-size: .95rem;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.descr-btn span {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #334155;
font-weight: 600;
}
.descr-cell {
grid-column: span 3;
min-width: 0;
}
@media (max-width: 700px) {
.descr-cell {
grid-column: 1 / -1;
}
}
.descr-btn .bi-card-text {
color: #ea580c;
font-size: 1.2rem;
}
#descrText {
white-space: pre-wrap;
text-align: left;
font-size: 1.15rem;
line-height: 1.5;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: .8rem;
padding: 1rem 1.2rem;
max-height: 60vh;
overflow-y: auto;
}
</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 style="color:#ffffff;">Linea di Produzione</h5>
<div style="display:flex; gap:0.8rem; align-items:center;">
<button class="back-btn" id="btnNuovoQuick"
style="background:rgba(16,185,129,0.9); border-color:rgba(16,185,129,1);">
+ Nuovo
</button>
<button class="back-btn" onclick="location.href='production_dashboard.php'">Torna</button>
</div>
</div>
<div class="filters">
<div class="filter-group" style="flex:1 1 100%;">
<div id="lineTabs" style="display:flex; gap:0.6rem; flex-wrap:wrap;">
<button class="line-tab" data-line="all">TUTTI</button>
<?php foreach ($linee as $l): ?>
<button class="line-tab"
data-line="<?= (int)$l['id'] ?>"
style="--line-color: <?= htmlspecialchars($l['color']) ?>;">
<?= htmlspecialchars($l['name']) ?>
</button>
<?php endforeach; ?>
</div>
<input type="hidden" id="filterLine" value="">
<input type="hidden" id="preferredLine" value="<?= $preferredLineId !== null ? (int)$preferredLineId : '' ?>">
</div>
</div>
<div id="recordsContainer">
<!-- Caricato via JS -->
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE NUOVO -->
<div id="quickCreateModal" class="custom-modal">
<div class="modal-content" style="max-width:900px; width:92%; text-align:left;">
<h3 style="text-align:center;">Nuovo avvio rapido</h3>
<label style="font-weight:600; display:block; margin-top:1rem;">Descrizione</label>
<input type="text" id="qcDescrizione" class="input-big" placeholder="Descrizione...">
<div style="display:flex; gap:1.2rem; flex-wrap:wrap;">
<div style="flex:1; min-width:260px;">
<label style="font-weight:600; display:block;">Matrice *</label>
<select id="qcMatrice" class="input-big">
<option value="">Seleziona matrice</option>
<?php foreach ($allMatrici as $mx): ?>
<option value="<?= (int)$mx['id'] ?>"><?= htmlspecialchars($mx['nome']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="flex:1; min-width:260px;">
<label style="font-weight:600; display:block;">Linea *</label>
<select id="qcLinea" class="input-big">
<option value="">Seleziona linea</option>
<?php foreach ($linee as $l): ?>
<option value="<?= (int)$l['id'] ?>"><?= htmlspecialchars($l['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<label style="font-weight:600; display:block;">Mescole (una o più, opzionale)</label>
<select id="qcMescola" class="input-big" multiple>
<?php foreach ($allMescole as $me): ?>
<option value="<?= (int)$me['id'] ?>"><?= htmlspecialchars($me['nome']) ?></option>
<?php endforeach; ?>
</select>
<div class="modal-buttons" style="flex-wrap:wrap;">
<button class="modal-btn modal-cancel" id="qcCancel">Annulla</button>
<button class="modal-btn modal-confirm" id="qcCreateQueue"
style="background:#3b82f6;">Crea in coda</button>
<button class="modal-btn modal-confirm" id="qcCreateStart">Crea e avvia</button>
</div>
</div>
</div>
<!-- Notifica sonora -->
<audio id="beepSound" src="beep.wav" preload="auto"></audio>
<!-- Modal di conferma azioni produzione -->
<div id="reasonModal" class="custom-modal">
<div class="modal-content">
<h3 id="modalTitle">Conferma azione</h3>
<p id="modalQuestion"></p>
<!-- Opzioni visibili solo per STOP o PAUSA -->
<div class="modal-options" id="modalOptions" style="display:none;margin-top:1rem;">
<label><input type="radio" name="reason" value="fine"> Fine produzione</label><br>
<label><input type="radio" name="reason" value="tecnica"> Pausa tecnica</label><br>
<label><input type="radio" name="reason" value="problema"> Problema tecnico</label>
</div>
<!-- Motivo pausa (dropdown, solo per PAUSA) -->
<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>
<select id="pauseReason"
style="width:100%;padding:0.5rem;border-radius:0.5rem;border:1px solid #ccc;">
<option value="">Seleziona un motivo</option>
<?php foreach ($pauseReasons as $pr): ?>
<option value="<?= $pr['id'] ?>"
data-problem="<?= (int)$pr['is_problem'] ?>">
<?= htmlspecialchars($pr['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Campo nota -->
<div id="noteContainer" style="margin-top:1rem;display:none;">
<label for="modalNote"><strong>Nota (obbligatoria se problema):</strong></label>
<textarea id="modalNote" rows="3" style="width:100%;padding:0.5rem;border-radius:0.5rem;border:1px solid #ccc;"></textarea>
</div>
<div class="modal-buttons">
<button id="modalCancel" class="modal-btn modal-cancel">Annulla</button>
<button id="modalConfirm" class="modal-btn modal-confirm">Conferma</button>
</div>
</div>
</div>
<!-- Modal foto -->
<?php include __DIR__ . "/components/photo_modal.php"; ?>
<!-- Modal dati fine produzione -->
<div id="finalDataModal" class="custom-modal">
<div class="modal-content final-wide">
<h3>Dati Fine Produzione</h3>
<p>Confronta i valori teorici e inserisci quelli reali:</p>
<div class="final-flex">
<!-- COLONNA SINISTRA (TEORICI) -->
<div class="final-col teo-box">
<h4>Teorici</h4>
<div class="teo-row">
<label><strong>Kg teorici:</strong></label>
<span id="teoKg">0.00</span>
</div>
<div class="teo-row">
<label><strong>Metri teorici:</strong></label>
<span id="teoMt">0.00</span>
</div>
</div>
<!-- COLONNA DESTRA (REALi DA INSERIRE) -->
<div class="final-col real-box">
<h4>Reali</h4>
<label><strong>Kg prodotti:</strong></label>
<input type="number" id="kgprod" step="0.01" min="0" placeholder="0.00"
class="input-big">
<label><strong>Metri prodotti:</strong></label>
<input type="number" id="mtprod" step="0.01" min="0" placeholder="0.00"
class="input-big">
<label><strong>Scarto (kg):</strong></label>
<input type="number" id="scarto" step="0.01" min="0" placeholder="0.00"
class="input-big">
<label><strong>Scarto (%):</strong></label>
<input type="text" id="scartoPerc" class="input-big" readonly>
<label><strong>Note operatore:</strong></label>
<textarea id="finalNote" rows="3" class="input-big"></textarea>
</div>
</div>
<div class="modal-buttons" style="margin-top:1.5rem; flex-wrap:wrap;">
<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>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
function showPhotoSuccess() {
$("#photoMessageError").hide();
$("#photoMessageSuccess").fadeIn(150);
setTimeout(() => {
$("#photoMessageSuccess").fadeOut(200);
}, 2000);
}
function showPhotoError(msg) {
$("#photoMessageSuccess").hide();
$("#photoMessageError").text("⚠️ " + msg).fadeIn(150);
}
function loadPhotoGallery(productionId, type, slot = null) {
$("#photoGallery").html('<div style="color:#64748b;">Caricamento foto...</div>');
$.getJSON("get_photos.php", {
production_id: productionId,
photo_type: type,
param_position: slot
}, function(r) {
if (!r.success) {
$("#photoGallery").html(
'<div style="color:#b91c1c;">Errore nel caricamento delle foto.</div>'
);
return;
}
const photos = r.photos || [];
if (photos.length === 0) {
$("#photoGallery").html(
'<div style="color:#64748b;">Nessuna foto per questa posizione.</div>'
);
return;
}
let html = "";
photos.forEach(p => {
html += `
<div style="width:90px; text-align:center; font-size:0.7rem;">
<div class="photo-thumb"
data-full="photos/${p.filename}"
style="width:90px; height:70px; border-radius:8px; overflow:hidden; border:1px solid #e2e8f0; margin-bottom:4px; cursor:pointer;">
<img src="photos/${p.filename}" alt="photo"
style="width:100%; height:100%; object-fit:cover;">
</div>
<div style="color:#475569;">#${p.id}</div>
</div>
`;
});
$("#photoGallery").html(html);
});
}
// APRI PREVIEW — evento CLICK (non mousedown)
$(document).on("click", ".photo-thumb", function(e) {
e.stopPropagation(); // evita che il click chiuda subito il modale
const fullImage = $(this).data("full");
$("#previewImage").attr("src", fullImage);
$("#imagePreviewModal").addClass("active");
});
// 🔥 chiusura preview con X - DELEGATO
$(document).on("click", "#previewCloseX", function(e) {
e.preventDefault();
$("#imagePreviewModal").removeClass("active");
});
// opzionale: chiudi cliccando fuori dal contenuto (sull’overlay scuro)
$(document).on("click", "#imagePreviewModal", function(e) {
if ($(e.target).is("#imagePreviewModal")) {
$("#imagePreviewModal").removeClass("active");
}
});
// chiusura modale principale (upload foto)
$("#photoModalCloseX").on("click", function() {
$("#photoModal").removeClass("active");
});
</script>
<?php include('include/worksheets-linked-modals.php'); ?>
<script src="assets/js/worksheets-linked-modals.js"></script>
<?php include('jsinclude.php'); ?>
<script>
$(function() {
$('#filterLine').on('change', loadRecords);
const beep = $('#beepSound')[0];
let timers = {};
// --- FUNZIONE PER RIDIMENSIONARE LE IMMAGINI PRIMA DELL'UPLOAD ---
async function resizeImage(file, maxSize, quality = 0.7) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = event => {
const img = new Image();
img.src = event.target.result;
img.onload = () => {
let width = img.width;
let height = img.height;
// scala mantenendo proporzioni
if (width > height && width > maxSize) {
height = height * (maxSize / width);
width = maxSize;
} else if (height > maxSize) {
width = width * (maxSize / height);
height = maxSize;
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
blob => resolve(blob),
"image/jpeg",
quality
);
};
};
reader.onerror = error => reject(error);
});
}
function playBeep() {
beep.currentTime = 0;
beep.play().catch(() => {});
}
// --- TAB LINEE (selezione singola) ---
// Imposta il filtro nascosto in base al tab: "all" = vuoto (tutte), altrimenti l'id.
function setActiveTab($tab) {
$('#lineTabs .line-tab').removeClass('active');
$tab.addClass('active');
const line = $tab.data('line');
$('#filterLine').val(line === 'all' ? '' : String(line));
}
// Click sui tab
$(document).on('click', '.line-tab', function() {
setActiveTab($(this));
loadRecords();
});
// Tab iniziale: preferita dell'utente se impostata, altrimenti "TUTTI"
(function initLineTab() {
const pref = $('#preferredLine').val();
let $start = $('#lineTabs .line-tab[data-line="all"]');
if (pref !== '') {
const $prefTab = $('#lineTabs .line-tab[data-line="' + pref + '"]');
if ($prefTab.length) $start = $prefTab;
}
setActiveTab($start);
})();
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getFileExt(filename) {
const parts = String(filename || '').split('.');
return parts.length > 1 ? parts.pop().toLowerCase() : '';
}
function isImageExt(ext) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic', 'heif'].includes(ext);
}
function getFileIcon(ext) {
if (isImageExt(ext)) return '🖼️';
if (ext === 'pdf') return '📕';
if (['doc', 'docx'].includes(ext)) return '📘';
return '📎';
}
function getFileBadge(ext) {
if (isImageExt(ext)) return '<span style="background:#cff4fc;color:#055160;padding:3px 8px;border-radius:999px;font-size:0.78rem;font-weight:700;">Immagine</span>';
if (ext === 'pdf') return '<span style="background:#f8d7da;color:#842029;padding:3px 8px;border-radius:999px;font-size:0.78rem;font-weight:700;">PDF</span>';
if (['doc', 'docx'].includes(ext)) return '<span style="background:#cfe2ff;color:#084298;padding:3px 8px;border-radius:999px;font-size:0.78rem;font-weight:700;">DOC</span>';
return '<span style="background:#e2e3e5;color:#41464b;padding:3px 8px;border-radius:999px;font-size:0.78rem;font-weight:700;">File</span>';
}
function openFilePreview(filePath, fileName) {
const ext = getFileExt(fileName);
$("#filePreviewTitle").text(fileName || 'Anteprima file');
let html = '';
if (isImageExt(ext)) {
html = `
<div style="width:100%; height:100%; min-height:82vh; display:flex; justify-content:center; align-items:center;">
<img src="${escapeHtml(filePath)}"
alt="${escapeHtml(fileName)}"
style="max-width:100%; max-height:82vh; border-radius:10px;">
</div>
`;
} else if (ext === 'pdf') {
html = `
<iframe src="${escapeHtml(filePath)}#zoom=page-width"
style="display:block; width:100%; height:calc(92vh - 64px); border:none; background:#fff;">
</iframe>
`;
} else {
html = `
<div style="text-align:center; padding:40px 20px;">
<div style="font-size:3rem; margin-bottom:12px;">${getFileIcon(ext)}</div>
<div style="color:#475569; margin-bottom:16px;">Anteprima non disponibile per questo formato</div>
<a href="${escapeHtml(filePath)}" target="_blank"
style="display:inline-block; padding:10px 18px; border-radius:10px; text-decoration:none; background:#0d6efd; color:#fff; font-weight:600;">
Apri file
</a>
</div>
`;
}
$("#filePreviewContainer").html(html);
$("#filePreviewModal").addClass("active");
}
function loadMatriceAttachments(matriceId, matriceNome) {
$("#mf_matrice_name").text(matriceNome || '');
$("#matriceFilesList").html('<div style="color:#64748b;">Caricamento file...</div>');
$.getJSON("get_matrice_attachments.php", {
id: matriceId
}, function(r) {
if (!r.success) {
$("#matriceFilesList").html('<div style="color:#b91c1c;">Errore nel caricamento degli allegati.</div>');
return;
}
const files = (r.attachments || []).sort((a, b) =>
(parseInt(b.is_disegno, 10) || 0) - (parseInt(a.is_disegno, 10) || 0)
);
if (!files.length) {
$("#matriceFilesList").html('<div style="color:#64748b;">Nessun allegato disponibile per questa matrice.</div>');
return;
}
let html = '';
files.forEach(f => {
const fileName = f.file_name || '';
const filePath = f.file_url || f.file_path || '';
const desc = f.description || '';
const createdAt = f.created_at || '';
const ext = getFileExt(fileName);
const icon = getFileIcon(ext);
const badge = getFileBadge(ext);
const isDisegno = (parseInt(f.is_disegno, 10) || 0) === 1;
let actionHtml = '';
if (isImageExt(ext) || ext === 'pdf') {
actionHtml = `
<button type="button"
class="open-matrice-file-preview"
data-file="${escapeHtml(filePath)}"
data-name="${escapeHtml(fileName)}"
style="padding:8px 14px; border-radius:10px; border:none; background:#0d6efd; color:#fff; font-weight:600; cursor:pointer;">
Apri
</button>
`;
} else {
actionHtml = `
<a href="${escapeHtml(filePath)}" target="_blank"
style="display:inline-block; padding:8px 14px; border-radius:10px; text-decoration:none; background:#0d6efd; color:#fff; font-weight:600;">
Apri
</a>
`;
}
let previewHtml = '';
if (isImageExt(ext)) {
previewHtml = `
<img src="${escapeHtml(filePath)}"
alt="${escapeHtml(fileName)}"
style="width:70px; height:70px; object-fit:cover; border-radius:10px; border:1px solid #dbe2ea;">
`;
} else {
previewHtml = `
<div style="width:70px; height:70px; border-radius:10px; background:#f1f5f9; display:flex; align-items:center; justify-content:center; font-size:1.8rem; border:1px solid #dbe2ea;">
${icon}
</div>
`;
}
html += `
<div style="${isDisegno
? 'border:2px solid #6f42c1; background:#f7f3ff;'
: 'border:1px solid #e2e8f0; background:#fff;'} border-radius:14px; padding:12px;">
<div style="display:flex; gap:14px; align-items:flex-start;">
<div>${previewHtml}</div>
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<div style="font-weight:700; color:#1f2d3d;">${escapeHtml(fileName)}</div>
<div style="margin-top:4px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
${badge}
${isDisegno ? '<span style="background:#6f42c1;color:#fff;padding:3px 10px;border-radius:999px;font-size:0.78rem;font-weight:700;">📐 Disegno tecnico</span>' : ''}
</div>
${createdAt ? `<div style="margin-top:6px; color:#64748b; font-size:0.88rem;">${escapeHtml(createdAt)}</div>` : ''}
</div>
<div>${actionHtml}</div>
</div>
<div style="margin-top:10px; color:#475569; white-space:normal;">
${desc ? escapeHtml(desc) : '<em style="color:#94a3b8;">Nessuna descrizione</em>'}
</div>
</div>
</div>
</div>
`;
});
$("#matriceFilesList").html(html);
}).fail(function() {
$("#matriceFilesList").html('<div style="color:#b91c1c;">Errore di comunicazione col server.</div>');
});
}
$(document).on("click", ".show-matrice-files", function(e) {
e.stopPropagation();
const matriceId = $(this).data("matrice-id");
const matriceNome = $(this).data("matrice-nome") || "";
if (!matriceId) {
alert("Nessuna matrice associata a questo record.");
return;
}
loadMatriceAttachments(matriceId, matriceNome);
$("#matriceFilesModal").addClass("active");
});
$(document).on("click", "#closeMatriceFilesModal", function() {
$("#matriceFilesModal").removeClass("active");
});
$(document).on("click", "#matriceFilesModal", function(e) {
if ($(e.target).is("#matriceFilesModal")) {
$("#matriceFilesModal").removeClass("active");
}
});
$(document).on("click", ".open-matrice-file-preview", function(e) {
e.stopPropagation();
const file = $(this).data("file") || "";
const name = $(this).data("name") || "Anteprima file";
openFilePreview(file, name);
});
$(document).on("click", "#closeFilePreviewModal", function() {
$("#filePreviewModal").removeClass("active");
$("#filePreviewContainer").html("");
});
$(document).on("click", "#filePreviewModal", function(e) {
if ($(e.target).is("#filePreviewModal")) {
$("#filePreviewModal").removeClass("active");
$("#filePreviewContainer").html("");
}
});
$(document).on("click", "#filePreviewModal .modal-content", function(e) {
e.stopPropagation();
});
// --- CARICA RECORD ---
function loadRecords() {
$('#recordsContainer').html('<div class="text-center p-5"><i class="bi bi-hourglass-split" style="font-size:2.5rem;color:#94a3b8;"></i></div>');
const line = $('#filterLine').val();
$.get('', {
ajax: 1,
line: line
}, function(html) {
// 🔥 INSERIRE L’HTML NEL CONTAINER
$('#recordsContainer').html(html);
startAllTimers();
setupEventHandlers();
// 🔧 Sposta l'icona qualità all'estrema destra della riga
$('.photo-tools-row').each(function() {
const $row = $(this);
const $qc = $row.find('.qc-btn.quality-btn').first();
if ($qc.length) {
$qc.detach();
$qc.appendTo($row);
$qc.addClass('qc-right');
}
});
$(document).on("click", ".photo-btn", function(e) {
e.stopPropagation();
const type = $(this).data("type");
const production = $(this).data("production");
$("#photoType").val(type);
$("#photoProductionId").val(production);
let titles = {
lotto_mescola: "Foto Lotto Mescola",
parametri_macchina: "Foto Parametri Macchina",
problema: "Foto Problema di Produzione"
};
$("#photoModalTitle").text(titles[type] || "Carica Foto");
$("#photoModalSubtitle").text("Produzione ID: " + production);
// pulisci messaggi
$("#photoMessageSuccess").hide();
$("#photoMessageError").hide();
// carica galleria foto esistenti per questo record + tipologia
loadPhotoGallery(production, type);
$("#photoModal").addClass("active");
});
// FOTO PARAMETRI MACCHINA (7 posizioni)
$(document).on("click", ".param-slot", function(e) {
e.stopPropagation();
const slot = $(this).data("slot");
const productionId = $(this).data("production");
$("#photoType").val("parametri_macchina");
$("#photoProductionId").val(productionId);
$("#photoParamPosition").val(slot);
$("#photoModalTitle").text("Foto Parametro P" + slot);
$("#photoModalSubtitle").text("Produzione ID: " + productionId);
loadPhotoGallery(productionId, "parametri_macchina", slot);
$("#photoModal").addClass("active");
});
$("#photoCancel").off("click").on("click", () => {
$("#photoModal").removeClass("active");
// 🔥 ricarica le card (e quindi le thumbnail) via AJAX
loadRecords();
});
$("#photoModalCloseX").off("click").on("click", function() {
$("#photoModal").removeClass("active");
// 🔥 stessa logica: al close ricarico le card
loadRecords();
});
// --- SUBMIT FORM FOTO (con resize + upload) ---
$("#photoForm").off("submit").on("submit", async function(e) {
e.preventDefault();
const fileInput = document.getElementById("photoInput");
const file = fileInput.files[0];
if (!file) {
showPhotoError("Seleziona una foto prima di caricare.");
return;
}
// mostra loader, disabilita submit
$("#photoMessageSuccess, #photoMessageError").hide();
$("#photoLoading").show();
$("#photoForm .modal-confirm").prop("disabled", true);
try {
// ridimensiona lato lungo max 1600 px, qualità 0.7
const resizedBlob = await resizeImage(file, 1600, 0.7);
const formData = new FormData();
formData.append("production_id", $("#photoProductionId").val());
formData.append("photo_type", $("#photoType").val());
formData.append("param_position", $("#photoParamPosition").val());
formData.append("photo", resizedBlob, file.name);
$.ajax({
url: "upload_photo.php",
type: "POST",
data: formData,
processData: false,
contentType: false,
dataType: "json",
success: function(r) {
$("#photoLoading").hide();
$("#photoForm .modal-confirm").prop("disabled", false);
if (r.success) {
showPhotoSuccess();
loadPhotoGallery(
$("#photoProductionId").val(),
$("#photoType").val(),
$("#photoParamPosition").val() || null
);
} else {
showPhotoError(r.message || "Errore durante il caricamento della foto.");
}
},
error: function() {
$("#photoLoading").hide();
$("#photoForm .modal-confirm").prop("disabled", false);
showPhotoError("Errore di comunicazione con il server.");
}
});
} catch (err) {
console.error(err);
$("#photoLoading").hide();
$("#photoForm .modal-confirm").prop("disabled", false);
showPhotoError("Errore durante l'elaborazione dell'immagine.");
}
});
$('.record-card.in-production').each(function() {
const $card = $(this);
const $expanded = $card.find('.record-expanded');
$card.addClass('expanded');
$expanded.css('max-height', $expanded[0].scrollHeight + 50 + 'px');
});
}).fail(function() {
$('#recordsContainer').html('<div class="no-records">Errore di connessione</div>');
});
}
// --- EVENTI DEI PULSANTI ---
function setupEventHandlers() {
$(document).off('click', '.record-card');
$(document).on('click', '.record-card', function(e) {
if ($(e.target).closest('button').length) return;
const $card = $(this);
const $arrow = $card.find('.expand-arrow');
const $expanded = $card.find('.record-expanded');
if ($card.hasClass('expanded')) {
$card.removeClass('expanded');
$arrow.css('transform', 'rotate(0deg)');
$expanded.css('max-height', $expanded[0].scrollHeight + 'px');
setTimeout(() => $expanded.css('max-height', '0'), 10);
} else {
$card.addClass('expanded');
$arrow.css('transform', 'rotate(180deg)');
$expanded.css('max-height', '0');
setTimeout(() => $expanded.css('max-height', $expanded[0].scrollHeight + 50 + 'px'), 10);
}
});
// --- AVVIA PRODUZIONE ---
$(document).off('click', '.start-btn').on('click', '.start-btn', function(e) {
e.stopPropagation();
playBeep();
const id = $(this).data('id');
showReasonModal('start', id);
});
// --- RIPRENDI ---
$(document).off('click', '.resume-btn').on('click', '.resume-btn', function(e) {
e.stopPropagation();
playBeep();
const id = $(this).data('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 ---
$(document).off('click', '.pause-btn').on('click', '.pause-btn', function(e) {
e.stopPropagation();
playBeep();
const id = $(this).data('id');
showReasonModal('pause', id);
});
// --- STOP ---
$(document).off('click', '.stop-btn').on('click', '.stop-btn', function(e) {
e.stopPropagation();
playBeep();
const id = $(this).data('id');
showReasonModal('stop', id);
});
}
// --- CONVERT DATE ---
function formatItalianDate(isoDate) {
const months = [
"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno",
"luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"
];
const d = new Date(isoDate);
const day = d.getDate();
const month = months[d.getMonth()];
const year = d.getFullYear();
return `${day} ${month} ${year}`;
}
// Toggle "motivo dopo": nasconde/disabilita la tendina
$(document).off('change', '#pauseLater').on('change', '#pauseLater', function() {
const later = $(this).is(':checked');
if (later) {
$('#pauseReason').val('').prop('disabled', true);
$('#pauseReasonSelectWrap').slideUp(120);
} else {
$('#pauseReason').prop('disabled', false);
$('#pauseReasonSelectWrap').slideDown(120);
}
});
// --- MODALE UNIFICATA PER TUTTE LE AZIONI ---
function showReasonModal(action, id) {
$('#reasonModal').addClass('active');
$('#modalNote').val('');
$('input[name="reason"]').prop('checked', false);
$('#modalOptions').hide();
$('#noteContainer').hide();
$('#pauseReasonContainer').hide();
$('#pauseReason').val('');
// reset toggle "motivo dopo"
$('#pauseLater').prop('checked', false);
$('#pauseReasonSelectWrap').show();
$('#pauseReason').prop('disabled', false);
// Titolo e testo dinamici
switch (action) {
case 'start':
$('#modalTitle').text('Avvio produzione');
$('#modalQuestion').text('Vuoi avviare la produzione?');
break;
case 'resume':
// Motivo già presente in pausa → solo conferma
$('#modalTitle').text('Ripresa produzione');
$('#modalQuestion').text('Vuoi riprendere la produzione?');
$('#pauseReasonContainer').hide();
$('#noteContainer').hide();
break;
case 'resume_ask':
// Pausa senza motivo → lo chiedo ora (obbligatorio)
$('#modalTitle').text('Ripresa produzione');
$('#modalQuestion').text('Indica il motivo della pausa per riprendere:');
$('#pauseReasonContainer').show();
$('#laterToggleWrap').hide(); // niente "motivo dopo" al riprendi
$('#pauseReasonSelectWrap').show();
$('#pauseReason').prop('disabled', false);
$('#noteContainer').show();
break;
case 'pause':
$('#modalTitle').text('Pausa produzione');
$('#modalQuestion').text('Seleziona il motivo della pausa:');
$('#modalOptions').hide(); // niente radio
$('#pauseReasonContainer').show(); // mostra tendina
$('#laterToggleWrap').show(); // toggle "motivo dopo" visibile
$('#noteContainer').show(); // nota opzionale / obbligatoria se problema
break;
case 'stop':
$('#modalTitle').text('Stop produzione');
$('#modalQuestion').text('Seleziona il motivo dello stop:');
$('#modalOptions').show();
$('input[value="fine"]').closest('label').show();
$('input[value="tecnica"]').closest('label').hide();
$('input[value="problema"]').closest('label').show();
$('#noteContainer').show();
break;
}
// --- Pulsante conferma ---
$('#modalConfirm').off('click').on('click', function() {
const note = $('#modalNote').val().trim();
// --- LOGICA PAUSA ---
if (action === 'pause') {
const later = $('#pauseLater').is(':checked');
// Se NON ha scelto "motivo dopo", il motivo è obbligatorio ora
if (!later && !$('#pauseReason').val()) {
alert('Seleziona un motivo, oppure attiva "Inserisco il motivo dopo".');
return;
}
// Se "motivo dopo" → NULL (0), altrimenti l'id scelto
const pauseReasonId = later ? 0 : ($('#pauseReason').val() || 0);
const isProblem = $('#pauseReason option:selected').data('problem') == 1;
// "Problema" richiede la nota (solo se il motivo è stato scelto ora)
if (!later && isProblem && note === '') {
alert('Inserisci una nota per una pausa con problema tecnico.');
return;
}
$('#reasonModal').removeClass('active');
pauseProduction(id, pauseReasonId, note);
return;
}
// --- LOGICA STOP ---
let reason = null;
if (action === 'stop') {
reason = $('input[name="reason"]:checked').val();
if (!reason) {
alert('Seleziona un motivo.');
return;
}
if (reason === 'problema' && note === '') {
alert('Inserisci una nota per segnalare il problema.');
return;
}
}
$('#reasonModal').removeClass('active');
// Azioni specifiche
switch (action) {
case 'start':
startProduction(id);
break;
case 'resume':
// Motivo già presente: riprendo senza inviare reason_id
startProduction(id);
break;
case 'resume_ask':
// Motivo mancante: obbligatorio sceglierlo ora
const resumeReasonId = $('#pauseReason').val();
if (!resumeReasonId) {
alert('Seleziona il motivo della pausa prima di riprendere.');
return;
}
startProduction(id, resumeReasonId);
break;
case 'stop':
// Sia "fine" che "problema" aprono il modale dati finali.
// Il reason viene passato e conservato per il salvataggio.
openFinalDataModal(id, reason, note);
break;
}
});
// --- Pulsante annulla ---
$('#modalCancel').off('click').on('click', function() {
$('#reasonModal').removeClass('active');
});
}
// --- MODALE DATI FINE PRODUZIONE ---
function openFinalDataModal(id, reason, note) {
$('#finalDataModal').addClass('active');
// Pulisci i campi reali
$('#kgprod, #mtprod, #scarto').val('');
$('#finalNote').val(note || '');
$('#kgprod, #scarto').off('input').on('input', recalcScrapPerc);
$('#scartoPerc').val('');
// Recupera i dati teorici del record
const card = $(`.record-card[data-id="${id}"]`);
const teoKg = card.find('[data-field="kg_sp"]').text() || "0.00";
const teoMt = card.find('[data-field="metri"]').text() || "0.00";
// Mostra teorici
$('#teoKg').text(teoKg);
$('#teoMt').text(teoMt);
$('#finalConfirm').off('click').on('click', function() {
const kg = parseFloat($('#kgprod').val()) || 0;
const mt = parseFloat($('#mtprod').val()) || 0;
const scarto = parseFloat($('#scarto').val()) || 0;
const noteFinal = $('#finalNote').val().trim();
if (kg <= 0 && mt <= 0) {
alert('Inserisci almeno uno tra Kg o Metri prodotti.');
return;
}
$('#finalDataModal').removeClass('active');
$.post('', {
save_final_data: 1,
id: id,
reason: reason,
kgprod: kg,
mtprod: mt,
scarto: scarto,
note: noteFinal
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore nel salvataggio dei dati finali.');
}, 'json');
});
$('#finalCancel').off('click').on('click', function() {
$('#finalDataModal').removeClass('active');
});
// "Compila dopo": chiude la produzione senza kg/metri, va in stato 11
$('#finalLater').off('click').on('click', function() {
if (!confirm('La produzione verrà terminata e i dati (kg, metri, scarto) resteranno da inserire nella pagina dedicata. Confermi?')) {
return;
}
$('#finalDataModal').removeClass('active');
$.post('', {
save_final_data: 1,
id: id,
reason: reason,
compila_dopo: 1, // flag: rimanda la compilazione
kgprod: 0,
mtprod: 0,
scarto: 0,
note: $('#finalNote').val().trim()
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore nel salvataggio.');
}, 'json');
});
}
function recalcScrapPerc() {
const kg = parseFloat($('#kgprod').val()) || 0;
const sc = parseFloat($('#scarto').val()) || 0;
const tot = kg + sc;
let perc = 0;
if (tot > 0) {
perc = (sc / tot) * 100;
}
$('#scartoPerc').val(perc.toFixed(2) + ' %');
}
// --- START PRODUZIONE ---
function startProduction(id, reasonId) {
$.post('', {
start_production: 1,
id: id,
reason_id: reasonId || 0
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore durante l\'avvio.');
}, 'json');
}
// --- PAUSA PRODUZIONE ---
function pauseProduction(id, reasonId, note) {
$.post('', {
pause_production: 1,
id: id,
reason_id: reasonId,
note: note
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore durante la pausa.');
}, 'json');
}
// --- STOP PRODUZIONE ---
function stopProduction(id, reason, note) {
$.post('', {
stop_production: 1,
id: id,
reason: reason,
note: note
}, function(r) {
if (r.success) loadRecords();
else alert(r.msg || 'Errore durante lo stop.');
}, 'json');
}
// --- TIMER PRODUZIONE ---
function startAllTimers() {
clearInterval(window.timerInterval);
timers = {};
$('.timer-display').each(function() {
const $el = $(this);
const start = $el.data('start');
const end = $el.data('end');
if (!start) return;
const id = $el.closest('.record-card').data('id');
timers[id] = {
el: $el,
start: start * 1000,
end: end ? end * 1000 : null
};
});
window.timerInterval = setInterval(() => {
Object.keys(timers).forEach(id => updateTimer(id));
}, 1000);
}
function updateTimer(id) {
const t = timers[id];
if (!t) return;
let diff = t.end ? (t.end - t.start) : (Date.now() - t.start);
const h = String(Math.floor(diff / 3600000)).padStart(2, '0');
const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0');
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0');
t.el.find('.timer').text(`${h}:${m}:${s}`);
}
// --- NUOVO AVVIO RAPIDO ---
function initQuickSelects() {
$('#qcMatrice').select2({
dropdownParent: $('#quickCreateModal .modal-content'),
placeholder: 'Cerca matrice...',
allowClear: true,
width: '100%'
});
$('#qcMescola').select2({
dropdownParent: $('#quickCreateModal .modal-content'),
placeholder: 'Cerca e aggiungi mescole...',
allowClear: true,
width: '100%',
closeOnSelect: false
});
$('#qcLinea').select2({
dropdownParent: $('#quickCreateModal .modal-content'),
placeholder: 'Seleziona linea...',
allowClear: true,
width: '100%'
});
// Salvo l'HTML originale delle linee per poterlo ricostruire ad ogni cambio matrice
if (!window._qcLineaOriginalHtml) {
window._qcLineaOriginalHtml = $('#qcLinea').html();
}
// Ricostruisce il select linea raggruppando le associate in cima
function rebuildLineSelect(associatedIds) {
const current = $('#qcLinea').val();
const assocSet = new Set((associatedIds || []).map(String));
// Ripristino tutte le option originali
const $tmp = $('<select>').html(window._qcLineaOriginalHtml);
const allOptions = [];
$tmp.find('option').each(function() {
const v = $(this).val();
if (v === '') return; // scarto il placeholder
allOptions.push({
value: v,
label: $(this).text()
});
});
let html = '<option value="">Seleziona linea</option>';
const assoc = allOptions.filter(o => assocSet.has(String(o.value)));
const others = allOptions.filter(o => !assocSet.has(String(o.value)));
if (assoc.length) {
html += '<optgroup label="⭐ Consigliate per questa matrice">';
assoc.forEach(o => html += `<option value="${o.value}">⭐ ${o.label}</option>`);
html += '</optgroup>';
}
if (others.length) {
html += '<optgroup label="Altre linee">';
others.forEach(o => html += `<option value="${o.value}">${o.label}</option>`);
html += '</optgroup>';
}
$('#qcLinea').html(html);
// Preseleziono se è associata UNA sola linea, altrimenti mantengo la scelta corrente
if (assoc.length === 1) {
$('#qcLinea').val(String(assoc[0].value));
} else {
$('#qcLinea').val(current || '');
}
$('#qcLinea').trigger('change.select2');
}
// Quando cambio matrice, precarico mescole e riordino le linee
$('#qcMatrice').off('change.autoMescole').on('change.autoMescole', function() {
const idMatrice = $(this).val();
if (!idMatrice) {
$('#qcMescola').val(null).trigger('change');
rebuildLineSelect([]);
return;
}
$.getJSON('', {
mescole_by_matrice: 1,
id_matrice: idMatrice
}, function(r) {
if (r.success && Array.isArray(r.rows)) {
const ids = r.rows.map(m => String(m.id));
$('#qcMescola').val(ids).trigger('change');
}
rebuildLineSelect(r.lines || []);
});
});
}
function destroyQuickSelects() {
if ($('#qcMatrice').data('select2')) $('#qcMatrice').select2('destroy');
if ($('#qcMescola').data('select2')) $('#qcMescola').select2('destroy');
if ($('#qcLinea').data('select2')) $('#qcLinea').select2('destroy');
}
function closeQuickModal() {
destroyQuickSelects();
$('#quickCreateModal').removeClass('active');
}
$('#btnNuovoQuick').on('click', function() {
$('#qcDescrizione').val('');
$('#qcMatrice').val('');
$('#qcMescola').val(null);
$('#qcLinea').val('');
$('#quickCreateModal').addClass('active');
initQuickSelects();
});
$('#qcCancel').on('click', closeQuickModal);
$(document).on('click', '#quickCreateModal', function(e) {
if ($(e.target).is('#quickCreateModal')) closeQuickModal();
});
function submitQuickCreate(avvia) {
const idMatrice = $('#qcMatrice').val();
const idLinea = $('#qcLinea').val();
const mescoleArr = $('#qcMescola').val() || []; // array di id
const descr = $('#qcDescrizione').val().trim();
if (!idMatrice) {
alert('Seleziona una matrice.');
return;
}
if (!idLinea) {
alert('Seleziona una linea.');
return;
}
$('#qcCreateQueue, #qcCreateStart').prop('disabled', true);
$.post('', {
create_quick: 1,
avvia: avvia ? 1 : 0,
id_matrice: idMatrice,
id_linea: idLinea,
id_mescole: mescoleArr.join(','), // CSV di id
descrizione: descr
}, function(r) {
$('#qcCreateQueue, #qcCreateStart').prop('disabled', false);
if (r.success) {
closeQuickModal();
playBeep();
loadRecords();
} else {
alert(r.msg || 'Errore durante la creazione.');
}
}, 'json').fail(function() {
$('#qcCreateQueue, #qcCreateStart').prop('disabled', false);
alert('Errore di comunicazione con il server.');
});
}
$('#qcCreateQueue').on('click', function() {
submitQuickCreate(false);
});
$('#qcCreateStart').on('click', function() {
submitQuickCreate(true);
});
loadRecords();
});
// --- MODALE MESCOLE ---
$(document).on("click", ".showMescole", function(e) {
e.stopPropagation();
// prendo la stringa grezza dall’attributo, NON .data()
const raw = $(this).attr("data-list") || "[]";
let list = [];
try {
list = JSON.parse(raw);
} catch (err) {
console.error("Errore parsing mescole:", err, raw);
list = [];
}
let html = "";
list.forEach(m => {
html += `<li style="margin:6px 0;font-size:1.1rem;">${m}</li>`;
});
$("#mescoleList").html(html || '<li style="margin:6px 0;">Nessuna mescola</li>');
$("#mescoleModal").addClass("active");
});
$(document).on("click", "#closeMescole", function() {
$("#mescoleModal").removeClass("active");
});
// --- MODALE DESCRIZIONE ---
$(document).on("click", ".descr-btn", function(e) {
e.stopPropagation();
$("#descrTitle").text($(this).attr("data-title") || "Descrizione");
$("#descrText").text($(this).attr("data-descr") || "");
$("#descrModal").addClass("active");
});
$(document).on("click", "#closeDescr, #descrModal", function(e) {
if (e.target === this) $("#descrModal").removeClass("active");
});
</script>
<!-- MODALE PER PREVIEW IMMAGINE -->
<div id="imagePreviewModal" class="custom-modal">
<div class="modal-content preview-large">
<!-- X per chiudere -->
<button id="previewCloseX"
style="
position:absolute;
top:10px;
right:15px;
font-size:2rem;
color:white;
background:none;
border:none;
cursor:pointer;
z-index:9999; /* 🔥 aggiungi questo */
">
&times;
</button>
<img id="previewImage"
src=""
style="
max-width: 95%;
max-height: 85vh;
width: auto;
height: auto;
display: block;
margin: auto;
border-radius: 6px;
">
</div>
</div>
<!-- MODALE DESCRIZIONE -->
<div id="descrModal" class="custom-modal">
<div class="modal-content" style="max-width:760px; width:94%;">
<h3 id="descrTitle">Descrizione</h3>
<div id="descrText"></div>
<div class="modal-buttons">
<button class="modal-btn modal-cancel" id="closeDescr">Chiudi</button>
</div>
</div>
</div>
<!-- MODALE MESCOLE -->
<div id="mescoleModal" class="custom-modal">
<div class="modal-content" style="max-width:400px;">
<h3>Mescole utilizzate</h3>
<ul id="mescoleList" style="text-align:left; padding-left:1rem;"></ul>
<div class="modal-buttons">
<button class="modal-btn modal-cancel" id="closeMescole">Chiudi</button>
</div>
</div>
</div>
<script>
// apre il file picker sul campo interno al form
$(document).on("click", "#choosePhotoBtn", function(e) {
e.stopPropagation();
$("#photoInput").click();
});
// mostra nome file scelto E fa partire subito l'upload
$(document).on("change", "#photoInput", function() {
const file = this.files[0];
if (!file) return;
$("#selectedPhotoName").text(file.name);
// 🔥 auto-submit → parte resize + upload
$("#photoForm").submit();
});
</script>
<!-- MODALE ALLEGATI MATRICE (SOLO LETTURA) -->
<div id="matriceFilesModal" class="custom-modal">
<div class="modal-content" style="width:95%; max-width:1100px; max-height:88vh; overflow:hidden; padding:0; border-radius:16px;">
<div style="background:#b9ebc7; padding:1rem 1.4rem; display:flex; justify-content:space-between; align-items:center;">
<div>
<h3 style="margin:0; color:#1f2d3d;">File correlati matrice</h3>
<div id="mf_matrice_name" style="font-size:0.95rem; color:#475569; margin-top:4px;"></div>
</div>
<button type="button" id="closeMatriceFilesModal"
style="font-size:1.9rem; line-height:1; background:none; border:none; cursor:pointer; color:#1f2d3d;">
&times;
</button>
</div>
<div style="padding:1.2rem 1.4rem; overflow-y:auto;">
<div id="matriceFilesList" style="display:flex; flex-direction:column; gap:12px;">
<div style="color:#64748b;">Caricamento file...</div>
</div>
</div>
</div>
</div>
<!-- MODALE PREVIEW FILE -->
<div id="filePreviewModal" class="custom-modal">
<div class="modal-content" style="background:#fff; max-width:1400px; width:98%; height:92vh; padding:0; overflow:hidden; text-align:left;">
<div style="background:#b9ebc7; padding:0.9rem 1.2rem; display:flex; justify-content:space-between; align-items:center;">
<div id="filePreviewTitle" style="font-weight:700; color:#1f2d3d;">Anteprima file</div>
<button type="button" id="closeFilePreviewModal"
style="font-size:1.9rem; line-height:1; background:none; border:none; cursor:pointer; color:#1f2d3d;">
&times;
</button>
</div>
<div id="filePreviewContainer"
style="padding:0; background:#f8fafc; height:calc(92vh - 64px); overflow:hidden;">
</div>
</div>
</div>
</body>
</html>