1717 lines
66 KiB
PHP
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 => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
} [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>
|