1839 lines
82 KiB
PHP
1839 lines
82 KiB
PHP
<?php
|
|
|
|
/**
|
|
* production_programming_v2.php - programmazione produzione "classica" migliorata.
|
|
*
|
|
* Stesse tabelle e stesse funzioni della pagina attuale (in macchina, programmati in ordine, da programmare,
|
|
* filtro linee, foto, note, trascinamento), ma: modifica in finestra e non in riga, nessun ricaricamento
|
|
* della pagina, ordine salvato su tutte le righe (non solo la pagina visibile), azioni rapide e multiple.
|
|
*/
|
|
|
|
ob_start();
|
|
include('include/headscript.php');
|
|
require_once __DIR__ . '/class/MagoProduction.php';
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
const PV_TODO = 1; // Da programmare
|
|
const PV_PLAN = 6; // Programmato
|
|
const PV_ACTIVE = [2, 7, 8]; // In produzione, Pausa, Problema
|
|
|
|
function pvOut(array $a)
|
|
{
|
|
while (ob_get_level()) ob_end_clean();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($a);
|
|
exit;
|
|
}
|
|
|
|
if (!userCan('production.programming.view')) {
|
|
if (isset($_GET['action']) || isset($_POST['action'])) pvOut(['success' => false, 'message' => 'Non hai i permessi per questa pagina']);
|
|
while (ob_get_level()) ob_end_clean();
|
|
http_response_code(403);
|
|
echo '<div style="font-family:sans-serif;padding:60px;text-align:center;"><h3>Accesso non consentito</h3><a href="production_dashboard.php">Torna alla Dashboard</a></div>';
|
|
exit;
|
|
}
|
|
|
|
function pvHas(PDO $pdo, $t)
|
|
{
|
|
static $c = [];
|
|
if (!isset($c[$t])) {
|
|
try {
|
|
$pdo->query("SELECT 1 FROM `$t` LIMIT 0");
|
|
$c[$t] = true;
|
|
} catch (Throwable $e) {
|
|
$c[$t] = false;
|
|
}
|
|
}
|
|
return $c[$t];
|
|
}
|
|
|
|
function pvMs($utc)
|
|
{
|
|
return $utc ? (new DateTime($utc, new DateTimeZone('UTC')))->getTimestamp() * 1000 : null;
|
|
}
|
|
|
|
/** Lavori aperti (da programmare, programmati, in macchina) con tutto quello che serve alla pagina. */
|
|
function pvJobs(PDO $pdo)
|
|
{
|
|
$mago = pvHas($pdo, 'mago_prod_queue');
|
|
$photos = pvHas($pdo, 'production_photos')
|
|
? "(SELECT GROUP_CONCAT(ph.photo_type) FROM production_photos ph WHERE ph.production_id = p.id)" : 'NULL';
|
|
$in = implode(',', array_merge([PV_TODO, PV_PLAN], PV_ACTIVE));
|
|
$rows = $pdo->query("
|
|
SELECT p.id, p.id_status, p.id_linea, p.priority, p.conferma_ordine, p.idmatrice, m.nome AS matrice, m.photo,
|
|
p.id_cliente, c.nome AS cliente, p.data_zibo, p.data_cliente, p.data_produzione, p.metri, p.kg_sp, p.kg_p,
|
|
p.ore_previste, p.note_extra, p.note_operatore, p.start_time,
|
|
s.nome AS status_nome, COALESCE(s.badge_color, '#6c757d') AS status_color,
|
|
(SELECT GROUP_CONCAT(ms.nome ORDER BY ms.nome SEPARATOR '||') FROM productiondata_mescole pm JOIN mescole ms ON ms.id = pm.id_mescola WHERE pm.id_productiondata = p.id) AS mescole,
|
|
(SELECT GROUP_CONCAT(pm.id_mescola) FROM productiondata_mescole pm WHERE pm.id_productiondata = p.id) AS mescole_ids,
|
|
(SELECT GROUP_CONCAT(ml.idlinea) FROM matrici_lines ml WHERE ml.idmatrice = p.idmatrice) AS matrice_lines,
|
|
(SELECT COALESCE(SUM(TIMESTAMPDIFF(SECOND, pp.start_pause, pp.end_pause)), 0) FROM production_pauses pp WHERE pp.id_production = p.id AND pp.end_pause IS NOT NULL) AS pause_sec,
|
|
(SELECT pp.start_pause FROM production_pauses pp WHERE pp.id_production = p.id AND pp.end_pause IS NULL ORDER BY pp.start_pause DESC LIMIT 1) AS open_pause,
|
|
" . ($mago ? "(SELECT MAX(q.mago_changed) FROM mago_prod_queue q WHERE q.productiondata_id = p.id)" : "NULL") . " AS mago_changed,
|
|
" . ($mago ? "(SELECT COUNT(*) FROM mago_prod_queue q WHERE q.productiondata_id = p.id)" : "0") . " AS from_mago,
|
|
$photos AS photos
|
|
FROM productiondata p
|
|
LEFT JOIN matrice m ON m.id = p.idmatrice
|
|
LEFT JOIN clients c ON c.id = p.id_cliente
|
|
LEFT JOIN production_status s ON s.id = p.id_status
|
|
WHERE p.id_status IN ($in)
|
|
ORDER BY p.priority IS NULL, p.priority, p.id")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($rows as &$j) {
|
|
foreach (['id', 'id_status', 'id_linea', 'idmatrice', 'id_cliente', 'priority'] as $k) $j[$k] = $j[$k] === null ? null : (int)$j[$k];
|
|
foreach (['metri', 'kg_sp', 'kg_p', 'ore_previste'] as $k) $j[$k] = (float)$j[$k];
|
|
$j['mescole'] = $j['mescole'] ? explode('||', $j['mescole']) : [];
|
|
$j['mescole_ids'] = $j['mescole_ids'] ? array_map('intval', explode(',', $j['mescole_ids'])) : [];
|
|
$j['matrice_lines'] = $j['matrice_lines'] ? array_map('intval', explode(',', $j['matrice_lines'])) : [];
|
|
$j['from_mago'] = (int)$j['from_mago'] > 0;
|
|
$j['mago_changed'] = (int)$j['mago_changed'] === 1;
|
|
$j['start_ms'] = pvMs($j['start_time']);
|
|
$j['open_pause_ms'] = pvMs($j['open_pause']);
|
|
$j['pause_sec'] = (int)$j['pause_sec'];
|
|
$ph = ['lotto_mescola' => 0, 'parametri_macchina' => 0, 'problema' => 0];
|
|
foreach (array_filter(explode(',', (string)$j['photos'])) as $t) if (isset($ph[$t])) $ph[$t]++;
|
|
$j['photos'] = $ph;
|
|
unset($j['start_time'], $j['open_pause']);
|
|
}
|
|
unset($j);
|
|
return $rows;
|
|
}
|
|
|
|
function pvDate($k)
|
|
{
|
|
$v = trim((string)($_POST[$k] ?? ''));
|
|
return MagoSalesStats::isValidDate($v) ? $v : null;
|
|
}
|
|
|
|
/* ===================== AJAX ===================== */
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
|
if ($action !== '') {
|
|
try {
|
|
if ($action === 'list') {
|
|
$lines = [];
|
|
foreach ($pdo->query("SELECT * FROM production_lines ORDER BY line_number, id")->fetchAll(PDO::FETCH_ASSOC) as $l) {
|
|
$lines[] = [
|
|
'id' => (int)$l['id'],
|
|
'name' => $l['name'],
|
|
'color' => !empty($l['color']) ? $l['color'] : '#64748b',
|
|
'active' => !isset($l['status']) || $l['status'] === '' || $l['status'] === 'active'
|
|
];
|
|
}
|
|
$nuovi = pvHas($pdo, 'mago_prod_queue') ? (int)$pdo->query("SELECT COUNT(*) FROM mago_prod_queue WHERE queue_status = 'nuovo' OR (queue_status = 'importato' AND mago_changed = 1)")->fetchColumn() : 0;
|
|
pvOut(['success' => true, 'now_ms' => time() * 1000, 'lines' => $lines, 'jobs' => pvJobs($pdo), 'nuoviMago' => $nuovi]);
|
|
}
|
|
|
|
if ($action === 'options') {
|
|
pvOut([
|
|
'success' => true,
|
|
'matrici' => $pdo->query("SELECT id, nome FROM matrice WHERE TRIM(nome) <> '' ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC),
|
|
'mescole' => $pdo->query("SELECT id, nome FROM mescole WHERE is_active = 1 ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC),
|
|
'clienti' => $pdo->query("SELECT id, nome FROM clients ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'matinfo') {
|
|
pvOut(['success' => true, 'info' => MagoProduction::matriceInfo($pdo, (int)($_GET['id'] ?? 0))]);
|
|
}
|
|
|
|
// ---------- nuovo / modifica ----------
|
|
if ($action === 'save') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$idm = (int)($_POST['idmatrice'] ?? 0);
|
|
$lin = (int)($_POST['id_linea'] ?? 0);
|
|
if (!$idm) throw new RuntimeException('Scegli la matrice');
|
|
if (!$lin) throw new RuntimeException('Scegli la linea');
|
|
$status = (int)($_POST['id_status'] ?? PV_TODO);
|
|
if (!in_array($status, [PV_TODO, PV_PLAN], true)) $status = PV_TODO;
|
|
$num = function ($k, $max = 99999999) {
|
|
return min($max, round((float)str_replace(',', '.', (string)($_POST[$k] ?? 0)), 2));
|
|
};
|
|
$f = [
|
|
'conferma_ordine' => mb_substr(trim((string)($_POST['conferma_ordine'] ?? '')), 0, 50) ?: null,
|
|
'idmatrice' => $idm,
|
|
'id_linea' => $lin,
|
|
'id_cliente' => (int)($_POST['id_cliente'] ?? 0) ?: null,
|
|
'data_zibo' => pvDate('data_zibo'),
|
|
'data_cliente' => pvDate('data_cliente'),
|
|
'data_produzione' => pvDate('data_produzione'),
|
|
'metri' => $num('metri'),
|
|
'kg_sp' => $num('kg_sp'),
|
|
'kg_p' => $num('kg_p'),
|
|
'ore_previste' => $num('ore_previste', 999.99),
|
|
'note_extra' => trim((string)($_POST['note_extra'] ?? '')) ?: null,
|
|
];
|
|
$pdo->beginTransaction();
|
|
if ($id) {
|
|
$st = $pdo->prepare("SELECT id_status FROM productiondata WHERE id = ? FOR UPDATE");
|
|
$st->execute([$id]);
|
|
$cur = $st->fetchColumn();
|
|
if ($cur === false) throw new RuntimeException('Lavoro non trovato');
|
|
$cur = (int)$cur;
|
|
$set = implode(', ', array_map(fn($k) => "$k = :$k", array_keys($f)));
|
|
if (in_array($cur, [PV_TODO, PV_PLAN], true) && $status !== $cur) {
|
|
$set .= ', id_status = :st, priority = :pr';
|
|
$f['st'] = $status;
|
|
$f['pr'] = MagoProduction::nextPriority($pdo, $status);
|
|
}
|
|
$f['id'] = $id;
|
|
$pdo->prepare("UPDATE productiondata SET $set WHERE id = :id")->execute($f);
|
|
} else {
|
|
$f['id_status'] = $status;
|
|
$f['priority'] = MagoProduction::nextPriority($pdo, $status);
|
|
$cols = array_keys($f);
|
|
$pdo->prepare("INSERT INTO productiondata (Data, " . implode(', ', $cols) . ", fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto)
|
|
VALUES (CURDATE(), :" . implode(', :', $cols) . ", 0, '00:00:00', '00:00:00', 0, 0, 0)")->execute($f);
|
|
$id = (int)$pdo->lastInsertId();
|
|
}
|
|
$pdo->prepare("DELETE FROM productiondata_mescole WHERE id_productiondata = ?")->execute([$id]);
|
|
$ins = $pdo->prepare("INSERT INTO productiondata_mescole (id_productiondata, id_mescola) VALUES (?, ?)");
|
|
foreach (array_unique(array_filter(array_map('intval', (array)($_POST['mescole'] ?? [])))) as $m) $ins->execute([$id, $m]);
|
|
$pdo->commit();
|
|
pvOut(['success' => true, 'id' => $id]);
|
|
}
|
|
|
|
// ---------- ordine di tutte le righe (anche quelle nascoste dal filtro) ----------
|
|
if ($action === 'order') {
|
|
$p = json_decode((string)($_POST['payload'] ?? ''), true);
|
|
if (!is_array($p)) throw new RuntimeException('Dati non validi');
|
|
$upd = $pdo->prepare("UPDATE productiondata SET id_status = ?, priority = ? WHERE id = ? AND id_status IN (" . PV_TODO . "," . PV_PLAN . ")");
|
|
$pdo->beginTransaction();
|
|
foreach ([PV_PLAN => 'plan', PV_TODO => 'todo'] as $st => $k) {
|
|
$n = 0;
|
|
foreach ((array)($p[$k] ?? []) as $id) $upd->execute([$st, ++$n, (int)$id]);
|
|
}
|
|
$pdo->commit();
|
|
pvOut(['success' => true]);
|
|
}
|
|
|
|
// ---------- azioni su più righe ----------
|
|
if ($action === 'bulk') {
|
|
$ids = array_values(array_filter(array_map('intval', (array)($_POST['ids'] ?? []))));
|
|
if (!$ids) throw new RuntimeException('Nessuna riga selezionata');
|
|
$op = (string)($_POST['op'] ?? '');
|
|
$pdo->beginTransaction();
|
|
if ($op === 'program' || $op === 'unprogram') {
|
|
$to = $op === 'program' ? PV_PLAN : PV_TODO;
|
|
$prio = MagoProduction::nextPriority($pdo, $to);
|
|
$st = $pdo->prepare("UPDATE productiondata SET id_status = ?, priority = ? WHERE id = ? AND id_status IN (" . PV_TODO . "," . PV_PLAN . ")");
|
|
foreach ($ids as $id) $st->execute([$to, $prio++, $id]);
|
|
} elseif ($op === 'line') {
|
|
$lin = (int)($_POST['line'] ?? 0);
|
|
if (!$lin) throw new RuntimeException('Scegli la linea');
|
|
$st = $pdo->prepare("UPDATE productiondata SET id_linea = ? WHERE id = ? AND id_status IN (" . PV_TODO . "," . PV_PLAN . ")");
|
|
foreach ($ids as $id) $st->execute([$lin, $id]);
|
|
} else {
|
|
throw new RuntimeException('Azione sconosciuta');
|
|
}
|
|
$pdo->commit();
|
|
pvOut(['success' => true, 'n' => count($ids)]);
|
|
}
|
|
|
|
// ---------- duplica (per ripetere un lavoro) ----------
|
|
if ($action === 'duplicate') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$pdo->beginTransaction();
|
|
$prio = MagoProduction::nextPriority($pdo, PV_TODO);
|
|
$st = $pdo->prepare("INSERT INTO productiondata (Data, conferma_ordine, idmatrice, id_linea, id_cliente, data_zibo, data_cliente, metri, kg_sp, kg_p,
|
|
ore_previste, note_extra, id_status, priority, fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto)
|
|
SELECT CURDATE(), conferma_ordine, idmatrice, id_linea, id_cliente, data_zibo, data_cliente, metri, kg_sp, kg_p,
|
|
ore_previste, note_extra, ?, ?, 0, '00:00:00', '00:00:00', 0, 0, 0 FROM productiondata WHERE id = ?");
|
|
$st->execute([PV_TODO, $prio, $id]);
|
|
if (!$st->rowCount()) throw new RuntimeException('Lavoro non trovato');
|
|
$new = (int)$pdo->lastInsertId();
|
|
$pdo->prepare("INSERT INTO productiondata_mescole (id_productiondata, id_mescola) SELECT ?, id_mescola FROM productiondata_mescole WHERE id_productiondata = ?")->execute([$new, $id]);
|
|
$pdo->commit();
|
|
pvOut(['success' => true, 'id' => $new]);
|
|
}
|
|
|
|
// ---------- elimina (solo non avviati) ----------
|
|
if ($action === 'delete') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$pdo->beginTransaction();
|
|
$st = $pdo->prepare("SELECT id_status FROM productiondata WHERE id = ? FOR UPDATE");
|
|
$st->execute([$id]);
|
|
$cur = $st->fetchColumn();
|
|
if ($cur === false) throw new RuntimeException('Lavoro non trovato');
|
|
if (!in_array((int)$cur, [PV_TODO, PV_PLAN], true)) throw new RuntimeException('Si possono eliminare solo i lavori non ancora avviati');
|
|
if (pvHas($pdo, 'mago_prod_queue')) {
|
|
$pdo->prepare("UPDATE mago_prod_queue SET queue_status = 'nuovo', productiondata_id = NULL, imported_at = NULL, mago_changed = 0, mago_changes = '', updated_at = NOW() WHERE productiondata_id = ?")->execute([$id]);
|
|
}
|
|
$pdo->prepare("DELETE FROM productiondata_mescole WHERE id_productiondata = ?")->execute([$id]);
|
|
$pdo->prepare("DELETE FROM productiondata WHERE id = ?")->execute([$id]);
|
|
$pdo->commit();
|
|
pvOut(['success' => true]);
|
|
}
|
|
|
|
// ---------- foto di un lavoro in macchina ----------
|
|
if ($action === 'photos') {
|
|
$id = (int)($_GET['id'] ?? 0);
|
|
$st = $pdo->prepare("SELECT id, photo_type, param_position, filename, created_at FROM production_photos WHERE production_id = ? ORDER BY created_at DESC, id DESC");
|
|
$st->execute([$id]);
|
|
$params = [];
|
|
$st2 = $pdo->prepare("SELECT pl.position, pl.short_label, pl.label FROM production_line_params pl JOIN productiondata p ON p.id_linea = pl.line_id WHERE p.id = ? ORDER BY pl.position");
|
|
try {
|
|
$st2->execute([$id]);
|
|
$params = $st2->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
}
|
|
pvOut(['success' => true, 'rows' => $st->fetchAll(PDO::FETCH_ASSOC), 'params' => $params]);
|
|
}
|
|
|
|
pvOut(['success' => false, 'message' => 'Azione sconosciuta']);
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
$msg = $e->getMessage();
|
|
if (strpos($msg, '1451') !== false) $msg = 'Il lavoro ha dati collegati (foto, registrazioni) e non si può eliminare';
|
|
pvOut(['success' => false, 'message' => $msg]);
|
|
}
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="it">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
|
<?php include('cssinclude.php'); ?>
|
|
<title>Programmazione produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<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" />
|
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
|
|
<style>
|
|
:root {
|
|
--ink: #1f2d3d;
|
|
--muted: #64748b;
|
|
--line: #e2e8f0;
|
|
--late: #dc2626;
|
|
--soon: #d97706;
|
|
}
|
|
|
|
body {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.pv-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.pv-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.pv-icon {
|
|
width: 52px;
|
|
height: 52px;
|
|
border-radius: 14px;
|
|
background: linear-gradient(135deg, #0ea5e9, #2563eb);
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.45rem;
|
|
box-shadow: 0 6px 16px rgba(37, 99, 235, .28);
|
|
}
|
|
|
|
.pv-title h5 {
|
|
font-weight: 800;
|
|
margin: 0 0 2px;
|
|
}
|
|
|
|
.pv-title small {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* filtri */
|
|
.pv-tools {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.lpill {
|
|
border: 1.5px solid color-mix(in srgb, var(--c) 30%, #fff);
|
|
background: #fff;
|
|
background: color-mix(in srgb, var(--c) 10%, #fff);
|
|
border-radius: 12px;
|
|
padding: 6px 14px 6px 10px;
|
|
font-weight: 700;
|
|
color: var(--ink);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
line-height: 1.15;
|
|
text-align: left;
|
|
}
|
|
|
|
.lpill .dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--c);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.lpill small {
|
|
display: block;
|
|
font-weight: 600;
|
|
color: var(--muted);
|
|
font-size: .75rem;
|
|
}
|
|
|
|
.lpill:hover {
|
|
background: color-mix(in srgb, var(--c) 18%, #fff);
|
|
}
|
|
|
|
.lpill.on {
|
|
border-color: var(--c);
|
|
background: color-mix(in srgb, var(--c) 24%, #fff);
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--c) 25%, transparent);
|
|
}
|
|
|
|
.pv-search {
|
|
position: relative;
|
|
flex: 0 1 300px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.pv-search i {
|
|
position: absolute;
|
|
left: 11px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.pv-search input {
|
|
padding-left: 32px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* sezioni */
|
|
.sec {
|
|
background: #fff;
|
|
border: 1px solid var(--line);
|
|
border-radius: 16px;
|
|
padding: 16px 18px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.sec-h {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.sec-h h6 {
|
|
font-weight: 800;
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
}
|
|
|
|
.sec-h .cnt {
|
|
background: #f1f5f9;
|
|
border-radius: 999px;
|
|
padding: 1px 10px;
|
|
font-weight: 700;
|
|
font-size: .85rem;
|
|
}
|
|
|
|
.sec-h .tot {
|
|
color: var(--muted);
|
|
font-size: .85rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* in macchina */
|
|
.act-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 12px;
|
|
align-items: start;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.act-grid {
|
|
grid-template-columns: 1fr !important;
|
|
}
|
|
}
|
|
|
|
.act {
|
|
border: 1px solid var(--line);
|
|
border-left: 6px solid var(--c);
|
|
border-radius: 12px;
|
|
padding: 12px 14px;
|
|
background: #fff;
|
|
background: color-mix(in srgb, var(--c) 12%, #fff);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.act:hover {
|
|
background: color-mix(in srgb, var(--c) 18%, #fff);
|
|
}
|
|
|
|
.act.free {
|
|
cursor: default;
|
|
background: color-mix(in srgb, var(--c) 5%, #fff);
|
|
border-style: dashed;
|
|
border-left-style: solid;
|
|
}
|
|
|
|
.act .lname {
|
|
font-weight: 800;
|
|
font-size: .9rem;
|
|
}
|
|
|
|
.act .r1 .st {
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.act .r1 {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.act .mat {
|
|
font-weight: 800;
|
|
font-size: 1.15rem;
|
|
}
|
|
|
|
.act .tm {
|
|
margin-left: auto;
|
|
font-weight: 800;
|
|
font-size: 1.25rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.act .sub {
|
|
color: var(--muted);
|
|
font-size: .88rem;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.act .prog {
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, .8);
|
|
border-radius: 99px;
|
|
overflow: hidden;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.act .prog span {
|
|
display: block;
|
|
height: 100%;
|
|
background: #16a34a;
|
|
}
|
|
|
|
.act .tools {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.ph-btn {
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 3px 9px;
|
|
font-size: .85rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.ph-btn .n {
|
|
background: #e2e8f0;
|
|
border-radius: 6px;
|
|
padding: 0 5px;
|
|
margin-left: 3px;
|
|
font-size: .75rem;
|
|
}
|
|
|
|
.ph-btn .n.yes {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.st {
|
|
font-size: .72rem;
|
|
font-weight: 800;
|
|
color: #fff;
|
|
border-radius: 6px;
|
|
padding: 2px 8px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* tabelle */
|
|
.pv-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
font-size: .9rem;
|
|
}
|
|
|
|
.pv-table th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: #f8fafc;
|
|
font-weight: 700;
|
|
font-size: .78rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: .03em;
|
|
color: var(--muted);
|
|
padding: 8px;
|
|
border-bottom: 1px solid var(--line);
|
|
white-space: nowrap;
|
|
z-index: 1;
|
|
}
|
|
|
|
.pv-table th.sortable {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.pv-table th.sortable:hover {
|
|
color: var(--ink);
|
|
}
|
|
|
|
.pv-table td {
|
|
padding: 7px 8px;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
vertical-align: middle;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pv-table tbody tr {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background .1s;
|
|
}
|
|
|
|
.pv-table tbody tr:hover td {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.pv-table tbody tr td:first-child {
|
|
border-left: 5px solid var(--c, transparent);
|
|
}
|
|
|
|
.pv-table tbody tr.sel td {
|
|
background: #eff6ff;
|
|
}
|
|
|
|
.pv-table tfoot td {
|
|
font-weight: 800;
|
|
padding: 8px;
|
|
border-top: 2px solid var(--line);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.drag {
|
|
cursor: grab;
|
|
color: #94a3b8;
|
|
font-size: 1.15rem;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.drag:hover {
|
|
color: var(--ink);
|
|
}
|
|
|
|
.pos {
|
|
font-weight: 800;
|
|
color: #3730a3;
|
|
background: #eef2ff;
|
|
border-radius: 6px;
|
|
padding: 1px 7px;
|
|
font-size: .8rem;
|
|
}
|
|
|
|
.mat-c {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.mat-c img {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 6px;
|
|
object-fit: cover;
|
|
border: 1px solid var(--line);
|
|
}
|
|
|
|
.mat-c b {
|
|
font-size: .98rem;
|
|
}
|
|
|
|
.tag {
|
|
font-size: .68rem;
|
|
font-weight: 800;
|
|
border-radius: 5px;
|
|
padding: 1px 6px;
|
|
}
|
|
|
|
.tag.mago {
|
|
background: #fff7ed;
|
|
color: #c2410c;
|
|
border: 1px solid #fed7aa;
|
|
}
|
|
|
|
.tag.chg {
|
|
background: #fee2e2;
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.tag.warn {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.mchip {
|
|
display: inline-block;
|
|
font-size: .74rem;
|
|
line-height: 1.35;
|
|
background: #f1f5f9;
|
|
border-radius: 6px;
|
|
padding: 1px 6px;
|
|
color: #334155;
|
|
max-width: 170px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.ldot {
|
|
display: inline-block;
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--c);
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.cli {
|
|
max-width: 190px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.dz.late {
|
|
color: var(--late);
|
|
font-weight: 800;
|
|
}
|
|
|
|
.dz.soon {
|
|
color: var(--soon);
|
|
font-weight: 800;
|
|
}
|
|
|
|
.num {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.zero {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.note-ic {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.note-ic.has {
|
|
color: #dc2626;
|
|
}
|
|
|
|
.acts {
|
|
text-align: right;
|
|
}
|
|
|
|
.acts .btn {
|
|
padding: 2px 7px;
|
|
}
|
|
|
|
.hide {
|
|
display: none !important;
|
|
}
|
|
|
|
.empty-row td {
|
|
text-align: center;
|
|
color: var(--muted);
|
|
padding: 22px !important;
|
|
cursor: default;
|
|
}
|
|
|
|
.sortable-ghost td {
|
|
background: #dbeafe !important;
|
|
}
|
|
|
|
/* barra azioni multiple */
|
|
.bulk {
|
|
position: sticky;
|
|
bottom: 12px;
|
|
z-index: 20;
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
background: #0f172a;
|
|
color: #fff;
|
|
border-radius: 14px;
|
|
padding: 10px 16px;
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, .35);
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.bulk select {
|
|
width: auto;
|
|
}
|
|
|
|
/* toast */
|
|
.pv-toast {
|
|
position: fixed;
|
|
right: 20px;
|
|
bottom: 20px;
|
|
z-index: 3000;
|
|
background: #0f172a;
|
|
color: #fff;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
font-weight: 600;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all .25s;
|
|
max-width: 420px;
|
|
}
|
|
|
|
.pv-toast.err {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
.pv-toast.warn {
|
|
background: #b45309;
|
|
}
|
|
|
|
.pv-toast.show {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
#mJob .form-label {
|
|
font-weight: 700;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.hint {
|
|
font-size: .8rem;
|
|
color: var(--muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.hint a {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.gal {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.gal a {
|
|
position: relative;
|
|
display: block;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--line);
|
|
}
|
|
|
|
.gal img {
|
|
width: 100%;
|
|
aspect-ratio: 4/3;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
|
|
.gal span {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(transparent, rgba(0, 0, 0, .7));
|
|
color: #fff;
|
|
font-size: .75rem;
|
|
font-weight: 700;
|
|
padding: 12px 6px 4px;
|
|
}
|
|
|
|
#noteBody {
|
|
white-space: pre-wrap;
|
|
font-size: 1rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrapper toggled" id="appWrapper">
|
|
<?php include('include/navbar.php'); ?>
|
|
<?php include('include/topbar.php'); ?>
|
|
|
|
<div class="page-wrapper">
|
|
<div class="page-content">
|
|
<div class="pv-head mb-3">
|
|
<div class="pv-title">
|
|
<div class="pv-icon"><i class="bi bi-list-check"></i></div>
|
|
<div>
|
|
<h5>Programmazione produzione</h5>
|
|
<small>Clic su una riga per modificarla · trascina <i class="bi bi-grip-vertical"></i> per cambiare l'ordine</small>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<a href="production_mago_queue.php" class="btn btn-warning fw-bold"><i class="bi bi-inbox-fill me-1"></i> Nuovi da Mago <span class="badge bg-dark ms-1 hide" id="nMago"></span></a>
|
|
<a href="production_board.php" class="btn btn-outline-primary fw-bold"><i class="bi bi-kanban me-1"></i> Vista tabellone</a>
|
|
<button class="btn btn-primary fw-bold" id="btnNew"><i class="bi bi-plus-lg me-1"></i> Nuovo lavoro</button>
|
|
<a href="production_dashboard.php" class="btn btn-outline-secondary">Torna</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pv-tools mb-3">
|
|
<div class="d-flex gap-2 flex-wrap" id="linePills"></div>
|
|
<div class="pv-search"><i class="bi bi-search"></i><input type="search" class="form-control" id="q" placeholder="Cerca ordine, matrice, cliente, mescola"></div>
|
|
</div>
|
|
|
|
<div class="sec" id="secActive">
|
|
<div class="sec-h">
|
|
<h6><i class="bi bi-gear-wide-connected text-success me-1"></i> In macchina</h6><span class="cnt" id="cActive">0</span>
|
|
</div>
|
|
<div class="act-grid" id="actGrid"></div>
|
|
</div>
|
|
|
|
<div class="sec">
|
|
<div class="sec-h">
|
|
<h6><i class="bi bi-list-ol text-primary me-1"></i> Programmati</h6><span class="cnt" id="cPlan">0</span><span class="tot" id="tPlan"></span>
|
|
<span class="ms-auto hint">L'ordine vale per tutte le righe, anche quelle nascoste dal filtro</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="pv-table" id="tPlanTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:34px"></th>
|
|
<th>#</th>
|
|
<th>Ordine</th>
|
|
<th>Matrice</th>
|
|
<th>Mescole</th>
|
|
<th>Linea</th>
|
|
<th>Cliente</th>
|
|
<th>Data Zibo</th>
|
|
<th>Data cliente</th>
|
|
<th class="num">Metri</th>
|
|
<th class="num">Kg SP</th>
|
|
<th class="num">Kg P</th>
|
|
<th class="num">Ore</th>
|
|
<th>Note</th>
|
|
<th class="acts">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="planBody"></tbody>
|
|
<tfoot id="planFoot"></tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sec">
|
|
<div class="sec-h">
|
|
<h6><i class="bi bi-inbox text-warning me-1"></i> Da programmare</h6><span class="cnt" id="cTodo">0</span><span class="tot" id="tTodo"></span>
|
|
<span class="ms-auto hint">Clic sulle intestazioni per ordinare · seleziona più righe per azioni multiple</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="pv-table" id="tTodoTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:34px"><input type="checkbox" class="form-check-input" id="selAll" title="Seleziona tutte le righe visibili"></th>
|
|
<th style="width:28px"></th>
|
|
<th class="sortable" data-sort="conferma_ordine">Ordine</th>
|
|
<th class="sortable" data-sort="matrice">Matrice</th>
|
|
<th>Mescole</th>
|
|
<th class="sortable" data-sort="linea">Linea</th>
|
|
<th class="sortable" data-sort="cliente">Cliente</th>
|
|
<th class="sortable" data-sort="data_zibo">Data Zibo</th>
|
|
<th class="sortable" data-sort="data_cliente">Data cliente</th>
|
|
<th class="num sortable" data-sort="metri">Metri</th>
|
|
<th class="num sortable" data-sort="kg_sp">Kg SP</th>
|
|
<th class="num sortable" data-sort="kg_p">Kg P</th>
|
|
<th class="num sortable" data-sort="ore_previste">Ore</th>
|
|
<th>Note</th>
|
|
<th class="acts">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="todoBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="bulk hide" id="bulk">
|
|
<b id="bulkN"></b>
|
|
<button class="btn btn-success btn-sm fw-bold" id="bProgram"><i class="bi bi-arrow-up-circle me-1"></i> Programma (in fondo)</button>
|
|
<div class="d-flex gap-1 align-items-center">
|
|
<select class="form-select form-select-sm" id="bLine"></select>
|
|
<button class="btn btn-light btn-sm fw-bold" id="bLineGo">Cambia linea</button>
|
|
</div>
|
|
<button class="btn btn-outline-light btn-sm ms-auto" id="bClear">Annulla selezione</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
|
|
<!-- MODULO LAVORO -->
|
|
<div class="modal fade" id="mJob" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<form class="modal-content" id="fJob">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" id="mTitle">Nuovo lavoro</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="fId">
|
|
<div class="alert alert-info py-2 hide" id="wActive"><i class="bi bi-gear-fill me-1"></i> Lavoro in macchina: puoi correggere i dati, lo stato si cambia dalla pagina di produzione.</div>
|
|
<div class="alert alert-danger py-2 hide" id="wChanged"></div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Matrice</label>
|
|
<select class="form-select" name="idmatrice" id="fMat" required></select>
|
|
<div class="hint" id="hMat"></div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Linea</label>
|
|
<select class="form-select" name="id_linea" id="fLin" required></select>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Mescole</label>
|
|
<select class="form-select" name="mescole[]" id="fMes" multiple></select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Cliente</label>
|
|
<select class="form-select" name="id_cliente" id="fCli"></select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Conferma d'ordine</label>
|
|
<input type="text" class="form-control" name="conferma_ordine" id="fOrd" maxlength="50">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Data Zibo</label>
|
|
<input type="date" class="form-control" name="data_zibo" id="fDz">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Data cliente</label>
|
|
<input type="date" class="form-control" name="data_cliente" id="fDc">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Data produzione <span class="text-muted fw-normal">(facolt.)</span></label>
|
|
<input type="date" class="form-control" name="data_produzione" id="fDp">
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label">Metri</label>
|
|
<input type="number" step="0.01" min="0" class="form-control" name="metri" id="fMt">
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label">Kg SP</label>
|
|
<input type="number" step="0.01" min="0" class="form-control" name="kg_sp" id="fKs">
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label">Kg P</label>
|
|
<input type="number" step="0.01" min="0" class="form-control" name="kg_p" id="fKp">
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label">Ore previste</label>
|
|
<input type="number" step="0.01" min="0" class="form-control" name="ore_previste" id="fOre">
|
|
<div class="hint" id="hOre"></div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Note</label>
|
|
<textarea class="form-control" name="note_extra" id="fNote" rows="4"></textarea>
|
|
</div>
|
|
<div class="col-12" id="wSt">
|
|
<label class="form-label d-block">Stato</label>
|
|
<div class="btn-group" role="group">
|
|
<input type="radio" class="btn-check" name="id_status" id="fSt1" value="1">
|
|
<label class="btn btn-outline-secondary" for="fSt1">Da programmare</label>
|
|
<input type="radio" class="btn-check" name="id_status" id="fSt6" value="6">
|
|
<label class="btn btn-outline-primary" for="fSt6">Programmato (in fondo)</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-danger" id="btnDel"><i class="bi bi-trash me-1"></i> Elimina</button>
|
|
<button type="button" class="btn btn-outline-secondary me-auto" id="btnDup"><i class="bi bi-copy me-1"></i> Duplica</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
|
<button type="submit" class="btn btn-primary" id="btnSave"><i class="bi bi-check-lg me-1"></i> Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NOTE -->
|
|
<div class="modal fade" id="mNote" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" id="noteTitle">Note</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="noteBody"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FOTO -->
|
|
<div class="modal fade" id="mPhoto" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" id="phTitle">Foto</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-pills mb-3" id="phTabs">
|
|
<li class="nav-item"><button class="nav-link" data-pt="lotto_mescola"><i class="bi bi-camera-fill me-1"></i> Lotto mescola</button></li>
|
|
<li class="nav-item"><button class="nav-link" data-pt="parametri_macchina"><i class="bi bi-sliders me-1"></i> Parametri macchina</button></li>
|
|
<li class="nav-item"><button class="nav-link" data-pt="problema"><i class="bi bi-exclamation-triangle-fill me-1"></i> Problema</button></li>
|
|
</ul>
|
|
<div class="d-flex gap-2 align-items-center mb-3 flex-wrap">
|
|
<select class="form-select w-auto hide" id="phSlot"></select>
|
|
<label class="btn btn-primary mb-0"><i class="bi bi-upload me-1"></i> Carica foto<input type="file" accept="image/*" id="phFile" hidden></label>
|
|
<span class="hint" id="phMsg"></span>
|
|
</div>
|
|
<div class="gal" id="phGal"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
<script>
|
|
$(function() {
|
|
const ST = {
|
|
TODO: <?= PV_TODO ?>,
|
|
PLAN: <?= PV_PLAN ?>,
|
|
ACTIVE: <?= json_encode(PV_ACTIVE) ?>
|
|
};
|
|
const H = 3600e3;
|
|
let D = {
|
|
lines: [],
|
|
jobs: []
|
|
};
|
|
let skew = 0,
|
|
busy = false,
|
|
opts = null,
|
|
editing = null,
|
|
matInfo = null;
|
|
let lineFilter = 0,
|
|
sortKey = 'data_zibo',
|
|
sortDir = 1;
|
|
const sel = new Set();
|
|
try {
|
|
lineFilter = +(localStorage.getItem('pv_line') || 0);
|
|
} catch (e) {}
|
|
|
|
/* ---------- utilità ---------- */
|
|
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
} [c]));
|
|
const num = (v, d = 0) => (+v || 0).toLocaleString('it-IT', {
|
|
minimumFractionDigits: d,
|
|
maximumFractionDigits: d
|
|
});
|
|
const now = () => Date.now() - skew;
|
|
const GG = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
|
|
const dIt = s => {
|
|
if (!s) return '<span class="zero">—</span>';
|
|
const d = new Date(s + 'T00:00:00');
|
|
return `${GG[d.getDay()]} ${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getFullYear()).slice(2)}`;
|
|
};
|
|
const daysTo = s => s ? Math.round((new Date(s + 'T00:00:00') - new Date(new Date(now()).toDateString())) / 864e5) : null;
|
|
const lineOf = id => D.lines.find(l => l.id === id) || {
|
|
name: '?',
|
|
color: '#94a3b8'
|
|
};
|
|
const hms = ms => {
|
|
const s = Math.max(0, Math.floor(ms / 1000));
|
|
return `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s % 3600 / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
|
};
|
|
|
|
function toast(msg, kind) {
|
|
const el = $(`<div class="pv-toast ${kind || ''}">${esc(msg)}</div>`).appendTo('body');
|
|
setTimeout(() => el.addClass('show'), 10);
|
|
setTimeout(() => {
|
|
el.removeClass('show');
|
|
setTimeout(() => el.remove(), 300);
|
|
}, kind ? 5000 : 2200);
|
|
}
|
|
|
|
function post(data) {
|
|
return $.post('?', data, null, 'json').then(r => {
|
|
if (!r.success) {
|
|
toast(r.message || 'Errore', 'err');
|
|
return $.Deferred().reject().promise();
|
|
}
|
|
return r;
|
|
}, () => {
|
|
toast('Errore di rete', 'err');
|
|
return $.Deferred().reject().promise();
|
|
});
|
|
}
|
|
|
|
/* ---------- caricamento ---------- */
|
|
function load() {
|
|
return $.getJSON('?action=list').then(r => {
|
|
if (!r.success) return toast(r.message || 'Errore', 'err');
|
|
skew = Date.now() - r.now_ms;
|
|
D = r;
|
|
if (lineFilter && !D.lines.some(l => l.id === lineFilter)) lineFilter = 0;
|
|
render();
|
|
});
|
|
}
|
|
|
|
/* ---------- disegno ---------- */
|
|
const userNote = n => String(n || '').trim() !== '';
|
|
const isMagoNote = n => /^Da Mago:/.test(String(n || '').trim());
|
|
|
|
function matCell(j) {
|
|
const tags = [];
|
|
if (j.from_mago) tags.push('<span class="tag mago">Mago</span>');
|
|
if (j.mago_changed) tags.push('<span class="tag chg" title="La riga è cambiata in Mago dopo l\'import">cambiato in Mago</span>');
|
|
if (j.matrice_lines.length && !j.matrice_lines.includes(j.id_linea)) tags.push('<span class="tag warn" title="La linea non è tra quelle della matrice">linea insolita</span>');
|
|
return `<div class="mat-c">${j.photo ? `<img src="photos/matrici/${esc(j.photo)}" alt="" loading="lazy">` : ''}<div><b>${esc(j.matrice || '?')}</b> ${tags.join(' ')}</div></div>`;
|
|
}
|
|
|
|
function mesCell(j) {
|
|
if (!j.mescole.length) return '<span class="zero">—</span>';
|
|
return j.mescole.slice(0, 2).map(m => `<span class="mchip" title="${esc(m)}">${esc(m)}</span>`).join(' ') + (j.mescole.length > 2 ? ` <span class="mchip" title="${esc(j.mescole.join(', '))}">+${j.mescole.length - 2}</span>` : '');
|
|
}
|
|
|
|
function dzCell(s) {
|
|
const d = daysTo(s);
|
|
const cls = d === null ? '' : (d < 0 ? 'late' : (d <= 3 ? 'soon' : ''));
|
|
return `<span class="dz ${cls}" title="${d === null ? '' : (d < 0 ? 'scaduta da ' + (-d) + ' giorni' : (d === 0 ? 'oggi' : 'tra ' + d + ' giorni'))}">${dIt(s)}</span>`;
|
|
}
|
|
const nCell = (v, d = 0) => `<td class="num">${v ? num(v, d) : '<span class="zero">0</span>'}</td>`;
|
|
|
|
function noteCell(j) {
|
|
const n = j.note_extra || '';
|
|
if (!userNote(n)) return '<td><i class="bi bi-sticky note-ic"></i></td>';
|
|
return `<td><a href="#" class="note-open" data-id="${j.id}" title="${esc(n.slice(0, 200))}"><i class="bi bi-sticky-fill note-ic ${isMagoNote(n) ? '' : 'has'}"></i></a></td>`;
|
|
}
|
|
|
|
function visible(j) {
|
|
if (lineFilter && j.id_linea !== lineFilter) return false;
|
|
const q = $('#q').val().trim().toLowerCase();
|
|
if (!q) return true;
|
|
return [j.conferma_ordine, j.matrice, j.cliente, ...j.mescole, j.note_extra].join(' ').toLowerCase().includes(q);
|
|
}
|
|
|
|
function render() {
|
|
// filtri linea con conteggi
|
|
const plan = D.jobs.filter(j => j.id_status === ST.PLAN);
|
|
const todo = D.jobs.filter(j => j.id_status === ST.TODO);
|
|
const act = D.jobs.filter(j => ST.ACTIVE.includes(j.id_status));
|
|
const pill = (id, name, color, n, h) => `<button class="lpill ${lineFilter === id ? 'on' : ''}" data-line="${id}" style="--c:${esc(color)}"><span class="dot"></span><span>${esc(name)}<small>${n} programmati · ${num(h, 0)} h</small></span></button>`;
|
|
$('#linePills').html(pill(0, 'Tutte', '#0f172a', plan.length, plan.reduce((s, j) => s + j.ore_previste, 0)) +
|
|
D.lines.filter(l => l.active || D.jobs.some(j => j.id_linea === l.id)).map(l => {
|
|
const p = plan.filter(j => j.id_linea === l.id);
|
|
return pill(l.id, l.name, l.color, p.length, p.reduce((s, j) => s + j.ore_previste, 0));
|
|
}).join(''));
|
|
$('#nMago').text(D.nuoviMago || '').toggleClass('hide', !D.nuoviMago);
|
|
|
|
// in macchina: una colonna per linea (anche quelle libere), così si usa tutta la larghezza
|
|
const shown = D.lines.filter(l => (l.active || act.some(j => j.id_linea === l.id)) && (!lineFilter || l.id === lineFilter));
|
|
const actV = act.filter(visible);
|
|
$('#cActive').text(actV.length);
|
|
$('#actGrid').css('grid-template-columns', `repeat(${Math.max(1, shown.length)}, minmax(0, 1fr))`);
|
|
$('#actGrid').html(shown.map(l => {
|
|
const jobs = actV.filter(j => j.id_linea === l.id);
|
|
if (!jobs.length) {
|
|
const next = plan.find(j => j.id_linea === l.id);
|
|
return `<div class="act free" style="--c:${esc(l.color)}">
|
|
<div class="r1"><span class="lname"><span class="ldot" style="--c:${esc(l.color)}"></span>${esc(l.name)}</span><span class="st" style="background:#94a3b8">Libera</span></div>
|
|
<div class="sub mt-2">${next ? `Prossimo: <b>${esc(next.matrice)}</b>${next.cliente ? ' · ' + esc(next.cliente) : ''}` : 'Nessun lavoro programmato'}</div>
|
|
</div>`;
|
|
}
|
|
return jobs.map(j => {
|
|
const ph = j.photos;
|
|
return `<div class="act" data-edit="${j.id}" style="--c:${esc(l.color)}">
|
|
<div class="r1"><span class="lname"><span class="ldot" style="--c:${esc(l.color)}"></span>${esc(l.name)}</span><span class="st" style="background:${esc(j.status_color)}">${esc(j.status_nome)}</span><span class="tm" data-timer="${j.id}">--:--:--</span></div>
|
|
<div class="r1 mt-1"><span class="mat">${esc(j.matrice)}</span></div>
|
|
<div class="sub">${esc(j.cliente || 'nessun cliente')}${j.conferma_ordine ? ' · ' + esc(j.conferma_ordine) : ''}</div>
|
|
${j.ore_previste ? `<div class="prog"><span data-prog="${j.id}"></span></div>` : ''}
|
|
<div class="tools">
|
|
<button class="ph-btn" data-photo="${j.id}" data-pt="lotto_mescola" title="Foto lotto mescola"><i class="bi bi-camera-fill"></i><span class="n ${ph.lotto_mescola ? 'yes' : ''}">${ph.lotto_mescola}</span></button>
|
|
<button class="ph-btn" data-photo="${j.id}" data-pt="parametri_macchina" title="Foto parametri macchina"><i class="bi bi-sliders"></i><span class="n ${ph.parametri_macchina ? 'yes' : ''}">${ph.parametri_macchina}</span></button>
|
|
<button class="ph-btn" data-photo="${j.id}" data-pt="problema" title="Foto problema"><i class="bi bi-exclamation-triangle-fill text-danger"></i><span class="n ${ph.problema ? 'yes' : ''}">${ph.problema}</span></button>
|
|
${userNote(j.note_extra) ? `<a href="#" class="note-open ms-auto" data-id="${j.id}"><i class="bi bi-sticky-fill note-ic ${isMagoNote(j.note_extra) ? '' : 'has'}"></i></a>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}).join(''));
|
|
|
|
// programmati: tutte le righe nel DOM (anche filtrate) così l'ordine resta completo
|
|
let pos = 0;
|
|
$('#planBody').html(plan.length ? plan.map(j => {
|
|
pos++;
|
|
const l = lineOf(j.id_linea);
|
|
return `<tr data-id="${j.id}" class="${visible(j) ? '' : 'hide'}" style="--c:${esc(l.color)}">
|
|
<td><span class="drag" title="Trascina per spostare"><i class="bi bi-grip-vertical"></i></span></td>
|
|
<td><span class="pos">${pos}</span></td>
|
|
<td>${esc(j.conferma_ordine || '—')}</td>
|
|
<td>${matCell(j)}</td>
|
|
<td>${mesCell(j)}</td>
|
|
<td><span class="ldot" style="--c:${esc(l.color)}"></span>${esc(l.name)}</td>
|
|
<td class="cli" title="${esc(j.cliente || '')}">${esc(j.cliente || '—')}</td>
|
|
<td>${dzCell(j.data_zibo)}</td><td>${dIt(j.data_cliente)}</td>
|
|
${nCell(j.metri)}${nCell(j.kg_sp)}${nCell(j.kg_p)}${nCell(j.ore_previste, 1)}
|
|
${noteCell(j)}
|
|
<td class="acts"><div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary act-edit" title="Modifica"><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-secondary act-down" title="Togli dalla programmazione"><i class="bi bi-arrow-down"></i></button>
|
|
<button class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" data-bs-popper-config='{"strategy":"fixed"}' title="Altro"></button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item act-top" href="#"><i class="bi bi-chevron-double-up me-2"></i>Porta in cima</a></li>
|
|
<li><a class="dropdown-item act-bottom" href="#"><i class="bi bi-chevron-double-down me-2"></i>Porta in fondo</a></li>
|
|
<li><a class="dropdown-item act-dup" href="#"><i class="bi bi-copy me-2"></i>Duplica</a></li>
|
|
</ul></div></td>
|
|
</tr>`;
|
|
}).join('') : '<tr class="empty-row"><td colspan="15">Nessun lavoro programmato: trascina qui dai "Da programmare" o usa la freccia ⬆</td></tr>');
|
|
const pv = plan.filter(visible);
|
|
$('#cPlan').text(pv.length);
|
|
$('#tPlan').text(pv.length ? `${num(pv.reduce((s, j) => s + j.metri, 0))} m · ${num(pv.reduce((s, j) => s + j.ore_previste, 0), 1)} ore` : '');
|
|
$('#planFoot').html(pv.length ? `<tr><td colspan="9" class="text-end text-muted">Totale ${lineFilter ? lineOf(lineFilter).name : 'visibili'}</td>${nCell(pv.reduce((s, j) => s + j.metri, 0))}${nCell(pv.reduce((s, j) => s + j.kg_sp, 0))}${nCell(pv.reduce((s, j) => s + j.kg_p, 0))}${nCell(pv.reduce((s, j) => s + j.ore_previste, 0), 1)}<td colspan="2"></td></tr>` : '');
|
|
|
|
// da programmare: ordinati con le intestazioni
|
|
const tv = todo.slice().sort(cmp);
|
|
$('#todoBody').html(tv.length ? tv.map(j => {
|
|
const l = lineOf(j.id_linea);
|
|
return `<tr data-id="${j.id}" class="${visible(j) ? '' : 'hide'} ${sel.has(j.id) ? 'sel' : ''}" style="--c:${esc(l.color)}">
|
|
<td><input type="checkbox" class="form-check-input rsel" ${sel.has(j.id) ? 'checked' : ''}></td>
|
|
<td><span class="drag" title="Trascina nei programmati"><i class="bi bi-grip-vertical"></i></span></td>
|
|
<td>${esc(j.conferma_ordine || '—')}</td>
|
|
<td>${matCell(j)}</td>
|
|
<td>${mesCell(j)}</td>
|
|
<td><span class="ldot" style="--c:${esc(l.color)}"></span>${esc(l.name)}</td>
|
|
<td class="cli" title="${esc(j.cliente || '')}">${esc(j.cliente || '—')}</td>
|
|
<td>${dzCell(j.data_zibo)}</td><td>${dIt(j.data_cliente)}</td>
|
|
${nCell(j.metri)}${nCell(j.kg_sp)}${nCell(j.kg_p)}${nCell(j.ore_previste, 1)}
|
|
${noteCell(j)}
|
|
<td class="acts"><div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary act-edit" title="Modifica"><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-success act-up" title="Programma (in fondo)"><i class="bi bi-arrow-up"></i></button>
|
|
<button class="btn btn-outline-secondary act-dup" title="Duplica"><i class="bi bi-copy"></i></button>
|
|
</div></td>
|
|
</tr>`;
|
|
}).join('') : '<tr class="empty-row"><td colspan="15">Nessun lavoro da programmare</td></tr>');
|
|
const tvv = todo.filter(visible);
|
|
$('#cTodo').text(tvv.length);
|
|
$('#tTodo').text(tvv.length ? `${num(tvv.reduce((s, j) => s + j.metri, 0))} m · ${num(tvv.reduce((s, j) => s + j.ore_previste, 0), 1)} ore` : '');
|
|
$('#tTodoTable th.sortable').each(function() {
|
|
$(this).find('.sarr').remove();
|
|
if (this.dataset.sort === sortKey) $(this).append(`<i class="bi ${sortDir > 0 ? 'bi-caret-up-fill' : 'bi-caret-down-fill'} sarr ms-1"></i>`);
|
|
});
|
|
renderBulk();
|
|
initSortable();
|
|
tick();
|
|
}
|
|
|
|
function cmp(a, b) {
|
|
const k = sortKey;
|
|
let x = k === 'linea' ? lineOf(a.id_linea).name : a[k],
|
|
y = k === 'linea' ? lineOf(b.id_linea).name : b[k];
|
|
if (x === null || x === '' || x === undefined) return 1;
|
|
if (y === null || y === '' || y === undefined) return -1;
|
|
const r = typeof x === 'number' ? x - y : String(x).localeCompare(String(y), 'it', {
|
|
numeric: true
|
|
});
|
|
return (r || a.id - b.id) * sortDir;
|
|
}
|
|
$(document).on('click', '#tTodoTable th.sortable', function() {
|
|
const k = this.dataset.sort;
|
|
sortDir = sortKey === k ? -sortDir : 1;
|
|
sortKey = k;
|
|
render();
|
|
});
|
|
|
|
/* ---------- timer dei lavori in macchina ---------- */
|
|
function tick() {
|
|
const t = now();
|
|
D.jobs.filter(j => ST.ACTIVE.includes(j.id_status)).forEach(j => {
|
|
if (!j.start_ms) return;
|
|
const cur = j.open_pause_ms ? t - j.open_pause_ms : 0;
|
|
const net = Math.max(0, t - j.start_ms - j.pause_sec * 1000 - cur);
|
|
$(`[data-timer="${j.id}"]`).text(hms(net)).css('color', j.open_pause_ms ? '#d97706' : '');
|
|
if (j.ore_previste) {
|
|
const p = net / (j.ore_previste * H);
|
|
$(`[data-prog="${j.id}"]`).css({
|
|
width: Math.min(100, p * 100) + '%',
|
|
background: p > 1 ? '#dc2626' : (j.open_pause_ms ? '#f59e0b' : '#16a34a')
|
|
});
|
|
}
|
|
});
|
|
}
|
|
setInterval(tick, 1000);
|
|
setInterval(() => {
|
|
if (!busy && !$('.modal.show').length && !sel.size) load();
|
|
}, 60000);
|
|
|
|
/* ---------- trascinamento ---------- */
|
|
let sortables = [];
|
|
|
|
function initSortable() {
|
|
sortables.forEach(s => s.destroy());
|
|
sortables = [Sortable.create(document.getElementById('planBody'), {
|
|
group: 'pv',
|
|
handle: '.drag',
|
|
animation: 150,
|
|
forceFallback: true,
|
|
fallbackOnBody: true,
|
|
fallbackTolerance: 3,
|
|
filter: '.empty-row',
|
|
onStart: () => busy = true,
|
|
onEnd: moved
|
|
}), Sortable.create(document.getElementById('todoBody'), {
|
|
group: 'pv',
|
|
handle: '.drag',
|
|
sort: false,
|
|
animation: 150,
|
|
forceFallback: true,
|
|
fallbackOnBody: true,
|
|
fallbackTolerance: 3,
|
|
filter: '.empty-row',
|
|
onStart: () => busy = true,
|
|
onEnd: moved
|
|
})];
|
|
}
|
|
|
|
function moved(e) {
|
|
busy = false;
|
|
if (e.from === e.to && e.oldIndex === e.newIndex) return;
|
|
const id = +e.item.dataset.id;
|
|
const j = D.jobs.find(x => x.id === id);
|
|
j.id_status = e.to.id === 'planBody' ? ST.PLAN : ST.TODO;
|
|
const order = [...document.querySelectorAll('#planBody tr[data-id]')].map(tr => +tr.dataset.id);
|
|
const plan = order.map(i => D.jobs.find(x => x.id === i)).filter(x => x && x.id_status === ST.PLAN);
|
|
D.jobs = [...plan, ...D.jobs.filter(x => x.id_status !== ST.PLAN)];
|
|
render();
|
|
saveOrder(e.from !== e.to ? (j.id_status === ST.PLAN ? 'Programmato' : 'Tolto dalla programmazione') : 'Ordine salvato');
|
|
}
|
|
|
|
function saveOrder(msg) {
|
|
busy = true;
|
|
const payload = {
|
|
plan: D.jobs.filter(j => j.id_status === ST.PLAN).map(j => j.id),
|
|
todo: D.jobs.filter(j => j.id_status === ST.TODO).sort(cmp).map(j => j.id)
|
|
};
|
|
return post({
|
|
action: 'order',
|
|
payload: JSON.stringify(payload)
|
|
}).then(() => toast(msg || 'Ordine salvato'), () => load()).always(() => busy = false);
|
|
}
|
|
|
|
/* ---------- azioni sulle righe ---------- */
|
|
const rowJob = el => D.jobs.find(j => j.id === +$(el).closest('[data-id]').data('id'));
|
|
|
|
function moveTo(j, status, where) {
|
|
const plan = D.jobs.filter(x => x.id_status === ST.PLAN && x.id !== j.id);
|
|
const others = D.jobs.filter(x => x.id_status !== ST.PLAN && x.id !== j.id);
|
|
j.id_status = status;
|
|
if (status === ST.PLAN) {
|
|
if (where === 'top') plan.unshift(j);
|
|
else plan.push(j);
|
|
} else others.push(j);
|
|
D.jobs = [...plan, ...others];
|
|
render();
|
|
return saveOrder(status === ST.PLAN ? (where === 'top' ? 'Portato in cima' : 'Programmato in fondo') : 'Tolto dalla programmazione');
|
|
}
|
|
$(document).on('click', '.act-up', function(e) {
|
|
e.stopPropagation();
|
|
moveTo(rowJob(this), ST.PLAN, 'bottom');
|
|
});
|
|
$(document).on('click', '.act-down', function(e) {
|
|
e.stopPropagation();
|
|
moveTo(rowJob(this), ST.TODO);
|
|
});
|
|
$(document).on('click', '.act-top', function(e) {
|
|
e.preventDefault();
|
|
moveTo(rowJob(this), ST.PLAN, 'top');
|
|
});
|
|
$(document).on('click', '.act-bottom', function(e) {
|
|
e.preventDefault();
|
|
moveTo(rowJob(this), ST.PLAN, 'bottom');
|
|
});
|
|
$(document).on('click', '.act-dup', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
duplicate(rowJob(this));
|
|
});
|
|
|
|
function duplicate(j) {
|
|
return post({
|
|
action: 'duplicate',
|
|
id: j.id
|
|
}).then(() => {
|
|
toast('Duplicato tra i "Da programmare"');
|
|
load();
|
|
});
|
|
}
|
|
|
|
// clic sulla riga (non su pulsanti, link o selezione) = modifica
|
|
$(document).on('click', '#planBody tr[data-id], #todoBody tr[data-id], .act[data-edit]', function(e) {
|
|
if ($(e.target).closest('button, a, input, .drag, .dropdown-menu').length) return;
|
|
openJob(D.jobs.find(j => j.id === +(this.dataset.id || this.dataset.edit)));
|
|
});
|
|
$(document).on('click', '.act-edit', function(e) {
|
|
e.stopPropagation();
|
|
openJob(rowJob(this));
|
|
});
|
|
|
|
/* ---------- filtri ---------- */
|
|
$(document).on('click', '.lpill', function() {
|
|
lineFilter = +this.dataset.line;
|
|
try {
|
|
localStorage.setItem('pv_line', String(lineFilter));
|
|
} catch (e) {}
|
|
render();
|
|
});
|
|
$('#q').on('input', render);
|
|
|
|
/* ---------- selezione multipla ---------- */
|
|
function renderBulk() {
|
|
$('#bulk').toggleClass('hide', !sel.size);
|
|
$('#bulkN').text(sel.size === 1 ? '1 selezionato' : sel.size + ' selezionati');
|
|
$('#bLine').html('<option value="">Linea…</option>' + D.lines.filter(l => l.active).map(l => `<option value="${l.id}">${esc(l.name)}</option>`).join(''));
|
|
const vis = D.jobs.filter(j => j.id_status === ST.TODO && visible(j));
|
|
$('#selAll').prop('checked', vis.length > 0 && vis.every(j => sel.has(j.id)));
|
|
}
|
|
$(document).on('change', '.rsel', function() {
|
|
const id = +$(this).closest('tr').data('id');
|
|
this.checked ? sel.add(id) : sel.delete(id);
|
|
$(this).closest('tr').toggleClass('sel', this.checked);
|
|
renderBulk();
|
|
});
|
|
$('#selAll').on('change', function() {
|
|
D.jobs.filter(j => j.id_status === ST.TODO && visible(j)).forEach(j => this.checked ? sel.add(j.id) : sel.delete(j.id));
|
|
render();
|
|
});
|
|
$('#bClear').on('click', () => {
|
|
sel.clear();
|
|
render();
|
|
});
|
|
$('#bProgram').on('click', () => post({
|
|
action: 'bulk',
|
|
op: 'program',
|
|
ids: [...sel]
|
|
}).then(r => {
|
|
toast(`${r.n} programmati in fondo`);
|
|
sel.clear();
|
|
load();
|
|
}));
|
|
$('#bLineGo').on('click', () => {
|
|
const l = +$('#bLine').val();
|
|
if (!l) return toast('Scegli la linea', 'warn');
|
|
post({
|
|
action: 'bulk',
|
|
op: 'line',
|
|
line: l,
|
|
ids: [...sel]
|
|
}).then(r => {
|
|
toast(`${r.n} spostati su ${lineOf(l).name}`);
|
|
sel.clear();
|
|
load();
|
|
});
|
|
});
|
|
|
|
/* ---------- note ---------- */
|
|
const mNote = new bootstrap.Modal(document.getElementById('mNote'));
|
|
$(document).on('click', '.note-open', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const j = D.jobs.find(x => x.id === +this.dataset.id);
|
|
$('#noteTitle').text(`Note · ${j.matrice}${j.conferma_ordine ? ' · ' + j.conferma_ordine : ''}`);
|
|
$('#noteBody').text(j.note_extra || '');
|
|
if (j.note_operatore) $('#noteBody').append($('<hr>'), $('<b>').text('Nota operatore: '), document.createTextNode(j.note_operatore));
|
|
mNote.show();
|
|
});
|
|
|
|
/* ---------- modulo nuovo / modifica ---------- */
|
|
const mJob = new bootstrap.Modal(document.getElementById('mJob'));
|
|
|
|
function loadOpts() {
|
|
if (opts) return $.Deferred().resolve().promise();
|
|
return $.getJSON('?action=options').then(r => {
|
|
opts = r;
|
|
$('#fMat').html('<option value=""></option>' + r.matrici.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join(''));
|
|
$('#fMes').html(r.mescole.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join(''));
|
|
$('#fCli').html('<option value="">Nessuno</option>' + r.clienti.map(c => `<option value="${c.id}">${esc(c.nome)}</option>`).join(''));
|
|
const s2 = {
|
|
theme: 'bootstrap-5',
|
|
width: '100%',
|
|
dropdownParent: $('#mJob')
|
|
};
|
|
$('#fMat').select2({
|
|
...s2,
|
|
placeholder: 'Cerca la matrice'
|
|
});
|
|
$('#fMes').select2({
|
|
...s2,
|
|
placeholder: 'Mescole',
|
|
closeOnSelect: false
|
|
});
|
|
$('#fCli').select2(s2);
|
|
});
|
|
}
|
|
|
|
function lineOptions(pref) {
|
|
const a = D.lines.filter(l => pref.includes(l.id)),
|
|
b = D.lines.filter(l => !pref.includes(l.id) && l.active);
|
|
let h = '<option value="">Scegli la linea…</option>';
|
|
if (a.length) h += '<optgroup label="Linee della matrice">' + a.map(l => `<option value="${l.id}">★ ${esc(l.name)}</option>`).join('') + '</optgroup>';
|
|
h += `<optgroup label="${a.length ? 'Altre linee' : 'Linee'}">` + b.map(l => `<option value="${l.id}">${esc(l.name)}</option>`).join('') + '</optgroup>';
|
|
return h;
|
|
}
|
|
|
|
function onMatrice(fill) {
|
|
const id = +$('#fMat').val();
|
|
matInfo = null;
|
|
$('#hMat, #hOre').html('');
|
|
const keep = $('#fLin').val();
|
|
if (!id) return $('#fLin').html(lineOptions([])).val(keep);
|
|
$.getJSON('?action=matinfo&id=' + id).then(r => {
|
|
const info = r.info;
|
|
matInfo = info;
|
|
$('#fLin').html(lineOptions(info.lines));
|
|
if (fill && info.lines.length === 1) $('#fLin').val(info.lines[0]);
|
|
else $('#fLin').val(keep && $(`#fLin option[value="${keep}"]`).length ? keep : '');
|
|
if (fill && info.mescole.length) $('#fMes').val(info.mescole.map(String)).trigger('change');
|
|
$('#hMat').html(info.worksheet ? `<i class="bi bi-file-earmark-text me-1"></i>Scheda di lavoro n° ${info.worksheet}${info.speed_m_h ? ' · ' + num(info.speed_m_h) + ' m/h' : ''}` : 'Nessuna scheda di lavoro attiva');
|
|
suggest();
|
|
});
|
|
}
|
|
|
|
function suggest() {
|
|
$('#hOre').html('');
|
|
if (!matInfo) return;
|
|
const mt = +$('#fMt').val() || 0,
|
|
kg = +$('#fKs').val() || 0;
|
|
let h = 0;
|
|
if (mt && matInfo.speed_m_h) h = mt / matInfo.speed_m_h;
|
|
else if (kg && matInfo.speed_kg_h) h = kg / matInfo.speed_kg_h;
|
|
if (h > 0) $('#hOre').html(`<a class="use-h" data-h="${h.toFixed(2)}">usa ${num(h, 1)} h</a> (da scheda)`);
|
|
}
|
|
$('#fMat').on('select2:select', () => onMatrice(true));
|
|
$('#fMt, #fKs').on('input', suggest);
|
|
$(document).on('click', '.use-h', function() {
|
|
$('#fOre').val($(this).data('h'));
|
|
});
|
|
|
|
function openJob(j) {
|
|
loadOpts().then(() => {
|
|
editing = j || null;
|
|
const isAct = j && ST.ACTIVE.includes(j.id_status);
|
|
$('#mTitle').text(j ? `${j.matrice}${j.conferma_ordine ? ' · ' + j.conferma_ordine : ''}` : 'Nuovo lavoro');
|
|
$('#fId').val(j ? j.id : '');
|
|
$('#fMat').val(j ? j.idmatrice : '').trigger('change');
|
|
$('#fLin').html(lineOptions(j ? j.matrice_lines : [])).val(j ? j.id_linea : (lineFilter || ''));
|
|
$('#fMes').val(j ? j.mescole_ids.map(String) : []).trigger('change');
|
|
$('#fCli').val(j && j.id_cliente ? j.id_cliente : '').trigger('change');
|
|
$('#fOrd').val(j ? j.conferma_ordine || '' : '');
|
|
$('#fDz').val(j ? j.data_zibo || '' : '');
|
|
$('#fDc').val(j ? j.data_cliente || '' : '');
|
|
$('#fDp').val(j ? j.data_produzione || '' : '');
|
|
$('#fMt').val(j ? j.metri || '' : '');
|
|
$('#fKs').val(j ? j.kg_sp || '' : '');
|
|
$('#fKp').val(j ? j.kg_p || '' : '');
|
|
$('#fOre').val(j ? j.ore_previste || '' : '');
|
|
$('#fNote').val(j ? j.note_extra || '' : '');
|
|
$(`#fSt${j && j.id_status === ST.PLAN ? 6 : 1}`).prop('checked', true);
|
|
$('#wSt').toggleClass('hide', !!isAct);
|
|
$('#wActive').toggleClass('hide', !isAct);
|
|
$('#wChanged').toggleClass('hide', !(j && j.mago_changed)).html('<i class="bi bi-exclamation-triangle-fill me-1"></i> Questa riga è cambiata in Mago dopo l\'import: controlla i dati nella pagina "Nuovi da Mago".');
|
|
$('#btnDel').toggleClass('hide', !j || isAct).removeClass('btn-danger').addClass('btn-outline-danger').html('<i class="bi bi-trash me-1"></i> Elimina');
|
|
$('#btnDup').toggleClass('hide', !j);
|
|
$('#hMat, #hOre').html('');
|
|
if (j) onMatrice(false);
|
|
mJob.show();
|
|
});
|
|
}
|
|
$('#btnNew').on('click', () => openJob(null));
|
|
|
|
$('#fJob').on('submit', function(e) {
|
|
e.preventDefault();
|
|
const d = $(this).serializeArray();
|
|
d.push({
|
|
name: 'action',
|
|
value: 'save'
|
|
});
|
|
if (editing && ST.ACTIVE.includes(editing.id_status)) d.push({
|
|
name: 'id_status',
|
|
value: editing.id_status
|
|
});
|
|
$('#btnSave').prop('disabled', true);
|
|
post(d).then(() => {
|
|
mJob.hide();
|
|
toast(editing ? 'Lavoro aggiornato' : 'Lavoro creato');
|
|
load();
|
|
}).always(() => $('#btnSave').prop('disabled', false));
|
|
});
|
|
$('#btnDup').on('click', () => {
|
|
mJob.hide();
|
|
duplicate(editing);
|
|
});
|
|
$('#btnDel').on('click', function() {
|
|
const b = $(this);
|
|
if (!b.hasClass('btn-danger')) return b.removeClass('btn-outline-danger').addClass('btn-danger').html('<i class="bi bi-trash me-1"></i> Conferma eliminazione');
|
|
post({
|
|
action: 'delete',
|
|
id: editing.id
|
|
}).then(() => {
|
|
mJob.hide();
|
|
toast(editing.from_mago ? 'Eliminato: la riga torna tra i Nuovi da Mago' : 'Lavoro eliminato');
|
|
load();
|
|
});
|
|
});
|
|
|
|
/* ---------- foto dei lavori in macchina ---------- */
|
|
const mPhoto = new bootstrap.Modal(document.getElementById('mPhoto'));
|
|
let phCtx = null;
|
|
$(document).on('click', '[data-photo]', function(e) {
|
|
e.stopPropagation();
|
|
const j = D.jobs.find(x => x.id === +this.dataset.photo);
|
|
phCtx = {
|
|
id: j.id,
|
|
type: this.dataset.pt,
|
|
rows: [],
|
|
params: []
|
|
};
|
|
$('#phTitle').text(`Foto · ${j.matrice} · ${lineOf(j.id_linea).name}`);
|
|
mPhoto.show();
|
|
loadPhotos();
|
|
});
|
|
$(document).on('click', '#phTabs [data-pt]', function() {
|
|
phCtx.type = this.dataset.pt;
|
|
drawPhotos();
|
|
});
|
|
|
|
function loadPhotos() {
|
|
$('#phGal').html('<div class="text-muted">Caricamento…</div>');
|
|
$.getJSON('?action=photos&id=' + phCtx.id).then(r => {
|
|
phCtx.rows = r.rows || [];
|
|
phCtx.params = r.params || [];
|
|
drawPhotos();
|
|
});
|
|
}
|
|
|
|
function drawPhotos() {
|
|
$('#phTabs .nav-link').each(function() {
|
|
const n = phCtx.rows.filter(p => p.photo_type === this.dataset.pt).length;
|
|
$(this).toggleClass('active', this.dataset.pt === phCtx.type).find('.badge').remove();
|
|
if (n) $(this).append(` <span class="badge bg-${this.dataset.pt === phCtx.type ? 'light text-dark' : 'secondary'}">${n}</span>`);
|
|
});
|
|
const isPar = phCtx.type === 'parametri_macchina' && phCtx.params.length;
|
|
$('#phSlot').toggleClass('hide', !isPar).html(isPar ? phCtx.params.map(p => `<option value="${p.position}">${esc(p.short_label)} · ${esc(p.label)}</option>`).join('') : '');
|
|
const list = phCtx.rows.filter(p => p.photo_type === phCtx.type);
|
|
const label = p => {
|
|
const par = p.param_position ? phCtx.params.find(x => +x.position === +p.param_position) : null;
|
|
return (par ? par.short_label + ' · ' : '') + String(p.created_at || '').slice(11, 16) + ' ' + String(p.created_at || '').slice(8, 10) + '/' + String(p.created_at || '').slice(5, 7);
|
|
};
|
|
$('#phGal').html(list.length ? list.map(p => `<a href="photos/${esc(p.filename)}" target="_blank"><img src="photos/${esc(p.filename)}" alt="" loading="lazy"><span>${esc(label(p))}</span></a>`).join('') : '<div class="text-muted">Nessuna foto</div>');
|
|
}
|
|
|
|
function resize(file, max, q) {
|
|
return new Promise((ok, ko) => {
|
|
const fr = new FileReader();
|
|
fr.onload = e => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
let w = img.width,
|
|
h = img.height;
|
|
if (w > h && w > max) {
|
|
h *= max / w;
|
|
w = max;
|
|
} else if (h > max) {
|
|
w *= max / h;
|
|
h = max;
|
|
}
|
|
const c = document.createElement('canvas');
|
|
c.width = w;
|
|
c.height = h;
|
|
c.getContext('2d').drawImage(img, 0, 0, w, h);
|
|
c.toBlob(b => ok(b), 'image/jpeg', q);
|
|
};
|
|
img.onerror = ko;
|
|
img.src = e.target.result;
|
|
};
|
|
fr.onerror = ko;
|
|
fr.readAsDataURL(file);
|
|
});
|
|
}
|
|
$('#phFile').on('change', async function() {
|
|
const f = this.files[0];
|
|
if (!f) return;
|
|
$('#phMsg').text('Caricamento…');
|
|
try {
|
|
const blob = await resize(f, 1600, .7);
|
|
const fd = new FormData();
|
|
fd.append('production_id', phCtx.id);
|
|
fd.append('photo_type', phCtx.type);
|
|
fd.append('param_position', phCtx.type === 'parametri_macchina' ? ($('#phSlot').val() || '') : '');
|
|
fd.append('photo', blob, f.name || 'foto.jpg');
|
|
$.ajax({
|
|
url: 'upload_photo.php',
|
|
type: 'POST',
|
|
data: fd,
|
|
processData: false,
|
|
contentType: false,
|
|
dataType: 'json'
|
|
}).then(r => {
|
|
$('#phMsg').text('');
|
|
if (!r.success) return toast(r.message || 'Foto non caricata', 'err');
|
|
toast('Foto salvata');
|
|
const j = D.jobs.find(x => x.id === phCtx.id);
|
|
if (j) j.photos[phCtx.type]++;
|
|
loadPhotos();
|
|
}, () => {
|
|
$('#phMsg').text('');
|
|
toast('Errore di rete', 'err');
|
|
});
|
|
} catch (e) {
|
|
$('#phMsg').text('');
|
|
toast('Immagine non leggibile', 'err');
|
|
}
|
|
this.value = '';
|
|
});
|
|
$('#mPhoto').on('hidden.bs.modal', render);
|
|
|
|
load();
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|