diff --git a/db/migrations/20261020150202_create_kiosk.php b/db/migrations/20261020150202_create_kiosk.php new file mode 100644 index 00000000..66ef89c2 --- /dev/null +++ b/db/migrations/20261020150202_create_kiosk.php @@ -0,0 +1,53 @@ +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(); + } +} diff --git a/public/userarea/class/Kiosk.php b/public/userarea/class/Kiosk.php new file mode 100644 index 00000000..ba8cb431 --- /dev/null +++ b/public/userarea/class/Kiosk.php @@ -0,0 +1,384 @@ + '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, + ]; + } +} diff --git a/public/userarea/class/MagoProduction.php b/public/userarea/class/MagoProduction.php index 42b937d0..a837d93e 100644 --- a/public/userarea/class/MagoProduction.php +++ b/public/userarea/class/MagoProduction.php @@ -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, diff --git a/public/userarea/kiosk.php b/public/userarea/kiosk.php new file mode 100644 index 00000000..6ecfd866 --- /dev/null +++ b/public/userarea/kiosk.php @@ -0,0 +1,1486 @@ +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 '

Schermo non autorizzato

Apri il link completo dalla pagina di gestione dello schermo.

'; + 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; +} +?> + + + + + + + + Bacheca di reparto + + + + + + +
+
+
+ +
Bacheca di reparto
+
+
+
+
+
--:--
+
+
+
+
+
+ +
+
dati non aggiornati
+ + + + + \ No newline at end of file diff --git a/public/userarea/kiosk_admin.php b/public/userarea/kiosk_admin.php new file mode 100644 index 00000000..7908df29 --- /dev/null +++ b/public/userarea/kiosk_admin.php @@ -0,0 +1,1078 @@ +getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +/** Permesso richiesto: cambialo se crei un permesso dedicato (es. 'kiosk.manage'). */ +const KIOSK_PERM = 'production.programming.view'; +const KIOSK_IMG_DIR = __DIR__ . '/photos/kiosk/'; + +function kOut(array $a) +{ + while (ob_get_level()) ob_end_clean(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($a); + exit; +} + +if (!userCan(KIOSK_PERM)) { + if (isset($_GET['action']) || isset($_POST['action'])) kOut(['success' => false, 'message' => 'Non hai i permessi per questa pagina']); + while (ob_get_level()) ob_end_clean(); + http_response_code(403); + echo '

Accesso non consentito

Torna alla Dashboard
'; + exit; +} + +$userName = ''; +if (!empty($iduserlogin)) { + try { + $st = $pdo->prepare("SELECT COALESCE(NULLIF(TRIM(CONCAT_WS(' ', first_name, last_name)), ''), username, email) FROM auth_users WHERE id = ?"); + $st->execute([(int)$iduserlogin]); + $userName = (string)$st->fetchColumn(); + } catch (Throwable $e) { + } +} + +function kDate($v) +{ + $v = trim((string)$v); + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null; +} + +/** Salva l'immagine caricata (ridotta a 1920 px se serve) e restituisce il nome del file. */ +function kSaveImage(array $f) +{ + if (($f['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return null; + if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException('Immagine non caricata (errore ' . $f['error'] . ')'); + if ($f['size'] > 15 * 1024 * 1024) throw new RuntimeException('Immagine troppo grande (massimo 15 MB)'); + $info = @getimagesize($f['tmp_name']); + $ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$info['mime'] ?? ''] ?? null; + if (!$ext) throw new RuntimeException('Formato non supportato: usa JPG, PNG o WEBP'); + if (!is_dir(KIOSK_IMG_DIR) && !mkdir(KIOSK_IMG_DIR, 0775, true)) throw new RuntimeException('Impossibile creare la cartella photos/kiosk'); + $name = date('Ymd_His') . '_' . bin2hex(random_bytes(4)); + + // riduco le foto grandi (dal telefono) a 1920 px: lo schermo non ne ha bisogno di più + if (function_exists('imagecreatefromstring') && max($info[0], $info[1]) > 1920 && $ext !== 'gif') { + $src = @imagecreatefromstring(file_get_contents($f['tmp_name'])); + if ($src) { + $r = 1920 / max($info[0], $info[1]); + $w = (int)round($info[0] * $r); + $h = (int)round($info[1] * $r); + $dst = imagecreatetruecolor($w, $h); + imagecopyresampled($dst, $src, 0, 0, 0, 0, $w, $h, $info[0], $info[1]); + imagejpeg($dst, KIOSK_IMG_DIR . $name . '.jpg', 82); + imagedestroy($src); + imagedestroy($dst); + return $name . '.jpg'; + } + } + if (!move_uploaded_file($f['tmp_name'], KIOSK_IMG_DIR . $name . '.' . $ext)) throw new RuntimeException('Impossibile salvare l\'immagine'); + return $name . '.' . $ext; +} + +function kDeleteImage($name) +{ + $name = basename((string)$name); + if ($name !== '' && is_file(KIOSK_IMG_DIR . $name)) @unlink(KIOSK_IMG_DIR . $name); +} + +function kioskUrl(PDO $pdo) +{ + $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; + $dir = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])), '/'); + return ($https ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $dir . '/kiosk.php?k=' . Kiosk::token($pdo); +} + +/* ===================== AJAX ===================== */ +$action = $_GET['action'] ?? $_POST['action'] ?? ''; +if ($action !== '') { + try { + if ($action === 'load') { + $s = Kiosk::settings($pdo); + unset($s['token']); + $goals = Kiosk::goals($pdo, false); + kOut([ + 'success' => true, + 'url' => kioskUrl($pdo), + 'settings' => $s, + 'safety' => Kiosk::safety(Kiosk::settings($pdo)), + 'items' => Kiosk::items($pdo, false), + 'goals' => $goals, + 'slides' => Kiosk::SLIDES, + 'types' => Kiosk::ITEM_TYPES, + 'sources' => Kiosk::GOAL_SOURCES, + 'today' => Kiosk::day()->format('Y-m-d'), + ]); + } + + if ($action === 'save_settings') { + $slides = array_values(array_intersect(array_keys(Kiosk::SLIDES), (array)($_POST['slides'] ?? []))); + $vals = [ + 'company' => mb_substr(trim((string)($_POST['company'] ?? '')), 0, 60), + 'slide_seconds' => (string)max(6, min(120, (int)($_POST['slide_seconds'] ?? 15))), + 'slides' => implode(',', $slides), + 'scarto_target' => (string)max(0, (float)str_replace(',', '.', (string)($_POST['scarto_target'] ?? 3))), + 'city' => mb_substr(trim((string)($_POST['city'] ?? '')), 0, 60), + 'lat' => is_numeric($_POST['lat'] ?? '') ? (string)(float)$_POST['lat'] : '', + 'lon' => is_numeric($_POST['lon'] ?? '') ? (string)(float)$_POST['lon'] : '', + 'last_incident' => (string)kDate($_POST['last_incident'] ?? ''), + 'record_days' => ($_POST['record_days'] ?? '') === '' ? '' : (string)max(0, (int)$_POST['record_days']), + 'safety_note' => mb_substr(trim((string)($_POST['safety_note'] ?? '')), 0, 250), + ]; + foreach ($vals as $k => $v) Kiosk::set($pdo, $k, $v); + kOut(['success' => true]); + } + + if ($action === 'new_token') { + Kiosk::token($pdo, true); + kOut(['success' => true, 'url' => kioskUrl($pdo)]); + } + + if ($action === 'item_save') { + $id = (int)($_POST['id'] ?? 0); + $type = (string)($_POST['item_type'] ?? ''); + if (!isset(Kiosk::ITEM_TYPES[$type])) throw new RuntimeException('Scegli il tipo di contenuto'); + $title = mb_substr(trim((string)($_POST['title'] ?? '')), 0, 150); + if ($title === '') throw new RuntimeException('Scrivi il titolo'); + $from = kDate($_POST['valid_from'] ?? ''); + $to = kDate($_POST['valid_to'] ?? ''); + if ($from && $to && $to < $from) throw new RuntimeException('La data di fine è prima di quella di inizio'); + + $old = ''; + if ($id) { + $st = $pdo->prepare("SELECT image FROM kiosk_items WHERE id = ?"); + $st->execute([$id]); + $old = $st->fetchColumn(); + if ($old === false) throw new RuntimeException('Contenuto non trovato'); + } + $img = $old; + $new = kSaveImage($_FILES['image'] ?? ['error' => UPLOAD_ERR_NO_FILE]); + if ($new !== null || !empty($_POST['remove_image'])) { + kDeleteImage($old); + $img = $new ?? ''; + } + $f = [ + 'item_type' => $type, + 'title' => $title, + 'body' => trim((string)($_POST['body'] ?? '')), + 'image' => $img, + 'valid_from' => $from, + 'valid_to' => $to, + 'sort' => (int)($_POST['sort'] ?? 0), + 'active' => !empty($_POST['active']) ? 1 : 0, + ]; + if ($id) { + $f['id'] = $id; + $pdo->prepare("UPDATE kiosk_items SET item_type = :item_type, title = :title, body = :body, image = :image, valid_from = :valid_from, + valid_to = :valid_to, sort = :sort, active = :active, updated_at = NOW() WHERE id = :id")->execute($f); + } else { + $f['by'] = mb_substr($userName, 0, 100); + $pdo->prepare("INSERT INTO kiosk_items (item_type, title, body, image, valid_from, valid_to, sort, active, created_by, created_at, updated_at) + VALUES (:item_type, :title, :body, :image, :valid_from, :valid_to, :sort, :active, :by, NOW(), NOW())")->execute($f); + } + kOut(['success' => true]); + } + + if ($action === 'item_toggle') { + $pdo->prepare("UPDATE kiosk_items SET active = 1 - active, updated_at = NOW() WHERE id = ?")->execute([(int)($_POST['id'] ?? 0)]); + kOut(['success' => true]); + } + + if ($action === 'item_delete') { + $id = (int)($_POST['id'] ?? 0); + $st = $pdo->prepare("SELECT image FROM kiosk_items WHERE id = ?"); + $st->execute([$id]); + kDeleteImage($st->fetchColumn()); + $pdo->prepare("DELETE FROM kiosk_items WHERE id = ?")->execute([$id]); + kOut(['success' => true]); + } + + if ($action === 'goal_save') { + $id = (int)($_POST['id'] ?? 0); + $src = (string)($_POST['source'] ?? 'manuale'); + if (!isset(Kiosk::GOAL_SOURCES[$src])) $src = 'manuale'; + $title = mb_substr(trim((string)($_POST['title'] ?? '')), 0, 150); + if ($title === '') throw new RuntimeException('Scrivi il nome dell\'obiettivo'); + $target = (float)str_replace(',', '.', (string)($_POST['target'] ?? 0)); + if ($target <= 0) throw new RuntimeException('Indica il valore da raggiungere'); + $from = kDate($_POST['period_from'] ?? ''); + $to = kDate($_POST['period_to'] ?? ''); + if ($from && $to && $to < $from) throw new RuntimeException('La data di fine è prima di quella di inizio'); + $unit = mb_substr(trim((string)($_POST['unit'] ?? '')), 0, 20); + if ($src !== 'manuale') $unit = Kiosk::GOAL_SOURCES[$src][1]; + $cur = ($_POST['current_value'] ?? '') === '' ? null : (float)str_replace(',', '.', (string)$_POST['current_value']); + $f = [ + 'title' => $title, + 'source' => $src, + 'unit' => $unit, + 'target' => $target, + 'cur' => $src === 'manuale' ? $cur : null, + 'lower' => $src === 'manuale' ? (!empty($_POST['lower_is_better']) ? 1 : 0) : (Kiosk::GOAL_SOURCES[$src][2] ? 1 : 0), + 'pf' => $from, + 'pt' => $to, + 'sort' => (int)($_POST['sort'] ?? 0), + 'active' => !empty($_POST['active']) ? 1 : 0, + ]; + if ($id) { + $f['id'] = $id; + $pdo->prepare("UPDATE kiosk_goals SET title = :title, source = :source, unit = :unit, target = :target, current_value = :cur, + lower_is_better = :lower, period_from = :pf, period_to = :pt, sort = :sort, active = :active, updated_at = NOW() WHERE id = :id")->execute($f); + } else { + $pdo->prepare("INSERT INTO kiosk_goals (title, source, unit, target, current_value, lower_is_better, period_from, period_to, sort, active, updated_at) + VALUES (:title, :source, :unit, :target, :cur, :lower, :pf, :pt, :sort, :active, NOW())")->execute($f); + } + kOut(['success' => true]); + } + + if ($action === 'goal_delete') { + $pdo->prepare("DELETE FROM kiosk_goals WHERE id = ?")->execute([(int)($_POST['id'] ?? 0)]); + kOut(['success' => true]); + } + + kOut(['success' => false, 'message' => 'Azione sconosciuta']); + } catch (Throwable $e) { + kOut(['success' => false, 'message' => $e->getMessage()]); + } +} +?> + + + + + + + + + Schermo di reparto - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + +
+ + + +
+
+
+
+
+
+
Schermo di reparto
+ Cosa vedono i dipendenti in sala pausa: contenuti, obiettivi e sicurezza +
+
+
+ + Apri lo schermo +
+
+ +
+
+
+
Contenuti + +
+
+
+
+ +
+
Obiettivi + +
+
+
+
+ +
+
+
Anteprima dal vivo
+
+
Apri il link sul PC o sul TV della sala pausa e mettilo a schermo intero (F11). Nessun login: chi ha il link vede lo schermo.
+
+ + +
+
+ +
+
Sicurezza
+
+
—
+
giorni senza infortuni
+
+
+
+
+
+
+ +
+ +
+
Impostazioni
+
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/photos/13-19-1792504568.jpeg b/public/userarea/photos/13-19-1792504568.jpeg new file mode 100644 index 00000000..a71d65ce Binary files /dev/null and b/public/userarea/photos/13-19-1792504568.jpeg differ diff --git a/public/userarea/photos/13-20-1792504579.jpeg b/public/userarea/photos/13-20-1792504579.jpeg new file mode 100644 index 00000000..3f5b74d2 Binary files /dev/null and b/public/userarea/photos/13-20-1792504579.jpeg differ diff --git a/public/userarea/photos/25-23-1792505763.jpeg b/public/userarea/photos/25-23-1792505763.jpeg new file mode 100644 index 00000000..a71d65ce Binary files /dev/null and b/public/userarea/photos/25-23-1792505763.jpeg differ diff --git a/public/userarea/photos/25-24-1792505777.jpg b/public/userarea/photos/25-24-1792505777.jpg new file mode 100644 index 00000000..5a6d80f7 Binary files /dev/null and b/public/userarea/photos/25-24-1792505777.jpg differ diff --git a/public/userarea/photos/25-25-1792505798.jpeg b/public/userarea/photos/25-25-1792505798.jpeg new file mode 100644 index 00000000..3f5b74d2 Binary files /dev/null and b/public/userarea/photos/25-25-1792505798.jpeg differ diff --git a/public/userarea/photos/26-21-1792504777.jpeg b/public/userarea/photos/26-21-1792504777.jpeg new file mode 100644 index 00000000..a71d65ce Binary files /dev/null and b/public/userarea/photos/26-21-1792504777.jpeg differ diff --git a/public/userarea/photos/26-22-1792504790.jpeg b/public/userarea/photos/26-22-1792504790.jpeg new file mode 100644 index 00000000..3f5b74d2 Binary files /dev/null and b/public/userarea/photos/26-22-1792504790.jpeg differ diff --git a/public/userarea/photos/26-26-1792505854.jpeg b/public/userarea/photos/26-26-1792505854.jpeg new file mode 100644 index 00000000..3f5b74d2 Binary files /dev/null and b/public/userarea/photos/26-26-1792505854.jpeg differ diff --git a/public/userarea/photos/kiosk/20261020_150706_5b7c1814.jpg b/public/userarea/photos/kiosk/20261020_150706_5b7c1814.jpg new file mode 100644 index 00000000..f57a4fdc Binary files /dev/null and b/public/userarea/photos/kiosk/20261020_150706_5b7c1814.jpg differ diff --git a/public/userarea/photos/kiosk/20261021_071545_cadf2806.png b/public/userarea/photos/kiosk/20261021_071545_cadf2806.png new file mode 100644 index 00000000..c006c132 Binary files /dev/null and b/public/userarea/photos/kiosk/20261021_071545_cadf2806.png differ diff --git a/public/userarea/production_board.php b/public/userarea/production_board.php new file mode 100644 index 00000000..67ce2e0b --- /dev/null +++ b/public/userarea/production_board.php @@ -0,0 +1,1468 @@ +getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +/* ===================== IMPOSTAZIONI ===================== */ +const PB_STATUS_TODO = 1; // Da programmare +const PB_STATUS_PLAN = 6; // Programmato +const PB_STATUS_ACTIVE = [2, 7, 8]; // In produzione, Pausa, Problema: "in macchina" +const PB_SHIFT_START = 6; // ora di inizio turno per le linee che non lavorano 24 ore +const PB_WORK_DAYS = [1, 2, 3, 4, 5]; // giorni lavorativi per le stime (1 = lunedì … 7 = domenica) +const PB_DEFAULT_HOURS = 24; // ore al giorno se la linea non ha max_hours_per_day + +if (!userCan('production.programming.view')) { + if (isset($_GET['action']) || isset($_POST['action'])) { + MagoSalesStats::jsonOut(['success' => false, 'message' => 'Non hai i permessi per questa pagina']); + } + while (ob_get_level()) ob_end_clean(); + http_response_code(403); + echo '

Accesso non consentito

Non hai i permessi per visualizzare questa pagina.

Torna alla Dashboard
'; + exit; +} + +function pbTableExists(PDO $pdo, $t) +{ + try { + $pdo->query("SELECT 1 FROM `$t` LIMIT 0"); + return true; + } catch (Throwable $e) { + return false; + } +} + +function pbLines(PDO $pdo) +{ + $out = []; + foreach ($pdo->query("SELECT * FROM production_lines ORDER BY line_number, id")->fetchAll(PDO::FETCH_ASSOC) as $l) { + $out[] = [ + 'id' => (int)$l['id'], + 'name' => $l['name'], + 'color' => !empty($l['color']) ? $l['color'] : '#64748b', + 'hours' => !empty($l['max_hours_per_day']) && (float)$l['max_hours_per_day'] > 0 ? min(24, (float)$l['max_hours_per_day']) : PB_DEFAULT_HOURS, + 'active' => !isset($l['status']) || $l['status'] === '' || $l['status'] === 'active', + ]; + } + return $out; +} + +/* ===================== AJAX ===================== */ +$action = $_GET['action'] ?? $_POST['action'] ?? ''; +if ($action !== '') { + try { + // ---------- tabellone ---------- + if ($action === 'board') { + $hasQueue = pbTableExists($pdo, 'mago_prod_queue'); + $mago = $hasQueue + ? "LEFT JOIN (SELECT productiondata_id, MAX(mago_changed) AS mago_changed, MAX(mago_changes) AS mago_changes + FROM mago_prod_queue WHERE productiondata_id IS NOT NULL GROUP BY productiondata_id) q ON q.productiondata_id = p.id" + : ''; + $magoCols = $hasQueue ? 'q.productiondata_id IS NOT NULL AS from_mago, COALESCE(q.mago_changed, 0) AS mago_changed, q.mago_changes' : '0 AS from_mago, 0 AS mago_changed, NULL AS mago_changes'; + + $in = implode(',', array_merge([PB_STATUS_TODO, PB_STATUS_PLAN], PB_STATUS_ACTIVE)); + $jobs = $pdo->query(" + SELECT p.id, p.id_status, p.id_linea, p.priority, p.conferma_ordine, p.idmatrice, m.nome AS matrice, m.photo, + p.id_cliente, c.nome AS cliente, p.data_zibo, p.data_cliente, p.metri, p.kg_sp, p.kg_p, p.ore_previste, + p.note_extra, p.start_time, p.Data AS created, + s.nome AS status_nome, COALESCE(s.badge_color, '#6c757d') AS status_color, + (SELECT GROUP_CONCAT(ms.nome ORDER BY ms.nome SEPARATOR '||') FROM productiondata_mescole pm + JOIN mescole ms ON ms.id = pm.id_mescola WHERE pm.id_productiondata = p.id) AS mescole, + (SELECT GROUP_CONCAT(pm.id_mescola) FROM productiondata_mescole pm WHERE pm.id_productiondata = p.id) AS mescole_ids, + (SELECT GROUP_CONCAT(ml.idlinea) FROM matrici_lines ml WHERE ml.idmatrice = p.idmatrice) AS matrice_lines, + $magoCols + FROM productiondata p + LEFT JOIN matrice m ON m.id = p.idmatrice + LEFT JOIN clients c ON c.id = p.id_cliente + LEFT JOIN production_status s ON s.id = p.id_status + $mago + WHERE p.id_status IN ($in) + ORDER BY p.priority IS NULL, p.priority, p.id + ")->fetchAll(PDO::FETCH_ASSOC); + + foreach ($jobs as &$j) { + foreach (['id', 'id_status', 'id_linea', 'idmatrice', 'id_cliente', 'from_mago', 'mago_changed'] as $k) $j[$k] = $j[$k] === null ? null : (int)$j[$k]; + foreach (['metri', 'kg_sp', 'kg_p', 'ore_previste'] as $k) $j[$k] = (float)$j[$k]; + $j['mescole'] = $j['mescole'] ? explode('||', $j['mescole']) : []; + $j['mescole_ids'] = $j['mescole_ids'] ? array_map('intval', explode(',', $j['mescole_ids'])) : []; + $j['matrice_lines'] = $j['matrice_lines'] ? array_map('intval', explode(',', $j['matrice_lines'])) : []; + } + unset($j); + + $nuoviMago = $hasQueue ? (int)$pdo->query("SELECT COUNT(*) FROM mago_prod_queue WHERE queue_status = 'nuovo' OR (queue_status = 'importato' AND mago_changed = 1)")->fetchColumn() : 0; + + MagoSalesStats::jsonOut([ + 'success' => true, + 'lines' => pbLines($pdo), + 'jobs' => $jobs, + 'nuoviMago' => $nuoviMago, + 'now' => date('Y-m-d H:i:s'), + ]); + } + + // ---------- liste per il modulo ---------- + if ($action === 'options') { + MagoSalesStats::jsonOut([ + 'success' => true, + 'matrici' => $pdo->query("SELECT id, nome FROM matrice WHERE TRIM(nome) <> '' ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC), + 'mescole' => $pdo->query("SELECT id, nome FROM mescole WHERE is_active = 1 ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC), + 'clienti' => $pdo->query("SELECT id, nome FROM clients ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC), + ]); + } + + // ---------- dati tecnici della matrice (linee, mescole, velocità) ---------- + if ($action === 'matinfo') { + MagoSalesStats::jsonOut(['success' => true, 'info' => MagoProduction::matriceInfo($pdo, (int)($_GET['id'] ?? 0))]); + } + + // ---------- nuovo / modifica ---------- + if ($action === 'save') { + $id = (int)($_POST['id'] ?? 0); + $idm = (int)($_POST['idmatrice'] ?? 0); + $lin = (int)($_POST['id_linea'] ?? 0); + if (!$idm) throw new RuntimeException('Scegli la matrice'); + if (!$lin) throw new RuntimeException('Scegli la linea'); + $status = (int)($_POST['id_status'] ?? PB_STATUS_TODO); + if (!in_array($status, [PB_STATUS_TODO, PB_STATUS_PLAN], true)) $status = PB_STATUS_TODO; + $date = function ($k) { + $v = trim((string)($_POST[$k] ?? '')); + return MagoSalesStats::isValidDate($v) ? $v : null; + }; + $num = function ($k, $max = 99999999) { + return min($max, round((float)str_replace(',', '.', (string)($_POST[$k] ?? 0)), 2)); + }; + $f = [ + 'conferma_ordine' => mb_substr(trim((string)($_POST['conferma_ordine'] ?? '')), 0, 50) ?: null, + 'idmatrice' => $idm, + 'id_linea' => $lin, + 'id_cliente' => (int)($_POST['id_cliente'] ?? 0) ?: null, + 'data_zibo' => $date('data_zibo'), + 'data_cliente' => $date('data_cliente'), + 'metri' => $num('metri'), + 'kg_sp' => $num('kg_sp'), + 'kg_p' => $num('kg_p'), + 'ore_previste' => $num('ore_previste', 999.99), + 'note_extra' => trim((string)($_POST['note_extra'] ?? '')) ?: null, + ]; + + $pdo->beginTransaction(); + if ($id) { + $st = $pdo->prepare("SELECT id_status FROM productiondata WHERE id = ? FOR UPDATE"); + $st->execute([$id]); + $cur = $st->fetchColumn(); + if ($cur === false) throw new RuntimeException('Lavoro non trovato'); + $cur = (int)$cur; + $set = implode(', ', array_map(fn($k) => "$k = :$k", array_keys($f))); + // lo stato si cambia da qui solo tra Da programmare e Programmato; chi cambia stato va in fondo + if (in_array($cur, [PB_STATUS_TODO, PB_STATUS_PLAN], true) && $status !== $cur) { + $set .= ', id_status = :st, priority = :pr'; + $f['st'] = $status; + $f['pr'] = MagoProduction::nextPriority($pdo, $status); + } + $f['id'] = $id; + $pdo->prepare("UPDATE productiondata SET $set WHERE id = :id")->execute($f); + } else { + $f['id_status'] = $status; + $f['priority'] = MagoProduction::nextPriority($pdo, $status); + $cols = array_keys($f); + $pdo->prepare("INSERT INTO productiondata (Data, " . implode(', ', $cols) . ", fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto) + VALUES (CURDATE(), :" . implode(', :', $cols) . ", 0, '00:00:00', '00:00:00', 0, 0, 0)")->execute($f); + $id = (int)$pdo->lastInsertId(); + } + $pdo->prepare("DELETE FROM productiondata_mescole WHERE id_productiondata = ?")->execute([$id]); + $mescole = array_values(array_unique(array_filter(array_map('intval', (array)($_POST['mescole'] ?? []))))); + $ins = $pdo->prepare("INSERT INTO productiondata_mescole (id_productiondata, id_mescola) VALUES (?, ?)"); + foreach ($mescole as $m) $ins->execute([$id, $m]); + $pdo->commit(); + MagoSalesStats::jsonOut(['success' => true, 'id' => $id]); + } + + // ---------- ordine dopo un trascinamento ---------- + if ($action === 'order') { + $p = json_decode((string)($_POST['payload'] ?? ''), true); + if (!is_array($p)) throw new RuntimeException('Dati non validi'); + $upd = $pdo->prepare("UPDATE productiondata SET id_status = ?, id_linea = ?, priority = ? + WHERE id = ? AND id_status IN (" . PB_STATUS_TODO . "," . PB_STATUS_PLAN . ")"); + $updTodo = $pdo->prepare("UPDATE productiondata SET id_status = ?, priority = ? + WHERE id = ? AND id_status IN (" . PB_STATUS_TODO . "," . PB_STATUS_PLAN . ")"); + $pdo->beginTransaction(); + // i programmati sono numerati in un'unica sequenza, linea dopo linea (come la vista classica) + $n = 0; + foreach ((array)($p['lines'] ?? []) as $l) { + foreach ((array)($l['ids'] ?? []) as $id) $upd->execute([PB_STATUS_PLAN, (int)$l['line'], ++$n, (int)$id]); + } + $n = 0; + foreach ((array)($p['backlog'] ?? []) as $id) $updTodo->execute([PB_STATUS_TODO, ++$n, (int)$id]); + $pdo->commit(); + MagoSalesStats::jsonOut(['success' => true]); + } + + // ---------- elimina (solo lavori non ancora avviati) ---------- + if ($action === 'delete') { + $id = (int)($_POST['id'] ?? 0); + $pdo->beginTransaction(); + $st = $pdo->prepare("SELECT id_status FROM productiondata WHERE id = ? FOR UPDATE"); + $st->execute([$id]); + $cur = $st->fetchColumn(); + if ($cur === false) throw new RuntimeException('Lavoro non trovato'); + if (!in_array((int)$cur, [PB_STATUS_TODO, PB_STATUS_PLAN], true)) throw new RuntimeException('Si possono eliminare solo i lavori non ancora avviati'); + if (pbTableExists($pdo, 'mago_prod_queue')) { + // la riga Mago torna tra i "Nuovi da Mago" + $pdo->prepare("UPDATE mago_prod_queue SET queue_status = 'nuovo', productiondata_id = NULL, imported_at = NULL, + mago_changed = 0, mago_changes = '', updated_at = NOW() WHERE productiondata_id = ?")->execute([$id]); + } + $pdo->prepare("DELETE FROM productiondata_mescole WHERE id_productiondata = ?")->execute([$id]); + $pdo->prepare("DELETE FROM productiondata WHERE id = ?")->execute([$id]); + $pdo->commit(); + MagoSalesStats::jsonOut(['success' => true]); + } + + MagoSalesStats::jsonOut(['success' => false, 'message' => 'Azione sconosciuta']); + } catch (Throwable $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + $msg = $e->getMessage(); + if (strpos($msg, '1451') !== false) $msg = 'Il lavoro ha dati collegati (foto, registrazioni): eliminalo dalla vista classica'; + MagoSalesStats::jsonOut(['success' => false, 'message' => $msg]); + } +} +?> + + + + + + + + + Programmazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + + + + + + + +
+ + + +
+
+
+ +
+
+
+
+
Programmazione produzione
+ Trascina i lavori nelle linee: l'ordine nella colonna è l'ordine di produzione +
+
+
+ Nuovi da Mago + + Vista classica +
+
+ +
+ +
+ +
+
+ fine stimata oltre la data Zibo + margine sotto 1 giorno +
+
+ +
+
Caricamento…
+
+
+
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/production_line_view2.php b/public/userarea/production_line_view2.php index 7e1b861e..344d25e7 100644 --- a/public/userarea/production_line_view2.php +++ b/public/userarea/production_line_view2.php @@ -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; + } @@ -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"); + }); @@ -2864,6 +2931,17 @@ if (!empty($_GET['ajax'])) { + +
+ +
+
diff --git a/public/userarea/render_production_card.php b/public/userarea/render_production_card.php index a8346654..e6bdeb5d 100644 --- a/public/userarea/render_production_card.php +++ b/public/userarea/render_production_card.php @@ -81,10 +81,17 @@ if ($matricePhoto) {
- -
+ + +
Descrizione - +
@@ -161,8 +168,8 @@ if ($matricePhoto) {
+ data-start="" + data-end=""> 00:00:00