Files
zibo-dashboard/public/userarea/production_dashboard_v2.php
T

1208 lines
45 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* production_dashboard_v2.php - nuova dashboard del gestionale, pensata per le dita (tablet) e per il PC.
*
* Punto di partenza del lavoro quotidiano: linee adesso, cose da gestire, i tuoi preferiti,
* poi tutte le funzioni a schede. Stesse voci e stessi permessi della dashboard attuale.
* Numeri di produzione per ora di fine del lavoro (non per data di inserimento).
*/
include('include/headscript.php');
$pdo = DBHandlerSelect::getInstance()->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
/* ===================== MENU (stesse voci e permessi della dashboard attuale + pagine nuove) ===================== */
$dashboardSections = [
[
'id' => 'secOperativo',
'title' => 'Operativo',
'subtitle' => 'Produzione di tutti i giorni',
'icon' => '🚀',
'buttons' => [
['label' => 'Programmazione', 'icon' => '🗓️', 'class' => 'btn-programmazione', 'url' => 'produzione_programmazione_drag.php', 'permission' => 'production.programming.view'],
['label' => 'Programmazione (nuova)', 'icon' => '📋', 'class' => 'btn-programmazione', 'url' => 'production_programming_v2.php', 'permission' => 'production.programming.view'],
['label' => 'Tabellone', 'icon' => '🧱', 'class' => 'btn-programmazione', 'url' => 'production_board.php', 'permission' => 'production.programming.view'],
['label' => 'Nuovi da Mago', 'icon' => '📥', 'class' => 'btn-mago', 'url' => 'production_mago_queue.php', 'permission' => 'production.programming.view', 'badge' => 'mago'],
['label' => 'Line View', 'icon' => '⚙️', 'class' => 'btn-status', 'url' => 'production_line_view2.php', 'permission' => 'production.line_view.view'],
['label' => 'Postazione tablet', 'icon' => '📱', 'class' => 'btn-status', 'url' => 'production_station.php', 'permission' => 'production.line_view.view'],
['label' => 'Magazzino', 'icon' => '📦', 'class' => 'btn-magazzino', 'url' => 'warehouse_dashboard.php', 'permission' => 'warehouse.dashboard.view'],
['label' => 'Smart-Alert', 'icon' => '⏰', 'class' => 'btn-scadenziario', 'url' => 'scadenzario/index.php', 'permission' => 'deadlines.view'],
],
],
[
'id' => 'secDirezione',
'title' => 'Direzione',
'subtitle' => 'Statistiche, fermi e supervisione',
'icon' => '📊',
'buttons' => [
['label' => 'Statistiche Produzione', 'icon' => '📈', 'class' => 'btn-statistiche', 'url' => 'production_stats.php', 'permission' => 'production.stats.view'],
['label' => 'Pause & Fermi', 'icon' => '🛑', 'class' => 'btn-pause-stats', 'url' => 'pause_stats.php', 'permission' => 'production.stats.view'],
['label' => 'Manager', 'icon' => '👔', 'class' => 'btn-manager', 'url' => 'manager_produzione.php', 'permission' => 'production.manager.view'],
['label' => 'Manager Stats', 'icon' => '📊', 'class' => 'btn-manager-stats', 'url' => 'manager_stats.php', 'permission' => 'production.manager_stats.view'],
['label' => 'Schermo reparto', 'icon' => '📺', 'class' => 'btn-kiosk', 'url' => 'kiosk_admin.php', 'permission' => 'production.programming.view'],
],
],
[
'id' => 'secVendite',
'title' => 'Business Overview',
'subtitle' => 'Ordini, fatturato, clienti e consegne (Mago)',
'icon' => '💶',
'buttons' => [
['label' => 'Sommario', 'icon' => '🚦', 'class' => 'btn-sales-summary', 'url' => 'sales_summary.php', 'permission' => 'sales.stats.view'],
['label' => 'Andamento ordini', 'icon' => '🧾', 'class' => 'btn-sales', 'url' => 'sales_orders_stats.php', 'permission' => 'sales.stats.view'],
['label' => 'Confronto anni', 'icon' => '📊', 'class' => 'btn-sales', 'url' => 'sales_orders_years.php', 'permission' => 'sales.stats.view'],
['label' => 'Scheda cliente', 'icon' => '🪪', 'class' => 'btn-sales', 'url' => 'sales_customer.php', 'permission' => 'sales.stats.view'],
['label' => 'Consegne', 'icon' => '🚚', 'class' => 'btn-sales-deliveries', 'url' => 'sales_orders_deliveries.php', 'permission' => 'sales.deliveries.view'],
],
],
[
'id' => 'secAnagrafiche',
'title' => 'Anagrafiche',
'subtitle' => 'Dati di base e setup di produzione',
'icon' => '🗂️',
'buttons' => [
['label' => 'Mescole', 'icon' => '⚗️', 'class' => 'btn-mescole', 'url' => 'mescole.php', 'permission' => 'masterdata.mescole.view'],
['label' => 'Elenco Profili', 'icon' => '🧩', 'class' => 'btn-matrici', 'url' => 'matrici.php', 'permission' => 'masterdata.matrici.view'],
['label' => 'Linee Produzione', 'icon' => '🏭', 'class' => 'btn-linee', 'url' => 'linee.php', 'permission' => 'masterdata.linee.view'],
['label' => 'Imballaggi', 'icon' => '📦', 'class' => 'btn-setup', 'url' => 'packaging_items.php', 'permission' => 'masterdata.packaging.view'],
['label' => 'Suppliers', 'icon' => '🏷️', 'class' => 'btn-setup', 'url' => 'suppliers.php', 'permission' => 'masterdata.suppliers.view'],
['label' => 'Setup', 'icon' => '⚙️', 'class' => 'btn-setup', 'url' => 'lookup_values.php', 'permission' => 'masterdata.lookup.view'],
['label' => 'Fogli di lavoro', 'icon' => '🗒️', 'class' => 'btn-setup', 'url' => 'worksheets.php', 'permission' => 'masterdata.worksheets.view'],
],
],
[
'id' => 'secServizi',
'title' => 'Servizi',
'subtitle' => 'Status, cause pausa, attrezzature',
'icon' => '🧰',
'buttons' => [
['label' => 'Status', 'icon' => '📋', 'class' => 'btn-setup', 'url' => 'production_status.php', 'permission' => 'services.status.view'],
['label' => 'Cause di Pausa', 'icon' => '🛑', 'class' => 'btn-problem', 'url' => 'production_pause_reasons.php', 'permission' => 'services.pause_reasons.view'],
['label' => 'Attrezzature', 'icon' => '🛠️', 'class' => 'btn-tools', 'url' => 'production_tools.php', 'permission' => 'services.tools.view'],
],
],
[
'id' => 'secPersonale',
'title' => 'Personale',
'subtitle' => 'Dipendenti, formazione, skill',
'icon' => '👥',
'buttons' => [
['label' => 'Dipendenti', 'icon' => '👥', 'class' => 'btn-employees', 'url' => 'employees.php', 'permission' => 'hr.employees.view'],
['label' => 'Mansioni', 'icon' => '🧩', 'class' => 'btn-setup', 'url' => 'job-roles.php', 'permission' => 'hr.employees.view'],
['label' => 'Departments', 'icon' => '🏢', 'class' => 'btn-departments', 'url' => 'departments.php', 'permission' => 'hr.departments.view'],
['label' => 'DPI', 'icon' => '🦺', 'class' => 'btn-setup', 'url' => 'ppe-items.php', 'permission' => 'hr.employees.view'],
['label' => 'Gestione Formazione', 'icon' => '🎓', 'class' => 'btn-setup', 'url' => 'trainings.php', 'permission' => 'hr.trainings.view'],
['label' => 'Skills', 'icon' => '🧠', 'class' => 'btn-setup', 'url' => 'skills.php', 'permission' => 'hr.skills.view'],
],
],
];
/* ===================== DATI ===================== */
$TZ = new DateTimeZone('Europe/Rome');
$UTC = new DateTimeZone('UTC');
$u = function (DateTime $d) use ($UTC) {
$x = clone $d;
return $x->setTimezone($UTC)->format('Y-m-d H:i:s');
};
const DV2_NOT_FINISHED = '1,6,2,7'; // da programmare, programmato, in produzione, pausa
/** Lavori conclusi tra due istanti locali (per ora di fine, salvata in UTC). */
$produced = function (DateTime $from, DateTime $to) use ($pdo, $u) {
$st = $pdo->prepare("SELECT COUNT(*) n, COALESCE(SUM(kgprod),0) kg, COALESCE(SUM(mtprod),0) mt, COALESCE(SUM(scarto),0) sc
FROM productiondata WHERE end_time >= ? AND end_time < ? AND id_status NOT IN (" . DV2_NOT_FINISHED . ")");
$st->execute([$u($from), $u($to)]);
$r = $st->fetch(PDO::FETCH_ASSOC);
$kg = (float)$r['kg'];
$sc = (float)$r['sc'];
return ['n' => (int)$r['n'], 'kg' => $kg, 'mt' => (float)$r['mt'], 'scarto' => ($kg + $sc) > 0 ? $sc / ($kg + $sc) * 100 : null];
};
$now = new DateTime('now', $TZ);
$today = new DateTime('today', $TZ);
$yest = (clone $today)->modify('-1 day');
$yestSame = (clone $yest)->setTime((int)$now->format('H'), (int)$now->format('i'));
$monthStart = new DateTime('first day of this month 00:00', $TZ);
$prevMonthStart = (clone $monthStart)->modify('-1 month');
$prevMonthSame = (clone $prevMonthStart)->modify('+' . ($now->getTimestamp() - $monthStart->getTimestamp()) . ' seconds');
$K = ['today' => null, 'yest' => null, 'month' => null, 'prevMonth' => null];
$stops = null;
$todo = ['mago' => 0, 'problemi' => 0, 'compilare' => 0, 'daprog' => 0, 'ritardo' => 0];
$lines = [];
try {
$K['today'] = $produced($today, $now);
$K['yest'] = $produced($yest, $yestSame);
$K['month'] = $produced($monthStart, $now);
$K['prevMonth'] = $produced($prevMonthStart, $prevMonthSame);
// ore di fermo della settimana
$mon = new DateTime('monday this week', $TZ);
$st = $pdo->prepare("SELECT COALESCE(SUM(GREATEST(0, TIMESTAMPDIFF(SECOND, GREATEST(start_pause, ?), COALESCE(end_pause, UTC_TIMESTAMP())))), 0)
FROM production_pauses WHERE COALESCE(end_pause, UTC_TIMESTAMP()) > ?");
$st->execute([$u($mon), $u($mon)]);
$stops = (int)$st->fetchColumn() / 3600;
// cose da gestire
$todo['problemi'] = (int)$pdo->query("SELECT COUNT(*) FROM productiondata WHERE id_status = 8")->fetchColumn();
$todo['compilare'] = (int)$pdo->query("SELECT COUNT(*) FROM productiondata WHERE id_status = 11")->fetchColumn();
$todo['daprog'] = (int)$pdo->query("SELECT COUNT(*) FROM productiondata WHERE id_status = 1")->fetchColumn();
$todo['ritardo'] = (int)$pdo->query("SELECT COUNT(*) FROM productiondata WHERE id_status IN (1,6) AND data_zibo IS NOT NULL AND data_zibo < CURDATE()")->fetchColumn();
try {
$todo['mago'] = (int)$pdo->query("SELECT COUNT(*) FROM mago_prod_queue WHERE queue_status = 'nuovo' OR (queue_status = 'importato' AND mago_changed = 1)")->fetchColumn();
} catch (Throwable $e) {
}
// linee adesso
foreach ($pdo->query("SELECT * FROM production_lines ORDER BY line_number, id")->fetchAll(PDO::FETCH_ASSOC) as $l) {
if (isset($l['status']) && $l['status'] !== '' && $l['status'] !== 'active') continue;
$lines[(int)$l['id']] = ['id' => (int)$l['id'], 'name' => $l['name'], 'color' => $l['color'] ?: '#64748b', 'job' => null, 'queue' => 0, 'next' => null];
}
$rows = $pdo->query("
SELECT p.id_linea, p.id_status, p.start_time, p.ore_previste, m.nome AS matrice,
(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
FROM productiondata p LEFT JOIN matrice m ON m.id = p.idmatrice WHERE p.id_status IN (2,7) ORDER BY p.start_time")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
$lid = (int)$r['id_linea'];
if (!isset($lines[$lid]) || $lines[$lid]['job']) continue;
$lines[$lid]['job'] = [
'matrice' => $r['matrice'],
'paused' => (int)$r['id_status'] === 7,
'start_ms' => $r['start_time'] ? (new DateTime($r['start_time'], $UTC))->getTimestamp() * 1000 : null,
'open_pause_ms' => $r['open_pause'] ? (new DateTime($r['open_pause'], $UTC))->getTimestamp() * 1000 : null,
'pause_sec' => (int)$r['pause_sec'],
'ore' => (float)$r['ore_previste'],
];
}
foreach ($pdo->query("SELECT p.id_linea, m.nome FROM productiondata p LEFT JOIN matrice m ON m.id = p.idmatrice WHERE p.id_status = 6 ORDER BY p.priority IS NULL, p.priority, p.id") as $r) {
$lid = (int)$r['id_linea'];
if (!isset($lines[$lid])) continue;
$lines[$lid]['queue']++;
if ($lines[$lid]['next'] === null) $lines[$lid]['next'] = $r['nome'];
}
} catch (Throwable $e) {
// la dashboard resta usabile anche se un dato non è disponibile
}
$userName = '';
if (!empty($iduserlogin)) {
try {
$st = $pdo->prepare("SELECT COALESCE(NULLIF(TRIM(first_name), ''), username, email) FROM auth_users WHERE id = ?");
$st->execute([(int)$iduserlogin]);
$userName = (string)$st->fetchColumn();
} catch (Throwable $e) {
}
}
if (!function_exists('visibleButtons')) {
function visibleButtons(array $buttons)
{
return array_values(array_filter($buttons, fn($b) => empty($b['permission']) || userCan($b['permission'])));
}
}
function dv2Num($v, $d = 0)
{
return number_format((float)$v, $d, ',', '.');
}
/** Freccia di confronto (▲ ▼) con percentuale. */
function dv2Cmp($cur, $prev, $lowerBetter = false)
{
if ($cur === null || $prev === null) return '<span class="flat">—</span>';
if (!$prev && !$cur) return '<span class="flat">= uguale</span>';
if (!$prev) return '<span class="' . ($lowerBetter ? 'down' : 'up') . '">▲ nuovo</span>';
$p = round(($cur - $prev) / $prev * 100);
if ($p == 0) return '<span class="flat">= uguale</span>';
$good = $lowerBetter ? $p < 0 : $p > 0;
return '<span class="' . ($good ? 'up' : 'down') . '">' . ($p > 0 ? '▲' : '▼') . ' ' . abs($p) . '%</span>';
}
$hour = (int)$now->format('H');
$greet = $hour < 13 ? 'Buongiorno' : ($hour < 18 ? 'Buon pomeriggio' : 'Buonasera');
$GIORNI = ['domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato'];
$MESI = ['', 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'];
$canProg = userCan('production.programming.view');
$canLine = userCan('production.line_view.view');
$canStats = userCan('production.stats.view');
?>
<?php
// voci visibili all'utente, per sezione (stessa regola dei permessi della dashboard attuale)
$visibleSections = [];
foreach ($dashboardSections as $sec) {
$b = visibleButtons($sec['buttons']);
if (!$b) continue;
foreach ($b as &$x) $x['badge_n'] = (($x['badge'] ?? '') === 'mago') ? (int)$todo['mago'] : 0;
unset($x);
$visibleSections[] = ['id' => $sec['id'], 'title' => $sec['title'], 'icon' => $sec['icon'], 'buttons' => $b];
}
$todoItems = [];
if ($canProg && $todo['mago']) $todoItems[] = ['n' => $todo['mago'], 'l' => 'Nuovi ordini da Mago', 'u' => 'production_mago_queue.php', 'c' => 'info', 'i' => '📥'];
if ($canProg && $todo['ritardo']) $todoItems[] = ['n' => $todo['ritardo'], 'l' => 'Data Zibo già passata', 'u' => 'production_programming_v2.php', 'c' => 'hot', 'i' => '⏰'];
if ($todo['problemi']) $todoItems[] = ['n' => $todo['problemi'], 'l' => 'Chiusi con problema', 'u' => $canLine ? 'production_line_view2.php' : '', 'c' => 'hot', 'i' => '⚠️'];
if ($todo['compilare']) $todoItems[] = ['n' => $todo['compilare'], 'l' => 'Dati di fine da compilare', 'u' => $canLine ? 'production_line_view2.php' : '', 'c' => 'warn', 'i' => '📝'];
if ($canProg && $todo['daprog']) $todoItems[] = ['n' => $todo['daprog'], 'l' => 'Da programmare', 'u' => 'production_programming_v2.php', 'c' => 'plain', 'i' => '🗓️'];
$t = $K['today'];
$y = $K['yest'];
$m = $K['month'];
$pm = $K['prevMonth'];
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
:root {
--ink: #1f2d3d;
--muted: #64748b;
--line: #e2e8f0;
--r: 20px;
}
body {
background: #eef2f7;
color: var(--ink);
-webkit-tap-highlight-color: transparent;
}
.hm {
max-width: 1320px;
margin: 0 auto;
width: 100%;
}
.hm button,
.hm a {
touch-action: manipulation;
}
.hm h4 {
font-weight: 900;
font-size: 1.25rem;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.blk {
margin-bottom: 22px;
}
.blk-h {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.blk-h .act {
margin-left: auto;
}
/* ---------- testata ---------- */
.top {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.top h2 {
font-weight: 900;
margin: 0;
font-size: 1.75rem;
}
.top .dt {
color: var(--muted);
font-weight: 700;
font-size: 1.05rem;
}
.top .clock {
margin-left: auto;
font-size: 2.2rem;
font-weight: 900;
font-variant-numeric: tabular-nums;
}
.pill-btn {
min-height: 56px;
border-radius: 16px;
border: 0;
background: #fff;
box-shadow: 0 4px 14px rgba(15, 23, 42, .08);
padding: 0 20px;
font-weight: 800;
font-size: 1.05rem;
color: var(--ink);
display: inline-flex;
align-items: center;
gap: 10px;
}
.pill-btn:active {
transform: scale(.97);
}
.pill-btn.on {
background: #0f172a;
color: #fff;
}
/* ---------- linee adesso ---------- */
.lines {
display: grid;
gap: 14px;
}
.ln {
position: relative;
display: flex;
flex-direction: column;
min-height: 168px;
padding: 16px 18px;
border-radius: var(--r);
text-decoration: none;
color: var(--ink);
background: color-mix(in srgb, var(--c) 12%, #fff);
border: 2px solid color-mix(in srgb, var(--c) 30%, #fff);
box-shadow: 0 6px 16px rgba(15, 23, 42, .06);
}
a.ln:active {
transform: scale(.98);
}
a.ln:hover {
color: var(--ink);
}
.ln .h {
display: flex;
align-items: center;
gap: 8px;
font-weight: 900;
font-size: 1.15rem;
}
.ln .h .dot {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--c);
}
.ln .st {
margin-left: auto;
font-size: .78rem;
font-weight: 900;
border-radius: 8px;
padding: 4px 10px;
color: #fff;
letter-spacing: .03em;
}
.ln .mat {
font-size: 1.9rem;
font-weight: 900;
margin-top: 10px;
line-height: 1;
}
.ln .tm {
font-size: 2.1rem;
font-weight: 900;
font-variant-numeric: tabular-nums;
margin-top: auto;
line-height: 1.1;
}
.ln .tm.run {
color: #15803d;
}
.ln .tm.pause {
color: #c2410c;
}
.ln .bar {
height: 8px;
border-radius: 99px;
background: rgba(255, 255, 255, .9);
overflow: hidden;
margin-top: 8px;
}
.ln .bar span {
display: block;
height: 100%;
background: #16a34a;
}
.ln .q {
font-size: .95rem;
color: var(--muted);
font-weight: 700;
margin-top: 8px;
}
.ln .go {
position: absolute;
right: 16px;
bottom: 14px;
font-size: 1.4rem;
color: color-mix(in srgb, var(--c) 70%, #000);
opacity: .55;
}
.nums {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 12px;
}
.num {
background: #fff;
border-radius: 14px;
padding: 10px 16px;
box-shadow: 0 3px 10px rgba(15, 23, 42, .05);
font-weight: 700;
color: var(--muted);
font-size: .95rem;
text-decoration: none;
}
.num b {
color: var(--ink);
font-size: 1.15rem;
margin-left: 4px;
}
.up {
color: #16a34a;
font-weight: 800;
}
.down {
color: #dc2626;
font-weight: 800;
}
.flat {
color: var(--muted);
font-weight: 700;
}
/* ---------- da gestire ---------- */
.todo {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none;
}
.td {
flex: 1 0 190px;
min-height: 96px;
display: flex;
align-items: center;
gap: 14px;
padding: 14px 18px;
border-radius: var(--r);
background: #fff;
box-shadow: 0 4px 14px rgba(15, 23, 42, .06);
text-decoration: none;
color: var(--ink);
border-left: 6px solid var(--k);
}
a.td:active {
transform: scale(.98);
}
.td .n {
font-size: 2.2rem;
font-weight: 900;
color: var(--k);
line-height: 1;
}
.td .l {
font-weight: 800;
font-size: 1rem;
line-height: 1.2;
}
.td .i {
font-size: .85rem;
color: var(--muted);
font-weight: 700;
}
.td.hot {
--k: #dc2626;
}
.td.warn {
--k: #ea580c;
}
.td.info {
--k: #0284c7;
}
.td.plain {
--k: #475569;
}
.td.ok {
--k: #16a34a;
}
/* ---------- riquadri (preferiti e funzioni) ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
.tile {
position: relative;
min-height: 132px;
border-radius: var(--r);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 10px;
text-align: center;
text-decoration: none;
color: var(--ink);
font-weight: 800;
font-size: 1.02rem;
line-height: 1.2;
background: #fff;
box-shadow: 0 4px 14px rgba(15, 23, 42, .06);
border: 2px solid transparent;
user-select: none;
-webkit-user-select: none;
}
.tile:active {
transform: scale(.96);
}
.tile:hover {
color: var(--ink);
border-color: color-mix(in srgb, var(--a) 35%, #fff);
}
.tile .ti {
width: 64px;
height: 64px;
border-radius: 18px;
background: color-mix(in srgb, var(--a) 22%, #fff);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
.tile .bdg {
position: absolute;
top: 10px;
right: 10px;
background: #dc2626;
color: #fff;
border-radius: 999px;
font-size: .85rem;
font-weight: 900;
padding: 3px 9px;
}
.tile .star {
display: none;
position: absolute;
top: 6px;
left: 6px;
width: 44px;
height: 44px;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 1.4rem;
background: #f1f5f9;
color: #94a3b8;
}
.editing .tile .star {
display: flex;
}
.editing .tile.fav .star {
background: #fef3c7;
color: #d97706;
}
.editing .tile {
border-style: dashed;
border-color: #cbd5e1;
}
.fav-empty {
background: #fff;
border-radius: var(--r);
padding: 22px;
text-align: center;
color: var(--muted);
font-weight: 700;
border: 2px dashed #cbd5e1;
}
/* schede delle sezioni */
.tabs {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 6px;
margin-bottom: 14px;
scrollbar-width: none;
}
.tab {
flex-shrink: 0;
min-height: 52px;
padding: 0 18px;
border-radius: 14px;
border: 0;
background: #fff;
font-weight: 800;
font-size: 1rem;
color: var(--muted);
box-shadow: 0 3px 10px rgba(15, 23, 42, .05);
display: inline-flex;
align-items: center;
gap: 8px;
}
.tab.on {
background: #0f172a;
color: #fff;
}
.tab:active {
transform: scale(.97);
}
.hide {
display: none !important;
}
/* ricerca a tutto schermo */
.srch {
position: fixed;
inset: 0;
background: #eef2f7;
z-index: 2000;
padding: 22px;
overflow-y: auto;
display: none;
}
.srch.on {
display: block;
}
.srch .bar {
display: flex;
gap: 12px;
max-width: 1320px;
margin: 0 auto 18px;
}
.srch input {
flex: 1;
min-height: 64px;
border-radius: 18px;
border: 2px solid #cbd5e1;
font-size: 1.4rem;
font-weight: 700;
padding: 0 22px;
}
.srch input:focus {
outline: none;
border-color: #2563eb;
}
.srch .grid {
max-width: 1320px;
margin: 0 auto;
}
/* colori delle voci (gli stessi della dashboard attuale) */
.btn-programmazione {
--a: #7c7afa;
}
.btn-status {
--a: #22c55e;
}
.btn-mago {
--a: #f59e0b;
}
.btn-scadenziario {
--a: #fb923c;
}
.btn-statistiche {
--a: #eab308;
}
.btn-manager {
--a: #3b82f6;
}
.btn-manager-stats {
--a: #22c55e;
}
.btn-pause-stats,
.btn-problem {
--a: #ef4444;
}
.btn-magazzino,
.btn-linee {
--a: #0ea5e9;
}
.btn-mescole {
--a: #f59e0b;
}
.btn-matrici {
--a: #f43f5e;
}
.btn-tools {
--a: #8b5cf6;
}
.btn-employees {
--a: #6366f1;
}
.btn-departments {
--a: #60a5fa;
}
.btn-setup {
--a: #94a3b8;
}
.btn-sales-summary {
--a: #16a34a;
}
.btn-sales {
--a: #34d399;
}
.btn-sales-deliveries {
--a: #facc15;
}
.btn-kiosk {
--a: #334155;
}
/* widget scadenze (come nella dashboard attuale) */
.my-deadlines-widgets {
display: flex;
flex-wrap: wrap;
gap: .75rem;
margin-bottom: 1rem;
width: 100%;
}
.my-deadlines-widgets:empty {
display: none;
}
.my-deadlines-widgets .my-deadlines-widgets {
display: contents;
}
.my-deadlines-widgets .mdw {
flex: 1 1 0;
min-width: 0;
display: flex;
align-items: center;
gap: .75rem;
padding: .9rem 1rem;
border-radius: 16px;
text-decoration: none;
color: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, .08);
min-height: 64px;
}
.my-deadlines-widgets .mdw-red {
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
}
.my-deadlines-widgets .mdw-orange {
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
}
.my-deadlines-widgets .mdw-gray {
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
}
.my-deadlines-widgets .mdw-icon {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, .22);
flex-shrink: 0;
}
.my-deadlines-widgets .mdw-body {
flex: 1;
line-height: 1.2;
min-width: 0;
}
.my-deadlines-widgets .mdw-count {
font-size: 1.5rem;
font-weight: 700;
}
.my-deadlines-widgets .mdw-label {
font-size: .8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 900px) {
.ln {
min-height: 150px;
padding: 14px;
}
.ln .mat {
font-size: 1.5rem;
}
.ln .tm {
font-size: 1.7rem;
}
.my-deadlines-widgets .mdw {
flex: 1 1 100%;
}
}
@media (max-width: 640px) {
.lines {
grid-template-columns: 1fr !important;
}
.top .clock {
display: none;
}
}
@media (max-width: 560px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
</head>
<body>
<div class="wrapper toggled" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="hm">
<div class="top">
<div>
<h2><?= $greet ?><?= $userName !== '' ? ', ' . htmlspecialchars($userName, ENT_QUOTES, 'UTF-8') : '' ?></h2>
<div class="dt"><?= $GIORNI[(int)$now->format('w')] . ' ' . $now->format('j') . ' ' . $MESI[(int)$now->format('n')] ?></div>
</div>
<div class="clock" id="clock"><?= $now->format('H:i') ?></div>
<button class="pill-btn" id="btnSearch">🔎 Cerca</button>
</div>
<div class="my-deadlines-widgets"><?php
foreach ([__DIR__ . '/scadenzario/include/my_deadlines_widget.php', __DIR__ . '/include/training_widget.php'] as $w) {
if (is_file($w)) include($w);
}
?></div>
<!-- LINEE ADESSO -->
<?php if ($lines): ?>
<div class="blk">
<div class="blk-h">
<h4>⚙️ Linee adesso</h4>
</div>
<div class="lines" style="grid-template-columns:repeat(<?= min(4, count($lines)) ?>, minmax(0,1fr))">
<?php foreach ($lines as $l):
$j = $l['job']; ?>
<a class="ln" style="--c:<?= htmlspecialchars($l['color']) ?>" <?= $canLine ? 'href="production_station.php?line=' . $l['id'] . '"' : '' ?>>
<div class="h"><span class="dot"></span><?= htmlspecialchars($l['name']) ?>
<span class="st" style="background:<?= $j ? ($j['paused'] ? '#f59e0b' : '#16a34a') : '#94a3b8' ?>"><?= $j ? ($j['paused'] ? 'IN PAUSA' : 'IN PRODUZIONE') : 'LIBERA' ?></span>
</div>
<?php if ($j): ?>
<div class="mat"><?= htmlspecialchars((string)$j['matrice']) ?></div>
<div class="tm <?= $j['paused'] ? 'pause' : 'run' ?>" data-job='<?= htmlspecialchars(json_encode($j), ENT_QUOTES) ?>'>--:--:--</div>
<?php if ($j['ore']): ?><div class="bar"><span></span></div><?php endif; ?>
<?php else: ?>
<div class="mat" style="color:#94a3b8">—</div>
<div class="q" style="margin-top:auto"><?= $l['next'] ? 'Prossimo: <b style="color:#1f2d3d">' . htmlspecialchars($l['next']) . '</b>' : 'Nessun lavoro in coda' ?></div>
<?php endif; ?>
<div class="q"><?= $l['queue'] ?> in coda</div>
<?php if ($canLine): ?><span class="go">›</span><?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php if ($t): ?>
<div class="nums">
<a class="num" <?= $canStats ? 'href="production_stats.php"' : '' ?>>Oggi <b><?= dv2Num($t['kg']) ?> kg</b> · <?= dv2Num($t['mt']) ?> m · <?= dv2Cmp($t['kg'], $y['kg']) ?></a>
<a class="num" <?= $canStats ? 'href="production_stats.php"' : '' ?>>Mese <b><?= dv2Num($m['kg']) ?> kg</b> · <?= dv2Cmp($m['kg'], $pm['kg']) ?></a>
<a class="num" <?= $canStats ? 'href="production_stats.php"' : '' ?>>Scarto mese <b><?= $m['scarto'] === null ? '—' : dv2Num($m['scarto'], 2) . '%' ?></b></a>
<a class="num" <?= $canStats ? 'href="pause_stats.php"' : '' ?>>Fermi settimana <b><?= dv2Num($stops ?? 0, 1) ?> h</b></a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- DA GESTIRE -->
<div class="blk">
<div class="blk-h">
<h4>📌 Da gestire</h4>
</div>
<div class="todo">
<?php foreach ($todoItems as $it): ?>
<a class="td <?= $it['c'] ?>" <?= $it['u'] ? 'href="' . $it['u'] . '"' : '' ?>>
<span class="n"><?= $it['n'] ?></span>
<span><span class="l"><?= htmlspecialchars($it['l']) ?></span><br><span class="i"><?= $it['i'] ?> apri</span></span>
</a>
<?php endforeach; ?>
<?php if (!$todoItems): ?><div class="td ok"><span class="n">✓</span><span class="l">Tutto in ordine</span></div><?php endif; ?>
</div>
</div>
<!-- PREFERITI -->
<div class="blk" id="blkFav">
<div class="blk-h">
<h4>⭐ I tuoi preferiti</h4>
<button class="pill-btn act" id="btnEdit">✏️ Modifica</button>
</div>
<div class="grid" id="favGrid"></div>
</div>
<!-- TUTTE LE FUNZIONI -->
<div class="blk" id="blkAll">
<div class="blk-h">
<h4>🧭 Tutte le funzioni</h4>
</div>
<div class="tabs" id="tabs"></div>
<div class="grid" id="allGrid"></div>
</div>
<?php if (!$visibleSections): ?><div class="fav-empty">Nessuna funzione disponibile per il tuo profilo.</div><?php endif; ?>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<?php include('include/footer.php'); ?>
</div>
<!-- RICERCA -->
<div class="srch" id="srch">
<div class="bar">
<input type="search" id="q" placeholder="Cosa cerchi? (es. mescole, consegne)" autocomplete="off">
<button class="pill-btn" id="srchClose">✕ Chiudi</button>
</div>
<div class="grid" id="srchGrid"></div>
</div>
<script>
$(function() {
const SECTIONS = <?= json_encode($visibleSections, JSON_UNESCAPED_UNICODE) ?>;
const ALL = SECTIONS.flatMap(s => s.buttons.map(b => ({
...b,
sec: s.title
})));
const FAV_KEY = 'dv2_fav_<?= (int)($iduserlogin ?? 0) ?>';
const store = {
get(k, d) {
try {
return JSON.parse(localStorage.getItem(k)) ?? d;
} catch (e) {
return d;
}
},
set(k, v) {
try {
localStorage.setItem(k, JSON.stringify(v));
} catch (e) {}
}
};
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
} [c]));
// preferiti: all'inizio le prime voci di Operativo, poi quelli scelti dall'utente
let fav = store.get(FAV_KEY, null);
if (!Array.isArray(fav)) fav = (SECTIONS[0] ? SECTIONS[0].buttons.slice(0, 6).map(b => b.url) : []);
fav = fav.filter(u => ALL.some(b => b.url === u));
let editing = false;
let tab = store.get('dv2_tab', SECTIONS[0] ? SECTIONS[0].id : '');
if (!SECTIONS.some(s => s.id === tab) && SECTIONS[0]) tab = SECTIONS[0].id;
const tile = b => `<a class="tile ${esc(b.class)} ${fav.includes(b.url) ? 'fav' : ''}" href="${esc(b.url)}" data-url="${esc(b.url)}">
<span class="star">${fav.includes(b.url) ? '★' : '☆'}</span>
<span class="ti">${esc(b.icon)}</span><span>${esc(b.label)}</span>
${b.badge_n ? `<span class="bdg">${b.badge_n}</span>` : ''}
</a>`;
function render() {
$('body').toggleClass('editing', editing);
$('#btnEdit').toggleClass('on', editing).html(editing ? '✓ Fatto' : '✏️ Modifica');
const favItems = fav.map(u => ALL.find(b => b.url === u)).filter(Boolean);
$('#favGrid').html(favItems.length ? favItems.map(tile).join('') :
`<div class="fav-empty" style="grid-column:1/-1">${editing ? 'Tocca ☆ sulle funzioni qui sotto per aggiungerle' : 'Nessun preferito: tocca ✏️ Modifica per sceglierli'}</div>`);
$('#tabs').html(SECTIONS.map(s => `<button class="tab ${s.id === tab ? 'on' : ''}" data-tab="${esc(s.id)}">${esc(s.icon)} ${esc(s.title)}</button>`).join(''));
const sec = SECTIONS.find(s => s.id === tab);
$('#allGrid').html(sec ? sec.buttons.map(tile).join('') : '');
// in modifica le funzioni restano tutte visibili per scegliere
if (editing) $('#blkAll')[0].scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
$('#btnEdit').on('click', () => {
editing = !editing;
render();
});
$(document).on('click', '.tab', function() {
tab = this.dataset.tab;
store.set('dv2_tab', tab);
render();
});
// in modifica il tocco su una funzione la aggiunge o la toglie dai preferiti
$(document).on('click', '.tile', function(e) {
if (!editing) return;
e.preventDefault();
const u = this.dataset.url;
fav = fav.includes(u) ? fav.filter(x => x !== u) : [...fav, u];
store.set(FAV_KEY, fav);
const y = window.scrollY;
editing = true;
$('body').addClass('editing');
const favItems = fav.map(x => ALL.find(b => b.url === x)).filter(Boolean);
$('#favGrid').html(favItems.length ? favItems.map(tile).join('') : '<div class="fav-empty" style="grid-column:1/-1">Tocca ☆ sulle funzioni qui sotto per aggiungerle</div>');
const sec = SECTIONS.find(s => s.id === tab);
$('#allGrid').html(sec ? sec.buttons.map(tile).join('') : '');
window.scrollTo(0, y);
});
/* ---------- ricerca a tutto schermo ---------- */
function openSearch() {
$('#srch').addClass('on');
$('#q').val('').trigger('focus');
search();
}
function search() {
const q = $('#q').val().trim().toLowerCase();
const hits = ALL.filter(b => !q || (b.label + ' ' + b.sec).toLowerCase().includes(q));
$('#srchGrid').html(hits.length ? hits.map(tile).join('') : '<div class="fav-empty" style="grid-column:1/-1">Nessuna funzione trovata</div>');
}
$('#btnSearch').on('click', openSearch);
$('#srchClose').on('click', () => $('#srch').removeClass('on'));
$('#q').on('input', search).on('keydown', function(e) {
if (e.key === 'Enter') {
const f = $('#srchGrid .tile').first();
if (f.length) location.href = f.attr('href');
}
if (e.key === 'Escape') $('#srch').removeClass('on');
});
$(document).on('keydown', function(e) {
if (e.key === '/' && !$(e.target).is('input, textarea, select')) {
e.preventDefault();
openSearch();
}
});
/* ---------- orologio e timer delle linee ---------- */
const skew = Date.now() - <?= time() * 1000 ?>;
const hms = ms => {
const s = Math.max(0, Math.floor(ms / 1000));
return `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s % 3600 / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
};
function tick() {
const t = Date.now() - skew;
const d = new Date(t);
$('#clock').text(`${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`);
$('[data-job]').each(function() {
const j = $(this).data('job');
if (!j.start_ms) return;
const cur = j.open_pause_ms ? t - j.open_pause_ms : 0;
const net = Math.max(0, t - j.start_ms - j.pause_sec * 1000 - cur);
$(this).text(hms(j.paused ? cur : net));
if (j.ore) {
const p = net / (j.ore * 3600e3);
$(this).closest('.ln').find('.bar span').css({
width: Math.min(100, p * 100) + '%',
background: p > 1 ? '#dc2626' : (j.paused ? '#f59e0b' : '#16a34a')
});
}
});
}
render();
tick();
setInterval(tick, 1000);
// aggiorna linee e numeri ogni 5 minuti (non mentre si modifica o si cerca)
setInterval(() => {
if (!editing && !$('#srch').hasClass('on')) location.reload();
}, 300000);
});
</script>
</body>
</html>