This commit is contained in:
2026-10-21 10:03:59 +02:00
parent 76ed4d11ca
commit 5444ea943a
20 changed files with 7332 additions and 7 deletions
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Schermo di reparto (kiosk) per la sala pausa.
*
* kiosk_settings impostazioni chiave/valore (durata slide, slide attive, sicurezza, meteo, codice di accesso)
* kiosk_items contenuti scritti dal manager: comunicazioni, benvenuti, turni, menu, avvisi scorrevoli
* kiosk_goals obiettivi con barra di avanzamento (manuali o calcolati dai dati di produzione)
*/
final class CreateKiosk extends AbstractMigration
{
public function change(): void
{
$this->table('kiosk_settings', ['id' => false, 'primary_key' => ['setting_key'], 'comment' => 'Impostazioni schermo di reparto'])
->addColumn('setting_key', 'string', ['limit' => 50])
->addColumn('setting_value', 'text', ['null' => true])
->addColumn('updated_at', 'datetime', ['null' => true])
->create();
$this->table('kiosk_items', ['comment' => 'Contenuti dello schermo di reparto'])
->addColumn('item_type', 'string', ['limit' => 20, 'comment' => 'comunicazione, benvenuto, turni, menu, ticker'])
->addColumn('title', 'string', ['limit' => 150, 'default' => ''])
->addColumn('body', 'text', ['null' => true])
->addColumn('image', 'string', ['limit' => 255, 'default' => ''])
->addColumn('valid_from', 'date', ['null' => true])
->addColumn('valid_to', 'date', ['null' => true])
->addColumn('sort', 'integer', ['default' => 0])
->addColumn('active', 'boolean', ['default' => true])
->addColumn('created_by', 'string', ['limit' => 100, 'default' => ''])
->addColumn('created_at', 'datetime', ['null' => true])
->addColumn('updated_at', 'datetime', ['null' => true])
->addIndex(['item_type', 'active'])
->create();
$this->table('kiosk_goals', ['comment' => 'Obiettivi mostrati sullo schermo di reparto'])
->addColumn('title', 'string', ['limit' => 150])
->addColumn('source', 'string', ['limit' => 30, 'default' => 'manuale', 'comment' => 'manuale, metri_mese, kg_mese, lavori_mese, scarto_mese, fermi_mese'])
->addColumn('unit', 'string', ['limit' => 20, 'default' => ''])
->addColumn('target', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
->addColumn('current_value', 'decimal', ['precision' => 15, 'scale' => 2, 'null' => true, 'comment' => 'Solo per gli obiettivi manuali'])
->addColumn('lower_is_better', 'boolean', ['default' => false])
->addColumn('period_from', 'date', ['null' => true])
->addColumn('period_to', 'date', ['null' => true])
->addColumn('sort', 'integer', ['default' => 0])
->addColumn('active', 'boolean', ['default' => true])
->addColumn('updated_at', 'datetime', ['null' => true])
->create();
}
}
+384
View File
@@ -0,0 +1,384 @@
<?php
/**
* Kiosk - dati dello schermo di reparto (sala pausa).
*
* Mostra solo dati di produzione e comunicazioni: niente fatturato, prezzi o clienti.
* I tempi di produzione sono salvati in UTC (UTC_TIMESTAMP): i giorni si calcolano in ora italiana.
*/
class Kiosk
{
const TZ = 'Europe/Rome';
/** Stati non conclusi: tutto il resto con end_time è un lavoro finito. */
const NOT_FINISHED = [1, 6, 2, 7];
const ST_PROGRAMMATO = 6;
const ST_PRODUZIONE = 2;
const ST_PAUSA = 7;
const SLIDES = [
'linee' => 'Linee adesso',
'oggi' => 'Oggi e settimana',
'andamento' => 'Andamento 14 giorni',
'fermi' => 'Fermi della settimana',
'coda' => 'Lavoro in arrivo',
'sicurezza' => 'Giorni senza infortuni',
'obiettivi' => 'Obiettivi',
'comunicazioni' => 'Comunicazioni',
'benvenuti' => 'Benvenuti e ringraziamenti',
'turni' => 'Turni e calendario',
'menu' => 'Menu',
];
const ITEM_TYPES = [
'comunicazione' => 'Comunicazione',
'benvenuto' => 'Benvenuto / grazie',
'turni' => 'Turni e calendario',
'menu' => 'Menu',
'ticker' => 'Avviso scorrevole',
];
/** Obiettivi: calcolati dai dati (sul periodo dell'obiettivo) o inseriti a mano. */
const GOAL_SOURCES = [
'manuale' => ['Valore inserito a mano', '', false],
'metri_mese' => ['Metri prodotti', 'm', false],
'kg_mese' => ['Kg prodotti', 'kg', false],
'lavori_mese' => ['Lavori completati', '', false],
'scarto_mese' => ['Scarto %', '%', true],
'fermi_mese' => ['Ore di fermo', 'h', true],
];
const DEFAULTS = [
'company' => 'Zibo',
'slide_seconds' => '15',
'slides' => 'linee,oggi,andamento,fermi,coda,sicurezza,obiettivi,comunicazioni,benvenuti,turni,menu',
'scarto_target' => '3',
'last_incident' => '',
'record_days' => '',
'safety_note' => '',
'city' => '',
'lat' => '',
'lon' => '',
'token' => '',
];
/* ===================== IMPOSTAZIONI ===================== */
public static function settings(PDO $pdo)
{
$s = self::DEFAULTS;
foreach ($pdo->query("SELECT setting_key, setting_value FROM kiosk_settings") as $r) {
$s[$r['setting_key']] = (string)$r['setting_value'];
}
return $s;
}
public static function set(PDO $pdo, $key, $value)
{
$pdo->prepare("INSERT INTO kiosk_settings (setting_key, setting_value, updated_at) VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = NOW()")
->execute([$key, (string)$value]);
}
/** Codice segreto del link dello schermo (creato al primo uso). */
public static function token(PDO $pdo, $renew = false)
{
$t = self::settings($pdo)['token'];
if ($t === '' || $renew) {
$t = bin2hex(random_bytes(12));
self::set($pdo, 'token', $t);
}
return $t;
}
/* ===================== DATE ===================== */
private static function tz()
{
return new DateTimeZone(self::TZ);
}
/** Mezzanotte locale di oggi + $days giorni. */
public static function day($days = 0)
{
$d = new DateTime('today', self::tz());
if ($days) $d->modify(($days > 0 ? '+' : '') . $days . ' day');
return $d;
}
public static function monday($weeks = 0)
{
$d = new DateTime('monday this week', self::tz());
if ($weeks) $d->modify(($weeks > 0 ? '+' : '') . $weeks . ' week');
return $d;
}
/** DateTime locale → stringa UTC per confrontare con start_time / end_time. */
public static function utc(DateTime $d)
{
$x = clone $d;
$x->setTimezone(new DateTimeZone('UTC'));
return $x->format('Y-m-d H:i:s');
}
public static function ms($utc)
{
return $utc ? (new DateTime($utc, new DateTimeZone('UTC')))->getTimestamp() * 1000 : null;
}
/* ===================== PRODUZIONE ===================== */
/** Lavori conclusi tra due istanti locali. */
public static function produced(PDO $pdo, DateTime $from, DateTime $to)
{
$st = $pdo->prepare("SELECT COUNT(*) n, COALESCE(SUM(mtprod), 0) mt, COALESCE(SUM(kgprod), 0) kg, COALESCE(SUM(scarto), 0) sc
FROM productiondata
WHERE end_time >= ? AND end_time < ? AND id_status NOT IN (" . implode(',', self::NOT_FINISHED) . ")");
$st->execute([self::utc($from), self::utc($to)]);
$r = $st->fetch(PDO::FETCH_ASSOC);
$kg = (float)$r['kg'];
$sc = (float)$r['sc'];
return [
'n' => (int)$r['n'],
'mt' => round((float)$r['mt']),
'kg' => round($kg),
'scarto' => ($kg + $sc) > 0 ? round($sc / ($kg + $sc) * 100, 1) : null,
];
}
/** Ore di fermo (pause) tra due istanti locali, per motivo. */
public static function stops(PDO $pdo, DateTime $from, DateTime $to)
{
$f = self::utc($from);
$t = self::utc($to);
$st = $pdo->prepare("
SELECT COALESCE(pr.name, 'Motivo non indicato') AS name, COALESCE(MAX(pr.is_problem), 0) AS problem,
SUM(GREATEST(0, TIMESTAMPDIFF(SECOND, GREATEST(pp.start_pause, ?), LEAST(COALESCE(pp.end_pause, UTC_TIMESTAMP()), ?)))) AS sec
FROM production_pauses pp
LEFT JOIN pause_reasons pr ON pr.id = pp.reason_id
WHERE pp.start_pause < ? AND COALESCE(pp.end_pause, UTC_TIMESTAMP()) > ?
GROUP BY COALESCE(pr.name, 'Motivo non indicato')
ORDER BY sec DESC");
$st->execute([$f, $t, $t, $f]);
$items = [];
$tot = 0;
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$h = (int)$r['sec'] / 3600;
if ($h <= 0) continue;
$tot += $h;
$items[] = ['name' => $r['name'], 'h' => round($h, 1), 'problem' => (int)$r['problem'] === 1];
}
return ['total_h' => round($tot, 1), 'items' => $items];
}
private static function lines(PDO $pdo)
{
$lines = [];
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' => !empty($l['color']) ? $l['color'] : '#64748b',
'job' => null,
'queue_n' => 0,
'queue_h' => 0,
'next' => null,
];
}
// in macchina
$st = $pdo->query("
SELECT p.id, p.id_linea, p.id_status, p.start_time, p.ore_previste, m.nome AS matrice,
(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
FROM productiondata p LEFT JOIN matrice m ON m.id = p.idmatrice
WHERE p.id_status IN (" . self::ST_PRODUZIONE . "," . self::ST_PAUSA . ")
ORDER BY p.start_time");
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $j) {
$lid = (int)$j['id_linea'];
if (!isset($lines[$lid]) || $lines[$lid]['job']) continue;
$lines[$lid]['job'] = [
'matrice' => $j['matrice'],
'mescole' => (string)$j['mescole'],
'paused' => (int)$j['id_status'] === self::ST_PAUSA,
'start_ms' => self::ms($j['start_time']),
'pause_sec' => (int)$j['pause_sec'],
'open_pause_ms' => self::ms($j['open_pause']),
'ore' => (float)$j['ore_previste'],
];
}
// coda
foreach (
$pdo->query("SELECT p.id_linea, m.nome, p.ore_previste FROM productiondata p LEFT JOIN matrice m ON m.id = p.idmatrice
WHERE p.id_status = " . self::ST_PROGRAMMATO . " 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_n']++;
$lines[$lid]['queue_h'] += (float)$r['ore_previste'];
if ($lines[$lid]['next'] === null) $lines[$lid]['next'] = $r['nome'];
}
foreach ($lines as &$l) $l['queue_h'] = round($l['queue_h'], 1);
unset($l);
return array_values($lines);
}
/** Metri prodotti negli ultimi $days giorni, per giorno e linea. */
private static function trend(PDO $pdo, array $lines, $days = 14)
{
$from = self::day(- ($days - 1));
$labels = [];
$idx = [];
$GG = ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'];
for ($i = 0; $i < $days; $i++) {
$d = (clone $from)->modify("+$i day");
$idx[$d->format('Y-m-d')] = $i;
$labels[] = $GG[(int)$d->format('w')] . ' ' . $d->format('d/m');
}
$series = [];
foreach ($lines as $l) $series[$l['id']] = ['name' => $l['name'], 'color' => $l['color'], 'data' => array_fill(0, $days, 0)];
$st = $pdo->prepare("SELECT id_linea, end_time, mtprod FROM productiondata
WHERE end_time >= ? AND id_status NOT IN (" . implode(',', self::NOT_FINISHED) . ")");
$st->execute([self::utc($from)]);
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
$d = (new DateTime($r['end_time'], new DateTimeZone('UTC')))->setTimezone(self::tz())->format('Y-m-d');
$lid = (int)$r['id_linea'];
if (isset($idx[$d], $series[$lid])) $series[$lid]['data'][$idx[$d]] += round((float)$r['mtprod']);
}
return ['labels' => $labels, 'series' => array_values($series)];
}
/** Contenuti validi oggi, raggruppati per tipo. */
public static function items(PDO $pdo, $onlyValid = true)
{
$sql = "SELECT * FROM kiosk_items" . ($onlyValid ? " WHERE active = 1 AND (valid_from IS NULL OR valid_from <= :d) AND (valid_to IS NULL OR valid_to >= :d2)" : "") . " ORDER BY sort, id DESC";
$st = $pdo->prepare($sql);
$today = self::day()->format('Y-m-d');
$st->execute($onlyValid ? ['d' => $today, 'd2' => $today] : []);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
public static function goals(PDO $pdo, $onlyActive = true)
{
$rows = $pdo->query("SELECT * FROM kiosk_goals" . ($onlyActive ? " WHERE active = 1" : "") . " ORDER BY sort, id")->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$g) {
$from = $g['period_from'] ? new DateTime($g['period_from'], self::tz()) : new DateTime('first day of this month 00:00', self::tz());
$to = $g['period_to'] ? (new DateTime($g['period_to'], self::tz()))->modify('+1 day') : (new DateTime('first day of next month 00:00', self::tz()));
$now = new DateTime('now', self::tz());
$end = $to < $now ? $to : $now;
$cur = null;
switch ($g['source']) {
case 'metri_mese':
$cur = self::produced($pdo, $from, $end)['mt'];
break;
case 'kg_mese':
$cur = self::produced($pdo, $from, $end)['kg'];
break;
case 'lavori_mese':
$cur = self::produced($pdo, $from, $end)['n'];
break;
case 'scarto_mese':
$cur = self::produced($pdo, $from, $end)['scarto'];
break;
case 'fermi_mese':
$cur = self::stops($pdo, $from, $end)['total_h'];
break;
default:
$cur = $g['current_value'] !== null ? (float)$g['current_value'] : null;
}
$target = (float)$g['target'];
$lower = (int)$g['lower_is_better'] === 1;
$pct = null;
if ($cur !== null && $target > 0) {
$pct = $lower ? ($cur <= $target ? 100 : max(0, round($target / max($cur, 0.0001) * 100))) : min(100, round($cur / $target * 100));
}
$g['current'] = $cur;
$g['pct'] = $pct;
$g['ok'] = $cur !== null && ($lower ? $cur <= $target : $cur >= $target);
$g['period_label'] = $from->format('d/m') . ' – ' . (clone $to)->modify('-1 day')->format('d/m');
}
unset($g);
return $rows;
}
public static function safety(array $s)
{
if ($s['last_incident'] === '') return null;
$last = new DateTime($s['last_incident'], self::tz());
$days = (int)$last->diff(self::day())->format('%r%a');
$record = $s['record_days'] !== '' ? (int)$s['record_days'] : null;
return [
'days' => max(0, $days),
'record' => $record !== null ? max($record, $days) : null,
'since' => $last->format('d/m/Y'),
'note' => $s['safety_note'],
];
}
/* ===================== TUTTO PER LO SCHERMO ===================== */
public static function data(PDO $pdo)
{
$s = self::settings($pdo);
$now = new DateTime('now', self::tz());
$lines = self::lines($pdo);
// oggi a quest'ora contro ieri alla stessa ora; settimana in corso contro la scorsa allo stesso punto
$today = self::day();
$yest = self::day(-1);
$yestSame = (clone $yest)->setTime((int)$now->format('H'), (int)$now->format('i'), (int)$now->format('s'));
$mon = self::monday();
$monPrev = self::monday(-1);
$samePrev = (clone $monPrev)->modify('+' . ($now->getTimestamp() - $mon->getTimestamp()) . ' seconds');
$items = ['comunicazione' => [], 'benvenuto' => [], 'turni' => [], 'menu' => [], 'ticker' => []];
foreach (self::items($pdo) as $it) {
if (!isset($items[$it['item_type']])) continue;
$items[$it['item_type']][] = [
'id' => (int)$it['id'],
'title' => $it['title'],
'body' => (string)$it['body'],
'image' => $it['image'],
];
}
$goals = array_map(function ($g) {
return [
'title' => $g['title'],
'unit' => $g['unit'],
'target' => (float)$g['target'],
'current' => $g['current'],
'pct' => $g['pct'],
'ok' => $g['ok'],
'lower' => (int)$g['lower_is_better'] === 1,
'period' => $g['period_label'],
];
}, self::goals($pdo));
return [
'success' => true,
'now_ms' => time() * 1000,
'company' => $s['company'],
'settings' => [
'slide_seconds' => max(6, min(120, (int)$s['slide_seconds'] ?: 15)),
'slides' => array_values(array_filter(explode(',', $s['slides']))),
'scarto_target' => (float)str_replace(',', '.', $s['scarto_target']),
'city' => $s['city'],
'lat' => $s['lat'],
'lon' => $s['lon'],
],
'lines' => $lines,
'today' => ['cur' => self::produced($pdo, $today, $now), 'prev' => self::produced($pdo, $yest, $yestSame)],
'week' => ['cur' => self::produced($pdo, $mon, $now), 'prev' => self::produced($pdo, $monPrev, $samePrev)],
'trend' => self::trend($pdo, $lines),
'stops' => self::stops($pdo, $mon, $now),
'safety' => self::safety($s),
'goals' => $goals,
'items' => $items,
];
}
}
+17 -2
View File
@@ -447,6 +447,18 @@ class MagoProduction
return (int)$pdo->lastInsertId();
}
/**
* Prossima posizione libera (in fondo) per uno stato della programmazione.
* La pagina numera le posizioni di tutte le linee in un'unica sequenza per stato,
* quindi MAX + 1 mette il lavoro in fondo anche alla sua linea.
*/
public static function nextPriority(PDO $pdo, $status)
{
$st = $pdo->prepare("SELECT COALESCE(MAX(priority), 0) + 1 FROM productiondata WHERE id_status = ?");
$st->execute([(int)$status]);
return (int)$st->fetchColumn();
}
/**
* Crea il lavoro in productiondata e collega la riga della coda.
* $d: idmatrice, id_linea, mescole[], metri, kg_sp, kg_p, ore_previste, data_zibo, data_cliente,
@@ -475,16 +487,19 @@ class MagoProduction
return MagoSalesStats::isValidDate((string)$v) ? $v : null;
};
$priority = self::nextPriority($pdo, $status);
$pdo->prepare("
INSERT INTO productiondata
(Data, data_produzione, conferma_ordine, idmatrice, id_linea, id_cliente, data_zibo, data_cliente,
metri, kg_sp, kg_p, ore_previste, note_extra, id_status,
metri, kg_sp, kg_p, ore_previste, note_extra, id_status, priority,
fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto)
VALUES
(CURDATE(), NULL, :conferma, :idmatrice, :id_linea, :id_cliente, :data_zibo, :data_cliente,
:metri, :kg_sp, :kg_p, :ore, :note, :status,
:metri, :kg_sp, :kg_p, :ore, :note, :status, :priority,
0, '00:00:00', '00:00:00', 0, 0, 0)
")->execute([
'priority' => $priority,
'conferma' => mb_substr($q['internal_ord_no'], 0, 50),
'idmatrice' => $idm,
'id_linea' => $lin,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff
+78
View File
@@ -1426,6 +1426,62 @@ if (!empty($_GET['ajax'])) {
color: #fff;
opacity: .95;
}
.descr-btn {
display: flex;
align-items: center;
gap: .5rem;
width: 100%;
min-height: 44px;
padding: .55rem .8rem;
border: 1px solid #cbd5e1;
border-radius: .7rem;
background: #f8fafc;
color: #334155;
font-size: .95rem;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.descr-btn span {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #334155;
font-weight: 600;
}
.descr-cell {
grid-column: span 3;
min-width: 0;
}
@media (max-width: 700px) {
.descr-cell {
grid-column: 1 / -1;
}
}
.descr-btn .bi-card-text {
color: #ea580c;
font-size: 1.2rem;
}
#descrText {
white-space: pre-wrap;
text-align: left;
font-size: 1.15rem;
line-height: 1.5;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: .8rem;
padding: 1rem 1.2rem;
max-height: 60vh;
overflow-y: auto;
}
</style>
</head>
@@ -2824,6 +2880,17 @@ if (!empty($_GET['ajax'])) {
$(document).on("click", "#closeMescole", function() {
$("#mescoleModal").removeClass("active");
});
// --- MODALE DESCRIZIONE ---
$(document).on("click", ".descr-btn", function(e) {
e.stopPropagation();
$("#descrTitle").text($(this).attr("data-title") || "Descrizione");
$("#descrText").text($(this).attr("data-descr") || "");
$("#descrModal").addClass("active");
});
$(document).on("click", "#closeDescr, #descrModal", function(e) {
if (e.target === this) $("#descrModal").removeClass("active");
});
</script>
<!-- MODALE PER PREVIEW IMMAGINE -->
@@ -2864,6 +2931,17 @@ if (!empty($_GET['ajax'])) {
</div>
<!-- MODALE DESCRIZIONE -->
<div id="descrModal" class="custom-modal">
<div class="modal-content" style="max-width:760px; width:94%;">
<h3 id="descrTitle">Descrizione</h3>
<div id="descrText"></div>
<div class="modal-buttons">
<button class="modal-btn modal-cancel" id="closeDescr">Chiudi</button>
</div>
</div>
</div>
<!-- MODALE MESCOLE -->
<div id="mescoleModal" class="custom-modal">
<div class="modal-content" style="max-width:400px;">
File diff suppressed because it is too large Load Diff
@@ -394,6 +394,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
<a href="production_mago_queue.php" class="btn btn-warning fw-bold">
🆕 Nuovi da Mago <?php if ($nuoviMago): ?><span class="badge bg-dark ms-1"><?= $nuoviMago ?></span><?php endif; ?>
</a>
<a href="production_board.php" class="btn btn-outline-primary fw-bold">
<i class="bi bi-kanban me-1"></i> Vista tabellone
</a>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNuovo">Aggiungi</button>
<button class="btn btn-secondary" onclick="location.href='production_dashboard.php'">Torna</button>
</div>
+12 -5
View File
@@ -81,10 +81,17 @@ if ($matricePhoto) {
</span>
</div>
<?php if (!empty($r['note_extra'])): ?>
<div>
<?php $descr = trim((string)($r['note_extra'] ?? '')); ?>
<?php if ($descr !== ''): ?>
<div class="descr-cell">
<strong>Descrizione</strong>
<span data-field="note_extra"><?= htmlspecialchars($r['note_extra']) ?></span>
<button type="button" class="descr-btn"
data-title="<?= htmlspecialchars($r['matrice'] ?? 'Descrizione', ENT_QUOTES) ?>"
data-descr="<?= htmlspecialchars($descr, ENT_QUOTES) ?>">
<i class="bi bi-card-text"></i>
<span><?= htmlspecialchars(strtok($descr, "\r\n")) ?></span>
<i class="bi bi-chevron-right" style="color:#94a3b8;font-size:1rem;"></i>
</button>
</div>
<?php endif; ?>
@@ -161,8 +168,8 @@ if ($matricePhoto) {
<?php if ($isInProduction || $isPaused): ?>
<div class="timer-display"
data-start="<?= $r['start_time'] ? strtotime($r['start_time']) : '' ?>"
data-end="<?= $r['end_time'] ? strtotime($r['end_time']) : '' ?>">
data-start="<?= $r['start_time'] ? strtotime($r['start_time'] . ' UTC') : '' ?>"
data-end="<?= $r['end_time'] ? strtotime($r['end_time'] . ' UTC') : '' ?>">
<span class="timer">00:00:00</span>
</div>