kiosk
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -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;">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||