2753 lines
104 KiB
PHP
2753 lines
104 KiB
PHP
<?php
|
||
|
||
/**
|
||
* production_station.php - DEMO postazione di linea per tablet (touch).
|
||
*
|
||
* Una schermata per linea: cosa c'è in macchina con timer grande, pulsanti enormi (pausa, riprendi, fine),
|
||
* foto/parametri/disegni a portata di dito e la coda dei prossimi lavori. Vista "Tutte" con lo stato di ogni linea.
|
||
* Stessa logica e stesse tabelle della pagina di produzione attuale (productiondata, production_pauses, ...).
|
||
* Usa gli stessi servizi per foto e allegati: get_photos.php, upload_photo.php, get_matrice_attachments.php.
|
||
*/
|
||
|
||
ob_start();
|
||
include('include/headscript.php');
|
||
$db = DBHandlerSelect::getInstance();
|
||
$pdo = $db->getConnection();
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
|
||
const ST_PROGRAMMATO = 6;
|
||
const ST_PRODUZIONE = 2;
|
||
const ST_PAUSA = 7;
|
||
const ST_QUALITA = 3;
|
||
const ST_PROBLEMA = 8;
|
||
const ST_DA_COMPILARE = 11;
|
||
|
||
function stOut(array $a)
|
||
{
|
||
while (ob_get_level()) ob_end_clean();
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode($a);
|
||
exit;
|
||
}
|
||
|
||
/** I tempi in tabella sono salvati in UTC (UTC_TIMESTAMP): li passo al browser in millisecondi. */
|
||
function stMs($dt)
|
||
{
|
||
if (!$dt) return null;
|
||
return (new DateTime($dt, new DateTimeZone('UTC')))->getTimestamp() * 1000;
|
||
}
|
||
|
||
function stHas(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];
|
||
}
|
||
|
||
/** Lavori con tutti i dati utili alla postazione. */
|
||
function stJobs(PDO $pdo, $where, array $params = [])
|
||
{
|
||
$tools = stHas($pdo, 'productiondata_tools') && stHas($pdo, 'inv_equipment')
|
||
? "(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)"
|
||
: 'NULL';
|
||
$photos = stHas($pdo, 'production_photos')
|
||
? "(SELECT GROUP_CONCAT(CONCAT(ph.photo_type, ':', COALESCE(ph.param_position, 0))) FROM production_photos ph WHERE ph.production_id = p.id)"
|
||
: 'NULL';
|
||
$st = $pdo->prepare("
|
||
SELECT p.id, p.id_status, p.id_linea, p.priority, p.conferma_ordine, p.idmatrice, m.nome AS matrice, m.photo,
|
||
c.nome AS cliente, p.data_zibo, p.data_cliente, 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, '#64748b') 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 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,
|
||
(SELECT pr.name FROM production_pauses pp JOIN pause_reasons pr ON pr.id = pp.reason_id
|
||
WHERE pp.id_production = p.id AND pp.end_pause IS NULL ORDER BY pp.start_pause DESC LIMIT 1) AS open_reason,
|
||
$tools AS tools,
|
||
$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 $where
|
||
ORDER BY p.priority IS NULL, p.priority, p.id");
|
||
$st->execute($params);
|
||
$out = [];
|
||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $j) {
|
||
$ph = ['lotto_mescola' => 0, 'parametri_macchina' => 0, 'problema' => 0, 'slots' => []];
|
||
foreach (array_filter(explode(',', (string)$j['photos'])) as $x) {
|
||
[$type, $pos] = array_pad(explode(':', $x), 2, 0);
|
||
if (isset($ph[$type])) $ph[$type]++;
|
||
if ($type === 'parametri_macchina' && $pos) $ph['slots'][(int)$pos] = true;
|
||
}
|
||
$out[] = [
|
||
'id' => (int)$j['id'],
|
||
'status' => (int)$j['id_status'],
|
||
'status_nome' => $j['status_nome'],
|
||
'status_color' => $j['status_color'],
|
||
'line' => (int)$j['id_linea'],
|
||
'matrice' => $j['matrice'],
|
||
'matrice_id' => (int)$j['idmatrice'],
|
||
'photo' => $j['photo'],
|
||
'cliente' => $j['cliente'],
|
||
'ordine' => $j['conferma_ordine'],
|
||
'data_zibo' => $j['data_zibo'],
|
||
'data_cliente' => $j['data_cliente'],
|
||
'metri' => (float)$j['metri'],
|
||
'kg_sp' => (float)$j['kg_sp'],
|
||
'kg_p' => (float)$j['kg_p'],
|
||
'ore' => (float)$j['ore_previste'],
|
||
'note' => (string)$j['note_extra'],
|
||
'note_operatore' => (string)$j['note_operatore'],
|
||
'mescole' => $j['mescole'] ? explode('||', $j['mescole']) : [],
|
||
'tools' => $j['tools'] ? explode('||', $j['tools']) : [],
|
||
'start_ms' => stMs($j['start_time']),
|
||
'pause_sec' => (int)$j['pause_sec'],
|
||
'open_pause_ms' => stMs($j['open_pause']),
|
||
'open_reason' => $j['open_reason'],
|
||
'photos' => $ph,
|
||
];
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/** Secondi di occupazione linea (lordo) e di pausa chiusa, come la pagina di produzione. */
|
||
function stTimes(PDO $pdo, $id)
|
||
{
|
||
$st = $pdo->prepare("SELECT start_time FROM productiondata WHERE id = ?");
|
||
$st->execute([$id]);
|
||
$start = $st->fetchColumn();
|
||
$occ = 0;
|
||
if ($start) {
|
||
$occ = max(0, time() - (new DateTime($start, new DateTimeZone('UTC')))->getTimestamp());
|
||
}
|
||
$st = $pdo->prepare("SELECT COALESCE(SUM(TIMESTAMPDIFF(SECOND, start_pause, end_pause)), 0) FROM production_pauses
|
||
WHERE id_production = ? AND end_pause IS NOT NULL");
|
||
$st->execute([$id]);
|
||
$pause = (int)$st->fetchColumn();
|
||
return [$occ, $pause];
|
||
}
|
||
|
||
/* ===================== AJAX ===================== */
|
||
$a = $_GET['a'] ?? $_POST['a'] ?? '';
|
||
if ($a !== '') {
|
||
try {
|
||
// ---------- stato della postazione ----------
|
||
if ($a === 'state') {
|
||
$lines = $pdo->query("SELECT id, name, COALESCE(NULLIF(color, ''), '#64748b') AS color FROM production_lines ORDER BY line_number, id")->fetchAll(PDO::FETCH_ASSOC);
|
||
foreach ($lines as &$l) $l['id'] = (int)$l['id'];
|
||
unset($l);
|
||
$line = (int)($_GET['line'] ?? 0);
|
||
|
||
$res = ['success' => true, 'now_ms' => time() * 1000, 'lines' => $lines];
|
||
// panoramica: in macchina e coda di ogni linea
|
||
$res['overview'] = stJobs($pdo, 'p.id_status IN (' . ST_PRODUZIONE . ',' . ST_PAUSA . ',' . ST_PROBLEMA . ')');
|
||
$cnt = [];
|
||
foreach ($pdo->query("SELECT id_linea, COUNT(*) n, SUM(ore_previste) h FROM productiondata WHERE id_status = " . ST_PROGRAMMATO . " GROUP BY id_linea") as $r) {
|
||
$cnt[(int)$r['id_linea']] = ['n' => (int)$r['n'], 'h' => (float)$r['h']];
|
||
}
|
||
$res['queue_count'] = $cnt;
|
||
// primo lavoro in coda per ogni linea (per la panoramica)
|
||
foreach (
|
||
$pdo->query("SELECT p.id_linea, m.nome FROM productiondata p JOIN matrice m ON m.id = p.idmatrice
|
||
WHERE p.id_status = " . ST_PROGRAMMATO . " ORDER BY p.priority IS NULL, p.priority, p.id") as $r
|
||
) {
|
||
foreach ($res['lines'] as &$l) if ($l['id'] === (int)$r['id_linea'] && !isset($l['next'])) $l['next'] = $r['nome'];
|
||
unset($l);
|
||
}
|
||
|
||
if ($line) {
|
||
$res['queue'] = stJobs($pdo, 'p.id_status = ' . ST_PROGRAMMATO . ' AND p.id_linea = ?', [$line]);
|
||
$st = $pdo->prepare("SELECT position, short_label, label, icon FROM production_line_params WHERE line_id = ? ORDER BY position");
|
||
$st->execute([$line]);
|
||
$res['params'] = $st->fetchAll(PDO::FETCH_ASSOC);
|
||
}
|
||
$res['reasons'] = $pdo->query("SELECT id, name, is_problem FROM pause_reasons ORDER BY is_problem, name")->fetchAll(PDO::FETCH_ASSOC);
|
||
stOut($res);
|
||
}
|
||
|
||
// ---------- foto di un lavoro ----------
|
||
if ($a === 'photos') {
|
||
$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([(int)($_GET['id'] ?? 0)]);
|
||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||
foreach ($rows as &$r) {
|
||
$r['id'] = (int)$r['id'];
|
||
$r['param_position'] = $r['param_position'] === null ? null : (int)$r['param_position'];
|
||
}
|
||
unset($r);
|
||
stOut(['success' => true, 'rows' => $rows]);
|
||
}
|
||
|
||
// ---------- avvio (da programmato) ----------
|
||
if ($a === 'start') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$pdo->beginTransaction();
|
||
$st = $pdo->prepare("SELECT id_status, id_linea FROM productiondata WHERE id = ? FOR UPDATE");
|
||
$st->execute([$id]);
|
||
$r = $st->fetch(PDO::FETCH_ASSOC);
|
||
if (!$r) throw new RuntimeException('Lavoro non trovato');
|
||
if ((int)$r['id_status'] !== ST_PROGRAMMATO) throw new RuntimeException('Il lavoro non è più tra i programmati');
|
||
$st = $pdo->prepare("SELECT COUNT(*) FROM productiondata WHERE id_linea = ? AND id_status IN (" . ST_PRODUZIONE . "," . ST_PAUSA . ")");
|
||
$st->execute([$r['id_linea']]);
|
||
if ($st->fetchColumn()) throw new RuntimeException('La linea è già occupata: termina prima il lavoro in macchina');
|
||
$pdo->prepare("UPDATE productiondata SET id_status = ?, start_time = UTC_TIMESTAMP(), end_time = NULL WHERE id = ? AND id_status = ?")
|
||
->execute([ST_PRODUZIONE, $id, ST_PROGRAMMATO]);
|
||
$pdo->commit();
|
||
stOut(['success' => true]);
|
||
}
|
||
|
||
// ---------- pausa ----------
|
||
if ($a === 'pause') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$reason = (int)($_POST['reason_id'] ?? 0) ?: null;
|
||
$note = trim((string)($_POST['note'] ?? ''));
|
||
if ($reason) {
|
||
$st = $pdo->prepare("SELECT is_problem FROM pause_reasons WHERE id = ?");
|
||
$st->execute([$reason]);
|
||
$isProblem = $st->fetchColumn();
|
||
if ($isProblem === false) throw new RuntimeException('Motivo non trovato');
|
||
if ((int)$isProblem && $note === '') throw new RuntimeException('Per un problema tecnico scrivi una nota');
|
||
}
|
||
$pdo->beginTransaction();
|
||
$st = $pdo->prepare("UPDATE productiondata SET id_status = ?, note_operatore = ? WHERE id = ? AND id_status = ?");
|
||
$st->execute([ST_PAUSA, $note ?: null, $id, ST_PRODUZIONE]);
|
||
if ($st->rowCount()) {
|
||
$pdo->prepare("INSERT INTO production_pauses (id_production, reason_id, start_pause, note) VALUES (?, ?, UTC_TIMESTAMP(), ?)")
|
||
->execute([$id, $reason, $note ?: null]);
|
||
}
|
||
$pdo->commit();
|
||
stOut(['success' => true]);
|
||
}
|
||
|
||
// ---------- ripresa ----------
|
||
if ($a === 'resume') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$reason = (int)($_POST['reason_id'] ?? 0);
|
||
$pdo->beginTransaction();
|
||
$st = $pdo->prepare("SELECT reason_id FROM production_pauses WHERE id_production = ? AND end_pause IS NULL ORDER BY start_pause DESC LIMIT 1");
|
||
$st->execute([$id]);
|
||
$has = $st->fetchColumn();
|
||
if (($has === false || $has === null) && $reason <= 0) throw new RuntimeException('Scegli il motivo della pausa per riprendere');
|
||
$pdo->prepare("UPDATE production_pauses SET end_pause = UTC_TIMESTAMP()" . ($reason > 0 ? ", reason_id = ?" : "") . "
|
||
WHERE id_production = ? AND end_pause IS NULL ORDER BY start_pause DESC LIMIT 1")
|
||
->execute($reason > 0 ? [$reason, $id] : [$id]);
|
||
$st = $pdo->prepare("UPDATE productiondata SET id_status = ?, end_time = NULL WHERE id = ? AND id_status = ?");
|
||
$st->execute([ST_PRODUZIONE, $id, ST_PAUSA]);
|
||
if (!$st->rowCount()) throw new RuntimeException('Il lavoro non è in pausa');
|
||
$pdo->commit();
|
||
stOut(['success' => true]);
|
||
}
|
||
|
||
// ---------- fine produzione (dati finali o "compila dopo") ----------
|
||
if ($a === 'finish') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$reason = ($_POST['reason'] ?? 'fine') === 'problema' ? 'problema' : 'fine';
|
||
$later = !empty($_POST['later']);
|
||
$kg = max(0, (float)str_replace(',', '.', (string)($_POST['kgprod'] ?? 0)));
|
||
$mt = max(0, (float)str_replace(',', '.', (string)($_POST['mtprod'] ?? 0)));
|
||
$sc = max(0, (float)str_replace(',', '.', (string)($_POST['scarto'] ?? 0)));
|
||
$note = trim((string)($_POST['note'] ?? ''));
|
||
if ($reason === 'problema' && $note === '') throw new RuntimeException('Descrivi il problema nella nota');
|
||
if (!$later && $kg <= 0 && $mt <= 0) throw new RuntimeException('Inserisci i kg o i metri prodotti, oppure "Compila dopo"');
|
||
|
||
$pdo->beginTransaction();
|
||
$pdo->prepare("UPDATE production_pauses SET end_pause = UTC_TIMESTAMP() WHERE id_production = ? AND end_pause IS NULL")->execute([$id]);
|
||
[$occ, $pause] = stTimes($pdo, $id);
|
||
$status = $later ? ST_DA_COMPILARE : ($reason === 'problema' ? ST_PROBLEMA : ST_QUALITA);
|
||
$st = $pdo->prepare("
|
||
UPDATE productiondata
|
||
SET kgprod = :kg, mtprod = :mt, scarto = :sc, scarto_perc = :scp, note_operatore = :note,
|
||
hourprod = SEC_TO_TIME(:occ), tempo_totale_produzione = :occ2, seconds_pause = :pause,
|
||
seconds_produzione = :netto, id_status = :status, end_time = COALESCE(end_time, UTC_TIMESTAMP())
|
||
WHERE id = :id AND id_status IN (" . ST_PRODUZIONE . "," . ST_PAUSA . ")");
|
||
$st->execute([
|
||
'kg' => $later ? 0 : $kg,
|
||
'mt' => $later ? 0 : $mt,
|
||
'sc' => $later ? 0 : $sc,
|
||
'scp' => (!$later && $kg + $sc > 0) ? round($sc / ($kg + $sc) * 100, 2) : 0,
|
||
'note' => $note,
|
||
'occ' => $occ,
|
||
'occ2' => $occ,
|
||
'pause' => $pause,
|
||
'netto' => max(0, $occ - $pause),
|
||
'status' => $status,
|
||
'id' => $id,
|
||
]);
|
||
if (!$st->rowCount()) throw new RuntimeException('Il lavoro non è più in macchina');
|
||
$pdo->commit();
|
||
stOut(['success' => true, 'status' => $status]);
|
||
}
|
||
|
||
// ---------- nuovo lavoro rapido sulla linea ----------
|
||
if ($a === 'matrici') {
|
||
stOut([
|
||
'success' => true,
|
||
'rows' => $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)
|
||
]);
|
||
}
|
||
if ($a === 'matmescole') {
|
||
$st = $pdo->prepare("SELECT me.id FROM matrici_mescole mm JOIN mescole me ON me.id = mm.idmescola WHERE mm.idmatrice = ? AND me.is_active = 1");
|
||
$st->execute([(int)($_GET['id'] ?? 0)]);
|
||
stOut(['success' => true, 'ids' => array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN))]);
|
||
}
|
||
if ($a === 'quick') {
|
||
$idm = (int)($_POST['idmatrice'] ?? 0);
|
||
$line = (int)($_POST['line'] ?? 0);
|
||
$avvia = !empty($_POST['avvia']);
|
||
if (!$idm) throw new RuntimeException('Scegli la matrice');
|
||
if (!$line) throw new RuntimeException('Scegli la linea');
|
||
$pdo->beginTransaction();
|
||
if ($avvia) {
|
||
$st = $pdo->prepare("SELECT COUNT(*) FROM productiondata WHERE id_linea = ? AND id_status IN (" . ST_PRODUZIONE . "," . ST_PAUSA . ")");
|
||
$st->execute([$line]);
|
||
if ($st->fetchColumn()) throw new RuntimeException('La linea è occupata: crealo in coda');
|
||
}
|
||
$prio = (int)$pdo->query("SELECT COALESCE(MAX(priority), 0) + 1 FROM productiondata WHERE id_status = " . ST_PROGRAMMATO)->fetchColumn();
|
||
$pdo->prepare("INSERT INTO productiondata (Data, idmatrice, id_linea, note_extra, priority, id_status, start_time, end_time,
|
||
fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto)
|
||
VALUES (CURDATE(), ?, ?, ?, ?, ?, " . ($avvia ? 'UTC_TIMESTAMP()' : 'NULL') . ", NULL, 0, '00:00:00', '00:00:00', 0, 0, 0)")
|
||
->execute([$idm, $line, trim((string)($_POST['descrizione'] ?? '')) ?: null, $prio, $avvia ? ST_PRODUZIONE : ST_PROGRAMMATO]);
|
||
$id = (int)$pdo->lastInsertId();
|
||
$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();
|
||
stOut(['success' => true, 'id' => $id]);
|
||
}
|
||
|
||
stOut(['success' => false, 'message' => 'Azione sconosciuta']);
|
||
} catch (Throwable $e) {
|
||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||
stOut(['success' => false, 'message' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
// linea preferita dell'utente (come la pagina di produzione)
|
||
$preferred = '';
|
||
if (!empty($iduserlogin)) {
|
||
try {
|
||
$st = $pdo->prepare("SELECT preferred_line_id FROM auth_users WHERE id = ?");
|
||
$st->execute([(int)$iduserlogin]);
|
||
$preferred = (string)($st->fetchColumn() ?: '');
|
||
} catch (Throwable $e) {
|
||
}
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="it">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="theme-color" content="#0b1220">
|
||
<title>Postazione linea - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></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" />
|
||
<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.full.min.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
--bg: #0a1120;
|
||
--panel: #131d33;
|
||
--panel2: #1a2742;
|
||
--line: #2a3a5c;
|
||
--line2: #3d5078;
|
||
--ink: #f1f5ff;
|
||
--muted: #b4c2dd;
|
||
--label: #c6d2ea;
|
||
--box: #22335a;
|
||
--box2: #1b2a4b;
|
||
--sub: #d5def0;
|
||
--chipink: #e6edfb;
|
||
--track: #24345a;
|
||
--top: #0e1628;
|
||
--pressed: #22325a;
|
||
--soft: #1e2a44;
|
||
--soft2: #3a4a6b;
|
||
--accent: #93c5fd;
|
||
--r3: #cfd8ea;
|
||
--badink: #f87171;
|
||
--warnink: #fbbf24;
|
||
--scrim: rgba(2, 6, 23, .7);
|
||
--sheet: #0f182e;
|
||
--field: #0b1426;
|
||
--selbg: #0c2a3d;
|
||
--selbad: #3a1212;
|
||
--disbg: #1d1538;
|
||
--toastbg: #0f172a;
|
||
--reasonink: #fcd34d;
|
||
--dotoff: #4b5b7a;
|
||
--runink: #4ade80;
|
||
--runglow: 0 0 28px rgba(74, 222, 128, .45);
|
||
--pauseink: #fbbf24;
|
||
--matink: #7dd3fc;
|
||
--matglow: 0 0 22px rgba(125, 211, 252, .35);
|
||
--run: #22c55e;
|
||
--pause: #f59e0b;
|
||
--bad: #ef4444;
|
||
--free: #38bdf8;
|
||
--lc: #3b82f6;
|
||
}
|
||
|
||
/* modalità chiara: per reparti luminosi */
|
||
[data-theme="light"] {
|
||
--bg: #d6e4f3;
|
||
--panel: #eaf2fb;
|
||
--panel2: #dce8f6;
|
||
--line: #c0d2e8;
|
||
--line2: #a6bcd9;
|
||
--ink: #0f172a;
|
||
--muted: #5b6b86;
|
||
--sub: #334155;
|
||
--chipink: #1e293b;
|
||
--track: #e2e8f0;
|
||
--top: #e3edf8;
|
||
--pressed: #cddcef;
|
||
--soft: #d6e4f3;
|
||
--soft2: #cbd5e1;
|
||
--accent: #1d4ed8;
|
||
--r3: #475569;
|
||
--badink: #dc2626;
|
||
--warnink: #b45309;
|
||
--scrim: rgba(15, 23, 42, .45);
|
||
--sheet: #eaf2fb;
|
||
--field: #f4f8fd;
|
||
--selbg: #e0f2fe;
|
||
--selbad: #fee2e2;
|
||
--disbg: #f3e8ff;
|
||
--toastbg: #0f172a;
|
||
--reasonink: #92400e;
|
||
--dotoff: #cbd5e1;
|
||
--runink: #15803d;
|
||
--runglow: none;
|
||
--pauseink: #c2410c;
|
||
--matink: #1e3a8a;
|
||
--label: #475569;
|
||
--box: #f4f8fd;
|
||
--box2: #f4f8fd;
|
||
--matglow: none;
|
||
--track: #c9d8ec;
|
||
}
|
||
|
||
[data-theme="light"] .hero::before {
|
||
opacity: .12;
|
||
}
|
||
|
||
[data-theme="light"] .hero,
|
||
[data-theme="light"] .side,
|
||
[data-theme="light"] .ovc {
|
||
box-shadow: 0 6px 20px rgba(15, 23, 42, .08);
|
||
}
|
||
|
||
[data-theme="light"] .tab.on {
|
||
color: #fff;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
height: 100%;
|
||
background: var(--bg);
|
||
color: var(--ink);
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
overscroll-behavior: none;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
}
|
||
|
||
button {
|
||
font-family: inherit;
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
.app {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* ---------- barra alta ---------- */
|
||
.top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 14px;
|
||
background: var(--top);
|
||
border-bottom: 1px solid var(--line);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ibtn {
|
||
width: 54px;
|
||
height: 54px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
color: var(--ink);
|
||
font-size: 1.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ibtn:active {
|
||
transform: scale(.95);
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex: 1;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.tab {
|
||
min-height: 54px;
|
||
padding: 0 22px;
|
||
border-radius: 14px;
|
||
border: 2px solid var(--line);
|
||
background: var(--panel);
|
||
color: var(--muted);
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.tab .dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: var(--dotoff);
|
||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .06);
|
||
}
|
||
|
||
.tab .dot.run {
|
||
background: var(--run);
|
||
animation: pulse 1.6s infinite;
|
||
}
|
||
|
||
.tab .dot.pause {
|
||
background: var(--pause);
|
||
}
|
||
|
||
.tab.on {
|
||
background: var(--c);
|
||
border-color: var(--c);
|
||
color: #fff;
|
||
}
|
||
|
||
.clock {
|
||
font-size: 1.6rem;
|
||
font-weight: 800;
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--ink);
|
||
padding: 0 6px;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(34, 197, 94, .7);
|
||
}
|
||
|
||
70% {
|
||
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
||
}
|
||
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||
}
|
||
}
|
||
|
||
/* ---------- area principale ---------- */
|
||
.main {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: grid;
|
||
grid-template-columns: 1fr 380px;
|
||
gap: 14px;
|
||
padding: 14px;
|
||
}
|
||
|
||
.hero {
|
||
position: relative;
|
||
border-radius: 24px;
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
padding: 22px 26px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.hero::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(1200px 400px at 0% 0%, var(--glow, transparent), transparent 60%);
|
||
opacity: .22;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.hero.run {
|
||
--glow: var(--run);
|
||
border-color: rgba(34, 197, 94, .45);
|
||
}
|
||
|
||
.hero.pause {
|
||
--glow: var(--pause);
|
||
border-color: rgba(245, 158, 11, .55);
|
||
}
|
||
|
||
.hero.free {
|
||
--glow: var(--free);
|
||
}
|
||
|
||
.hero>* {
|
||
position: relative;
|
||
}
|
||
|
||
.h-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.pill {
|
||
font-size: 1rem;
|
||
font-weight: 900;
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
border-radius: 999px;
|
||
padding: 6px 16px;
|
||
color: #0b1220;
|
||
}
|
||
|
||
.pill.run {
|
||
background: var(--run);
|
||
}
|
||
|
||
.pill.pause {
|
||
background: var(--pause);
|
||
animation: blink 1.4s infinite;
|
||
}
|
||
|
||
.pill.free {
|
||
background: var(--free);
|
||
}
|
||
|
||
@keyframes blink {
|
||
50% {
|
||
opacity: .55;
|
||
}
|
||
}
|
||
|
||
.h-order {
|
||
color: var(--muted);
|
||
font-size: 1.05rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.h-mat {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.h-mat .name {
|
||
color: var(--matink);
|
||
text-shadow: var(--matglow);
|
||
font-size: clamp(2.2rem, 4.5vw, 3.6rem);
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
letter-spacing: -.01em;
|
||
}
|
||
|
||
.h-mat img {
|
||
width: 76px;
|
||
height: 76px;
|
||
border-radius: 14px;
|
||
object-fit: cover;
|
||
border: 2px solid var(--line);
|
||
}
|
||
|
||
.h-cli {
|
||
font-size: 1.3rem;
|
||
color: var(--sub);
|
||
margin-top: 8px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.chips {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.schip {
|
||
display: inline-block;
|
||
height: auto;
|
||
line-height: 1.3;
|
||
background: var(--panel2);
|
||
border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
padding: 6px 12px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--chipink);
|
||
}
|
||
|
||
.h-body {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 28px;
|
||
margin: 18px 0;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.ring {
|
||
position: relative;
|
||
width: 230px;
|
||
height: 230px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ring svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.ring .bg {
|
||
stroke: var(--track);
|
||
}
|
||
|
||
.ring .fg {
|
||
transition: stroke-dashoffset .8s ease;
|
||
}
|
||
|
||
.ring .in {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ring .pct {
|
||
font-size: 2.6rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.ring .lbl {
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
font-size: .95rem;
|
||
}
|
||
|
||
.hero.run .timer {
|
||
color: var(--runink);
|
||
text-shadow: var(--runglow);
|
||
}
|
||
|
||
.ovc .tm.run {
|
||
color: var(--runink);
|
||
text-shadow: var(--runglow);
|
||
}
|
||
|
||
.timer {
|
||
font-size: clamp(3.2rem, 7.5vw, 6rem);
|
||
font-weight: 900;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1;
|
||
letter-spacing: -.02em;
|
||
}
|
||
|
||
.t-sub {
|
||
color: var(--muted);
|
||
font-size: 1.15rem;
|
||
font-weight: 600;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.t-sub b {
|
||
color: var(--ink);
|
||
}
|
||
|
||
.kpis {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.kpi {
|
||
background: var(--box);
|
||
border: 1px solid var(--line2);
|
||
border-radius: 14px;
|
||
padding: 8px 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.hero .kpi .v {
|
||
color: var(--ink);
|
||
font-size: 1.35rem;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-height: 860px) {
|
||
.ring {
|
||
width: 190px;
|
||
height: 190px;
|
||
}
|
||
|
||
.h-mat .name {
|
||
font-size: clamp(2rem, 4vw, 3rem);
|
||
}
|
||
|
||
.h-body {
|
||
margin: 12px 0;
|
||
}
|
||
}
|
||
|
||
.kpi .l {
|
||
font-size: .8rem;
|
||
color: var(--label);
|
||
font-weight: 800;
|
||
text-transform: uppercase;
|
||
letter-spacing: .05em;
|
||
}
|
||
|
||
.hero .kpi.late .v {
|
||
color: var(--badink);
|
||
}
|
||
|
||
.hero .kpi.soon .v {
|
||
color: var(--warnink);
|
||
}
|
||
|
||
.reason {
|
||
display: inline-flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
background: rgba(245, 158, 11, .15);
|
||
border: 1px solid rgba(245, 158, 11, .5);
|
||
color: var(--reasonink);
|
||
border-radius: 12px;
|
||
padding: 8px 14px;
|
||
font-weight: 800;
|
||
font-size: 1.1rem;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.reason.missing {
|
||
animation: blink 1.4s infinite;
|
||
}
|
||
|
||
/* ---------- pulsanti grandi ---------- */
|
||
.big-actions {
|
||
display: flex;
|
||
gap: 14px;
|
||
}
|
||
|
||
.big {
|
||
flex: 1;
|
||
min-height: 96px;
|
||
border: none;
|
||
border-radius: 20px;
|
||
font-size: 1.7rem;
|
||
font-weight: 900;
|
||
letter-spacing: .06em;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 14px;
|
||
box-shadow: 0 10px 24px rgba(0, 0, 0, .35), inset 0 -4px 0 rgba(0, 0, 0, .18);
|
||
transition: transform .08s;
|
||
}
|
||
|
||
.big:active {
|
||
transform: scale(.97);
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
||
}
|
||
|
||
.big i {
|
||
font-size: 2.1rem;
|
||
}
|
||
|
||
.big.go {
|
||
background: linear-gradient(135deg, #22c55e, #15803d);
|
||
}
|
||
|
||
.big.pause {
|
||
background: linear-gradient(135deg, #f59e0b, #b45309);
|
||
}
|
||
|
||
.big.stop {
|
||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||
}
|
||
|
||
.big[disabled] {
|
||
opacity: .35;
|
||
}
|
||
|
||
.tools {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.tool {
|
||
position: relative;
|
||
min-height: 76px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
font-weight: 800;
|
||
font-size: .9rem;
|
||
}
|
||
|
||
.tool i {
|
||
font-size: 1.7rem;
|
||
}
|
||
|
||
.tool:active {
|
||
transform: scale(.96);
|
||
background: var(--pressed);
|
||
}
|
||
|
||
.tool .n {
|
||
position: absolute;
|
||
top: 6px;
|
||
right: 8px;
|
||
min-width: 24px;
|
||
height: 24px;
|
||
border-radius: 12px;
|
||
background: var(--run);
|
||
color: #0b1220;
|
||
font-size: .8rem;
|
||
font-weight: 900;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 6px;
|
||
}
|
||
|
||
.tool .n.zero {
|
||
background: var(--soft2);
|
||
color: var(--ink);
|
||
}
|
||
|
||
.tool.danger i {
|
||
color: var(--badink);
|
||
}
|
||
|
||
.tool.mago i {
|
||
color: #fb923c;
|
||
}
|
||
|
||
/* ---------- coda ---------- */
|
||
.side {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 24px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.side-h {
|
||
padding: 16px 18px 12px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 10px;
|
||
}
|
||
|
||
.side-h .t {
|
||
font-size: 1.25rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.side-h .m {
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.qlist {
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.qc {
|
||
border-radius: 16px;
|
||
background: var(--panel2);
|
||
border: 1px solid var(--line);
|
||
border-left: 6px solid var(--lc);
|
||
padding: 12px 14px;
|
||
text-align: left;
|
||
color: var(--ink);
|
||
width: 100%;
|
||
}
|
||
|
||
.qc:active {
|
||
background: var(--pressed);
|
||
}
|
||
|
||
.qc .r1 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.qc .pos {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 10px;
|
||
background: var(--soft);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 900;
|
||
color: var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.qc .mat {
|
||
color: var(--matink);
|
||
font-size: 1.3rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.qc .h {
|
||
margin-left: auto;
|
||
font-weight: 900;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.qc .r2 {
|
||
color: var(--muted);
|
||
margin-top: 4px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.qc .r3 {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 6px;
|
||
font-size: .9rem;
|
||
font-weight: 700;
|
||
color: var(--r3);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.late {
|
||
color: var(--badink) !important;
|
||
}
|
||
|
||
.soon {
|
||
color: var(--warnink) !important;
|
||
}
|
||
|
||
.qc.next {
|
||
border-color: rgba(56, 189, 248, .6);
|
||
box-shadow: 0 0 0 2px rgba(56, 189, 248, .25);
|
||
}
|
||
|
||
.empty {
|
||
color: var(--muted);
|
||
text-align: center;
|
||
padding: 40px 10px;
|
||
font-weight: 700;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
/* ---------- panoramica ---------- */
|
||
.ov {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 14px;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 14px;
|
||
align-content: start;
|
||
}
|
||
|
||
.ovc {
|
||
border-radius: 22px;
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-top: 8px solid var(--c);
|
||
padding: 18px 20px;
|
||
text-align: left;
|
||
color: var(--ink);
|
||
min-height: 220px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ovc:active {
|
||
transform: scale(.99);
|
||
}
|
||
|
||
.ovc .n {
|
||
font-size: 1.5rem;
|
||
font-weight: 900;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.ovc .n .pill {
|
||
font-size: .8rem;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.ovc .mat {
|
||
color: var(--matink);
|
||
font-size: 2.2rem;
|
||
font-weight: 900;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.ovc .cli {
|
||
color: var(--muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ovc .tm {
|
||
font-size: 2.4rem;
|
||
font-weight: 900;
|
||
font-variant-numeric: tabular-nums;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.ovc .q {
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ---------- pannello dal basso ---------- */
|
||
.sheet-bg {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: var(--scrim);
|
||
backdrop-filter: blur(3px);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity .2s;
|
||
z-index: 50;
|
||
}
|
||
|
||
.sheet-bg.on {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.sheet {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 0;
|
||
transform: translate(-50%, 105%);
|
||
width: min(1500px, 96vw);
|
||
max-height: 94vh;
|
||
background: var(--sheet);
|
||
border: 1px solid var(--line);
|
||
border-bottom: none;
|
||
border-radius: 28px 28px 0 0;
|
||
z-index: 51;
|
||
transition: transform .28s cubic-bezier(.2, .9, .3, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sheet.on {
|
||
transform: translate(-50%, 0);
|
||
}
|
||
|
||
.sheet .grab {
|
||
width: 64px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background: var(--soft2);
|
||
margin: 10px auto 0;
|
||
}
|
||
|
||
.sheet-h {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 22px;
|
||
}
|
||
|
||
.sheet-h h3 {
|
||
margin: 0;
|
||
font-size: 1.6rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.sheet-h .ibtn {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.sheet-b {
|
||
overflow-y: auto;
|
||
padding: 6px 22px 26px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.tiles {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.tile {
|
||
min-height: 70px;
|
||
border-radius: 16px;
|
||
border: 2px solid var(--line);
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
padding: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
text-align: left;
|
||
}
|
||
|
||
.tile i {
|
||
font-size: 1.6rem;
|
||
}
|
||
|
||
.tile.problem {
|
||
border-color: rgba(239, 68, 68, .45);
|
||
}
|
||
|
||
.tile.problem i {
|
||
color: var(--badink);
|
||
}
|
||
|
||
.tile.sel {
|
||
border-color: var(--free);
|
||
background: var(--selbg);
|
||
box-shadow: 0 0 0 3px rgba(56, 189, 248, .25);
|
||
}
|
||
|
||
.tile.sel.problem {
|
||
border-color: var(--bad);
|
||
background: var(--selbad);
|
||
box-shadow: 0 0 0 3px rgba(239, 68, 68, .25);
|
||
}
|
||
|
||
.tile.huge {
|
||
min-height: 150px;
|
||
font-size: 1.6rem;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
|
||
.tile.huge i {
|
||
font-size: 2.6rem;
|
||
}
|
||
|
||
.fld {
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.fld label {
|
||
display: block;
|
||
font-weight: 800;
|
||
color: var(--label);
|
||
margin-bottom: 6px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .05em;
|
||
font-size: .85rem;
|
||
}
|
||
|
||
.fld input,
|
||
.fld textarea,
|
||
.fld select {
|
||
width: 100%;
|
||
background: var(--field);
|
||
border: 2px solid var(--line);
|
||
border-radius: 14px;
|
||
color: var(--ink);
|
||
font-size: 1.5rem;
|
||
padding: 14px 16px;
|
||
font-weight: 700;
|
||
user-select: text;
|
||
-webkit-user-select: text;
|
||
}
|
||
|
||
.fld textarea {
|
||
font-size: 1.15rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.fld input:focus,
|
||
.fld textarea:focus {
|
||
outline: none;
|
||
border-color: var(--free);
|
||
}
|
||
|
||
.grid2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
}
|
||
|
||
.teo {
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.teo b {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.sheet-f {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.sheet-f .big {
|
||
min-height: 80px;
|
||
font-size: 1.35rem;
|
||
}
|
||
|
||
.big.ghost {
|
||
background: var(--soft);
|
||
box-shadow: none;
|
||
color: var(--r3);
|
||
}
|
||
|
||
.big.violet {
|
||
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
|
||
}
|
||
|
||
.descr {
|
||
white-space: pre-wrap;
|
||
color: var(--ink);
|
||
background: var(--box2);
|
||
border: 1px solid var(--line2);
|
||
border-left: 6px solid var(--matink);
|
||
border-radius: 16px;
|
||
padding: 16px 18px;
|
||
font-size: 1.2rem;
|
||
line-height: 1.5;
|
||
user-select: text;
|
||
-webkit-user-select: text;
|
||
}
|
||
|
||
.dl {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.dl div {
|
||
background: var(--box);
|
||
border: 1px solid var(--line2);
|
||
border-radius: 14px;
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
.dl span {
|
||
display: block;
|
||
font-size: .85rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: .05em;
|
||
color: var(--label);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.dl b {
|
||
font-size: 1.45rem;
|
||
color: var(--ink);
|
||
}
|
||
|
||
.dl div.late {
|
||
border-color: var(--bad);
|
||
}
|
||
|
||
.dl div.late b {
|
||
color: var(--badink);
|
||
}
|
||
|
||
.dl div.soon {
|
||
border-color: var(--pause);
|
||
}
|
||
|
||
.dl div.soon b {
|
||
color: var(--warnink);
|
||
}
|
||
|
||
/* foto */
|
||
.seg {
|
||
display: flex;
|
||
gap: 8px;
|
||
background: var(--field);
|
||
border-radius: 16px;
|
||
padding: 6px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.seg button {
|
||
flex: 1;
|
||
min-height: 56px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font-weight: 900;
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.seg button.on {
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
|
||
}
|
||
|
||
.gal {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.gal img {
|
||
width: 100%;
|
||
aspect-ratio: 4/3;
|
||
object-fit: cover;
|
||
border-radius: 12px;
|
||
border: 1px solid var(--line);
|
||
}
|
||
|
||
.slots {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.slot {
|
||
border-radius: 16px;
|
||
border: 2px solid var(--line);
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
padding: 12px;
|
||
min-height: 120px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.slot i {
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.slot small {
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.slot.done {
|
||
border-color: rgba(34, 197, 94, .6);
|
||
}
|
||
|
||
.slot.done i {
|
||
color: var(--run);
|
||
}
|
||
|
||
.cam {
|
||
width: 100%;
|
||
min-height: 90px;
|
||
border-radius: 18px;
|
||
border: 2px dashed var(--line2);
|
||
background: var(--field);
|
||
color: var(--ink);
|
||
font-size: 1.4rem;
|
||
font-weight: 900;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.cam i {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.cam2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.cam2 .cam {
|
||
margin: 0;
|
||
}
|
||
|
||
.cam.alt {
|
||
border-style: solid;
|
||
border-color: var(--line);
|
||
background: var(--panel2);
|
||
}
|
||
|
||
.gal .ph,
|
||
.ph {
|
||
position: relative;
|
||
padding: 0;
|
||
border: 1px solid var(--line);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: var(--field);
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
.gal .ph img {
|
||
border: 0;
|
||
border-radius: 0;
|
||
display: block;
|
||
}
|
||
|
||
.ph span {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: linear-gradient(transparent, rgba(0, 0, 0, .75));
|
||
color: #fff;
|
||
font-size: .85rem;
|
||
font-weight: 800;
|
||
padding: 16px 8px 6px;
|
||
text-align: left;
|
||
}
|
||
|
||
.ph.big1 {
|
||
margin: 10px 0 14px;
|
||
}
|
||
|
||
.ph.big1 img {
|
||
width: 100%;
|
||
max-height: 46vh;
|
||
object-fit: contain;
|
||
background: #000;
|
||
display: block;
|
||
}
|
||
|
||
.slot img {
|
||
width: 100%;
|
||
height: 84px;
|
||
object-fit: cover;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.slot .sl {
|
||
font-size: 1.15rem;
|
||
}
|
||
|
||
.slot .sl em {
|
||
font-style: normal;
|
||
font-size: .75rem;
|
||
background: var(--run);
|
||
color: #0b1220;
|
||
border-radius: 8px;
|
||
padding: 0 6px;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.back {
|
||
min-height: 52px;
|
||
border-radius: 14px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
font-weight: 800;
|
||
font-size: 1.05rem;
|
||
padding: 0 18px;
|
||
}
|
||
|
||
.slot-h {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1.3rem;
|
||
margin-top: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.slot-h b {
|
||
font-weight: 900;
|
||
}
|
||
|
||
.sec2 {
|
||
font-size: .85rem;
|
||
font-weight: 900;
|
||
text-transform: uppercase;
|
||
letter-spacing: .05em;
|
||
color: var(--muted);
|
||
margin: 6px 0 8px;
|
||
}
|
||
|
||
.vnav {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 72px;
|
||
height: 72px;
|
||
border-radius: 50%;
|
||
border: 0;
|
||
background: rgba(255, 255, 255, .18);
|
||
color: #fff;
|
||
font-size: 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.vnav.prev {
|
||
left: 16px;
|
||
}
|
||
|
||
.vnav.next {
|
||
right: 16px;
|
||
}
|
||
|
||
.viewer .vb {
|
||
position: relative;
|
||
}
|
||
|
||
.files {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 12px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel2);
|
||
color: var(--ink);
|
||
text-align: left;
|
||
width: 100%;
|
||
}
|
||
|
||
.file.dis {
|
||
border-color: #7c3aed;
|
||
background: var(--disbg);
|
||
}
|
||
|
||
.file .ic {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 12px;
|
||
background: var(--field);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.8rem;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file .ic img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.file b {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.file small {
|
||
display: block;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* visore a schermo intero */
|
||
.viewer {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: #000;
|
||
z-index: 60;
|
||
display: none;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.viewer.on {
|
||
display: flex;
|
||
}
|
||
|
||
.viewer .vh {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 14px;
|
||
background: #0b1220;
|
||
}
|
||
|
||
.viewer .vh b {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.viewer .vh .ibtn {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.viewer .vb {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: auto;
|
||
}
|
||
|
||
.viewer img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
touch-action: pinch-zoom;
|
||
}
|
||
|
||
.viewer iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 0;
|
||
background: #fff;
|
||
}
|
||
|
||
/* toast */
|
||
.toast2 {
|
||
position: fixed;
|
||
left: 50%;
|
||
top: 18px;
|
||
transform: translate(-50%, -140%);
|
||
background: var(--toastbg);
|
||
border: 1px solid var(--line);
|
||
color: #fff;
|
||
border-radius: 16px;
|
||
padding: 16px 22px;
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
z-index: 80;
|
||
transition: transform .25s;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, .5);
|
||
max-width: 90%;
|
||
}
|
||
|
||
.toast2.on {
|
||
transform: translate(-50%, 0);
|
||
}
|
||
|
||
.toast2.ok {
|
||
border-color: var(--run);
|
||
}
|
||
|
||
.toast2.err {
|
||
border-color: var(--bad);
|
||
background: var(--selbad);
|
||
}
|
||
|
||
.spin {
|
||
animation: rot 1s linear infinite;
|
||
display: inline-block;
|
||
}
|
||
|
||
@keyframes rot {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.select2-container {
|
||
width: 100% !important;
|
||
}
|
||
|
||
.select2-container--default .select2-selection--single,
|
||
.select2-container--default .select2-selection--multiple {
|
||
background: var(--field);
|
||
border: 2px solid var(--line);
|
||
border-radius: 14px;
|
||
min-height: 60px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||
color: var(--ink);
|
||
font-size: 1.3rem;
|
||
font-weight: 700;
|
||
line-height: 40px;
|
||
}
|
||
|
||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||
font-size: 1.05rem;
|
||
padding: 4px 10px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.select2-dropdown {
|
||
z-index: 70;
|
||
}
|
||
|
||
.select2-results__option {
|
||
font-size: 1.15rem;
|
||
padding: 12px 14px !important;
|
||
}
|
||
|
||
@media (max-width: 1000px) {
|
||
.main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.hero,
|
||
.side {
|
||
flex: none;
|
||
overflow: visible;
|
||
}
|
||
|
||
.h-body {
|
||
flex: none;
|
||
}
|
||
|
||
.side {
|
||
min-height: 360px;
|
||
}
|
||
|
||
.ring {
|
||
width: 170px;
|
||
height: 170px;
|
||
}
|
||
|
||
.tools {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.h-body {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.grid2 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.big {
|
||
font-size: 1.3rem;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.clock {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="app">
|
||
<div class="top">
|
||
<button class="ibtn" onclick="location.href='production_dashboard.php'" aria-label="Indietro"><i class="bi bi-arrow-left"></i></button>
|
||
<div class="tabs" id="tabs"></div>
|
||
<div class="clock" id="clock">--:--</div>
|
||
<button class="ibtn" id="btnQuick" aria-label="Nuovo lavoro"><i class="bi bi-plus-lg"></i></button>
|
||
<button class="ibtn" id="btnTheme" aria-label="Cambia colori"><i class="bi bi-sun"></i></button>
|
||
<button class="ibtn" id="btnFull" aria-label="Schermo intero"><i class="bi bi-arrows-fullscreen"></i></button>
|
||
</div>
|
||
<div id="view" style="flex:1;min-height:0;display:flex;flex-direction:column;"></div>
|
||
</div>
|
||
|
||
<div class="sheet-bg" id="sheetBg"></div>
|
||
<div class="sheet" id="sheet">
|
||
<div class="grab"></div>
|
||
<div class="sheet-h">
|
||
<h3 id="sheetTitle"></h3>
|
||
<button class="ibtn" id="sheetClose" aria-label="Chiudi"><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
<div class="sheet-b" id="sheetBody"></div>
|
||
</div>
|
||
|
||
<div class="viewer" id="viewer">
|
||
<div class="vh"><b id="viewerTitle"></b><button class="ibtn" id="viewerClose" aria-label="Chiudi"><i class="bi bi-x-lg"></i></button></div>
|
||
<div class="vb" id="viewerBody"></div>
|
||
</div>
|
||
|
||
<input type="file" id="camInput" accept="image/*" capture="environment" style="display:none">
|
||
<input type="file" id="galInput" accept="image/*" style="display:none">
|
||
<audio id="beep" src="beep.wav" preload="auto"></audio>
|
||
|
||
<?php include('jsinclude.php'); ?>
|
||
<script>
|
||
$(function() {
|
||
const ST = {
|
||
PROG: <?= ST_PROGRAMMATO ?>,
|
||
RUN: <?= ST_PRODUZIONE ?>,
|
||
PAUSE: <?= ST_PAUSA ?>,
|
||
PROBLEM: <?= ST_PROBLEMA ?>
|
||
};
|
||
const PREF = '<?= (int)$preferred ?: '' ?>';
|
||
const H = 3600e3;
|
||
let S = null; // ultimo stato dal server
|
||
let skew = 0;
|
||
let line = 0; // 0 = panoramica
|
||
let sheetOpen = false;
|
||
let photoCtx = null;
|
||
|
||
try {
|
||
const saved = localStorage.getItem('station_line');
|
||
line = saved !== null ? +saved : +(PREF || 0);
|
||
} catch (e) {
|
||
line = +(PREF || 0);
|
||
}
|
||
|
||
/* ---------- utilità ---------- */
|
||
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
} [c]));
|
||
const now = () => Date.now() - skew;
|
||
const num = (v, d = 0) => (+v || 0).toLocaleString('it-IT', {
|
||
minimumFractionDigits: d,
|
||
maximumFractionDigits: d
|
||
});
|
||
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')}`;
|
||
};
|
||
const hm = d => `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||
const GG = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
|
||
const dShort = s => {
|
||
if (!s) return '—';
|
||
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')}`;
|
||
};
|
||
const daysTo = s => s ? Math.round((new Date(s + 'T00:00:00') - new Date(new Date(now()).toDateString())) / 864e5) : null;
|
||
const lineOf = id => (S.lines || []).find(l => l.id === id) || {};
|
||
const beep = () => {
|
||
try {
|
||
const b = document.getElementById('beep');
|
||
b.currentTime = 0;
|
||
b.play().catch(() => {});
|
||
} catch (e) {}
|
||
if (navigator.vibrate) navigator.vibrate(30);
|
||
};
|
||
|
||
function toast(msg, kind) {
|
||
const t = $(`<div class="toast2 ${kind || 'ok'}">${esc(msg)}</div>`).appendTo('body');
|
||
setTimeout(() => t.addClass('on'), 10);
|
||
setTimeout(() => {
|
||
t.removeClass('on');
|
||
setTimeout(() => t.remove(), 300);
|
||
}, kind === 'err' ? 4500 : 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('Connessione assente: riprova', 'err');
|
||
return $.Deferred().reject().promise();
|
||
});
|
||
}
|
||
|
||
/* tempi di un lavoro in macchina */
|
||
function times(j) {
|
||
const t = now();
|
||
if (!j.start_ms) return {
|
||
net: 0,
|
||
gross: 0,
|
||
pause: 0,
|
||
cur: 0
|
||
};
|
||
const gross = t - j.start_ms;
|
||
const cur = j.open_pause_ms ? t - j.open_pause_ms : 0;
|
||
const pause = j.pause_sec * 1000 + cur;
|
||
return {
|
||
net: Math.max(0, gross - pause),
|
||
gross,
|
||
pause,
|
||
cur
|
||
};
|
||
}
|
||
|
||
/* ---------- caricamento ---------- */
|
||
function load() {
|
||
return $.getJSON('?a=state&line=' + line).then(r => {
|
||
if (!r.success) return toast(r.message || 'Errore', 'err');
|
||
skew = Date.now() - r.now_ms;
|
||
S = r;
|
||
if (line && !r.lines.some(l => l.id === line)) line = 0;
|
||
render();
|
||
});
|
||
}
|
||
|
||
function render() {
|
||
renderTabs();
|
||
if (!line) renderOverview();
|
||
else renderLine();
|
||
tick();
|
||
}
|
||
|
||
function renderTabs() {
|
||
const act = id => S.overview.find(j => j.line === id && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
let h = `<button class="tab ${line ? '' : 'on'}" data-line="0" style="--c:#475569"><i class="bi bi-grid-3x3-gap-fill"></i> Tutte</button>`;
|
||
S.lines.forEach(l => {
|
||
const a = act(l.id);
|
||
h += `<button class="tab ${line === l.id ? 'on' : ''}" data-line="${l.id}" style="--c:${esc(l.color)}"><span class="dot ${a ? (a.status === ST.RUN ? 'run' : 'pause') : ''}"></span>${esc(l.name)}</button>`;
|
||
});
|
||
$('#tabs').html(h);
|
||
}
|
||
|
||
/* ---------- panoramica di tutte le linee ---------- */
|
||
function renderOverview() {
|
||
let h = '<div class="ov">';
|
||
S.lines.forEach(l => {
|
||
const a = S.overview.find(j => j.line === l.id && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
const pb = S.overview.filter(j => j.line === l.id && j.status === ST.PROBLEM).length;
|
||
const q = S.queue_count[l.id] || {
|
||
n: 0,
|
||
h: 0
|
||
};
|
||
const cls = a ? (a.status === ST.RUN ? 'run' : 'pause') : 'free';
|
||
h += `<button class="ovc" data-line="${l.id}" style="--c:${esc(l.color)}">
|
||
<div class="n">${esc(l.name)}<span class="pill ${cls}">${a ? (a.status === ST.RUN ? 'In produzione' : 'In pausa') : 'Libera'}</span></div>
|
||
${a ? `<div class="mat">${esc(a.matrice)}</div><div class="cli">${esc(a.cliente || '')}</div>
|
||
<div class="tm" data-ov="${a.id}">--:--:--</div>`
|
||
: `<div class="mat" style="color:var(--muted)">ferma</div><div class="cli">${l.next ? 'prossimo: <b style="color:var(--ink)">' + esc(l.next) + '</b>' : 'nessun lavoro in coda'}</div><div class="tm" style="color:var(--free);font-size:1.6rem">pronta</div>`}
|
||
<div class="q">${q.n} in coda · ${num(q.h, 0)} h${pb ? ` · <span class="late">${pb} problema</span>` : ''}</div>
|
||
</button>`;
|
||
});
|
||
$('#view').html(h + '</div>');
|
||
}
|
||
|
||
/* ---------- postazione di una linea ---------- */
|
||
function renderLine() {
|
||
const l = lineOf(line);
|
||
const a = S.overview.find(j => j.line === line && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
const problems = S.overview.filter(j => j.line === line && j.status === ST.PROBLEM);
|
||
const q = S.queue || [];
|
||
let hero;
|
||
|
||
if (a) {
|
||
const run = a.status === ST.RUN;
|
||
const dz = daysTo(a.data_zibo);
|
||
hero = `<div class="hero ${run ? 'run' : 'pause'}" style="--lc:${esc(l.color)}">
|
||
<div class="h-top"><span class="pill ${run ? 'run' : 'pause'}">${run ? 'In produzione' : 'In pausa'}</span>
|
||
<span class="h-order">${a.ordine ? 'Ordine ' + esc(a.ordine) : ''}</span></div>
|
||
<div class="h-mat"><div class="name">${esc(a.matrice)}</div>${a.photo ? `<img src="photos/matrici/${esc(a.photo)}" class="zoom" data-src="photos/matrici/${esc(a.photo)}" alt="">` : ''}</div>
|
||
<div class="h-cli">${esc(a.cliente || 'Nessun cliente')}</div>
|
||
${a.mescole.length ? `<div class="chips">${a.mescole.map(m => `<span class="schip">${esc(m)}</span>`).join('')}</div>` : ''}
|
||
${!run ? `<div><span class="reason ${a.open_reason ? '' : 'missing'}"><i class="bi bi-pause-circle-fill"></i>${a.open_reason ? esc(a.open_reason) : 'Motivo della pausa da indicare'}</span></div>` : ''}
|
||
<div class="h-body">
|
||
<div class="ring" id="ring">${ringSvg()}<div class="in"><div class="pct" id="ringPct">—</div><div class="lbl">${a.ore ? 'di ' + num(a.ore, 1) + ' h' : 'ore non previste'}</div></div></div>
|
||
<div>
|
||
<div class="timer" id="tMain">--:--:--</div>
|
||
<div class="t-sub" id="tSub"></div>
|
||
<div class="kpis">
|
||
${a.metri ? `<div class="kpi"><div class="v">${num(a.metri)}</div><div class="l">metri da fare</div></div>` : ''}
|
||
${a.kg_sp ? `<div class="kpi"><div class="v">${num(a.kg_sp)}</div><div class="l">kg teorici</div></div>` : ''}
|
||
<div class="kpi ${dz !== null && dz < 0 ? 'late' : (dz !== null && dz <= 1 ? 'soon' : '')}"><div class="v">${dShort(a.data_zibo)}</div><div class="l">data Zibo</div></div>
|
||
<div class="kpi" id="kEta"><div class="v">—</div><div class="l">fine prevista</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="big-actions">
|
||
${run ? `<button class="big pause" data-act="pause"><i class="bi bi-pause-fill"></i> PAUSA</button>`
|
||
: `<button class="big go" data-act="resume"><i class="bi bi-play-fill"></i> RIPRENDI</button>`}
|
||
<button class="big stop" data-act="finish"><i class="bi bi-stop-fill"></i> FINE</button>
|
||
</div>
|
||
<div class="tools">
|
||
<button class="tool" data-photo="lotto_mescola"><span class="n ${a.photos.lotto_mescola ? '' : 'zero'}">${a.photos.lotto_mescola}</span><i class="bi bi-camera-fill"></i>Lotto mescola</button>
|
||
<button class="tool" data-photo="parametri_macchina"><span class="n ${Object.keys(a.photos.slots).length ? '' : 'zero'}">${Object.keys(a.photos.slots).length}/${(S.params || []).length || '–'}</span><i class="bi bi-sliders"></i>Parametri</button>
|
||
<button class="tool danger" data-photo="problema"><span class="n ${a.photos.problema ? '' : 'zero'}">${a.photos.problema}</span><i class="bi bi-exclamation-triangle-fill"></i>Foto problema</button>
|
||
<button class="tool" data-files="${a.matrice_id}"><i class="bi bi-rulers"></i>Disegni</button>
|
||
<button class="tool mago" data-info="${a.id}"><i class="bi bi-card-text"></i>Dettagli</button>
|
||
</div>
|
||
</div>`;
|
||
} else {
|
||
const n = q[0];
|
||
hero = `<div class="hero free" style="--lc:${esc(l.color)}">
|
||
<div class="h-top"><span class="pill free">Linea libera</span></div>
|
||
${n ? `<div class="h-cli" style="margin-top:18px;color:var(--muted);font-size:1.1rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em">Prossimo lavoro</div>
|
||
<div class="h-mat" style="margin-top:6px"><div class="name">${esc(n.matrice)}</div>${n.photo ? `<img src="photos/matrici/${esc(n.photo)}" class="zoom" data-src="photos/matrici/${esc(n.photo)}" alt="">` : ''}</div>
|
||
<div class="h-cli">${esc(n.cliente || 'Nessun cliente')}${n.ordine ? ' · ' + esc(n.ordine) : ''}</div>
|
||
${n.mescole.length ? `<div class="chips">${n.mescole.map(m => `<span class="schip">${esc(m)}</span>`).join('')}</div>` : ''}
|
||
<div class="kpis">
|
||
${n.metri ? `<div class="kpi"><div class="v">${num(n.metri)}</div><div class="l">metri</div></div>` : ''}
|
||
<div class="kpi"><div class="v">${n.ore ? num(n.ore, 1) + ' h' : '?'}</div><div class="l">ore previste</div></div>
|
||
<div class="kpi ${daysTo(n.data_zibo) !== null && daysTo(n.data_zibo) < 0 ? 'late' : ''}"><div class="v">${dShort(n.data_zibo)}</div><div class="l">data Zibo</div></div>
|
||
</div>
|
||
<div style="flex:1"></div>
|
||
<div class="big-actions"><button class="big go" data-start="${n.id}"><i class="bi bi-play-fill"></i> AVVIA ${esc(n.matrice)}</button></div>
|
||
<div class="tools" style="grid-template-columns:repeat(2,1fr)">
|
||
<button class="tool" data-files="${n.matrice_id}"><i class="bi bi-rulers"></i>Disegni</button>
|
||
<button class="tool mago" data-info="${n.id}"><i class="bi bi-card-text"></i>Dettagli</button>
|
||
</div>`
|
||
: `<div class="empty" style="margin:auto"><i class="bi bi-cup-hot" style="font-size:3rem;display:block;margin-bottom:10px"></i>Nessun lavoro programmato su questa linea</div>
|
||
<div class="big-actions"><button class="big go" id="heroQuick"><i class="bi bi-plus-lg"></i> NUOVO LAVORO</button></div>`}
|
||
</div>`;
|
||
}
|
||
|
||
const totH = q.reduce((s, j) => s + j.ore, 0);
|
||
let side = `<div class="side"><div class="side-h"><span class="t">In coda</span><span class="m">${q.length} · ${num(totH, 0)} h</span></div><div class="qlist">`;
|
||
problems.forEach(p => side += qCard(p, '!', true));
|
||
side += q.length ? q.map((j, i) => qCard(j, i + 1, false, !a && i === 0)).join('') : '<div class="empty">Coda vuota</div>';
|
||
side += '</div></div>';
|
||
|
||
$('#view').html(`<div class="main">${hero}${side}</div>`);
|
||
}
|
||
|
||
function qCard(j, pos, problem, next) {
|
||
const dz = daysTo(j.data_zibo);
|
||
const l = lineOf(j.line);
|
||
return `<button class="qc ${next ? 'next' : ''}" data-info="${j.id}" style="--lc:${problem ? 'var(--bad)' : esc(l.color)}">
|
||
<div class="r1"><span class="pos" ${problem ? 'style="color:#f87171"' : ''}>${pos}</span><span class="mat">${esc(j.matrice)}</span><span class="h">${problem ? '<span class="late">PROBLEMA</span>' : (j.ore ? num(j.ore, 1) + ' h' : '<span class="soon">? h</span>')}</span></div>
|
||
<div class="r2">${esc(j.cliente || 'Nessun cliente')}${j.ordine ? ' · ' + esc(j.ordine) : ''}</div>
|
||
<div class="r3">${j.metri ? `<span>${num(j.metri)} m</span>` : ''}<span class="${dz !== null && dz < 0 ? 'late' : (dz !== null && dz <= 2 ? 'soon' : '')}"><i class="bi bi-calendar-event"></i> ${dShort(j.data_zibo)}</span>${j.mescole.length ? `<span>${esc(j.mescole[0])}${j.mescole.length > 1 ? ' +' + (j.mescole.length - 1) : ''}</span>` : ''}</div>
|
||
</button>`;
|
||
}
|
||
|
||
function ringSvg() {
|
||
const r = 100,
|
||
c = 2 * Math.PI * r;
|
||
return `<svg viewBox="0 0 230 230"><circle class="bg" cx="115" cy="115" r="${r}" fill="none" stroke-width="18"/>
|
||
<circle class="fg" id="ringFg" cx="115" cy="115" r="${r}" fill="none" stroke-width="18" stroke-linecap="round" stroke="var(--run)" stroke-dasharray="${c}" stroke-dashoffset="${c}"/></svg>`;
|
||
}
|
||
|
||
/* aggiornamento ogni secondo: orologio, timer, anello */
|
||
function tick() {
|
||
const d = new Date(now());
|
||
$('#clock').text(hm(d));
|
||
if (!S) return;
|
||
$('[data-ov]').each(function() {
|
||
const j = S.overview.find(x => x.id === +this.dataset.ov);
|
||
if (j) $(this).text(hms(j.status === ST.PAUSE ? times(j).cur : times(j).net)).toggleClass('run', j.status === ST.RUN).css('color', j.status === ST.PAUSE ? 'var(--pauseink)' : '');
|
||
});
|
||
if (!line) return;
|
||
const a = S.overview.find(j => j.line === line && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
if (!a) return;
|
||
const t = times(a);
|
||
const run = a.status === ST.RUN;
|
||
if (run) {
|
||
$('#tMain').text(hms(t.net)).css('color', '');
|
||
$('#tSub').html(`in macchina da <b>${hms(t.gross)}</b> · pause <b>${hms(t.pause)}</b>`);
|
||
} else {
|
||
$('#tMain').text(hms(t.cur)).css('color', 'var(--pauseink)');
|
||
$('#tSub').html(`in pausa · lavorato <b>${hms(t.net)}</b>`);
|
||
}
|
||
if (a.ore > 0) {
|
||
const p = t.net / (a.ore * H);
|
||
const c = 2 * Math.PI * 100;
|
||
$('#ringFg').attr('stroke-dashoffset', c * (1 - Math.min(1, p))).attr('stroke', p > 1 ? 'var(--bad)' : (run ? 'var(--run)' : 'var(--pause)'));
|
||
$('#ringPct').text(Math.round(p * 100) + '%').css('color', p > 1 ? 'var(--badink)' : (run ? 'var(--runink)' : 'var(--pauseink)'));
|
||
const end = new Date(now() + Math.max(0, a.ore * H - t.net));
|
||
const late = a.data_zibo && end > new Date(a.data_zibo + 'T23:59:59');
|
||
const dd = Math.round((new Date(end.toDateString()) - new Date(new Date(now()).toDateString())) / 864e5);
|
||
$('#kEta').toggleClass('late', !!late).find('.v').text(p >= 1 ? 'oltre' : (dd === 0 ? hm(end) : (dd < 7 ? GG[end.getDay()] + ' ' + hm(end) : `${String(end.getDate()).padStart(2, '0')}/${String(end.getMonth() + 1).padStart(2, '0')}`)));
|
||
}
|
||
}
|
||
setInterval(tick, 1000);
|
||
setInterval(() => {
|
||
if (!sheetOpen && !$('#viewer').hasClass('on')) load();
|
||
}, 20000);
|
||
|
||
/* ---------- navigazione ---------- */
|
||
$(document).on('click', '.tab, .ovc', function() {
|
||
line = +this.dataset.line;
|
||
try {
|
||
localStorage.setItem('station_line', String(line));
|
||
} catch (e) {}
|
||
beep();
|
||
load();
|
||
});
|
||
|
||
/* ---------- pannello dal basso ---------- */
|
||
function openSheet(title, html) {
|
||
$('#sheetTitle').html(title);
|
||
$('#sheetBody').html(html).scrollTop(0);
|
||
$('#sheet, #sheetBg').addClass('on');
|
||
sheetOpen = true;
|
||
}
|
||
|
||
function closeSheet() {
|
||
$('#sheet, #sheetBg').removeClass('on');
|
||
sheetOpen = false;
|
||
photoCtx = null;
|
||
if ($('#qMat').data('select2')) $('#qMat, #qMes').select2('destroy');
|
||
}
|
||
$('#sheetClose, #sheetBg').on('click', closeSheet);
|
||
|
||
const findJob = id => S.overview.find(j => j.id === id) || (S.queue || []).find(j => j.id === id);
|
||
|
||
/* ---------- dettagli lavoro (descrizione Mago compatta qui) ---------- */
|
||
$(document).on('click', '[data-info]', function(e) {
|
||
e.stopPropagation();
|
||
const j = findJob(+this.dataset.info);
|
||
if (!j) return;
|
||
const lineBusy = S.overview.some(x => x.line === j.line && (x.status === ST.RUN || x.status === ST.PAUSE));
|
||
const canStart = j.status === ST.PROG && !lineBusy;
|
||
openSheet(`${esc(j.matrice)} <small style="color:var(--muted);font-weight:700">· ${esc(j.cliente || '')}</small>`, `
|
||
<div class="dl">
|
||
<div><span>Ordine</span><b>${esc(j.ordine || '—')}</b></div>
|
||
<div class="${daysTo(j.data_zibo) !== null && daysTo(j.data_zibo) < 0 ? 'late' : (daysTo(j.data_zibo) !== null && daysTo(j.data_zibo) <= 2 ? 'soon' : '')}"><span>Data Zibo</span><b>${dShort(j.data_zibo)}</b></div>
|
||
<div><span>Data cliente</span><b>${dShort(j.data_cliente)}</b></div>
|
||
<div><span>Metri</span><b>${j.metri ? num(j.metri) : '—'}</b></div>
|
||
<div><span>Kg teorici</span><b>${j.kg_sp ? num(j.kg_sp) : '—'}</b></div>
|
||
<div><span>Ore previste</span><b>${j.ore ? num(j.ore, 1) : '—'}</b></div>
|
||
</div>
|
||
${j.mescole.length ? `<div class="fld"><label>Mescole</label><div class="chips" style="margin:0">${j.mescole.map(m => `<span class="schip">${esc(m)}</span>`).join('')}</div></div>` : ''}
|
||
${j.tools.length ? `<div class="fld"><label>Attrezzi</label><div class="chips" style="margin:0">${j.tools.map(m => `<span class="schip">${esc(m)}</span>`).join('')}</div></div>` : ''}
|
||
<div class="fld"><label>Descrizione</label><div class="descr">${j.note ? esc(j.note) : '<span style="color:var(--muted)">Nessuna descrizione</span>'}</div></div>
|
||
${j.note_operatore ? `<div class="fld"><label>Nota operatore</label><div class="descr">${esc(j.note_operatore)}</div></div>` : ''}
|
||
<div class="sheet-f">
|
||
<button class="big ghost" data-files="${j.matrice_id}"><i class="bi bi-rulers"></i> Disegni</button>
|
||
${j.status === ST.PROG ? `<button class="big go" data-start="${j.id}" ${canStart ? '' : 'disabled'}><i class="bi bi-play-fill"></i> ${canStart ? 'AVVIA' : 'LINEA OCCUPATA'}</button>` : ''}
|
||
</div>`);
|
||
});
|
||
|
||
/* ---------- avvio ---------- */
|
||
$(document).on('click', '[data-start]', function() {
|
||
const id = +this.dataset.start;
|
||
const j = findJob(id);
|
||
beep();
|
||
openSheet('Avviare la produzione?', `
|
||
<div class="h-mat" style="margin:0 0 6px"><div class="name" style="font-size:2.6rem">${esc(j.matrice)}</div></div>
|
||
<div class="h-cli" style="margin-bottom:10px">${esc(j.cliente || '')} · ${esc(lineOf(j.line).name || '')}</div>
|
||
<div class="sheet-f"><button class="big ghost" id="cancelSheet">ANNULLA</button><button class="big go" id="doStart"><i class="bi bi-play-fill"></i> AVVIA ORA</button></div>`);
|
||
$('#doStart').on('click', () => post({
|
||
a: 'start',
|
||
id
|
||
}).then(() => {
|
||
beep();
|
||
closeSheet();
|
||
toast('Produzione avviata');
|
||
load();
|
||
}));
|
||
});
|
||
$(document).on('click', '#cancelSheet', closeSheet);
|
||
|
||
/* ---------- pausa / ripresa / fine ---------- */
|
||
function reasonTiles(withLater) {
|
||
return `<div class="tiles" id="rTiles">
|
||
${S.reasons.map(r => `<button class="tile ${+r.is_problem ? 'problem' : ''}" data-r="${r.id}" data-p="${r.is_problem}"><i class="bi ${+r.is_problem ? 'bi-exclamation-octagon-fill' : 'bi-pause-circle'}"></i>${esc(r.name)}</button>`).join('')}
|
||
${withLater ? '<button class="tile" data-r="0" data-p="0"><i class="bi bi-clock-history"></i>Lo indico dopo</button>' : ''}
|
||
</div>`;
|
||
}
|
||
$(document).on('click', '#rTiles .tile', function() {
|
||
$('#rTiles .tile').removeClass('sel');
|
||
$(this).addClass('sel');
|
||
$('#needNote').toggle(+this.dataset.p === 1);
|
||
});
|
||
|
||
$(document).on('click', '[data-act]', function() {
|
||
const a = S.overview.find(j => j.line === line && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
if (!a) return;
|
||
const act = this.dataset.act;
|
||
beep();
|
||
if (act === 'pause') {
|
||
openSheet('<i class="bi bi-pause-fill" style="color:var(--pause)"></i> Motivo della pausa', `${reasonTiles(true)}
|
||
<div class="fld"><label>Nota <span id="needNote" style="display:none;color:#f87171">(obbligatoria per un problema)</span></label><textarea id="pNote" rows="2"></textarea></div>
|
||
<div class="sheet-f"><button class="big ghost" id="cancelSheet">ANNULLA</button><button class="big pause" id="doPause"><i class="bi bi-pause-fill"></i> METTI IN PAUSA</button></div>`);
|
||
$('#doPause').on('click', () => {
|
||
const sel = $('#rTiles .sel');
|
||
if (!sel.length) return toast('Tocca un motivo (o "Lo indico dopo")', 'err');
|
||
post({
|
||
a: 'pause',
|
||
id: a.id,
|
||
reason_id: sel.data('r'),
|
||
note: $('#pNote').val()
|
||
}).then(() => {
|
||
closeSheet();
|
||
toast('In pausa');
|
||
load();
|
||
});
|
||
});
|
||
}
|
||
if (act === 'resume') {
|
||
const doResume = rid => post({
|
||
a: 'resume',
|
||
id: a.id,
|
||
reason_id: rid || 0
|
||
}).then(() => {
|
||
beep();
|
||
closeSheet();
|
||
toast('Produzione ripresa');
|
||
load();
|
||
});
|
||
if (a.open_reason) {
|
||
openSheet('Riprendere la produzione?', `<div class="sheet-f"><button class="big ghost" id="cancelSheet">ANNULLA</button><button class="big go" id="doResume"><i class="bi bi-play-fill"></i> RIPRENDI</button></div>`);
|
||
$('#doResume').on('click', () => doResume(0));
|
||
} else {
|
||
openSheet('Prima di riprendere: perché si è fermata?', `${reasonTiles(false)}
|
||
<div class="sheet-f"><button class="big ghost" id="cancelSheet">ANNULLA</button><button class="big go" id="doResume"><i class="bi bi-play-fill"></i> RIPRENDI</button></div>`);
|
||
$('#doResume').on('click', () => {
|
||
const sel = $('#rTiles .sel');
|
||
if (!sel.length) return toast('Tocca il motivo della pausa', 'err');
|
||
doResume(sel.data('r'));
|
||
});
|
||
}
|
||
}
|
||
if (act === 'finish') finishStep1(a);
|
||
});
|
||
|
||
function finishStep1(a) {
|
||
openSheet('<i class="bi bi-stop-fill" style="color:var(--bad)"></i> Come è finita?', `
|
||
<div class="tiles" style="grid-template-columns:1fr 1fr">
|
||
<button class="tile huge" id="fOk"><i class="bi bi-check-circle-fill" style="color:var(--run)"></i>Fine produzione</button>
|
||
<button class="tile huge problem" id="fKo"><i class="bi bi-exclamation-octagon-fill"></i>Problema</button>
|
||
</div>`);
|
||
$('#fOk').on('click', () => finishStep2(a, 'fine'));
|
||
$('#fKo').on('click', () => finishStep2(a, 'problema'));
|
||
}
|
||
|
||
function finishStep2(a, reason) {
|
||
openSheet(reason === 'fine' ? 'Dati di fine produzione' : '<span class="late">Fine con problema</span>', `
|
||
<div class="grid2">
|
||
<div class="fld"><label>Kg prodotti</label><input type="text" inputmode="decimal" id="fKg" autocomplete="off"><div class="teo">teorici <b>${num(a.kg_sp, 1)}</b></div></div>
|
||
<div class="fld"><label>Metri prodotti</label><input type="text" inputmode="decimal" id="fMt" autocomplete="off"><div class="teo">teorici <b>${num(a.metri)}</b></div></div>
|
||
<div class="fld"><label>Scarto kg</label><input type="text" inputmode="decimal" id="fSc" autocomplete="off"></div>
|
||
<div class="fld"><label>Scarto %</label><input type="text" id="fScp" readonly tabindex="-1" style="color:var(--muted)"></div>
|
||
</div>
|
||
<div class="fld"><label>Nota${reason === 'problema' ? ' <span class="late">(obbligatoria)</span>' : ''}</label><textarea id="fNote" rows="2">${esc(a.note_operatore || '')}</textarea></div>
|
||
<div class="sheet-f">
|
||
<button class="big ghost" id="cancelSheet">ANNULLA</button>
|
||
<button class="big violet" id="fLater"><i class="bi bi-clock-history"></i> COMPILA DOPO</button>
|
||
<button class="big ${reason === 'fine' ? 'go' : 'stop'}" id="fSave"><i class="bi bi-check-lg"></i> CONFERMA</button>
|
||
</div>`);
|
||
const n = s => +String($(s).val() || '').replace(',', '.') || 0;
|
||
$('#fKg, #fSc').on('input', () => {
|
||
const t = n('#fKg') + n('#fSc');
|
||
$('#fScp').val(t > 0 ? num(n('#fSc') / t * 100, 2) + ' %' : '');
|
||
});
|
||
let armed = false;
|
||
const send = later => post({
|
||
a: 'finish',
|
||
id: a.id,
|
||
reason,
|
||
later: later ? 1 : 0,
|
||
kgprod: n('#fKg'),
|
||
mtprod: n('#fMt'),
|
||
scarto: n('#fSc'),
|
||
note: $('#fNote').val()
|
||
}).then(() => {
|
||
beep();
|
||
closeSheet();
|
||
toast(later ? 'Chiusa: dati da compilare' : (reason === 'fine' ? 'Produzione terminata · a Qualità' : 'Chiusa con problema'));
|
||
load();
|
||
});
|
||
$('#fSave').on('click', () => send(false));
|
||
// "compila dopo": secondo tocco per confermare
|
||
$('#fLater').on('click', function() {
|
||
if (!armed) {
|
||
armed = true;
|
||
$(this).html('<i class="bi bi-question-circle"></i> TOCCA DI NUOVO');
|
||
return;
|
||
}
|
||
send(true);
|
||
});
|
||
}
|
||
|
||
/* ---------- foto ---------- */
|
||
$(document).on('click', '[data-photo]', function() {
|
||
const a = S.overview.find(j => j.line === line && (j.status === ST.RUN || j.status === ST.PAUSE));
|
||
if (!a) return;
|
||
openPhotos(a, this.dataset.photo);
|
||
});
|
||
|
||
function openPhotos(a, type) {
|
||
photoCtx = {
|
||
id: a.id,
|
||
type,
|
||
slot: null
|
||
};
|
||
const T = {
|
||
lotto_mescola: 'Lotto mescola',
|
||
parametri_macchina: 'Parametri macchina',
|
||
problema: 'Problema'
|
||
};
|
||
openSheet(`<i class="bi bi-camera-fill"></i> Foto · ${esc(a.matrice)}`, `
|
||
<div class="seg">${Object.keys(T).map(k => `<button data-seg="${k}" class="${k === type ? 'on' : ''}">${T[k]}</button>`).join('')}</div>
|
||
<div id="photoArea"></div>`);
|
||
photoArea(a);
|
||
}
|
||
$(document).on('click', '[data-seg]', function() {
|
||
const a = S.overview.find(j => j.id === photoCtx.id);
|
||
photoCtx.type = this.dataset.seg;
|
||
photoCtx.slot = null;
|
||
$('[data-seg]').removeClass('on');
|
||
$(this).addClass('on');
|
||
photoArea(a);
|
||
});
|
||
|
||
// foto del lavoro lette direttamente da production_photos (con posizione e ora)
|
||
let PH = [];
|
||
|
||
function loadPhotos(id) {
|
||
return $.getJSON('?a=photos&id=' + id).then(r => {
|
||
PH = (r && r.rows) || [];
|
||
});
|
||
}
|
||
|
||
const camButtons = slot => `<div class="cam2">
|
||
<button class="cam" data-cam="${slot || 0}" data-src="cam"><i class="bi bi-camera-fill"></i> SCATTA</button>
|
||
<button class="cam alt" data-cam="${slot || 0}" data-src="gal"><i class="bi bi-images"></i> DALLA GALLERIA</button>
|
||
</div>`;
|
||
const when = s => s ? `${s.slice(11, 16)} · ${s.slice(8, 10)}/${s.slice(5, 7)}` : '';
|
||
const thumbs = list => list.length ?
|
||
`<div class="gal">${list.map((p, i) => `<button class="ph" data-ph="${p.id}"><img src="photos/${esc(p.filename)}" alt="" loading="lazy"><span>${p.param_position ? 'P' + p.param_position + ' · ' : ''}${when(p.created_at)}</span></button>`).join('')}</div>` :
|
||
'<div class="teo">Nessuna foto ancora</div>';
|
||
|
||
function photoArea(a) {
|
||
$('#photoArea').html('<div class="teo"><i class="bi bi-arrow-repeat spin"></i> Caricamento…</div>');
|
||
loadPhotos(a.id).then(() => drawPhotos(a), () => $('#photoArea').html('<div class="teo late">Foto non disponibili</div>'));
|
||
}
|
||
|
||
function drawPhotos(a) {
|
||
const type = photoCtx.type;
|
||
const list = PH.filter(p => p.photo_type === type);
|
||
if (type === 'parametri_macchina' && (S.params || []).length) {
|
||
// dettaglio di un parametro
|
||
if (photoCtx.slot) {
|
||
const p = S.params.find(x => +x.position === photoCtx.slot) || {};
|
||
const sl = list.filter(x => +x.param_position === photoCtx.slot);
|
||
$('#photoArea').html(`<button class="back" id="slotBack"><i class="bi bi-arrow-left"></i> Tutti i parametri</button>
|
||
<div class="slot-h"><i class="bi ${esc(p.icon || 'bi-sliders')}"></i><b>${esc(p.short_label || '')}</b> ${esc(p.label || '')}</div>
|
||
${sl.length ? `<button class="ph big1" data-ph="${sl[0].id}"><img src="photos/${esc(sl[0].filename)}" alt=""><span>ultima · ${when(sl[0].created_at)}</span></button>` : '<div class="teo" style="margin:10px 0">Nessuna foto per questo parametro</div>'}
|
||
${camButtons(photoCtx.slot)}
|
||
${sl.length > 1 ? '<div class="sec2">Foto precedenti</div>' + thumbs(sl.slice(1)) : ''}`);
|
||
return;
|
||
}
|
||
// griglia dei parametri con l'ultima foto di ciascuno
|
||
$('#photoArea').html(`<div class="slots">${S.params.map(p => {
|
||
const last = list.find(x => +x.param_position === +p.position);
|
||
const n = list.filter(x => +x.param_position === +p.position).length;
|
||
return `<button class="slot ${last ? 'done' : ''}" data-slot="${p.position}">
|
||
${last ? `<img src="photos/${esc(last.filename)}" alt="" loading="lazy">` : `<i class="bi ${esc(p.icon || 'bi-camera')}"></i>`}
|
||
<span class="sl">${esc(p.short_label)}${n > 1 ? ` <em>${n}</em>` : ''}</span><small>${last ? when(last.created_at) : esc(p.label)}</small></button>`;
|
||
}).join('')}</div>
|
||
<div class="teo" style="margin:12px 0">Tocca un parametro per vedere la foto o farne una nuova. Bordo verde = foto presente.</div>`);
|
||
return;
|
||
}
|
||
$('#photoArea').html(camButtons(0) + thumbs(list));
|
||
}
|
||
|
||
$(document).on('click', '[data-slot]', function() {
|
||
photoCtx.slot = +this.dataset.slot;
|
||
drawPhotos(S.overview.find(j => j.id === photoCtx.id));
|
||
});
|
||
$(document).on('click', '#slotBack', function() {
|
||
photoCtx.slot = null;
|
||
drawPhotos(S.overview.find(j => j.id === photoCtx.id));
|
||
});
|
||
|
||
// scatta con la fotocamera oppure scegli dalla galleria del tablet
|
||
$(document).on('click', '[data-cam]', function() {
|
||
photoCtx.slot = +this.dataset.cam || photoCtx.slot || null;
|
||
$(this.dataset.src === 'gal' ? '#galInput' : '#camInput').val('').trigger('click');
|
||
});
|
||
|
||
// visore foto a schermo intero, con scorrimento tra le foto dello stesso gruppo
|
||
let viewList = [],
|
||
viewIdx = 0;
|
||
$(document).on('click', '[data-ph]', function() {
|
||
const id = +this.dataset.ph;
|
||
const cur = PH.find(p => p.id === id);
|
||
viewList = PH.filter(p => p.photo_type === cur.photo_type && (!photoCtx.slot || +p.param_position === photoCtx.slot));
|
||
viewIdx = Math.max(0, viewList.findIndex(p => p.id === id));
|
||
showPhoto();
|
||
});
|
||
|
||
function showPhoto() {
|
||
const p = viewList[viewIdx];
|
||
if (!p) return;
|
||
const par = p.param_position ? (S.params || []).find(x => +x.position === +p.param_position) : null;
|
||
$('#viewerTitle').text(`${par ? par.short_label + ' · ' + par.label + ' · ' : ''}${when(p.created_at)} (${viewIdx + 1}/${viewList.length})`);
|
||
$('#viewerBody').html(`<img src="photos/${esc(p.filename)}" alt="">
|
||
${viewList.length > 1 ? '<button class="vnav prev" id="vPrev"><i class="bi bi-chevron-left"></i></button><button class="vnav next" id="vNext"><i class="bi bi-chevron-right"></i></button>' : ''}`);
|
||
$('#viewer').addClass('on');
|
||
}
|
||
$(document).on('click', '#vPrev', () => {
|
||
viewIdx = (viewIdx - 1 + viewList.length) % viewList.length;
|
||
showPhoto();
|
||
});
|
||
$(document).on('click', '#vNext', () => {
|
||
viewIdx = (viewIdx + 1) % viewList.length;
|
||
showPhoto();
|
||
});
|
||
// scorrimento col dito
|
||
let tx = null;
|
||
$('#viewerBody').on('touchstart', e => tx = e.originalEvent.touches[0].clientX).on('touchend', e => {
|
||
if (tx === null || viewList.length < 2 || !$('#vNext').length) return;
|
||
const dx = e.originalEvent.changedTouches[0].clientX - tx;
|
||
tx = null;
|
||
if (Math.abs(dx) > 60) $(dx < 0 ? '#vNext' : '#vPrev').trigger('click');
|
||
});
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
$('#camInput, #galInput').on('change', async function() {
|
||
const f = this.files[0];
|
||
if (!f || !photoCtx) return;
|
||
toast('Caricamento foto…');
|
||
try {
|
||
const blob = await resize(f, 1600, .7);
|
||
const fd = new FormData();
|
||
fd.append('production_id', photoCtx.id);
|
||
fd.append('photo_type', photoCtx.type);
|
||
fd.append('param_position', photoCtx.slot || '');
|
||
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 => {
|
||
if (!r.success) return toast(r.message || 'Foto non caricata', 'err');
|
||
beep();
|
||
toast('Foto salvata');
|
||
const a = S.overview.find(j => j.id === photoCtx.id);
|
||
if (a) {
|
||
if (photoCtx.slot) a.photos.slots[photoCtx.slot] = true;
|
||
a.photos[photoCtx.type] = (a.photos[photoCtx.type] || 0) + 1;
|
||
loadPhotos(a.id).then(() => drawPhotos(a));
|
||
}
|
||
}, () => toast('Errore di rete: foto non caricata', 'err'));
|
||
} catch (e) {
|
||
toast('Immagine non leggibile', 'err');
|
||
}
|
||
});
|
||
|
||
/* ---------- disegni e allegati della matrice ---------- */
|
||
$(document).on('click', '[data-files]', function(e) {
|
||
e.stopPropagation();
|
||
const id = +this.dataset.files;
|
||
openSheet('<i class="bi bi-rulers"></i> Disegni e file', '<div class="files" id="files"><div class="teo"><i class="bi bi-arrow-repeat spin"></i> Caricamento…</div></div>');
|
||
$.getJSON('get_matrice_attachments.php', {
|
||
id
|
||
}).then(r => {
|
||
const f = ((r && r.attachments) || []).sort((a, b) => (+b.is_disegno || 0) - (+a.is_disegno || 0));
|
||
if (!f.length) return $('#files').html('<div class="empty">Nessun file per questa matrice</div>');
|
||
$('#files').html(f.map(x => {
|
||
const name = x.file_name || '',
|
||
url = x.file_url || x.file_path || '',
|
||
ext = name.split('.').pop().toLowerCase();
|
||
const img = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext);
|
||
return `<button class="file ${+x.is_disegno ? 'dis' : ''}" data-view="${esc(url)}" data-name="${esc(name)}" data-kind="${img ? 'img' : (ext === 'pdf' ? 'pdf' : 'other')}">
|
||
<div class="ic">${img ? `<img src="${esc(url)}" alt="">` : (ext === 'pdf' ? '<i class="bi bi-file-earmark-pdf-fill" style="color:#f87171"></i>' : '<i class="bi bi-paperclip"></i>')}</div>
|
||
<div><b>${esc(name)}</b>${+x.is_disegno ? '<small style="color:#c4b5fd">Disegno tecnico</small>' : ''}<small>${esc(x.description || '')}</small></div>
|
||
</button>`;
|
||
}).join(''));
|
||
}).fail(() => $('#files').html('<div class="empty late">File non disponibili</div>'));
|
||
});
|
||
|
||
function view(url, name, kind) {
|
||
$('#viewerTitle').text(name || '');
|
||
$('#viewerBody').html(kind === 'img' ? `<img src="${esc(url)}" alt="">` : (kind === 'pdf' ? `<iframe src="${esc(url)}#zoom=page-width"></iframe>` : `<a href="${esc(url)}" target="_blank" class="big go" style="max-width:360px;text-decoration:none">APRI FILE</a>`));
|
||
$('#viewer').addClass('on');
|
||
}
|
||
$(document).on('click', '[data-view]', function() {
|
||
view(this.dataset.view, this.dataset.name, this.dataset.kind);
|
||
});
|
||
$(document).on('click', '.zoom', function(e) {
|
||
e.stopPropagation();
|
||
view(this.dataset.src, '', 'img');
|
||
});
|
||
$('#viewerClose').on('click', () => {
|
||
$('#viewer').removeClass('on').find('#viewerBody').html('');
|
||
viewList = [];
|
||
});
|
||
|
||
/* ---------- nuovo lavoro rapido ---------- */
|
||
let M = null;
|
||
$(document).on('click', '#btnQuick, #heroQuick', function() {
|
||
const go = () => {
|
||
openSheet('<i class="bi bi-plus-lg"></i> Nuovo lavoro', `
|
||
<div class="fld"><label>Matrice</label><select id="qMat"><option value=""></option>${M.rows.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join('')}</select></div>
|
||
<div class="fld"><label>Linea</label><select id="qLin">${S.lines.map(l => `<option value="${l.id}" ${l.id === line ? 'selected' : ''}>${esc(l.name)}</option>`).join('')}</select></div>
|
||
<div class="fld"><label>Mescole</label><select id="qMes" multiple>${M.mescole.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join('')}</select></div>
|
||
<div class="fld"><label>Descrizione</label><input type="text" id="qDes" autocomplete="off"></div>
|
||
<div class="sheet-f"><button class="big ghost" id="qQueue"><i class="bi bi-list-ol"></i> IN CODA</button><button class="big go" id="qStart"><i class="bi bi-play-fill"></i> CREA E AVVIA</button></div>`);
|
||
$('#qMat').select2({
|
||
dropdownParent: $('#sheet'),
|
||
placeholder: 'Cerca la matrice'
|
||
}).on('change', function() {
|
||
if (!this.value) return;
|
||
$.getJSON('?a=matmescole&id=' + this.value).then(r => $('#qMes').val(r.ids.map(String)).trigger('change'));
|
||
});
|
||
$('#qMes').select2({
|
||
dropdownParent: $('#sheet'),
|
||
closeOnSelect: false
|
||
});
|
||
const send = avvia => post({
|
||
a: 'quick',
|
||
idmatrice: $('#qMat').val(),
|
||
line: $('#qLin').val(),
|
||
mescole: $('#qMes').val() || [],
|
||
descrizione: $('#qDes').val(),
|
||
avvia: avvia ? 1 : 0
|
||
}).then(() => {
|
||
beep();
|
||
closeSheet();
|
||
toast(avvia ? 'Creato e avviato' : 'Aggiunto in coda');
|
||
load();
|
||
});
|
||
$('#qQueue').on('click', () => send(false));
|
||
$('#qStart').on('click', () => send(true));
|
||
};
|
||
if (M) go();
|
||
else $.getJSON('?a=matrici').then(r => {
|
||
M = r;
|
||
go();
|
||
});
|
||
});
|
||
|
||
/* ---------- colori: scuro / chiaro ---------- */
|
||
function setTheme(t) {
|
||
document.documentElement.setAttribute('data-theme', t);
|
||
$('#btnTheme i').attr('class', t === 'light' ? 'bi bi-moon-stars' : 'bi bi-sun');
|
||
$('meta[name="theme-color"]').attr('content', t === 'light' ? '#e3edf8' : '#0b1220');
|
||
try {
|
||
localStorage.setItem('station_theme', t);
|
||
} catch (e) {}
|
||
}
|
||
$('#btnTheme').on('click', () => setTheme(document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'));
|
||
try {
|
||
setTheme(localStorage.getItem('station_theme') || 'dark');
|
||
} catch (e) {
|
||
setTheme('dark');
|
||
}
|
||
|
||
/* ---------- schermo intero e schermo sempre acceso ---------- */
|
||
$('#btnFull').on('click', () => {
|
||
try {
|
||
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
||
else document.exitFullscreen();
|
||
} catch (e) {}
|
||
});
|
||
let lock = null;
|
||
async function keepAwake() {
|
||
try {
|
||
if ('wakeLock' in navigator && document.visibilityState === 'visible') lock = await navigator.wakeLock.request('screen');
|
||
} catch (e) {}
|
||
}
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.visibilityState === 'visible') {
|
||
keepAwake();
|
||
load();
|
||
}
|
||
});
|
||
keepAwake();
|
||
|
||
load();
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|