1758 lines
58 KiB
PHP
1758 lines
58 KiB
PHP
<?php
|
|
|
|
/**
|
|
* kiosk.php - schermo di reparto per la sala pausa (TV 16:9, senza mouse né tastiera).
|
|
*
|
|
* Si apre senza login con il link che contiene il codice segreto: kiosk.php?k=CODICE
|
|
* (il codice si copia o si rigenera da kiosk_admin.php). Mostra solo dati di produzione e comunicazioni.
|
|
* Parametro facoltativo &solo=linee per fissare una sola slide (es. schermo vicino alle linee).
|
|
*/
|
|
|
|
// --- vendor e .env come gli script CLI: niente headscript (lo schermo non fa login) ---
|
|
$dirs = [__DIR__, dirname(__DIR__), dirname(__DIR__, 2), dirname(__DIR__, 3)];
|
|
foreach ($dirs as $d) {
|
|
if (file_exists($d . '/vendor/autoload.php')) {
|
|
require_once $d . '/vendor/autoload.php';
|
|
break;
|
|
}
|
|
}
|
|
if (class_exists('Dotenv\Dotenv')) {
|
|
foreach ($dirs as $d) {
|
|
if (file_exists($d . '/.env')) {
|
|
Dotenv\Dotenv::createImmutable($d)->safeLoad();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
require_once __DIR__ . '/class/db-functions.php';
|
|
require_once __DIR__ . '/class/Kiosk.php';
|
|
|
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
$k = (string)($_GET['k'] ?? '');
|
|
$token = Kiosk::settings($pdo)['token'];
|
|
if ($token === '' || !hash_equals($token, $k)) {
|
|
http_response_code(403);
|
|
echo '<!doctype html><meta charset="utf-8"><body style="background:#0a1120;color:#cbd5e1;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><div style="text-align:center"><h1>Schermo non autorizzato</h1><p>Apri il link completo dalla pagina di gestione dello schermo.</p></div>';
|
|
exit;
|
|
}
|
|
|
|
if (($_GET['a'] ?? '') === 'data') {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
try {
|
|
echo json_encode(Kiosk::data($pdo));
|
|
} catch (Throwable $e) {
|
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="it">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex">
|
|
<title>Bacheca di reparto</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.49.1"></script>
|
|
<style>
|
|
:root {
|
|
--bg: #0a1120;
|
|
--panel: #131d33;
|
|
--panel2: #1a2742;
|
|
--line: #2a3a5c;
|
|
--ink: #f1f5ff;
|
|
--muted: #b4c2dd;
|
|
--label: #c6d2ea;
|
|
--run: #22c55e;
|
|
--runink: #4ade80;
|
|
--pause: #f59e0b;
|
|
--pauseink: #fbbf24;
|
|
--bad: #ef4444;
|
|
--badink: #f87171;
|
|
--accent: #7dd3fc;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
font-size: clamp(12px, 1.04vw, 44px);
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: radial-gradient(1400px 700px at 10% -10%, #15284d 0%, transparent 60%), var(--bg);
|
|
color: var(--ink);
|
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
cursor: none;
|
|
}
|
|
|
|
.k {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ---------- testata ---------- */
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2rem;
|
|
padding: 1.2rem 2.2rem .9rem;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
min-width: 18rem;
|
|
}
|
|
|
|
.logo {
|
|
width: 3.6rem;
|
|
height: 3.6rem;
|
|
border-radius: 1rem;
|
|
background: linear-gradient(135deg, #2563eb, #38bdf8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 900;
|
|
font-size: 1.9rem;
|
|
box-shadow: 0 .5rem 1.5rem rgba(37, 99, 235, .4);
|
|
}
|
|
|
|
.brand b {
|
|
display: block;
|
|
font-size: 1.6rem;
|
|
font-weight: 900;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.brand small {
|
|
color: var(--muted);
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stitle {
|
|
flex: 1;
|
|
text-align: center;
|
|
font-size: 2.2rem;
|
|
font-weight: 900;
|
|
letter-spacing: .01em;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.stitle i {
|
|
color: var(--accent);
|
|
margin-right: .8rem;
|
|
}
|
|
|
|
.right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2rem;
|
|
min-width: 18rem;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.wx {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
font-size: 2rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.wx i {
|
|
font-size: 2.6rem;
|
|
color: #fcd34d;
|
|
}
|
|
|
|
.wx small {
|
|
display: block;
|
|
font-size: .9rem;
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.clock {
|
|
text-align: right;
|
|
}
|
|
|
|
.clock b {
|
|
display: block;
|
|
font-size: 3rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.clock small {
|
|
color: var(--label);
|
|
font-size: 1.05rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.prog {
|
|
height: .3rem;
|
|
margin: 0 2.2rem;
|
|
background: #1b2846;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.prog span {
|
|
display: block;
|
|
height: 100%;
|
|
width: 0;
|
|
background: linear-gradient(90deg, #2563eb, #38bdf8);
|
|
}
|
|
|
|
/* ---------- area slide ---------- */
|
|
main {
|
|
flex: 1;
|
|
min-height: 0;
|
|
position: relative;
|
|
padding: 1.6rem 2.2rem 1rem;
|
|
}
|
|
|
|
.slide {
|
|
position: absolute;
|
|
inset: 1.6rem 2.2rem 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
opacity: 0;
|
|
transform: translateY(1.5rem);
|
|
transition: opacity .7s ease, transform .7s ease;
|
|
}
|
|
|
|
.slide.on {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
.dots {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: .5rem;
|
|
padding: .3rem 0 .7rem;
|
|
}
|
|
|
|
.dots span {
|
|
width: .7rem;
|
|
height: .7rem;
|
|
border-radius: 50%;
|
|
background: #2a3a5c;
|
|
transition: all .3s;
|
|
}
|
|
|
|
.dots span.on {
|
|
width: 2.2rem;
|
|
border-radius: 1rem;
|
|
background: var(--accent);
|
|
}
|
|
|
|
/* ---------- avvisi scorrevoli ---------- */
|
|
footer {
|
|
display: flex;
|
|
align-items: stretch;
|
|
height: 4.6rem;
|
|
background: #0e1830;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
|
|
.tk-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
padding: 0 1.6rem;
|
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
color: #1a1205;
|
|
font-weight: 900;
|
|
font-size: 1.3rem;
|
|
letter-spacing: .08em;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tk-track {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.tk-move {
|
|
white-space: nowrap;
|
|
font-size: 1.7rem;
|
|
font-weight: 700;
|
|
padding-left: 100%;
|
|
animation: tk linear infinite;
|
|
}
|
|
|
|
.tk-move .sep {
|
|
color: var(--pauseink);
|
|
margin: 0 2rem;
|
|
}
|
|
|
|
@keyframes tk {
|
|
from {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
to {
|
|
transform: translateX(-100%);
|
|
}
|
|
}
|
|
|
|
.offline {
|
|
position: fixed;
|
|
right: 1rem;
|
|
bottom: 5.4rem;
|
|
background: #7f1d1d;
|
|
color: #fff;
|
|
border-radius: 1rem;
|
|
padding: .4rem .9rem;
|
|
font-weight: 800;
|
|
font-size: .9rem;
|
|
display: none;
|
|
}
|
|
|
|
/* ---------- elementi comuni ---------- */
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: 1.6rem;
|
|
}
|
|
|
|
.lbl {
|
|
font-size: 1.15rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
color: var(--label);
|
|
}
|
|
|
|
.up {
|
|
color: var(--runink);
|
|
}
|
|
|
|
.down {
|
|
color: var(--badink);
|
|
}
|
|
|
|
.flat {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* linee */
|
|
.lines {
|
|
flex: 1;
|
|
display: grid;
|
|
gap: 1.4rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.ln {
|
|
padding: 1.6rem 1.8rem;
|
|
border-top: .6rem solid var(--c);
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.ln .h {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.ln .name {
|
|
font-size: 2.1rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.pill {
|
|
margin-left: auto;
|
|
border-radius: 2rem;
|
|
padding: .35rem 1.1rem;
|
|
font-weight: 900;
|
|
font-size: 1rem;
|
|
letter-spacing: .08em;
|
|
color: #0a1120;
|
|
}
|
|
|
|
.pill.run {
|
|
background: var(--run);
|
|
}
|
|
|
|
.pill.pause {
|
|
background: var(--pause);
|
|
}
|
|
|
|
.pill.free {
|
|
background: #64748b;
|
|
color: #fff;
|
|
}
|
|
|
|
.ln .mat {
|
|
font-size: 4.2rem;
|
|
font-weight: 900;
|
|
color: var(--accent);
|
|
margin-top: 1.4rem;
|
|
line-height: 1;
|
|
text-shadow: 0 0 2rem rgba(125, 211, 252, .3);
|
|
}
|
|
|
|
.ln .mes {
|
|
color: var(--label);
|
|
font-size: 1.3rem;
|
|
font-weight: 700;
|
|
margin-top: .6rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ln .tm {
|
|
font-size: 4.6rem;
|
|
font-weight: 900;
|
|
font-variant-numeric: tabular-nums;
|
|
margin-top: auto;
|
|
line-height: 1;
|
|
}
|
|
|
|
.ln .tm.run {
|
|
color: var(--runink);
|
|
text-shadow: 0 0 1.8rem rgba(74, 222, 128, .4);
|
|
}
|
|
|
|
.ln .tm.pause {
|
|
color: var(--pauseink);
|
|
}
|
|
|
|
.ln .tm.free {
|
|
color: var(--muted);
|
|
font-size: 3rem;
|
|
}
|
|
|
|
.bar {
|
|
height: .8rem;
|
|
background: #1f2d4f;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.bar span {
|
|
display: block;
|
|
height: 100%;
|
|
border-radius: 1rem;
|
|
background: var(--run);
|
|
}
|
|
|
|
.ln .eta {
|
|
font-size: 1.35rem;
|
|
font-weight: 800;
|
|
margin-top: .7rem;
|
|
color: var(--label);
|
|
}
|
|
|
|
.ln .q {
|
|
color: var(--muted);
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
margin-top: .9rem;
|
|
}
|
|
|
|
/* KPI */
|
|
.kgrid {
|
|
flex: 1.5;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1.4rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.kt {
|
|
padding: 1.8rem 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
|
|
.kt .v {
|
|
font-size: 5.4rem;
|
|
font-weight: 900;
|
|
line-height: 1.05;
|
|
margin-top: .4rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.kt .v small {
|
|
font-size: 2rem;
|
|
color: var(--muted);
|
|
margin-left: .4rem;
|
|
}
|
|
|
|
.kt .cmp {
|
|
font-size: 1.3rem;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
margin-top: .6rem;
|
|
}
|
|
|
|
.kt .cmp b {
|
|
display: block;
|
|
font-size: 2.2rem;
|
|
margin-top: .3rem;
|
|
}
|
|
|
|
.kt.good {
|
|
border-color: rgba(34, 197, 94, .6);
|
|
}
|
|
|
|
.kt.warn {
|
|
border-color: rgba(245, 158, 11, .7);
|
|
}
|
|
|
|
.kt.bad {
|
|
border-color: rgba(239, 68, 68, .7);
|
|
}
|
|
|
|
.kt.good .v {
|
|
color: var(--runink);
|
|
}
|
|
|
|
.kt.warn .v {
|
|
color: var(--pauseink);
|
|
}
|
|
|
|
.kt.bad .v {
|
|
color: var(--badink);
|
|
}
|
|
|
|
.wk {
|
|
flex: 1;
|
|
margin-top: 1.4rem;
|
|
padding: 1.4rem 2rem;
|
|
display: grid;
|
|
grid-template-columns: auto repeat(3, 1fr);
|
|
gap: 2rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.wk .t {
|
|
font-size: 1.8rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.wk .v {
|
|
font-size: 3.8rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.wk .v small {
|
|
font-size: 1.3rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wk .cmp {
|
|
font-size: 1.4rem;
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* grafico */
|
|
.chartbox {
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 1.4rem 1.6rem .6rem;
|
|
}
|
|
|
|
/* fermi */
|
|
.stops {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 26rem 1fr;
|
|
gap: 1.4rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.stops .tot {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.stops .tot b {
|
|
font-size: 8rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
color: var(--pauseink);
|
|
}
|
|
|
|
.stops .tot span {
|
|
font-size: 1.6rem;
|
|
font-weight: 800;
|
|
color: var(--label);
|
|
margin-top: .6rem;
|
|
}
|
|
|
|
.sbars {
|
|
padding: 1.8rem 2.2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 1.3rem;
|
|
}
|
|
|
|
.sb {
|
|
display: grid;
|
|
grid-template-columns: 18rem 1fr 7rem;
|
|
gap: 1.2rem;
|
|
align-items: center;
|
|
font-size: 1.5rem;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.sb .n {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.sb .t {
|
|
height: 2.2rem;
|
|
background: #1b2846;
|
|
border-radius: .8rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sb .t span {
|
|
display: block;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
|
border-radius: .8rem;
|
|
}
|
|
|
|
.sb.problem .t span {
|
|
background: linear-gradient(90deg, #dc2626, #f87171);
|
|
}
|
|
|
|
.sb .h {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* coda */
|
|
.queue {
|
|
flex: 1;
|
|
display: grid;
|
|
gap: 1.4rem;
|
|
}
|
|
|
|
.qq {
|
|
padding: 1.8rem;
|
|
border-top: .6rem solid var(--c);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.qq .name {
|
|
font-size: 2.2rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.qq .n {
|
|
font-size: 7rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
margin-top: 1rem;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.qq .l {
|
|
font-size: 1.5rem;
|
|
font-weight: 800;
|
|
color: var(--label);
|
|
}
|
|
|
|
.qq .h {
|
|
font-size: 2.4rem;
|
|
font-weight: 900;
|
|
margin-top: 1.2rem;
|
|
}
|
|
|
|
.qq .next {
|
|
font-size: 1.3rem;
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
margin-top: .6rem;
|
|
}
|
|
|
|
/* sicurezza */
|
|
.safe {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
background: radial-gradient(800px 400px at 50% 40%, rgba(34, 197, 94, .18), transparent 70%);
|
|
}
|
|
|
|
.safe .d {
|
|
font-size: 16rem;
|
|
font-weight: 900;
|
|
line-height: .95;
|
|
color: var(--runink);
|
|
text-shadow: 0 0 4rem rgba(74, 222, 128, .45);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.safe .t {
|
|
font-size: 3rem;
|
|
font-weight: 900;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.safe .s {
|
|
font-size: 1.6rem;
|
|
color: var(--label);
|
|
font-weight: 700;
|
|
margin-top: .8rem;
|
|
}
|
|
|
|
.safe .note {
|
|
font-size: 1.8rem;
|
|
margin-top: 1.6rem;
|
|
max-width: 70rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.safe .shield {
|
|
font-size: 5rem;
|
|
color: var(--runink);
|
|
margin-bottom: 2rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* obiettivi */
|
|
.goals {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 1.4rem;
|
|
}
|
|
|
|
.gl {
|
|
padding: 1.4rem 2rem;
|
|
}
|
|
|
|
.gl .r {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.gl .t {
|
|
font-size: 2rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.gl .p {
|
|
color: var(--muted);
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.gl .v {
|
|
margin-left: auto;
|
|
font-size: 2.2rem;
|
|
font-weight: 900;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.gl .v small {
|
|
color: var(--muted);
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
.gl .bar {
|
|
height: 1.6rem;
|
|
}
|
|
|
|
.gl .bar span {
|
|
background: linear-gradient(90deg, #2563eb, #38bdf8);
|
|
}
|
|
|
|
.gl.warn .bar span {
|
|
background: linear-gradient(90deg, #d97706, #fbbf24);
|
|
}
|
|
|
|
.gl.ok .bar span {
|
|
background: linear-gradient(90deg, #16a34a, #4ade80);
|
|
}
|
|
|
|
.gl.ok .v {
|
|
color: var(--runink);
|
|
}
|
|
|
|
/* comunicazioni */
|
|
.msg {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 2rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.msg.img {
|
|
grid-template-columns: 1.1fr 1fr;
|
|
}
|
|
|
|
.msg .txt {
|
|
padding: 3rem 3.4rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
min-height: 0;
|
|
}
|
|
|
|
.msg .tag {
|
|
align-self: flex-start;
|
|
background: rgba(125, 211, 252, .15);
|
|
color: var(--accent);
|
|
border-radius: 2rem;
|
|
padding: .4rem 1.2rem;
|
|
font-weight: 900;
|
|
font-size: 1.1rem;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.msg h2 {
|
|
font-size: 4rem;
|
|
font-weight: 900;
|
|
line-height: 1.1;
|
|
margin: 1.2rem 0 1.4rem;
|
|
}
|
|
|
|
.msg .body {
|
|
font-size: 2rem;
|
|
line-height: 1.45;
|
|
white-space: pre-wrap;
|
|
color: #e2e8f5;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.msg .pic {
|
|
border-radius: 1.6rem;
|
|
overflow: hidden;
|
|
background: #000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 0;
|
|
}
|
|
|
|
.msg .pic img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.msg.center .txt {
|
|
text-align: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.msg.center .tag {
|
|
align-self: center;
|
|
}
|
|
|
|
/* benvenuti */
|
|
.hello {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4rem;
|
|
background: radial-gradient(800px 400px at 50% 50%, rgba(56, 189, 248, .14), transparent 70%);
|
|
}
|
|
|
|
.hello .ph {
|
|
width: 26rem;
|
|
height: 26rem;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
border: .6rem solid rgba(125, 211, 252, .5);
|
|
box-shadow: 0 0 4rem rgba(56, 189, 248, .35);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.hello .ph img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.hello .ic {
|
|
font-size: 10rem;
|
|
color: #fcd34d;
|
|
}
|
|
|
|
.hello h2 {
|
|
font-size: 4.6rem;
|
|
font-weight: 900;
|
|
margin: .6rem 0 1rem;
|
|
line-height: 1.05;
|
|
}
|
|
|
|
.hello .body {
|
|
font-size: 2.1rem;
|
|
line-height: 1.45;
|
|
white-space: pre-wrap;
|
|
max-width: 60rem;
|
|
color: #e2e8f5;
|
|
}
|
|
|
|
/* turni / menu */
|
|
.list {
|
|
flex: 1;
|
|
padding: 2.4rem 3rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.list h2 {
|
|
font-size: 3rem;
|
|
font-weight: 900;
|
|
margin: 0 0 1.6rem;
|
|
}
|
|
|
|
.list .rows {
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--cols, 1), 1fr);
|
|
gap: .8rem 3rem;
|
|
align-content: start;
|
|
}
|
|
|
|
.list .row {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
padding: .8rem 1.2rem;
|
|
border-radius: 1rem;
|
|
background: var(--panel2);
|
|
border-left: .4rem solid var(--accent);
|
|
}
|
|
|
|
.list .row b {
|
|
color: var(--accent);
|
|
margin-right: .8rem;
|
|
}
|
|
|
|
/* meteo */
|
|
.wxs {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 34rem minmax(0, 1fr);
|
|
gap: 1.4rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.wnow {
|
|
padding: 2rem 2.2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: radial-gradient(600px 300px at 30% 20%, rgba(252, 211, 77, .14), transparent 70%), var(--panel);
|
|
}
|
|
|
|
.wbig {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.4rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.wbig .wi {
|
|
font-size: 8rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.wbig span {
|
|
font-size: 8.5rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
}
|
|
|
|
.wdesc {
|
|
font-size: 2.2rem;
|
|
font-weight: 800;
|
|
margin-top: .8rem;
|
|
}
|
|
|
|
.wmeta {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem 1.4rem;
|
|
margin-top: auto;
|
|
padding-top: 1.4rem;
|
|
}
|
|
|
|
.wmeta span {
|
|
display: block;
|
|
font-size: 1rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
color: var(--label);
|
|
}
|
|
|
|
.wmeta b {
|
|
font-size: 1.9rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.wright {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.4rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.whours {
|
|
padding: 1.4rem 1.8rem;
|
|
}
|
|
|
|
.hrow {
|
|
display: grid;
|
|
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
gap: .8rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.hc {
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: .3rem;
|
|
}
|
|
|
|
.hc .t {
|
|
font-size: 1.3rem;
|
|
font-weight: 800;
|
|
color: var(--label);
|
|
}
|
|
|
|
.hc .wi {
|
|
font-size: 2.8rem;
|
|
}
|
|
|
|
.hc b {
|
|
font-size: 2.2rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.hc .r,
|
|
.dc .dr {
|
|
font-size: 1.2rem;
|
|
font-weight: 800;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.hi {
|
|
color: #60a5fa !important;
|
|
}
|
|
|
|
.mid {
|
|
color: #93c5fd !important;
|
|
}
|
|
|
|
.wdays {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
gap: 1.2rem;
|
|
min-height: 0;
|
|
}
|
|
|
|
.dc {
|
|
min-width: 0;
|
|
padding: 1.4rem 1rem;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: .4rem;
|
|
}
|
|
|
|
.dc .dn {
|
|
white-space: nowrap;
|
|
font-size: 1.7rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.dc .wi {
|
|
font-size: 4rem;
|
|
margin: .4rem 0;
|
|
}
|
|
|
|
.dc .dt b {
|
|
font-size: 2.6rem;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.dc .dt span {
|
|
font-size: 1.8rem;
|
|
font-weight: 800;
|
|
color: var(--muted);
|
|
margin-left: .6rem;
|
|
}
|
|
|
|
.dc .dd {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: var(--label);
|
|
min-height: 2.8rem;
|
|
}
|
|
|
|
.empty {
|
|
margin: auto;
|
|
color: var(--muted);
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="k">
|
|
<header>
|
|
<div class="brand">
|
|
<div class="logo" id="logo">Z</div>
|
|
<div><b id="company"></b><small>Bacheca di reparto</small></div>
|
|
</div>
|
|
<div class="stitle" id="stitle"></div>
|
|
<div class="right">
|
|
<div class="wx" id="wx"></div>
|
|
<div class="clock"><b id="clock">--:--</b><small id="date"></small></div>
|
|
</div>
|
|
</header>
|
|
<div class="prog"><span id="prog"></span></div>
|
|
<main id="stage"></main>
|
|
<div class="dots" id="dots"></div>
|
|
<footer>
|
|
<div class="tk-label"><i class="bi bi-megaphone-fill"></i> AVVISI</div>
|
|
<div class="tk-track">
|
|
<div class="tk-move" id="ticker"></div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<div class="offline" id="offline"><i class="bi bi-wifi-off"></i> dati non aggiornati</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const K = <?= json_encode($k) ?>;
|
|
const SOLO = <?= json_encode((string)($_GET['solo'] ?? '')) ?>;
|
|
const H = 3600e3;
|
|
let D = null,
|
|
skew = 0,
|
|
lastOk = 0;
|
|
let seq = [],
|
|
pos = -1,
|
|
timer = null,
|
|
chart = null;
|
|
|
|
const $ = s => document.querySelector(s);
|
|
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')}`;
|
|
};
|
|
|
|
/* confronto con il periodo precedente: ▲ ▼ e percentuale */
|
|
function cmp(cur, prev, lowerBetter) {
|
|
if (cur === null || prev === null || prev === undefined) return '<span class="flat">—</span>';
|
|
if (!prev && !cur) return '<span class="flat">= uguale</span>';
|
|
if (!prev) return `<span class="${lowerBetter ? 'down' : 'up'}">▲ nuovo</span>`;
|
|
const p = Math.round((cur - prev) / prev * 100);
|
|
if (p === 0) return '<span class="flat">= uguale</span>';
|
|
const good = lowerBetter ? p < 0 : p > 0;
|
|
return `<span class="${good ? 'up' : 'down'}">${p > 0 ? '▲' : '▼'} ${Math.abs(p)}%</span>`;
|
|
}
|
|
|
|
/* ---------- dati ---------- */
|
|
function load() {
|
|
return fetch('?k=' + encodeURIComponent(K) + '&a=data', {
|
|
cache: 'no-store'
|
|
}).then(r => r.json()).then(r => {
|
|
if (!r.success) throw new Error(r.message || 'errore');
|
|
skew = Date.now() - r.now_ms;
|
|
D = r;
|
|
lastOk = Date.now();
|
|
$('#company').textContent = r.company || '';
|
|
$('#logo').textContent = (r.company || 'Z').trim().charAt(0).toUpperCase();
|
|
ticker();
|
|
weatherInit();
|
|
}).catch(() => {}).finally(() => {
|
|
$('#offline').style.display = lastOk && Date.now() - lastOk > 180000 ? 'block' : (lastOk ? 'none' : 'block');
|
|
});
|
|
}
|
|
|
|
/* ---------- sequenza delle slide ---------- */
|
|
function build() {
|
|
const S = [];
|
|
const on = SOLO ? [SOLO] : D.settings.slides;
|
|
const has = k => on.includes(k);
|
|
if (has('linee') && D.lines.length) S.push({
|
|
k: 'linee',
|
|
t: 'Linee adesso',
|
|
i: 'bi-gear-wide-connected',
|
|
f: slideLines
|
|
});
|
|
if (has('oggi')) S.push({
|
|
k: 'oggi',
|
|
t: 'Produzione di oggi',
|
|
i: 'bi-speedometer2',
|
|
f: slideToday
|
|
});
|
|
if (has('andamento') && D.trend.series.some(s => s.data.some(v => v > 0))) S.push({
|
|
k: 'andamento',
|
|
t: 'Metri prodotti · ultimi 14 giorni',
|
|
i: 'bi-bar-chart-fill',
|
|
f: slideTrend
|
|
});
|
|
if (has('fermi') && D.stops.total_h > 0) S.push({
|
|
k: 'fermi',
|
|
t: 'Fermi della settimana',
|
|
i: 'bi-pause-circle-fill',
|
|
f: slideStops
|
|
});
|
|
if (has('coda') && D.lines.length) S.push({
|
|
k: 'coda',
|
|
t: 'Lavoro in arrivo',
|
|
i: 'bi-list-ol',
|
|
f: slideQueue
|
|
});
|
|
if (has('meteo') && WX && WX.daily && WX.hourly) S.push({
|
|
k: 'meteo',
|
|
t: 'Meteo' + (D.settings.city ? ' · ' + D.settings.city : ''),
|
|
i: 'bi-cloud-sun-fill',
|
|
f: slideWeather
|
|
});
|
|
if (has('sicurezza') && D.safety) S.push({
|
|
k: 'sicurezza',
|
|
t: 'Sicurezza',
|
|
i: 'bi-shield-check',
|
|
f: slideSafety
|
|
});
|
|
if (has('obiettivi') && D.goals.length) S.push({
|
|
k: 'obiettivi',
|
|
t: 'I nostri obiettivi',
|
|
i: 'bi-bullseye',
|
|
f: slideGoals
|
|
});
|
|
if (has('comunicazioni')) D.items.comunicazione.forEach(it => S.push({
|
|
k: 'comunicazioni',
|
|
t: 'Comunicazioni',
|
|
i: 'bi-megaphone-fill',
|
|
f: () => slideMsg(it, 'Comunicazione')
|
|
}));
|
|
if (has('benvenuti')) D.items.benvenuto.forEach(it => S.push({
|
|
k: 'benvenuti',
|
|
t: 'Benvenuti e grazie',
|
|
i: 'bi-stars',
|
|
f: () => slideHello(it)
|
|
}));
|
|
if (has('turni')) D.items.turni.forEach(it => S.push({
|
|
k: 'turni',
|
|
t: 'Turni e calendario',
|
|
i: 'bi-calendar-week',
|
|
f: () => slideList(it)
|
|
}));
|
|
if (has('menu')) D.items.menu.forEach(it => S.push({
|
|
k: 'menu',
|
|
t: 'Menu',
|
|
i: 'bi-cup-hot-fill',
|
|
f: () => slideList(it)
|
|
}));
|
|
return S;
|
|
}
|
|
|
|
function next() {
|
|
if (!D) return setTimeout(next, 2000);
|
|
pos++;
|
|
if (pos >= seq.length) {
|
|
seq = build();
|
|
pos = 0;
|
|
}
|
|
if (!seq.length) {
|
|
$('#stage').innerHTML = '<div class="slide on"><div class="empty">Nessun contenuto da mostrare</div></div>';
|
|
return setTimeout(next, 10000);
|
|
}
|
|
show(seq[pos]);
|
|
// durata impostata per questo tipo di slide, altrimenti quella predefinita (un po' di più per i testi)
|
|
const sec = (D.settings.durations || {})[seq[pos].k] || D.settings.slide_seconds * (seq[pos].k === 'comunicazioni' || seq[pos].k === 'turni' || seq[pos].k === 'menu' ? 1.3 : 1);
|
|
const p = $('#prog');
|
|
p.style.transition = 'none';
|
|
p.style.width = '0';
|
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
p.style.transition = `width ${sec}s linear`;
|
|
p.style.width = '100%';
|
|
}));
|
|
clearTimeout(timer);
|
|
timer = setTimeout(next, sec * 1000);
|
|
}
|
|
|
|
function show(s) {
|
|
$('#stitle').innerHTML = `<i class="bi ${s.i}"></i>${esc(s.t)}`;
|
|
const old = $('#stage .slide');
|
|
const el = document.createElement('div');
|
|
el.className = 'slide';
|
|
el.innerHTML = s.f();
|
|
$('#stage').appendChild(el);
|
|
if (chart) {
|
|
try {
|
|
chart.destroy();
|
|
} catch (e) {}
|
|
chart = null;
|
|
}
|
|
if (s.k === 'andamento') drawChart(el.querySelector('#chart'));
|
|
requestAnimationFrame(() => el.classList.add('on'));
|
|
if (old) {
|
|
old.classList.remove('on');
|
|
setTimeout(() => old.remove(), 800);
|
|
}
|
|
$('#dots').innerHTML = seq.map((x, i) => `<span class="${i === pos ? 'on' : ''}"></span>`).join('');
|
|
tick();
|
|
}
|
|
|
|
/* ---------- slide ---------- */
|
|
function slideLines() {
|
|
const n = D.lines.length;
|
|
const cols = n <= 4 ? n : Math.ceil(n / 2);
|
|
return `<div class="lines" style="grid-template-columns:repeat(${cols},1fr)">${D.lines.map(l => {
|
|
const j = l.job;
|
|
const st = j ? (j.paused ? 'pause' : 'run') : 'free';
|
|
return `<div class="panel ln" style="--c:${esc(l.color)}">
|
|
<div class="h"><span class="name">${esc(l.name)}</span><span class="pill ${st}">${j ? (j.paused ? 'IN PAUSA' : 'IN PRODUZIONE') : 'FERMA'}</span></div>
|
|
${j ? `<div class="mat">${esc(j.matrice || '')}</div><div class="mes">${esc(j.mescole || '')}</div>
|
|
<div class="tm ${st}" data-line="${l.id}">--:--:--</div>
|
|
${j.ore ? `<div class="bar"><span data-bar="${l.id}" style="width:0"></span></div><div class="eta" data-eta="${l.id}"></div>` : ''}`
|
|
: `<div class="mat" style="color:var(--muted);text-shadow:none">—</div><div class="mes">${l.next ? 'Prossimo: ' + esc(l.next) : 'Nessun lavoro in coda'}</div><div class="tm free">pronta</div>`}
|
|
<div class="q">${l.queue_n} in coda · ${num(l.queue_h, 0)} ore programmate</div>
|
|
</div>`;
|
|
}).join('')}</div>`;
|
|
}
|
|
|
|
function slideToday() {
|
|
const c = D.today.cur,
|
|
p = D.today.prev,
|
|
w = D.week.cur,
|
|
wp = D.week.prev;
|
|
const tgt = D.settings.scarto_target;
|
|
const sc = c.scarto;
|
|
const scCls = sc === null ? '' : (sc <= tgt ? 'good' : (sc <= tgt * 1.5 ? 'warn' : 'bad'));
|
|
return `<div class="kgrid">
|
|
<div class="panel kt"><div class="lbl">Metri prodotti oggi</div><div class="v">${num(c.mt)}<small>m</small></div><div class="cmp">ieri a quest'ora ${num(p.mt)} m<b>${cmp(c.mt, p.mt)}</b></div></div>
|
|
<div class="panel kt"><div class="lbl">Kg prodotti oggi</div><div class="v">${num(c.kg)}<small>kg</small></div><div class="cmp">ieri a quest'ora ${num(p.kg)} kg<b>${cmp(c.kg, p.kg)}</b></div></div>
|
|
<div class="panel kt"><div class="lbl">Lavori completati oggi</div><div class="v">${c.n}</div><div class="cmp">ieri a quest'ora ${p.n}<b>${cmp(c.n, p.n)}</b></div></div>
|
|
<div class="panel kt ${scCls}"><div class="lbl">Scarto di oggi</div><div class="v">${sc === null ? '—' : num(sc, 1) + '<small>%</small>'}</div><div class="cmp">obiettivo ≤ ${num(tgt, 1)}%<b>${sc === null ? '<span class="flat">—</span>' : (sc <= tgt ? '<span class="up">✓ sotto obiettivo</span>' : '<span class="down">sopra obiettivo</span>')}</b></div></div>
|
|
</div>
|
|
<div class="panel wk">
|
|
<div class="t"><i class="bi bi-calendar3-week" style="color:var(--accent)"></i> Questa settimana</div>
|
|
<div><div class="lbl">Metri</div><div class="v">${num(w.mt)} <small>m</small></div><div class="cmp">vs settimana scorsa <b>${cmp(w.mt, wp.mt)}</b></div></div>
|
|
<div><div class="lbl">Kg</div><div class="v">${num(w.kg)} <small>kg</small></div><div class="cmp">vs settimana scorsa <b>${cmp(w.kg, wp.kg)}</b></div></div>
|
|
<div><div class="lbl">Lavori · scarto</div><div class="v">${w.n} <small>· ${w.scarto === null ? '—' : num(w.scarto, 1) + '%'}</small></div><div class="cmp">vs settimana scorsa <b>${cmp(w.n, wp.n)}</b></div></div>
|
|
</div>`;
|
|
}
|
|
|
|
function slideTrend() {
|
|
return '<div class="panel chartbox"><div id="chart" style="height:100%"></div></div>';
|
|
}
|
|
|
|
function drawChart(el) {
|
|
if (!window.ApexCharts || !el) return;
|
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
chart = new ApexCharts(el, {
|
|
chart: {
|
|
type: 'bar',
|
|
stacked: true,
|
|
height: '100%',
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
animations: {
|
|
enabled: true,
|
|
speed: 900
|
|
},
|
|
foreColor: '#c6d2ea',
|
|
fontFamily: 'Segoe UI, system-ui, sans-serif',
|
|
background: 'transparent'
|
|
},
|
|
theme: {
|
|
mode: 'dark'
|
|
},
|
|
series: D.trend.series.map(s => ({
|
|
name: s.name,
|
|
data: s.data
|
|
})),
|
|
colors: D.trend.series.map(s => s.color),
|
|
xaxis: {
|
|
categories: D.trend.labels,
|
|
labels: {
|
|
style: {
|
|
fontSize: (1.05 * rem) + 'px',
|
|
fontWeight: 700
|
|
}
|
|
},
|
|
axisBorder: {
|
|
show: false
|
|
},
|
|
axisTicks: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
style: {
|
|
fontSize: (1.05 * rem) + 'px'
|
|
},
|
|
formatter: v => num(v)
|
|
}
|
|
},
|
|
plotOptions: {
|
|
bar: {
|
|
columnWidth: '62%',
|
|
borderRadius: 6,
|
|
dataLabels: {
|
|
total: {
|
|
enabled: true,
|
|
style: {
|
|
fontSize: (1.1 * rem) + 'px',
|
|
fontWeight: 900,
|
|
color: '#f1f5ff'
|
|
},
|
|
formatter: v => v ? num(v) : ''
|
|
}
|
|
}
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
grid: {
|
|
borderColor: '#24345a',
|
|
strokeDashArray: 4,
|
|
padding: {
|
|
right: 3 * rem
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
fontSize: (1.3 * rem) + 'px',
|
|
fontWeight: 800,
|
|
markers: {
|
|
width: rem,
|
|
height: rem,
|
|
radius: 4
|
|
},
|
|
itemMargin: {
|
|
horizontal: rem
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
});
|
|
chart.render();
|
|
}
|
|
|
|
function slideStops() {
|
|
const it = D.stops.items.slice(0, 7);
|
|
const max = Math.max(...it.map(x => x.h), 0.1);
|
|
return `<div class="stops">
|
|
<div class="panel tot"><i class="bi bi-hourglass-split" style="font-size:4rem;color:var(--pauseink)"></i><b>${num(D.stops.total_h, D.stops.total_h < 10 ? 1 : 0)}</b><span>ore di fermo<br>da lunedì</span></div>
|
|
<div class="panel sbars">${it.map(x => `<div class="sb ${x.problem ? 'problem' : ''}"><span class="n">${esc(x.name)}</span><span class="t"><span style="width:${Math.max(2, x.h / max * 100)}%"></span></span><span class="h">${num(x.h, 1)} h</span></div>`).join('')}
|
|
<div class="lbl" style="margin-top:.6rem;font-size:1rem"><span style="color:#f87171">■</span> guasti e problemi tecnici <span style="color:#fbbf24">■</span> altre pause</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function slideQueue() {
|
|
const n = D.lines.length;
|
|
return `<div class="queue" style="grid-template-columns:repeat(${n <= 4 ? n : Math.ceil(n / 2)},1fr)">${D.lines.map(l => `
|
|
<div class="panel qq" style="--c:${esc(l.color)}">
|
|
<div class="name">${esc(l.name)}</div>
|
|
<div class="n">${l.queue_n}</div><div class="l">lavori in coda</div>
|
|
<div class="h">${num(l.queue_h, 0)} ore</div><div class="l">di lavoro programmato</div>
|
|
<div class="next">${l.next ? 'prossimo: ' + esc(l.next) : ' '}</div>
|
|
</div>`).join('')}</div>`;
|
|
}
|
|
|
|
function slideSafety() {
|
|
const s = D.safety;
|
|
return `<div class="safe">
|
|
<i class="bi bi-shield-fill-check shield"></i>
|
|
<div class="d">${s.days}</div>
|
|
<div class="t">giorni senza infortuni</div>
|
|
<div class="s">dal ${esc(s.since)}${s.record ? ` · record: <b>${s.record}</b> giorni` : ''}</div>
|
|
${s.note ? `<div class="note">${esc(s.note)}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
function slideGoals() {
|
|
return `<div class="goals">${D.goals.slice(0, 5).map(g => {
|
|
const unit = g.unit ? ' ' + esc(g.unit) : '';
|
|
const dec = g.unit === '%' || g.unit === 'h' ? 1 : 0;
|
|
return `<div class="panel gl ${g.ok ? 'ok' : (g.lower ? 'warn' : '')}">
|
|
<div class="r"><span class="t">${g.ok ? '<i class="bi bi-check-circle-fill" style="color:var(--runink)"></i> ' : ''}${esc(g.title)}</span><span class="p">${esc(g.period)}</span>
|
|
<span class="v">${g.current === null ? '—' : num(g.current, dec)}${unit} <small>/ ${g.lower ? '≤ ' : ''}${num(g.target, dec)}${unit}</small></span></div>
|
|
<div class="bar"><span style="width:${g.pct || 0}%"></span></div>
|
|
</div>`;
|
|
}).join('')}</div>`;
|
|
}
|
|
|
|
function slideMsg(it, tag) {
|
|
const img = it.image ? `<div class="pic"><img src="photos/kiosk/${esc(it.image)}" alt=""></div>` : '';
|
|
return `<div class="msg ${img ? 'img' : 'center'}">
|
|
<div class="panel txt"><span class="tag">${esc(tag)}</span><h2>${esc(it.title)}</h2><div class="body">${esc(it.body)}</div></div>${img}
|
|
</div>`;
|
|
}
|
|
|
|
function slideHello(it) {
|
|
return `<div class="hello">
|
|
${it.image ? `<div class="ph"><img src="photos/kiosk/${esc(it.image)}" alt=""></div>` : '<i class="bi bi-stars ic"></i>'}
|
|
<div><div class="lbl" style="color:var(--accent)">Benvenuti e grazie</div><h2>${esc(it.title)}</h2><div class="body">${esc(it.body)}</div></div>
|
|
</div>`;
|
|
}
|
|
|
|
// turni e menu: una riga per ogni riga di testo; "Lunedì: Mario" mette in evidenza la parte prima dei due punti
|
|
function slideList(it) {
|
|
const rows = String(it.body || '').split(/\r?\n/).map(r => r.trim()).filter(Boolean);
|
|
const cols = rows.length > 7 ? 2 : 1;
|
|
return `<div class="panel list"><h2>${esc(it.title)}</h2>
|
|
<div class="rows" style="--cols:${cols}">${rows.map(r => {
|
|
const i = r.indexOf(':');
|
|
return `<div class="row">${i > 0 && i < 30 ? `<b>${esc(r.slice(0, i))}</b>${esc(r.slice(i + 1))}` : esc(r)}</div>`;
|
|
}).join('')}</div>
|
|
${it.image ? `<img src="photos/kiosk/${esc(it.image)}" alt="" style="margin-top:1.4rem;max-height:30vh;border-radius:1.2rem;align-self:flex-start">` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
/* ---------- avvisi scorrevoli ---------- */
|
|
function ticker() {
|
|
let parts = D.items.ticker.map(t => `<b>${esc(t.title)}</b>${t.body ? ' — ' + esc(t.body) : ''}`);
|
|
if (!parts.length) {
|
|
// senza avvisi del manager: messaggi automatici
|
|
if (D.safety) parts.push(`<b>${D.safety.days}</b> giorni senza infortuni`);
|
|
if (D.today.cur.mt) parts.push(`Oggi prodotti <b>${num(D.today.cur.mt)}</b> metri`);
|
|
parts.push('Buon lavoro a tutti!');
|
|
}
|
|
const html = parts.join('<span class="sep">●</span>');
|
|
const el = $('#ticker');
|
|
if (el.dataset.html === html) return;
|
|
el.dataset.html = html;
|
|
el.innerHTML = html;
|
|
const len = el.textContent.length;
|
|
el.style.animationDuration = Math.max(20, len * 0.22) + 's';
|
|
}
|
|
|
|
/* ---------- orologio, timer delle linee ---------- */
|
|
const GIORNI = ['domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato'];
|
|
const MESI = ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'];
|
|
|
|
function tick() {
|
|
const d = new Date(now());
|
|
$('#clock').textContent = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
$('#date').textContent = `${GIORNI[d.getDay()]} ${d.getDate()} ${MESI[d.getMonth()]}`;
|
|
if (!D) return;
|
|
document.querySelectorAll('[data-line]').forEach(el => {
|
|
const l = D.lines.find(x => x.id === +el.dataset.line);
|
|
const j = l && l.job;
|
|
if (!j || !j.start_ms) return;
|
|
const t = now();
|
|
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);
|
|
el.textContent = hms(j.paused ? cur : net);
|
|
const bar = document.querySelector(`[data-bar="${l.id}"]`);
|
|
if (bar && j.ore) {
|
|
const p = net / (j.ore * H);
|
|
bar.style.width = Math.min(100, p * 100) + '%';
|
|
bar.style.background = p > 1 ? 'var(--bad)' : (j.paused ? 'var(--pause)' : 'var(--run)');
|
|
const eta = document.querySelector(`[data-eta="${l.id}"]`);
|
|
if (eta) {
|
|
const end = new Date(t + Math.max(0, j.ore * H - net));
|
|
eta.innerHTML = p >= 1 ? `<span class="down">oltre le ${num(j.ore, 1)} ore previste</span>` :
|
|
`${Math.round(p * 100)}% di ${num(j.ore, 1)} h · fine prevista ~ <b style="color:var(--ink)">${new Date(t).toDateString() === end.toDateString() ? '' : GIORNI[end.getDay()].slice(0, 3) + ' '}${String(end.getHours()).padStart(2, '0')}:${String(end.getMinutes()).padStart(2, '0')}</b>`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
setInterval(tick, 1000);
|
|
|
|
/* ---------- meteo (open-meteo, senza chiave) ---------- */
|
|
let wxDone = false;
|
|
|
|
function weatherInit() {
|
|
if (wxDone || !D.settings.lat || !D.settings.lon) return;
|
|
wxDone = true;
|
|
weather();
|
|
setInterval(weather, 30 * 60000);
|
|
}
|
|
|
|
// meteo: ora, prossime ore e prossimi giorni (open-meteo, gratuito e senza chiave)
|
|
let WX = null;
|
|
const WX_TXT = {
|
|
0: 'Sereno',
|
|
1: 'Quasi sereno',
|
|
2: 'Parzialmente nuvoloso',
|
|
3: 'Coperto',
|
|
45: 'Nebbia',
|
|
48: 'Nebbia con brina',
|
|
51: 'Pioviggine leggera',
|
|
53: 'Pioviggine',
|
|
55: 'Pioviggine intensa',
|
|
56: 'Pioviggine gelata',
|
|
57: 'Pioviggine gelata',
|
|
61: 'Pioggia debole',
|
|
63: 'Pioggia',
|
|
65: 'Pioggia forte',
|
|
66: 'Pioggia gelata',
|
|
67: 'Pioggia gelata forte',
|
|
71: 'Neve debole',
|
|
73: 'Neve',
|
|
75: 'Neve forte',
|
|
77: 'Nevischio',
|
|
80: 'Rovesci leggeri',
|
|
81: 'Rovesci',
|
|
82: 'Rovesci violenti',
|
|
85: 'Rovesci di neve',
|
|
86: 'Rovesci di neve forti',
|
|
95: 'Temporale',
|
|
96: 'Temporale con grandine',
|
|
99: 'Temporale con grandine'
|
|
};
|
|
|
|
function wxIcon(w, day = true) {
|
|
w = +w;
|
|
if (w === 0) return day ? ['bi-sun-fill', '#fcd34d'] : ['bi-moon-stars-fill', '#c7d2fe'];
|
|
if (w <= 2) return day ? ['bi-cloud-sun-fill', '#fcd34d'] : ['bi-cloud-moon-fill', '#c7d2fe'];
|
|
if (w === 3) return ['bi-cloud-fill', '#cbd5e1'];
|
|
if (w <= 48) return ['bi-cloud-fog2-fill', '#94a3b8'];
|
|
if (w <= 57) return ['bi-cloud-drizzle-fill', '#7dd3fc'];
|
|
if (w <= 67) return ['bi-cloud-rain-fill', '#60a5fa'];
|
|
if (w <= 77) return ['bi-snow', '#e0f2fe'];
|
|
if (w <= 82) return ['bi-cloud-rain-heavy-fill', '#3b82f6'];
|
|
if (w <= 86) return ['bi-cloud-snow-fill', '#e0f2fe'];
|
|
return ['bi-cloud-lightning-rain-fill', '#a78bfa'];
|
|
}
|
|
const ico = (w, day, cls = '') => {
|
|
const [i, c] = wxIcon(w, day);
|
|
return `<i class="bi ${i} wi ${cls}" style="color:${c}"></i>`;
|
|
};
|
|
|
|
function weather() {
|
|
const s = D.settings;
|
|
const q = `latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}&timezone=Europe%2FRome&forecast_days=7` +
|
|
'¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day' +
|
|
'&hourly=temperature_2m,weather_code,precipitation_probability,is_day' +
|
|
'&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset';
|
|
fetch('https://api.open-meteo.com/v1/forecast?' + q)
|
|
.then(r => r.json()).then(r => {
|
|
const c = r.current;
|
|
if (!c) return;
|
|
WX = r;
|
|
$('#wx').innerHTML = `${ico(c.weather_code, c.is_day !== 0)}<div>${Math.round(c.temperature_2m)}°<small>${esc(s.city || '')}</small></div>`;
|
|
}).catch(() => {});
|
|
}
|
|
|
|
function slideWeather() {
|
|
const c = WX.current,
|
|
d = WX.daily,
|
|
h = WX.hourly;
|
|
const hhmm = t => String(t).slice(11, 16);
|
|
// prossime 8 ore a partire dall'ora attuale
|
|
const nowH = String(c.time).slice(0, 13);
|
|
let i0 = h.time.findIndex(t => t.slice(0, 13) >= nowH);
|
|
if (i0 < 0) i0 = 0;
|
|
const hours = [];
|
|
for (let i = i0 + 1; i < h.time.length && hours.length < 8; i++) hours.push(i);
|
|
const GG = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
|
const days = d.time.map((t, i) => i).slice(1, 7);
|
|
const rainCls = p => p >= 60 ? 'hi' : (p >= 30 ? 'mid' : '');
|
|
return `<div class="wxs">
|
|
<div class="panel wnow">
|
|
<div class="lbl">Adesso${D.settings.city ? ' a ' + esc(D.settings.city) : ''}</div>
|
|
<div class="wbig">${ico(c.weather_code, c.is_day !== 0)}<span>${Math.round(c.temperature_2m)}°</span></div>
|
|
<div class="wdesc">${esc(WX_TXT[c.weather_code] || '')}</div>
|
|
<div class="wmeta">
|
|
<div><span>Percepita</span><b>${Math.round(c.apparent_temperature)}°</b></div>
|
|
<div><span>Min / max</span><b>${Math.round(d.temperature_2m_min[0])}° / ${Math.round(d.temperature_2m_max[0])}°</b></div>
|
|
<div><span>Umidità</span><b>${Math.round(c.relative_humidity_2m)}%</b></div>
|
|
<div><span>Vento</span><b>${Math.round(c.wind_speed_10m)} km/h</b></div>
|
|
<div><span>Pioggia oggi</span><b>${d.precipitation_probability_max[0] ?? 0}%</b></div>
|
|
<div><span>Alba · tramonto</span><b>${hhmm(d.sunrise[0])} · ${hhmm(d.sunset[0])}</b></div>
|
|
</div>
|
|
</div>
|
|
<div class="wright">
|
|
<div class="panel whours">
|
|
<div class="lbl">Prossime ore</div>
|
|
<div class="hrow">${hours.map(i => `<div class="hc"><span class="t">${hhmm(h.time[i])}</span>${ico(h.weather_code[i], h.is_day[i] !== 0)}<b>${Math.round(h.temperature_2m[i])}°</b><span class="r ${rainCls(h.precipitation_probability[i])}"><i class="bi bi-droplet-fill"></i> ${h.precipitation_probability[i] ?? 0}%</span></div>`).join('')}</div>
|
|
</div>
|
|
<div class="wdays">${days.map(i => {
|
|
const dt = new Date(d.time[i] + 'T12:00:00');
|
|
return `<div class="panel dc"><div class="dn">${i === 1 ? 'Domani' : GG[dt.getDay()] + ' ' + dt.getDate()}</div>${ico(d.weather_code[i], true)}
|
|
<div class="dt"><b>${Math.round(d.temperature_2m_max[i])}°</b><span>${Math.round(d.temperature_2m_min[i])}°</span></div>
|
|
<div class="dd">${esc(WX_TXT[d.weather_code[i]] || '')}</div>
|
|
<div class="dr ${rainCls(d.precipitation_probability_max[i])}"><i class="bi bi-droplet-fill"></i> ${d.precipitation_probability_max[i] ?? 0}%</div></div>`;
|
|
}).join('')}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
/* ---------- avvio ---------- */
|
|
tick();
|
|
load().then(() => next());
|
|
setInterval(load, 60000);
|
|
// ricarica completa ogni 6 ore (memoria e nuove versioni della pagina)
|
|
setTimeout(() => location.reload(), 6 * H);
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|