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

1717 lines
66 KiB
PHP

<?php
ob_start();
ini_set('display_errors', 1);
error_reporting(E_ALL);
include('include/headscript.php');
require_once __DIR__ . '/class/MagoProduction.php';
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// --- Permessi: stessa autorizzazione della programmazione ---
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 '<div style="font-family:sans-serif;padding:60px;text-align:center;"><h3>Accesso non consentito</h3><p>Non hai i permessi per visualizzare questa pagina.</p><a href="production_dashboard.php">Torna alla Dashboard</a></div>';
exit;
}
// chi importa (per la tracciabilità): nome dell'utente collegato
$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) {
$userName = 'utente ' . $iduserlogin;
}
}
/* ============================================================
* AJAX
* ============================================================ */
$action = $_POST['action'] ?? ($_GET['action'] ?? '');
if ($action !== '') {
try {
// liste per i menu a tendina (una volta sola)
if ($action === 'options') {
MagoSalesStats::jsonOut([
'success' => true,
'matrici' => $pdo->query("SELECT id, TRIM(nome) AS nome FROM matrice WHERE TRIM(nome) <> '' ORDER BY TRIM(nome)")->fetchAll(PDO::FETCH_ASSOC),
'linee' => $pdo->query("SELECT id, name FROM production_lines ORDER BY line_number")->fetchAll(PDO::FETCH_ASSOC),
'mescole' => $pdo->query("SELECT id, nome FROM mescole WHERE is_active = 1 ORDER BY nome")->fetchAll(PDO::FETCH_ASSOC),
'from' => MagoProduction::QUEUE_FROM_DATE,
'auto' => MagoProduction::AUTO_IMPORT,
]);
}
// elenco della coda
if ($action === 'list') {
$counts = ['nuovo' => 0, 'importato' => 0, 'scartato' => 0, 'chiuso_mago' => 0, 'modificati' => 0];
foreach ($pdo->query("SELECT queue_status, COUNT(*) n, SUM(mago_changed) ch FROM mago_prod_queue GROUP BY queue_status") as $r) {
$counts[$r['queue_status']] = (int)$r['n'];
if ($r['queue_status'] === 'importato') $counts['modificati'] = (int)$r['ch'];
}
$view = $_GET['view'] ?? 'nuovo';
$where = $view === 'modificati' ? "q.queue_status = 'importato' AND q.mago_changed = 1"
: (in_array($view, ['nuovo', 'importato', 'scartato', 'chiuso_mago'], true) ? "q.queue_status = " . $pdo->quote($view) : "q.queue_status = 'nuovo'");
$rows = $pdo->query("
SELECT q.*, TRIM(m.nome) AS matrice_nome,
p.id_status AS prod_status, s.nome AS prod_status_nome, COALESCE(s.badge_color, '#6c757d') AS prod_badge
FROM mago_prod_queue q
LEFT JOIN matrice m ON m.id = q.idmatrice
LEFT JOIN productiondata p ON p.id = q.productiondata_id
LEFT JOIN production_status s ON s.id = p.id_status
WHERE $where
ORDER BY q.zibo_date IS NULL, q.zibo_date, q.internal_ord_no, q.line_no
LIMIT 3000
")->fetchAll(PDO::FETCH_ASSOC);
MagoSalesStats::jsonOut(['success' => true, 'counts' => $counts, 'rows' => $rows]);
}
// proposta di compilazione (anche al cambio matrice nella finestra)
if ($action === 'prefill') {
$idm = isset($_GET['idmatrice']) && $_GET['idmatrice'] !== '' ? (int)$_GET['idmatrice'] : null;
$p = MagoProduction::prefill($pdo, (int)($_GET['id'] ?? 0), $idm);
if (!$p) MagoSalesStats::jsonOut(['success' => false, 'message' => 'Riga non trovata']);
MagoSalesStats::jsonOut(['success' => true, 'prefill' => $p]);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') MagoSalesStats::jsonOut(['success' => false, 'message' => 'Richiesta non valida']);
// porta una riga in programmazione
if ($action === 'import') {
$pid = MagoProduction::import($pdo, (int)$_POST['id'], [
'idmatrice' => $_POST['idmatrice'] ?? 0,
'id_linea' => $_POST['id_linea'] ?? 0,
'mescole' => $_POST['mescole'] ?? [],
'metri' => $_POST['metri'] ?? 0,
'kg_sp' => $_POST['kg_sp'] ?? 0,
'kg_p' => $_POST['kg_p'] ?? 0,
'ore_previste' => $_POST['ore_previste'] ?? 0,
'data_zibo' => $_POST['data_zibo'] ?? null,
'data_cliente' => $_POST['data_cliente'] ?? null,
'note_extra' => $_POST['note_extra'] ?? '',
'id_status' => $_POST['id_status'] ?? MagoProduction::STATUS_DA_PROGRAMMARE,
'remember' => !empty($_POST['remember']),
], $userName);
MagoSalesStats::jsonOut(['success' => true, 'productiondata_id' => $pid]);
}
// più righe insieme: solo quelle con matrice e linea già certe
if ($action === 'bulk_import') {
$ok = [];
$skip = [];
$status = (int)($_POST['id_status'] ?? MagoProduction::STATUS_DA_PROGRAMMARE);
foreach ((array)($_POST['ids'] ?? []) as $id) {
$p = MagoProduction::prefill($pdo, (int)$id);
if (!$p) continue;
$label = $p['queue']['internal_ord_no'] . ' riga ' . $p['queue']['line_no'];
if (!$p['idmatrice']) {
$skip[] = "$label: manca la matrice";
continue;
}
if (!$p['id_linea']) {
$skip[] = "$label: la matrice ha " . (count($p['lines']) ? count($p['lines']) . ' linee possibili' : 'nessuna linea') . ', sceglila a mano';
continue;
}
try {
$p['id_status'] = $status;
$ok[] = MagoProduction::import($pdo, (int)$id, $p, $userName);
} catch (Throwable $e) {
$skip[] = $e->getMessage();
}
}
MagoSalesStats::jsonOut(['success' => true, 'imported' => count($ok), 'skipped' => $skip]);
}
if ($action === 'discard') {
$pdo->prepare("UPDATE mago_prod_queue SET queue_status = 'scartato', discard_reason = ?, updated_at = NOW() WHERE id = ? AND queue_status = 'nuovo'")
->execute([mb_substr(trim((string)($_POST['reason'] ?? '')), 0, 255), (int)$_POST['id']]);
MagoSalesStats::jsonOut(['success' => true]);
}
if ($action === 'restore') {
$pdo->prepare("UPDATE mago_prod_queue SET queue_status = 'nuovo', discard_reason = '', updated_at = NOW() WHERE id = ? AND queue_status = 'scartato'")
->execute([(int)$_POST['id']]);
MagoSalesStats::jsonOut(['success' => true]);
}
if ($action === 'ack') {
$pdo->prepare("UPDATE mago_prod_queue SET mago_changed = 0, updated_at = NOW() WHERE id = ?")->execute([(int)$_POST['id']]);
MagoSalesStats::jsonOut(['success' => true]);
}
if ($action === 'refresh') {
MagoSalesStats::jsonOut(['success' => true, 'report' => MagoProduction::refreshQueue($pdo)]);
}
MagoSalesStats::jsonOut(['success' => false, 'message' => 'Azione sconosciuta']);
} catch (Throwable $e) {
MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]);
}
}
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Nuovi da Mago - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- DataTables + export -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
<!-- Select2 -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
<!-- Grafici -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.49.1"></script>
<style>
:root {
--c-ink: #1f2d3d;
--c-muted: #64748b;
--c-line: #e2e8f0;
}
body {
font-size: 1.02rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #b9ebc7 !important;
color: var(--c-ink) !important;
border-radius: 10px;
font-weight: 600;
padding: 10px 18px;
}
/* ---------- HEADER ---------- */
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.stats-title {
display: flex;
align-items: center;
gap: 14px;
}
.stats-title-icon {
width: 52px;
height: 52px;
border-radius: 14px;
background: linear-gradient(135deg, #198754, #3bb77e);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
box-shadow: 0 6px 16px rgba(25, 135, 84, .25);
}
.sync-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
border-radius: 999px;
background: #eef6ff;
color: #0b3d91;
font-size: .85rem;
font-weight: 600;
}
.sync-pill .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #94a3b8;
}
.sync-pill.ok .dot {
background: #22c55e;
}
.sync-pill.err .dot {
background: #ef4444;
}
/* ---------- TAB PAGINE ---------- */
.stats-tabs {
display: flex;
gap: 6px;
border-bottom: 2px solid var(--c-line);
margin-bottom: 18px;
flex-wrap: wrap;
}
.stats-tabs a {
padding: 10px 18px;
border-radius: 12px 12px 0 0;
font-weight: 700;
color: var(--c-muted);
text-decoration: none;
background: #f1f5f9;
margin-bottom: -2px;
border: 2px solid transparent;
}
.stats-tabs a:hover {
color: var(--c-ink);
}
.stats-tabs a.active {
background: #fff;
color: #198754;
border-color: var(--c-line);
border-bottom-color: #fff;
}
/* ---------- FILTRI ---------- */
.filters-box {
background: #fff;
border: 1px solid var(--c-line);
border-radius: 16px;
padding: 16px;
box-shadow: 0 6px 18px rgba(15, 23, 42, .05);
}
.filters-box label.flabel {
font-weight: 700;
font-size: .8rem;
color: var(--c-muted);
text-transform: uppercase;
letter-spacing: .4px;
margin-bottom: 4px;
display: block;
}
.chip-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
border: 1.5px solid var(--c-line);
background: #fff;
color: var(--c-ink);
border-radius: 999px;
padding: 6px 14px;
font-weight: 700;
font-size: .92rem;
cursor: pointer;
transition: all .15s ease;
display: inline-flex;
align-items: center;
gap: 7px;
}
.chip .dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--yc, #94a3b8);
}
.chip.active {
background: var(--yc, #198754);
border-color: var(--yc, #198754);
color: #fff;
}
.chip.active .dot {
background: #fff;
}
.metric-switch {
display: inline-flex;
background: #f1f5f9;
border-radius: 12px;
padding: 4px;
gap: 4px;
}
.metric-switch button {
border: 0;
background: transparent;
padding: 7px 16px;
border-radius: 9px;
font-weight: 700;
color: var(--c-muted);
}
.metric-switch button.active {
background: #fff;
color: var(--c-ink);
box-shadow: 0 2px 6px rgba(15, 23, 42, .1);
}
.ytd-switch {
display: inline-flex;
align-items: center;
gap: 10px;
background: #f1f5f9;
border-radius: 12px;
padding: 7px 14px;
font-weight: 700;
color: var(--c-ink);
cursor: pointer;
user-select: none;
}
.ytd-switch .form-check-input {
width: 2.6em;
height: 1.35em;
margin: 0;
cursor: pointer;
}
.ytd-switch .form-check-input:checked {
background-color: #198754;
border-color: #198754;
}
.select2-container--bootstrap-5 .select2-selection {
border-radius: 10px;
min-height: 42px;
}
/* ---------- CARTE ANNO ---------- */
.year-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
}
.year-card {
background: color-mix(in srgb, var(--yc) 12%, #fff);
border: 1px solid color-mix(in srgb, var(--yc) 28%, #fff);
border-radius: 16px;
padding: 16px 18px;
}
.year-card .y {
font-size: 1.05rem;
font-weight: 800;
color: color-mix(in srgb, var(--yc) 75%, #1f2d3d);
display: flex;
justify-content: space-between;
align-items: center;
}
.year-card .y small {
font-weight: 600;
font-size: .78rem;
color: var(--c-muted);
}
.year-card .big {
font-size: 1.9rem;
font-weight: 800;
color: var(--c-ink);
font-variant-numeric: tabular-nums;
line-height: 1.15;
margin-top: 6px;
}
.year-card .stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-top: 10px;
}
.year-card .stats div {
background: #fff;
border-radius: 10px;
padding: 6px 8px;
}
.year-card .stats b {
display: block;
font-size: 1rem;
color: var(--c-ink);
font-variant-numeric: tabular-nums;
}
.year-card .stats span {
font-size: .72rem;
color: var(--c-muted);
font-weight: 700;
text-transform: uppercase;
}
/* ---------- CRUSCOTTO ---------- */
.dash-head {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
font-weight: 800;
color: var(--c-ink);
margin-bottom: 10px;
}
.dash-head small {
font-weight: 600;
color: var(--c-muted);
font-size: .8rem;
}
.dash-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 14px;
}
@media (min-width: 1200px) {
.dash-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.dash-card {
background: color-mix(in srgb, var(--k) 12%, #fff);
border: 1px solid color-mix(in srgb, var(--k) 28%, #fff);
border-radius: 16px;
padding: 16px 18px;
position: relative;
}
.dash-card .lbl {
font-size: .78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .5px;
color: color-mix(in srgb, var(--k) 70%, #1f2d3d);
padding-right: 40px;
}
.dash-card .ico {
position: absolute;
right: 14px;
top: 14px;
width: 36px;
height: 36px;
border-radius: 10px;
background: #fff;
color: var(--k);
display: flex;
align-items: center;
justify-content: center;
}
.dash-card .big {
font-size: 1.75rem;
font-weight: 800;
color: var(--c-ink);
line-height: 1.15;
margin-top: 4px;
font-variant-numeric: tabular-nums;
}
.dash-card .sub {
font-size: .84rem;
color: var(--c-muted);
margin-top: 3px;
line-height: 1.4;
}
.dash-card .sub b {
color: var(--c-ink);
}
.b2b-pill {
display: inline-block;
font-size: .72rem;
font-weight: 800;
border-radius: 999px;
padding: 2px 9px;
margin-left: 6px;
vertical-align: middle;
}
.b2b-pill.up {
background: #dcfce7;
color: #166534;
}
.b2b-pill.flat {
background: #fef3c7;
color: #92400e;
}
.b2b-pill.down {
background: #fee2e2;
color: #991b1b;
}
/* ---------- CLIENTI DA RICHIAMARE ---------- */
.dorm-sum {
font-size: .9rem;
color: var(--c-muted);
}
.dorm-sum b {
color: var(--c-ink);
}
.lvl {
display: inline-block;
font-size: .72rem;
font-weight: 800;
border-radius: 999px;
padding: 2px 9px;
white-space: nowrap;
}
.lvl.watch {
background: #fef9c3;
color: #854d0e;
}
.lvl.dormant {
background: #ffedd5;
color: #9a3412;
}
.lvl.lost {
background: #fee2e2;
color: #991b1b;
}
.ratio-bar {
display: inline-block;
width: 54px;
height: 6px;
border-radius: 99px;
background: #eef2f7;
vertical-align: middle;
margin-left: 6px;
overflow: hidden;
}
.ratio-bar i {
display: block;
height: 100%;
border-radius: 99px;
}
#dormTable tbody tr {
cursor: pointer;
}
.inv-toggle {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
background: none;
border: 0;
padding: 0;
cursor: pointer;
font-weight: 800;
color: var(--c-ink);
text-align: left;
}
.inv-toggle>span:first-child {
flex: 0 0 auto;
}
.inv-toggle .inv-chev {
margin-left: auto;
transition: transform .2s ease;
}
.inv-toggle[aria-expanded="true"] .inv-chev {
transform: rotate(180deg);
}
#invMonthTable tfoot th {
border-top: 2px solid var(--c-line);
font-weight: 800;
}
.doc-type {
display: inline-block;
font-size: .72rem;
font-weight: 800;
border-radius: 999px;
padding: 2px 8px;
white-space: nowrap;
}
.doc-type.t-inv {
background: #dcfce7;
color: #166534;
}
.doc-type.t-adv {
background: #e0f2fe;
color: #075985;
}
.doc-type.t-cn {
background: #fee2e2;
color: #991b1b;
}
.year-card .inv {
margin-top: 8px;
background: #fff;
border-radius: 10px;
padding: 6px 10px;
font-size: .85rem;
color: var(--c-muted);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.year-card .inv b {
color: var(--c-ink);
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
.year-card .deltas {
margin-top: 10px;
font-size: .88rem;
color: var(--c-muted);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.delta-up {
color: #198754;
font-weight: 800;
}
.delta-down {
color: #dc3545;
font-weight: 800;
}
/* ---------- GRAFICI / TABELLA ---------- */
.chart-card {
background: #fff;
border: 1px solid var(--c-line);
border-radius: 16px;
padding: 16px;
height: 100%;
}
.chart-title {
font-weight: 800;
color: var(--c-ink);
font-size: 1rem;
}
.chart-hint {
font-size: .8rem;
color: var(--c-muted);
}
#custTable thead th {
background: #b9ebc7;
color: var(--c-ink);
white-space: nowrap;
}
#custTable tbody tr {
cursor: pointer;
}
.amount {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.trend {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-weight: 800;
font-size: .78rem;
white-space: nowrap;
}
.trend-new {
background: #e0f2fe;
color: #0369a1;
}
.trend-lost {
background: #fde8ea;
color: #b02a37;
}
.trend-up {
background: #dcfce7;
color: #15803d;
}
.trend-down {
background: #ffedd5;
color: #c2410c;
}
.trend-flat {
background: #f1f5f9;
color: #475569;
}
.warn-strip {
background: #fff8e6;
border: 1px solid #ffe2a8;
color: #8a5a00;
border-radius: 12px;
padding: 10px 14px;
font-size: .9rem;
}
.dataTables_wrapper .dt-buttons .btn {
border-radius: 10px;
font-weight: 600;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--c-muted);
}
.sc-link {
color: #198754;
margin-left: 6px;
opacity: .75;
text-decoration: none;
}
.sc-link:hover {
opacity: 1;
}
/* ================= NUOVI DA MAGO ================= */
.q-kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
}
.q-kpi {
--k: #64748b;
border-radius: 14px;
padding: 12px 14px;
background: color-mix(in srgb, var(--k) 12%, #fff);
border: 1px solid color-mix(in srgb, var(--k) 28%, #fff);
cursor: pointer;
transition: transform .15s ease, box-shadow .15s ease;
}
.q-kpi:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(15, 23, 42, .08);
}
.q-kpi.active {
box-shadow: 0 0 0 2px var(--k) inset;
background: color-mix(in srgb, var(--k) 22%, #fff);
}
.q-kpi .n {
font-size: 1.6rem;
font-weight: 900;
color: var(--c-ink);
line-height: 1.1;
}
.q-kpi .l {
font-size: .78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .4px;
color: color-mix(in srgb, var(--k) 70%, #1f2d3d);
}
.q-kpi .h {
font-size: .78rem;
color: var(--c-muted);
}
.mm-raw {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 800;
font-size: .95rem;
background: #f1f5f9;
border-radius: 6px;
padding: 1px 7px;
white-space: nowrap;
}
.ms {
display: inline-block;
font-size: .7rem;
font-weight: 800;
border-radius: 999px;
padding: 2px 8px;
white-space: nowrap;
margin-top: 3px;
}
.ms-esatta,
.ms-mappa,
.ms-manuale {
background: #dcfce7;
color: #166534;
}
.ms-revisione {
background: #fef3c7;
color: #92400e;
}
.ms-da_creare {
background: #fee2e2;
color: #991b1b;
}
.ms-profilo {
background: #e0f2fe;
color: #075985;
}
.ms-anomalia {
background: #e2e8f0;
color: #334155;
}
.q-table thead th {
background: #b9ebc7;
color: var(--c-ink);
white-space: nowrap;
}
.q-table td {
vertical-align: middle;
}
/* il tema (Rocker) mette white-space: nowrap dentro .table-responsive: qui il testo deve andare a capo */
.chart-card .table-responsive {
white-space: normal !important;
overflow-x: auto;
}
.q-table {
table-layout: auto;
width: 100% !important;
}
.q-table th {
white-space: nowrap !important;
}
.q-table td {
white-space: nowrap !important;
}
/* vanno a capo solo le colonne di testo */
.q-table td.col-art,
.q-table td.col-cli,
.q-table td.col-mm,
.q-table td.col-info {
white-space: normal !important;
overflow-wrap: anywhere;
}
.q-table td.col-art {
min-width: 160px;
max-width: 320px;
}
.q-table td.col-cli {
min-width: 110px;
max-width: 220px;
}
.q-table td.col-mm {
min-width: 120px;
max-width: 240px;
}
.q-table td.col-act {
white-space: nowrap !important;
width: 1%;
}
.q-table .mm-raw {
white-space: normal;
word-break: break-all;
}
@media (max-width: 1400px) {
.q-table .act-import .lbl {
display: none;
}
}
.q-sub {
font-size: .78rem;
color: var(--c-muted);
}
.q-changes {
background: #fff7ed;
border-left: 3px solid #f97316;
padding: 4px 8px;
border-radius: 6px;
font-size: .82rem;
color: #9a3412;
}
.bulk-bar {
position: sticky;
bottom: 12px;
z-index: 5;
background: #0f172a;
color: #fff;
border-radius: 14px;
padding: 10px 14px;
display: none;
align-items: center;
gap: 12px;
flex-wrap: wrap;
box-shadow: 0 12px 30px rgba(15, 23, 42, .3);
margin-top: 12px;
}
.bulk-bar.show {
display: flex;
}
.bulk-bar select {
max-width: 200px;
}
/* finestra "porta in programmazione" */
.mago-box {
background: #f8fafc;
border: 1px solid var(--c-line);
border-radius: 12px;
padding: 12px 14px;
font-size: .9rem;
}
.mago-box .r {
display: grid;
grid-template-columns: 120px 1fr;
gap: 6px;
padding: 3px 0;
border-bottom: 1px dashed #e2e8f0;
}
.mago-box .r:last-child {
border-bottom: 0;
}
.mago-box .r span {
color: var(--c-muted);
font-weight: 600;
}
.mago-notes {
white-space: pre-wrap;
font-size: .85rem;
max-height: 180px;
overflow: auto;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 6px 8px;
margin-top: 6px;
}
.match-alert {
border-radius: 10px;
padding: 8px 10px;
font-size: .85rem;
margin-top: 6px;
}
.modal .select2-container {
width: 100% !important;
}
.q-toast {
position: fixed;
top: 18px;
right: 18px;
z-index: 3000;
background: #14532d;
color: #fff;
padding: 10px 16px;
border-radius: 12px;
box-shadow: 0 12px 30px rgba(15, 23, 42, .25);
font-weight: 600;
opacity: 0;
transform: translateY(-8px);
transition: all .25s ease;
max-width: 420px;
}
.q-toast.err {
background: #991b1b;
}
.q-toast.show {
opacity: 1;
transform: none;
}
.dest-switch .btn-check:checked+.btn {
font-weight: 800;
}
</style>
</head>
<body>
<div class="wrapper toggled" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header bg-white border-0 pb-3">
<div class="stats-header">
<div class="stats-title">
<div class="stats-title-icon" style="background:linear-gradient(135deg,#f59e0b,#fbbf24);box-shadow:0 6px 16px rgba(245,158,11,.3);"><i class="fa-solid fa-inbox"></i></div>
<div>
<h5 class="mb-1" style="font-weight:800;">Nuovi ordini da Mago</h5>
<span class="sync-pill"><span class="dot" style="background:#22c55e;"></span><span id="scopeText">Righe d'ordine da portare in programmazione</span></span>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-outline-secondary" id="btnRefresh" title="Ricalcola la lista dai dati Mago già sincronizzati"><i class="fa-solid fa-rotate me-1"></i> Ricalcola</button>
<a class="btn btn-primary" href="produzione_programmazione_drag.php"><i class="fa-solid fa-calendar-days me-1"></i> Programmazione</a>
</div>
</div>
</div>
<div class="card-body pt-0">
<div class="q-kpis mb-3" id="kpis">
<div class="q-kpi active" data-view="nuovo" style="--k:#f59e0b;">
<div class="l"><i class="fa-solid fa-inbox me-1"></i> Nuovi</div>
<div class="n" id="cNuovo">—</div>
<div class="h">da portare in programmazione</div>
</div>
<div class="q-kpi" data-view="modificati" style="--k:#ea580c;">
<div class="l"><i class="fa-solid fa-triangle-exclamation me-1"></i> Modificati in Mago</div>
<div class="n" id="cModificati">—</div>
<div class="h">già in produzione, cambiati dopo</div>
</div>
<div class="q-kpi" data-view="importato" style="--k:#16a34a;">
<div class="l"><i class="fa-solid fa-circle-check me-1"></i> In programmazione</div>
<div class="n" id="cImportato">—</div>
<div class="h">già portati in produzione</div>
</div>
<div class="q-kpi" data-view="scartato" style="--k:#64748b;">
<div class="l"><i class="fa-solid fa-ban me-1"></i> Scartati</div>
<div class="n" id="cScartato">—</div>
<div class="h">da non produrre</div>
</div>
<div class="q-kpi" data-view="chiuso_mago" style="--k:#0ea5e9;">
<div class="l"><i class="fa-solid fa-flag-checkered me-1"></i> Chiusi in Mago</div>
<div class="n" id="cChiuso">—</div>
<div class="h">consegnati o annullati prima</div>
</div>
</div>
<div class="filters-box mb-3" id="matchBox">
<label class="flabel">Matrice</label>
<div class="chip-row" id="matchChips">
<button type="button" class="chip active" data-m="">Tutte <span class="badge bg-light text-dark" data-c="">0</span></button>
<button type="button" class="chip" data-m="ok" style="--yc:#16a34a;">Collegata <span class="badge bg-light text-dark" data-c="ok">0</span></button>
<button type="button" class="chip" data-m="revisione" style="--yc:#d97706;">Revisione diversa <span class="badge bg-light text-dark" data-c="revisione">0</span></button>
<button type="button" class="chip" data-m="da_creare" style="--yc:#dc2626;">Da creare in anagrafica <span class="badge bg-light text-dark" data-c="da_creare">0</span></button>
<button type="button" class="chip" data-m="profilo" style="--yc:#0284c7;">Profilo standard (D/R/T/Q) <span class="badge bg-light text-dark" data-c="profilo">0</span></button>
<button type="button" class="chip" data-m="anomalia" style="--yc:#64748b;">Anomalie <span class="badge bg-light text-dark" data-c="anomalia">0</span></button>
</div>
</div>
<div class="chart-card">
<div class="table-responsive">
<table id="qTable" class="table table-striped align-middle w-100 q-table"></table>
</div>
<div class="bulk-bar" id="bulkBar">
<b><span id="bulkN">0</span> righe selezionate</b>
<span class="small opacity-75">verranno portate solo quelle con matrice collegata e linea unica</span>
<select class="form-select form-select-sm" id="bulkStatus">
<option value="1">in "Da programmare"</option>
<option value="6">in "Programmati"</option>
</select>
<button class="btn btn-warning btn-sm fw-bold" id="btnBulk"><i class="fa-solid fa-arrow-right-to-bracket me-1"></i> Porta in programmazione</button>
<button class="btn btn-outline-light btn-sm ms-auto" id="btnBulkClear">Annulla selezione</button>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- FINESTRA: PORTA IN PROGRAMMAZIONE -->
<div class="modal fade" id="mImport" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<form id="fImport">
<div class="modal-header">
<h5 class="modal-title"><i class="fa-solid fa-arrow-right-to-bracket me-2 text-warning"></i>Porta in programmazione · <span id="mTitle"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="id" id="fId">
<div class="row g-3">
<div class="col-lg-5">
<div class="mago-box" id="mMago"></div>
</div>
<div class="col-lg-7">
<div class="row g-2">
<div class="col-12">
<label class="form-label fw-bold mb-1">Matrice</label>
<select class="form-select" name="idmatrice" id="fMatrice" required></select>
<div id="fMatchInfo"></div>
<div class="form-check mt-1" id="fRememberBox" style="display:none;">
<input class="form-check-input" type="checkbox" name="remember" value="1" id="fRemember">
<label class="form-check-label small" for="fRemember">Ricorda: il codice Mago <b id="fRememberCode"></b> corrisponde sempre a questa matrice</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-bold mb-1">Linea</label>
<select class="form-select" name="id_linea" id="fLinea" required></select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold mb-1">Mescole</label>
<select class="form-select" name="mescole[]" id="fMescole" multiple></select>
</div>
<div class="col-6 col-md-3">
<label class="form-label mb-1">Metri</label>
<input type="number" step="0.01" class="form-control" name="metri" id="fMetri">
</div>
<div class="col-6 col-md-3">
<label class="form-label mb-1">Kg SP</label>
<input type="number" step="0.01" class="form-control" name="kg_sp" id="fKgSp" value="0">
</div>
<div class="col-6 col-md-3">
<label class="form-label mb-1">Kg P</label>
<input type="number" step="0.01" class="form-control" name="kg_p" id="fKgP" value="0">
</div>
<div class="col-6 col-md-3">
<label class="form-label mb-1">Ore previste</label>
<input type="number" step="0.01" class="form-control" name="ore_previste" id="fOre">
</div>
<div class="col-6">
<label class="form-label mb-1">Data ZIBO</label>
<input type="date" class="form-control" name="data_zibo" id="fZibo">
</div>
<div class="col-6">
<label class="form-label mb-1">Data cliente</label>
<input type="date" class="form-control" name="data_cliente" id="fCliente">
</div>
<div class="col-12">
<label class="form-label mb-1">Note extra</label>
<textarea class="form-control" name="note_extra" id="fNote" rows="5"></textarea>
</div>
<div class="col-12">
<label class="form-label fw-bold mb-1">Dove metterlo</label>
<div class="btn-group dest-switch w-100" role="group">
<input type="radio" class="btn-check" name="id_status" id="fSt1" value="1" checked>
<label class="btn btn-outline-secondary" for="fSt1">Da programmare</label>
<input type="radio" class="btn-check" name="id_status" id="fSt6" value="6">
<label class="btn btn-outline-info" for="fSt6">Programmati</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<span class="small text-muted me-auto" id="fWs"></span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-warning fw-bold"><i class="fa-solid fa-check me-1"></i> Porta in programmazione</button>
</div>
</form>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
// Navbar collassata all'apertura: si espande al passaggio del mouse
$(function() {
$('.sidebar-wrapper').hover(
function() {
$('.wrapper.toggled').addClass('sidebar-hovered');
},
function() {
$('.wrapper').removeClass('sidebar-hovered');
}
);
});
</script>
<script>
(function() {
const ZIBO_WEEK = <?= MagoSalesStats::ZIBO_IS_WEEK ? 'true' : 'false' ?>;
const ZIBO_END_DAYS = <?= (int)MagoSalesStats::ZIBO_WEEK_END_DAYS ?>;
const MS_LABEL = {
esatta: 'Matrice collegata',
mappa: 'Collegata (corrispondenza salvata)',
manuale: 'Scelta a mano',
revisione: 'Revisione diversa',
da_creare: 'Da creare in anagrafica',
profilo: 'Profilo standard',
anomalia: 'Da verificare'
};
const MS_GROUP = s => ['esatta', 'mappa', 'manuale'].includes(s) ? 'ok' : s;
let opts = {
matrici: [],
linee: [],
mescole: []
};
let view = 'nuovo',
matchFilter = '',
rows = [],
dt = null,
selected = new Set(),
current = null;
/* ---------- utilità ---------- */
function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
} [c]));
}
const iso = d => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
const today = iso(new Date());
const fmtDate = v => v ? String(v).substring(0, 10).split('-').reverse().join('/') : '';
const fmtQty = v => (+v || 0).toLocaleString('it-IT', {
maximumFractionDigits: 2
});
function ziboEnd(z) {
if (!z || !ZIBO_WEEK) return z;
const d = new Date(String(z).substring(0, 10) + 'T00:00:00');
d.setDate(d.getDate() - ((d.getDay() + 6) % 7) + ZIBO_END_DAYS);
return iso(d);
}
function dateCell(z, c) {
let h = '';
if (z) {
const late = ziboEnd(z) < today;
h += `<div class="text-nowrap ${late ? 'text-danger fw-bold' : ''}"><span class="q-sub">ZIBO</span> ${fmtDate(z)}${late ? ' ⚠' : ''}</div>`;
}
if (c) h += `<div class="text-nowrap q-sub">Cliente ${fmtDate(c)}</div>`;
return h || '<span class="text-muted">—</span>';
}
function post(data) {
return $.ajax({
url: location.pathname,
method: 'POST',
data: data,
dataType: 'json'
});
}
// avviso a scomparsa in alto a destra
function toast(msg, ok) {
const el = $(`<div class="q-toast ${ok === false ? 'err' : ''}"><i class="fa-solid ${ok === false ? 'fa-circle-xmark' : 'fa-circle-check'} me-2"></i>${esc(msg)}</div>`).appendTo('body');
setTimeout(() => el.addClass('show'), 10);
setTimeout(() => el.removeClass('show'), ok === false ? 6000 : 3500);
setTimeout(() => el.remove(), ok === false ? 6500 : 4000);
}
/* ---------- caricamento ---------- */
function loadOptions() {
return $.getJSON('?action=options').then(d => {
opts = d;
$('#scopeText').text(`Righe d'ordine Mago dal ${fmtDate(d.from)} · quantità > 1 · ${d.auto ? 'import automatico ATTIVO' : 'import su conferma'}`);
$('#fMatrice').html('<option value="">Scegli la matrice…</option>' + d.matrici.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join(''));
$('#fMescole').html(d.mescole.map(m => `<option value="${m.id}">${esc(m.nome)}</option>`).join(''));
});
}
function load() {
$('.chart-card').css('opacity', .55);
$.getJSON('?action=list&view=' + encodeURIComponent(view)).then(d => {
$('.chart-card').css('opacity', 1);
if (!d.success) return toast(d.message || 'Errore', false);
const c = d.counts;
$('#cNuovo').text(c.nuovo);
$('#cModificati').text(c.modificati);
$('#cImportato').text(c.importato);
$('#cScartato').text(c.scartato);
$('#cChiuso').text(c.chiuso_mago);
rows = d.rows;
selected.clear();
renderMatchCounts();
render();
}).catch(() => {
$('.chart-card').css('opacity', 1);
toast('Errore nel caricamento', false);
});
}
function renderMatchCounts() {
const n = {
'': rows.length
};
rows.forEach(r => {
const g = MS_GROUP(r.match_status);
n[g] = (n[g] || 0) + 1;
});
$('#matchChips [data-c]').each(function() {
$(this).text(n[$(this).data('c')] || 0);
});
$('#matchBox').toggle(view === 'nuovo');
}
/* ---------- tabella ---------- */
function render() {
const data = rows.filter(r => !matchFilter || MS_GROUP(r.match_status) === matchFilter);
const cols = [];
if (view === 'nuovo') cols.push({
title: '<input type="checkbox" class="form-check-input" id="chkAll">',
data: null,
orderable: false,
width: '28px',
render: r => `<input type="checkbox" class="form-check-input chk" value="${r.id}" ${selected.has(+r.id) ? 'checked' : ''}>`
});
cols.push({
title: 'Ordine',
data: 'internal_ord_no',
render: (v, t, r) => t === 'display' ? `<b>${esc(v)}</b> <span class="q-sub">riga ${r.line_no}</span><div class="q-sub">${fmtDate(r.order_date)}</div>` : v + ' ' + String(r.line_no).padStart(3, '0')
}, {
title: 'Cliente',
data: 'company_name',
className: 'col-cli',
render: (v, t, r) => t === 'display' ? `<div class="fw-semibold">${esc(v)}</div><div class="q-sub">${esc(r.customer)}</div>` : v + ' ' + r.customer
}, {
title: 'Articolo',
data: 'item',
className: 'col-art',
render: (v, t, r) => t === 'display' ? `<b>${esc(v)}</b> <span class="q-sub">${esc(r.description)}</span>${r.cod_art_cli ? `<div class="q-sub">cod. cliente ${esc(r.cod_art_cli)}</div>` : ''}` : v + ' ' + r.description + ' ' + r.cod_art_cli
}, {
title: 'Matrice / Misura',
data: 'matrice_misura',
className: 'col-mm',
render: (v, t, r) => {
if (t !== 'display') return v + ' ' + (r.matrice_nome || '');
const linked = r.matrice_nome ? `<div class="small mt-1"><i class="fa-solid fa-link text-success me-1"></i>${esc(r.matrice_nome)}</div>` : '';
return `<span class="mm-raw">${esc(v || '—')}</span><br><span class="ms ms-${esc(r.match_status)}" title="${esc(r.match_info)}">${MS_LABEL[r.match_status] || esc(r.match_status)}</span>${linked}${r.match_info && !['esatta', 'mappa'].includes(r.match_status) ? `<div class="q-sub">${esc(r.match_info)}</div>` : ''}`;
}
}, {
title: 'Da produrre',
data: 'residual_qty',
className: 'text-end text-nowrap',
render: (v, t, r) => t === 'display' ? `<b>${fmtQty(v)}</b> ${esc(r.uom)}${+r.delivered_qty ? `<div class="q-sub">su ${fmtQty(r.qty)}</div>` : ''}` : +v
}, {
title: 'Consegna',
data: 'zibo_date',
render: (v, t, r) => t === 'display' ? dateCell(v, r.client_date) : (v || '9999')
});
if (view === 'nuovo') cols.push({
title: '',
data: null,
orderable: false,
className: 'text-end col-act',
render: r => `<div class="d-flex flex-column gap-1 align-items-end">
<button class="btn btn-warning btn-sm fw-bold act-import" data-id="${r.id}" title="Porta in programmazione"><i class="fa-solid fa-arrow-right-to-bracket"></i><span class="lbl ms-1">Programma</span></button>
<button class="btn btn-outline-secondary btn-sm act-discard" data-id="${r.id}" title="Da non produrre: scarta"><i class="fa-solid fa-ban"></i></button>
</div>`
});
else if (view === 'importato' || view === 'modificati') cols.push({
title: 'In produzione',
data: 'productiondata_id',
className: 'col-info',
render: (v, t, r) => {
if (t !== 'display') return +v || 0;
const st = r.prod_status_nome ? `<span class="badge" style="background:${esc(r.prod_badge)};">${esc(r.prod_status_nome)}</span>` : '<span class="badge bg-secondary">eliminato</span>';
const ch = +r.mago_changed ? `<div class="q-changes mt-1"><i class="fa-solid fa-triangle-exclamation me-1"></i>${esc(r.mago_changes)}</div><button class="btn btn-sm btn-outline-success mt-1 act-ack" data-id="${r.id}"><i class="fa-solid fa-check me-1"></i> Visto</button>` : '';
return `${st} <span class="q-sub">lavoro #${esc(v)} · ${fmtDate(r.imported_at)}${r.imported_by ? ' · ' + esc(r.imported_by) : ''}</span>${ch}`;
}
});
else if (view === 'scartato') cols.push({
title: 'Motivo',
data: 'discard_reason',
className: 'col-info',
render: (v, t, r) => t === 'display' ? `${esc(v || '—')} <button class="btn btn-sm btn-outline-primary ms-2 act-restore" data-id="${r.id}"><i class="fa-solid fa-rotate-left me-1"></i> Ripristina</button>` : v
});
else cols.push({
title: 'Motivo',
data: 'mago_changes',
className: 'col-info',
render: v => esc(v)
});
if (dt) {
dt.destroy();
$('#qTable').empty();
}
dt = $('#qTable').DataTable({
data: data,
columns: cols,
order: [
[view === 'nuovo' ? 6 : 5, 'asc']
],
pageLength: 50,
lengthMenu: [25, 50, 100, 500],
autoWidth: false,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
updateBulk();
}
function updateBulk() {
$('#bulkN').text(selected.size);
$('#bulkBar').toggleClass('show', view === 'nuovo' && selected.size > 0);
}
/* ---------- finestra import ---------- */
function linesOptions(prefLines) {
const pref = (prefLines || []).map(Number);
const a = opts.linee.filter(l => pref.includes(+l.id)),
b = opts.linee.filter(l => !pref.includes(+l.id));
let h = '<option value="">Scegli la linea…</option>';
if (a.length) h += '<optgroup label="Linee della matrice">' + a.map(l => `<option value="${l.id}">★ ${esc(l.name)}</option>`).join('') + '</optgroup>';
h += '<optgroup label="' + (a.length ? 'Altre linee' : 'Linee') + '">' + b.map(l => `<option value="${l.id}">${esc(l.name)}</option>`).join('') + '</optgroup>';
return h;
}
function applyTech(p) {
$('#fLinea').html(linesOptions(p.lines)).val(p.id_linea || '').trigger('change');
$('#fMescole').val((p.mescole || []).map(String)).trigger('change');
if (+p.ore_previste > 0) $('#fOre').val(p.ore_previste);
$('#fWs').text(p.worksheet ? `Mescole e velocità dalla scheda di lavoro n° ${p.worksheet}` : (p.idmatrice ? 'Nessuna scheda di lavoro attiva per questa matrice: completa a mano mescole e ore' : ''));
}
function openImport(id) {
$.getJSON('?action=prefill&id=' + id).then(d => {
if (!d.success) return toast(d.message, false);
const p = d.prefill,
q = p.queue;
current = q;
$('#fId').val(q.id);
$('#mTitle').text(`ordine ${q.internal_ord_no} riga ${q.line_no}`);
$('#mMago').html(`
<div class="fw-bold mb-2"><i class="fa-solid fa-file-invoice me-1"></i> Dati da Mago</div>
<div class="r"><span>Cliente</span><div><b>${esc(q.company_name)}</b> <span class="q-sub">${esc(q.customer)}</span></div></div>
<div class="r"><span>Articolo</span><div><b>${esc(q.item)}</b> ${esc(q.description)}</div></div>
${q.cod_art_cli ? `<div class="r"><span>Cod. cliente</span><div>${esc(q.cod_art_cli)}</div></div>` : ''}
<div class="r"><span>Matrice/Misura</span><div><span class="mm-raw">${esc(q.matrice_misura || '—')}</span></div></div>
<div class="r"><span>Da produrre</span><div><b>${fmtQty(q.residual_qty)} ${esc(q.uom)}</b>${+q.delivered_qty ? ` <span class="q-sub">(ordinati ${fmtQty(q.qty)})</span>` : ''}</div></div>
<div class="r"><span>Consegna</span><div>${dateCell(q.zibo_date, q.client_date)}</div></div>
${q.mago_notes ? `<div class="mt-2 fw-semibold small">Note della riga</div><div class="mago-notes">${esc(q.mago_notes)}</div>` : ''}`);
$('#fMatrice').val(p.idmatrice || '').trigger('change.select2');
const ok = ['esatta', 'mappa', 'manuale'].includes(q.match_status);
$('#fMatchInfo').html(`<div class="match-alert ms-${esc(q.match_status)}"><b>${MS_LABEL[q.match_status] || ''}</b>${q.match_info ? ' · ' + esc(q.match_info) : ''}</div>`);
$('#fRememberCode').text(q.matrice_misura);
$('#fRemember').prop('checked', false);
$('#fRememberBox').toggle(!!q.matrice_misura && !ok);
$('#fMetri').val(p.metri);
$('#fKgSp').val(0);
$('#fKgP').val(0);
$('#fOre').val(p.ore_previste);
$('#fZibo').val(p.data_zibo || '');
$('#fCliente').val(p.data_cliente || '');
$('#fNote').val(p.note_extra);
$('#fSt1').prop('checked', true);
applyTech(p);
bootstrap.Modal.getOrCreateInstance(document.getElementById('mImport')).show();
});
}
/* ---------- eventi ---------- */
$(function() {
$('#fMatrice').select2({
theme: 'bootstrap-5',
dropdownParent: $('#mImport'),
width: '100%',
placeholder: 'Scegli la matrice…'
}).on('select2:select', function() {
if (!current) return;
const idm = $(this).val();
$('#fRememberBox').toggle(!!current.matrice_misura && +idm !== +current.idmatrice);
$.getJSON(`?action=prefill&id=${current.id}&idmatrice=${idm}`).then(d => d.success && applyTech(d.prefill));
});
$('#fMescole').select2({
theme: 'bootstrap-5',
dropdownParent: $('#mImport'),
width: '100%',
placeholder: 'Mescole',
closeOnSelect: false
});
$('#fImport').on('submit', function(e) {
e.preventDefault();
const data = $(this).serializeArray();
data.push({
name: 'action',
value: 'import'
});
post(data).then(r => {
if (!r.success) return toast(r.message, false);
bootstrap.Modal.getInstance(document.getElementById('mImport')).hide();
toast(`Portato in programmazione (lavoro #${r.productiondata_id})`);
load();
});
});
$('#kpis').on('click', '.q-kpi', function() {
$('.q-kpi').removeClass('active');
$(this).addClass('active');
view = $(this).data('view');
matchFilter = '';
$('#matchChips .chip').removeClass('active').filter('[data-m=""]').addClass('active');
load();
});
$('#matchChips').on('click', '.chip', function() {
$('#matchChips .chip').removeClass('active');
$(this).addClass('active');
matchFilter = $(this).data('m') || '';
render();
});
$('#qTable').on('click', '.act-import', function() {
openImport($(this).data('id'));
})
.on('click', '.act-discard', function() {
const id = $(this).data('id');
const reason = prompt('Perché questa riga non va prodotta? (es. da magazzino, servizio, campione)');
if (reason === null) return;
post({
action: 'discard',
id,
reason
}).then(r => r.success ? load() : toast(r.message, false));
})
.on('click', '.act-restore', function() {
post({
action: 'restore',
id: $(this).data('id')
}).then(r => r.success ? load() : toast(r.message, false));
})
.on('click', '.act-ack', function() {
post({
action: 'ack',
id: $(this).data('id')
}).then(r => r.success ? load() : toast(r.message, false));
})
.on('change', '.chk', function() {
const id = +$(this).val();
this.checked ? selected.add(id) : selected.delete(id);
updateBulk();
})
.on('change', '#chkAll', function() {
const on = this.checked;
dt.rows({
search: 'applied'
}).data().each(r => on ? selected.add(+r.id) : selected.delete(+r.id));
$('#qTable .chk').prop('checked', on);
updateBulk();
});
$('#btnBulkClear').on('click', () => {
selected.clear();
$('#qTable .chk, #chkAll').prop('checked', false);
updateBulk();
});
$('#btnBulk').on('click', function() {
if (!selected.size) return;
const btn = $(this).prop('disabled', true);
post({
action: 'bulk_import',
ids: [...selected],
id_status: $('#bulkStatus').val()
}).then(r => {
btn.prop('disabled', false);
if (!r.success) return toast(r.message, false);
const msg = `${r.imported} righe portate in programmazione` + (r.skipped.length ? `\n\nNon portate (${r.skipped.length}):\n• ` + r.skipped.slice(0, 15).join('\n• ') + (r.skipped.length > 15 ? '\n…' : '') : '');
if (r.skipped.length) alert(msg);
else toast(msg);
load();
}).catch(() => btn.prop('disabled', false));
});
$('#btnRefresh').on('click', function() {
const b = $(this).prop('disabled', true);
post({
action: 'refresh'
}).then(r => {
b.prop('disabled', false);
if (!r.success) return toast(r.message, false);
const x = r.report;
toast(`${x.nuove} nuove · ${x.aggiornate} aggiornate · ${x.chiuse} chiuse · ${x.modificate} modificate`);
load();
}).catch(() => b.prop('disabled', false));
});
loadOptions().then(load);
});
})();
</script>
</body>
</html>