1312 lines
55 KiB
PHP
1312 lines
55 KiB
PHP
<?php
|
|
ob_start();
|
|
ini_set('display_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
include('include/headscript.php');
|
|
require_once __DIR__ . '/class/MagoSalesStats.php';
|
|
require_once __DIR__ . '/class/UiPrefs.php';
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
// --- Permessi: pagina visibile solo con 'sales.deliveries.view' ---
|
|
$canStats = userCan('sales.stats.view'); // per i tab e il link al dettaglio ordine
|
|
$canDeliveries = userCan('sales.deliveries.view');
|
|
if (!$canDeliveries) {
|
|
if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
MagoSalesStats::jsonOut(['success' => false, 'ok' => 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;
|
|
}
|
|
|
|
/* ============================================================
|
|
* AJAX
|
|
* ============================================================ */
|
|
|
|
/** Filtri della pagina Consegne applicati alla vista righe aperte (alias r). */
|
|
function deliveriesWhere()
|
|
{
|
|
// cliente / agente / area dalla classe comune (scritti su x.) → riportati su r.
|
|
[$w, $p] = MagoSalesStats::buildWhere(['from' => '', 'to' => '']);
|
|
$w = preg_replace('/\bx\./', 'r.', $w);
|
|
|
|
$items = MagoSalesStats::arrParam('items');
|
|
if ($items) {
|
|
$ph = [];
|
|
foreach ($items as $i => $v) {
|
|
$ph[] = ':it' . $i;
|
|
$p['it' . $i] = $v;
|
|
}
|
|
$w .= ' AND r.item IN (' . implode(',', $ph) . ')';
|
|
}
|
|
|
|
$horizons = [
|
|
'late_zibo' => 'r.due_date < CURDATE()',
|
|
'late_client' => 'r.client_date < CURDATE()',
|
|
'week' => 'r.due_date >= CURDATE() AND YEARWEEK(r.due_date, 3) = YEARWEEK(CURDATE(), 3)',
|
|
'next_week' => 'YEARWEEK(r.due_date, 3) = YEARWEEK(CURDATE() + INTERVAL 7 DAY, 3)',
|
|
'7d' => 'r.due_date >= CURDATE() AND r.due_date <= CURDATE() + INTERVAL 7 DAY',
|
|
'30d' => 'r.due_date >= CURDATE() AND r.due_date <= CURDATE() + INTERVAL 30 DAY',
|
|
'nodate' => 'r.due_date IS NULL',
|
|
];
|
|
$h = $_GET['horizon'] ?? '';
|
|
if (isset($horizons[$h])) $w .= ' AND ' . $horizons[$h];
|
|
|
|
$q = trim((string)($_GET['search'] ?? ''));
|
|
if ($q !== '') {
|
|
$w .= ' AND (r.internal_ord_no LIKE :s1 OR r.company_name LIKE :s2 OR r.customer LIKE :s3 OR r.item LIKE :s4 OR r.description LIKE :s5 OR r.your_reference LIKE :s6)';
|
|
$like = '%' . $q . '%';
|
|
foreach (['s1', 's2', 's3', 's4', 's5', 's6'] as $k) $p[$k] = $like;
|
|
}
|
|
|
|
return [$w, $p];
|
|
}
|
|
|
|
$action = $_GET['action'] ?? '';
|
|
|
|
if ($action === 'filters') {
|
|
$opts = MagoSalesStats::filterOptions($pdo);
|
|
try {
|
|
$opts['items'] = $pdo->query("
|
|
SELECT item AS code, MAX(description) AS name, COUNT(*) AS n
|
|
FROM mago_sale_ord_detail
|
|
WHERE delivered = 0 AND cancelled = 0 AND item <> ''
|
|
GROUP BY item
|
|
ORDER BY item
|
|
")->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
$opts['items'] = [];
|
|
}
|
|
MagoSalesStats::jsonOut($opts);
|
|
}
|
|
|
|
if ($action === 'data') {
|
|
try {
|
|
$from = MagoSalesStats::openLinesFrom();
|
|
[$where, $params] = deliveriesWhere();
|
|
|
|
$kpi = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT COUNT(*) AS n_lines,
|
|
COUNT(DISTINCT r.sale_ord_id) AS orders,
|
|
COALESCE(SUM(r.residual_value), 0) AS value,
|
|
SUM(r.due_date < CURDATE()) AS late_lines,
|
|
COALESCE(SUM(CASE WHEN r.due_date < CURDATE() THEN r.residual_value END), 0) AS late_value,
|
|
SUM(r.client_date < CURDATE()) AS late_client_lines,
|
|
COALESCE(SUM(CASE WHEN r.client_date < CURDATE() THEN r.residual_value END), 0) AS late_client_value,
|
|
SUM(r.due_date >= CURDATE() AND r.due_date <= CURDATE() + INTERVAL 7 DAY) AS due7_lines,
|
|
COALESCE(SUM(CASE WHEN r.due_date >= CURDATE() AND r.due_date <= CURDATE() + INTERVAL 7 DAY THEN r.residual_value END), 0) AS due7_value,
|
|
SUM(r.due_date IS NULL) AS nodate_lines,
|
|
AVG(CASE WHEN r.due_date < CURDATE() THEN DATEDIFF(CURDATE(), r.due_date) END) AS avg_late_days,
|
|
SUM(r.zibo_date IS NULL) AS no_zibo_lines
|
|
FROM $from
|
|
WHERE $where
|
|
", $params)[0];
|
|
|
|
// Carico per settimana (data ZIBO, se manca data cliente): scaduto + prossime 12 settimane + oltre + senza data
|
|
$weekly = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT CASE
|
|
WHEN r.due_date IS NULL THEN 'nd'
|
|
WHEN r.due_date < CURDATE() THEN 'late'
|
|
WHEN r.due_date >= (CURDATE() - INTERVAL WEEKDAY(CURDATE()) DAY) + INTERVAL 12 WEEK THEN 'over'
|
|
ELSE DATE_FORMAT(r.due_date - INTERVAL WEEKDAY(r.due_date) DAY, '%Y-%m-%d')
|
|
END AS wk,
|
|
COUNT(*) AS n,
|
|
COALESCE(SUM(r.residual_value), 0) AS v
|
|
FROM $from
|
|
WHERE $where
|
|
GROUP BY wk
|
|
", $params);
|
|
|
|
// Clienti con più ritardo (data ZIBO)
|
|
$lateCustomers = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT r.customer AS code, r.company_name AS name,
|
|
COUNT(*) AS n, COALESCE(SUM(r.residual_value), 0) AS v
|
|
FROM $from
|
|
WHERE $where AND r.due_date < CURDATE()
|
|
GROUP BY r.customer, r.company_name
|
|
ORDER BY v DESC
|
|
LIMIT 10
|
|
", $params);
|
|
|
|
$rows = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT r.sale_ord_id, r.internal_ord_no, r.order_date, r.customer, r.company_name,
|
|
r.item, r.description, r.uom, r.qty, r.delivered_qty, r.residual_qty, r.residual_value,
|
|
r.zibo_date, r.client_date, r.due_date, r.salesperson, r.status,
|
|
CASE WHEN r.due_date < CURDATE() THEN DATEDIFF(CURDATE(), r.due_date) ELSE 0 END AS late_days,
|
|
CASE WHEN r.due_date >= CURDATE() THEN DATEDIFF(r.due_date, CURDATE()) END AS days_left
|
|
FROM $from
|
|
WHERE $where
|
|
ORDER BY (r.due_date IS NULL), r.due_date, r.internal_ord_no, r.line
|
|
LIMIT 5000
|
|
", $params);
|
|
|
|
MagoSalesStats::jsonOut([
|
|
'success' => true,
|
|
'today' => date('Y-m-d'),
|
|
'kpi' => $kpi,
|
|
'weekly' => $weekly,
|
|
'late_customers' => $lateCustomers,
|
|
'rows' => $rows,
|
|
]);
|
|
} 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>Consegne - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.49.1"></script>
|
|
|
|
<style>
|
|
:root {
|
|
--c-ink: #1f2d3d;
|
|
--c-muted: #64748b;
|
|
--c-line: #e2e8f0;
|
|
--c-zibo: #dc3545;
|
|
--c-client: #ea580c;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.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: .9rem;
|
|
cursor: pointer;
|
|
transition: all .15s ease;
|
|
}
|
|
|
|
.chip.active {
|
|
background: var(--chip-c, #198754);
|
|
border-color: var(--chip-c, #198754);
|
|
color: #fff;
|
|
}
|
|
|
|
.metric-switch {
|
|
display: inline-flex;
|
|
background: #f1f5f9;
|
|
border-radius: 12px;
|
|
padding: 4px;
|
|
gap: 4px;
|
|
}
|
|
|
|
.metric-switch button {
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 6px 14px;
|
|
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);
|
|
}
|
|
|
|
.search-input {
|
|
border-radius: 12px !important;
|
|
border: 2px solid #198754 !important;
|
|
padding: 9px 14px !important;
|
|
}
|
|
|
|
.select2-container--bootstrap-5 .select2-selection {
|
|
border-radius: 10px;
|
|
min-height: 42px;
|
|
}
|
|
|
|
/* KPI pastello */
|
|
.kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
.kpi-grid {
|
|
grid-template-columns: repeat(5, 1fr);
|
|
}
|
|
}
|
|
|
|
.kpi {
|
|
background: color-mix(in srgb, var(--k) 14%, #fff);
|
|
border: 1px solid color-mix(in srgb, var(--k) 28%, #fff);
|
|
border-radius: 16px;
|
|
padding: 16px 18px;
|
|
position: relative;
|
|
cursor: pointer;
|
|
transition: transform .15s ease, box-shadow .15s ease;
|
|
}
|
|
|
|
.kpi:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 24px rgba(15, 23, 42, .08);
|
|
}
|
|
|
|
.kpi.selected {
|
|
background: color-mix(in srgb, var(--k) 24%, #fff);
|
|
box-shadow: 0 0 0 2px var(--k) inset;
|
|
}
|
|
|
|
.kpi-label {
|
|
padding-right: 44px;
|
|
font-size: .78rem;
|
|
font-weight: 800;
|
|
color: color-mix(in srgb, var(--k) 70%, #1f2d3d);
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
}
|
|
|
|
.kpi-value {
|
|
font-size: 1.75rem;
|
|
font-weight: 800;
|
|
color: var(--c-ink);
|
|
line-height: 1.15;
|
|
margin-top: 4px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.kpi-sub {
|
|
font-size: .85rem;
|
|
color: var(--c-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.kpi-icon {
|
|
position: absolute;
|
|
right: 14px;
|
|
top: 14px;
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 11px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #fff;
|
|
color: var(--k);
|
|
font-size: 1.05rem;
|
|
box-shadow: 0 2px 6px rgba(15, 23, 42, .06);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* date */
|
|
.dt-zibo {
|
|
font-weight: 700;
|
|
color: var(--c-ink);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.dt-zibo.late {
|
|
color: var(--c-zibo);
|
|
}
|
|
|
|
.dt-client {
|
|
font-size: .82rem;
|
|
color: #64748b;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.dt-client.late {
|
|
color: var(--c-client);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.dt-tag {
|
|
display: inline-block;
|
|
font-size: .68rem;
|
|
font-weight: 800;
|
|
letter-spacing: .3px;
|
|
text-transform: uppercase;
|
|
margin-right: 4px;
|
|
opacity: .75;
|
|
}
|
|
|
|
.dt-fallback {
|
|
font-size: .72rem;
|
|
color: #94a3b8;
|
|
font-style: italic;
|
|
}
|
|
|
|
.week-badge {
|
|
display: inline-block;
|
|
background: #fef3c7;
|
|
color: #b45309;
|
|
font-size: .66rem;
|
|
font-weight: 800;
|
|
border-radius: 999px;
|
|
padding: 1px 7px;
|
|
margin-left: 5px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.late-days {
|
|
display: inline-block;
|
|
background: #fde8ea;
|
|
color: var(--c-zibo);
|
|
font-weight: 800;
|
|
font-size: .8rem;
|
|
padding: 3px 9px;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.days-left {
|
|
font-size: .8rem;
|
|
color: var(--c-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.days-left.soon {
|
|
color: #b45309;
|
|
font-weight: 700;
|
|
}
|
|
|
|
#linesTable thead th {
|
|
background: #b9ebc7;
|
|
color: var(--c-ink);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#linesTable td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.amount {
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.legend-dates {
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
font-size: .85rem;
|
|
color: var(--c-muted);
|
|
}
|
|
|
|
.legend-dates b.z {
|
|
color: var(--c-zibo);
|
|
}
|
|
|
|
.legend-dates b.c {
|
|
color: var(--c-client);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.order-link {
|
|
font-weight: 800;
|
|
color: #0d6efd;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.order-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.sc-link {
|
|
color: #198754;
|
|
margin-left: 6px;
|
|
opacity: .75;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.sc-link:hover {
|
|
opacity: 1;
|
|
}
|
|
</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"><i class="fa-solid fa-truck-fast"></i></div>
|
|
<div>
|
|
<h5 class="mb-1" style="font-weight:800;">Statistiche Ordini Cliente</h5>
|
|
<span class="sync-pill" id="syncPill"><span class="dot"></span><span id="syncText">Dati Mago4</span></span>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">↩️ Torna alla Dashboard</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body pt-0">
|
|
|
|
<div class="stats-tabs">
|
|
<?php if ($canStats) : ?>
|
|
<a href="sales_summary.php"><i class="fa-solid fa-gauge-high me-1"></i> Sommario</a>
|
|
<a href="sales_orders_stats.php"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
|
|
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
|
|
<a href="sales_customer.php"><i class="fa-solid fa-address-card me-1"></i> Scheda cliente</a>
|
|
<?php if (userCan('sales.matrici.view')) : ?>
|
|
<a href="sales_matrici.php"><i class="fa-solid fa-shapes me-1"></i> Matrici</a>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
<a href="sales_orders_deliveries.php" class="active"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
|
|
<button type="button" class="sh-page ms-auto align-self-center mb-1" data-help-open="page.deliveries"><i class="fa-regular fa-circle-question me-1"></i> Come si legge</button>
|
|
</div>
|
|
|
|
<!-- FILTRI -->
|
|
<div class="filters-box mb-4">
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
|
<div>
|
|
<span class="flabel mb-1" data-help="dl.horizon">Scadenza</span>
|
|
<div class="chip-row" id="horizonChips">
|
|
<button type="button" class="chip active" data-h="">Tutte</button>
|
|
<button type="button" class="chip" data-h="late_zibo" style="--chip-c:var(--c-zibo);">Scadute (data ZIBO)</button>
|
|
<button type="button" class="chip" data-h="late_client" style="--chip-c:var(--c-client);">Oltre data cliente</button>
|
|
<button type="button" class="chip" data-h="week" style="--chip-c:#b45309;">Questa settimana</button>
|
|
<button type="button" class="chip" data-h="next_week" style="--chip-c:#ca8a04;">Prossima settimana</button>
|
|
<button type="button" class="chip" data-h="30d" style="--chip-c:#0d6efd;">Entro 30 gg</button>
|
|
<button type="button" class="chip" data-h="nodate" style="--chip-c:#64748b;">Senza data</button>
|
|
</div>
|
|
</div>
|
|
<div class="legend-dates">
|
|
<span><b class="z">Data ZIBO</b> = settimana di consegna comunicata (rossa a settimana finita)</span>
|
|
<span><b class="c">Data cliente</b> = desiderata (arancione se superata)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 align-items-end mt-1">
|
|
<div class="col-12 col-md-3">
|
|
<label class="flabel" for="fCustomers">Clienti</label>
|
|
<select id="fCustomers" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-12 col-md-3">
|
|
<label class="flabel" for="fItems">Articoli</label>
|
|
<select id="fItems" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<label class="flabel" for="fSalespeople">Agente</label>
|
|
<select id="fSalespeople" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-6 col-md-1">
|
|
<label class="flabel" for="fAreas">Area</label>
|
|
<select id="fAreas" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-12 col-md-3 d-flex gap-2">
|
|
<input type="text" id="fSearch" class="form-control search-input" placeholder="🔎 Ordine, cliente, articolo…">
|
|
<button type="button" class="btn btn-outline-secondary" id="btnReset" title="Azzera filtri" style="border-radius:12px;">
|
|
<i class="fa-solid fa-eraser"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="content">
|
|
<div class="warn-strip mb-3" id="noZibo" style="display:none;"></div>
|
|
|
|
<!-- KPI -->
|
|
<div class="kpi-grid mb-4" id="kpiGrid">
|
|
<div class="kpi" data-h="" style="--k:#198754;">
|
|
<div class="kpi-icon"><i class="fa-solid fa-boxes-stacked"></i></div>
|
|
<div class="kpi-label" data-help="dl.total">Da consegnare</div>
|
|
<div class="kpi-value" id="kValue">—</div>
|
|
<div class="kpi-sub" id="kValueSub"> </div>
|
|
</div>
|
|
<div class="kpi" data-h="late_zibo" style="--k:var(--c-zibo);">
|
|
<div class="kpi-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
|
<div class="kpi-label" data-help="dl.lateZibo">Scadute (data ZIBO)</div>
|
|
<div class="kpi-value" id="kLate">—</div>
|
|
<div class="kpi-sub" id="kLateSub"> </div>
|
|
</div>
|
|
<div class="kpi" data-h="late_client" style="--k:var(--c-client);">
|
|
<div class="kpi-icon"><i class="fa-solid fa-user-clock"></i></div>
|
|
<div class="kpi-label" data-help="dl.lateClient">Oltre data cliente</div>
|
|
<div class="kpi-value" id="kLateClient">—</div>
|
|
<div class="kpi-sub" id="kLateClientSub"> </div>
|
|
</div>
|
|
<div class="kpi" data-h="7d" style="--k:#b45309;">
|
|
<div class="kpi-icon"><i class="fa-solid fa-calendar-week"></i></div>
|
|
<div class="kpi-label" data-help="dl.next7">Prossimi 7 giorni</div>
|
|
<div class="kpi-value" id="kDue7">—</div>
|
|
<div class="kpi-sub" id="kDue7Sub"> </div>
|
|
</div>
|
|
<div class="kpi" data-h="nodate" style="--k:#64748b;">
|
|
<div class="kpi-icon"><i class="fa-solid fa-calendar-xmark"></i></div>
|
|
<div class="kpi-label" data-help="dl.nodate">Senza data</div>
|
|
<div class="kpi-value" id="kNoDate">—</div>
|
|
<div class="kpi-sub">righe senza data di consegna</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end mb-2"><button type="button" data-layout-btn="sales_orders_deliveries"></button></div>
|
|
<!-- RIQUADRI (ordine personalizzabile per utente: ui_layout.js) -->
|
|
<div class="row g-3 mb-3" <?= UiPrefs::attr($pdo, $iduserlogin ?? 0, 'sales_orders_deliveries') ?>>
|
|
<div class="col-xl-8" data-widget="weekly" data-title="Carico consegne per settimana">
|
|
<div class="chart-card">
|
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
|
|
<div>
|
|
<div class="chart-title" data-help="dl.weekly">Carico consegne per settimana</div>
|
|
<div class="chart-hint">Per data ZIBO (se manca: data cliente) · clicca una barra per filtrare</div>
|
|
</div>
|
|
<div class="metric-switch" id="metricSwitch">
|
|
<button type="button" data-metric="value" class="active">€ Valore</button>
|
|
<button type="button" data-metric="count"># Righe</button>
|
|
</div>
|
|
</div>
|
|
<div id="chWeekly" style="min-height:340px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4" data-widget="latecust" data-title="Clienti con più ritardo">
|
|
<div class="chart-card">
|
|
<div class="chart-title" data-help="dl.lateCust">Clienti con più ritardo</div>
|
|
<div class="chart-hint">Valore residuo scaduto (data ZIBO) · clicca per filtrare</div>
|
|
<div id="chLateCust" style="min-height:340px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12" data-widget="rows" data-title="Righe da consegnare">
|
|
<div class="chart-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap gap-2">
|
|
<div class="chart-title" data-help="dl.rows">Righe da consegnare <span class="text-muted fw-normal" id="rowsInfo"></span></div>
|
|
<div class="chart-hint">Ordinate per scadenza<?= $canStats ? ' · clicca il numero ordine per aprirlo' : '' ?></div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table id="linesTable" class="table table-striped align-middle w-100">
|
|
<thead>
|
|
<tr>
|
|
<th>Ordine</th>
|
|
<th>Cliente</th>
|
|
<th>Articolo</th>
|
|
<th class="text-end">Residuo</th>
|
|
<th>Consegna (ZIBO / cliente)</th>
|
|
<th>Ritardo</th>
|
|
<th class="text-end">Valore residuo</th>
|
|
<th>Agente</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include('include/footer.php'); ?>
|
|
</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 STORE_KEY = 'zibo_sales_deliveries_filters_v1';
|
|
const CAN_STATS = <?= $canStats ? 'true' : 'false' ?>; // il link al dettaglio ordine porta alle statistiche
|
|
const charts = {};
|
|
let table = null;
|
|
let metric = 'value';
|
|
let horizon = '';
|
|
let loadTimer = null;
|
|
let seq = 0;
|
|
|
|
/* ---------------- UTILS ---------------- */
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
} [c]));
|
|
}
|
|
const fmtNum = n => Number(n || 0).toLocaleString('it-IT');
|
|
const fmtQty = n => Number(Math.round(+n * 100) / 100).toLocaleString('it-IT');
|
|
const fmtEur = (n, d) => Number(n || 0).toLocaleString('it-IT', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: d || 0,
|
|
maximumFractionDigits: d || 0
|
|
});
|
|
|
|
function fmtEurShort(n) {
|
|
const v = Number(n || 0),
|
|
a = Math.abs(v);
|
|
if (a >= 1e6) return (v / 1e6).toLocaleString('it-IT', {
|
|
maximumFractionDigits: 2
|
|
}) + ' M€';
|
|
if (a >= 1e4) return (v / 1e3).toLocaleString('it-IT', {
|
|
maximumFractionDigits: 1
|
|
}) + ' k€';
|
|
return fmtEur(v);
|
|
}
|
|
|
|
function fmtDate(d) {
|
|
if (!d) return '';
|
|
const p = String(d).substring(0, 10).split('-');
|
|
return p.length === 3 ? `${p[2]}/${p[1]}/${p[0]}` : d;
|
|
}
|
|
|
|
function iso(d) {
|
|
const z = n => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
|
|
}
|
|
|
|
const fmtMetric = v => metric === 'value' ? fmtEur(v) : fmtNum(Math.round(v));
|
|
const fmtAxis = v => metric === 'value' ? fmtEurShort(v) : fmtNum(Math.round(v));
|
|
|
|
// Data ZIBO = settimana di consegna (impostazioni in MagoSalesStats)
|
|
const ZIBO_WEEK = <?= MagoSalesStats::ZIBO_IS_WEEK ? 'true' : 'false' ?>;
|
|
const ZIBO_END_DAYS = <?= (int)MagoSalesStats::ZIBO_WEEK_END_DAYS ?>;
|
|
|
|
// scadenza della data ZIBO: fine della settimana indicata
|
|
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);
|
|
}
|
|
|
|
// stato della data ZIBO: 'late' (settimana finita), 'week' (settimana in corso), '' (futura)
|
|
function ziboState(z, open) {
|
|
if (!z || !open) return '';
|
|
const today = iso(new Date());
|
|
if (ziboEnd(z) < today) return 'late';
|
|
if (ZIBO_WEEK && String(z).substring(0, 10) <= today) return 'week';
|
|
return '';
|
|
}
|
|
|
|
function ziboHtml(z, open, extra) {
|
|
const st = ziboState(z, open);
|
|
const title = ZIBO_WEEK ? ` title="Settimana del ${fmtDate(z)}: consegna entro ${fmtDate(ziboEnd(z))}"` : '';
|
|
return `<div class="dt-zibo${st === 'late' ? ' late' : ''}"${title}><span class="dt-tag">ZIBO${ZIBO_WEEK ? ' sett.' : ''}</span>${fmtDate(z)}` +
|
|
(st === 'week' ? '<span class="week-badge">IN SETTIMANA</span>' : '') + (extra || '') + '</div>';
|
|
}
|
|
|
|
function dateCell(zibo, client) {
|
|
const today = iso(new Date());
|
|
let h = zibo ? ziboHtml(zibo, true) : '<div class="dt-fallback">data ZIBO non indicata</div>';
|
|
if (client) h += `<div class="dt-client${client < today ? ' late' : ''}"><span class="dt-tag">Cliente</span>${fmtDate(client)}</div>`;
|
|
return h;
|
|
}
|
|
|
|
/* ---------------- FILTRI ---------------- */
|
|
function save() {
|
|
try {
|
|
localStorage.setItem(STORE_KEY, JSON.stringify({
|
|
horizon,
|
|
metric,
|
|
customers: $('#fCustomers').val() || [],
|
|
items: $('#fItems').val() || [],
|
|
salespeople: $('#fSalespeople').val() || [],
|
|
areas: $('#fAreas').val() || [],
|
|
search: $('#fSearch').val()
|
|
}));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function restore() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(STORE_KEY) || 'null');
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function scheduleLoad(d) {
|
|
clearTimeout(loadTimer);
|
|
loadTimer = setTimeout(loadData, d === undefined ? 300 : d);
|
|
}
|
|
|
|
function setHorizon(h) {
|
|
horizon = h || '';
|
|
$('#horizonChips .chip').removeClass('active');
|
|
$(`#horizonChips .chip[data-h="${horizon}"]`).addClass('active');
|
|
$('.kpi').removeClass('selected');
|
|
if (horizon) $(`.kpi[data-h="${horizon}"]`).addClass('selected');
|
|
scheduleLoad(0);
|
|
}
|
|
|
|
function renderSyncInfo(s) {
|
|
const $p = $('#syncPill').removeClass('ok err');
|
|
const o = s && s.sale_ord_details ? s.sale_ord_details : (s && s.sale_ord);
|
|
if (!s || s.error || !o || !o.last_run_at) {
|
|
$('#syncText').text(s && s.error ? s.error : 'Righe ordine mai sincronizzate');
|
|
if (s && s.error) $p.addClass('err');
|
|
return;
|
|
}
|
|
$p.addClass(o.last_status === 'ok' ? 'ok' : 'err');
|
|
$('#syncText').text(`Aggiornato il ${o.last_run_at}`);
|
|
}
|
|
|
|
function loadFilters() {
|
|
return fetch('?action=filters').then(r => r.json()).then(d => {
|
|
renderSyncInfo(d.sync);
|
|
const fill = (id, list, label) => {
|
|
const $s = $(id).empty();
|
|
(list || []).forEach(o => $s.append(new Option(label(o), o.code, false, false)));
|
|
};
|
|
fill('#fCustomers', d.customers, o => `${o.name} (${o.code})`);
|
|
fill('#fItems', d.items, o => `${o.code} · ${o.name || ''}`);
|
|
fill('#fSalespeople', d.salespeople, o => o.code);
|
|
fill('#fAreas', d.areas, o => o.code);
|
|
});
|
|
}
|
|
|
|
/* ---------------- DATI ---------------- */
|
|
function loadData() {
|
|
save();
|
|
const p = new URLSearchParams({
|
|
action: 'data'
|
|
});
|
|
if (horizon) p.append('horizon', horizon);
|
|
const s = $('#fSearch').val().trim();
|
|
if (s) p.append('search', s);
|
|
[
|
|
['customers', '#fCustomers'],
|
|
['items', '#fItems'],
|
|
['salespeople', '#fSalespeople'],
|
|
['areas', '#fAreas']
|
|
].forEach(([k, id]) => ($(id).val() || []).forEach(v => p.append(k + '[]', v)));
|
|
|
|
const my = ++seq;
|
|
$('#content').css('opacity', .55);
|
|
fetch('?' + p.toString()).then(r => r.json()).then(d => {
|
|
if (my !== seq) return;
|
|
$('#content').css('opacity', 1);
|
|
if (!d.success) {
|
|
alert(d.message || 'Errore');
|
|
return;
|
|
}
|
|
renderKpi(d.kpi);
|
|
renderWeekly(d.weekly);
|
|
renderLateCustomers(d.late_customers);
|
|
renderTable(d.rows);
|
|
}).catch(e => {
|
|
$('#content').css('opacity', 1);
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
function renderKpi(k) {
|
|
$('#kValue').text(fmtEurShort(k.value)).attr('title', fmtEur(k.value, 2));
|
|
$('#kValueSub').text(`${fmtNum(k.n_lines)} righe · ${fmtNum(k.orders)} ordini`);
|
|
$('#kLate').text(fmtNum(k.late_lines) + ' righe');
|
|
$('#kLateSub').html(`<b style="color:var(--c-zibo);">${fmtEurShort(k.late_value)}</b>` +
|
|
(k.avg_late_days ? ` · ritardo medio ${Math.round(k.avg_late_days)} gg` : ''));
|
|
$('#kLateClient').text(fmtNum(k.late_client_lines) + ' righe');
|
|
$('#kLateClientSub').html(`<b style="color:var(--c-client);">${fmtEurShort(k.late_client_value)}</b> desiderata superata`);
|
|
$('#kDue7').text(fmtNum(k.due7_lines) + ' righe');
|
|
$('#kDue7Sub').text(fmtEurShort(k.due7_value) + ' in scadenza');
|
|
$('#kNoDate').text(fmtNum(k.nodate_lines));
|
|
|
|
const nz = +k.no_zibo_lines || 0;
|
|
$('#noZibo').toggle(nz > 0).html(nz > 0 ?
|
|
`<i class="fa-solid fa-circle-info me-1"></i> ${fmtNum(nz)} righe senza data ZIBO: per queste la scadenza è la data cliente.` : '');
|
|
}
|
|
|
|
function upsert(key, el, opts) {
|
|
if (charts[key]) charts[key].updateOptions(opts, true, true);
|
|
else {
|
|
charts[key] = new ApexCharts(document.getElementById(el), opts);
|
|
charts[key].render();
|
|
}
|
|
}
|
|
|
|
const base = {
|
|
fontFamily: 'inherit',
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
animations: {
|
|
speed: 350
|
|
}
|
|
};
|
|
|
|
function renderWeekly(rows) {
|
|
const map = {};
|
|
rows.forEach(r => map[r.wk] = r);
|
|
const today = new Date();
|
|
const monday = new Date(today);
|
|
monday.setDate(today.getDate() - ((today.getDay() + 6) % 7));
|
|
|
|
const cats = [{
|
|
key: 'late',
|
|
label: 'Scaduto',
|
|
color: '#dc3545',
|
|
h: 'late_zibo'
|
|
}];
|
|
for (let i = 0; i < 12; i++) {
|
|
const d = new Date(monday);
|
|
d.setDate(monday.getDate() + i * 7);
|
|
cats.push({
|
|
key: iso(d),
|
|
label: i === 0 ? 'Questa sett.' : `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`,
|
|
color: i === 0 ? '#b45309' : (i === 1 ? '#ca8a04' : '#0d6efd'),
|
|
h: i === 0 ? 'week' : (i === 1 ? 'next_week' : '')
|
|
});
|
|
}
|
|
cats.push({
|
|
key: 'over',
|
|
label: 'Oltre',
|
|
color: '#64748b',
|
|
h: ''
|
|
}, {
|
|
key: 'nd',
|
|
label: 'Senza data',
|
|
color: '#cbd5e1',
|
|
h: 'nodate'
|
|
});
|
|
|
|
upsert('weekly', 'chWeekly', {
|
|
chart: {
|
|
...base,
|
|
type: 'bar',
|
|
height: 340,
|
|
events: {
|
|
dataPointSelection: (e, ctx, cfg) => {
|
|
const c = cats[cfg.dataPointIndex];
|
|
if (c && c.h) setHorizon(c.h);
|
|
}
|
|
}
|
|
},
|
|
series: [{
|
|
name: metric === 'value' ? 'Valore residuo' : 'Righe',
|
|
data: cats.map(c => map[c.key] ? (metric === 'value' ? +map[c.key].v : +map[c.key].n) : 0)
|
|
}],
|
|
colors: cats.map(c => c.color),
|
|
plotOptions: {
|
|
bar: {
|
|
distributed: true,
|
|
borderRadius: 5,
|
|
columnWidth: '60%'
|
|
}
|
|
},
|
|
legend: {
|
|
show: false
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
xaxis: {
|
|
categories: cats.map(c => c.label),
|
|
labels: {
|
|
rotate: -45
|
|
}
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: fmtAxis
|
|
}
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: fmtMetric
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#eef2f7'
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderLateCustomers(rows) {
|
|
upsert('latecust', 'chLateCust', {
|
|
chart: {
|
|
...base,
|
|
type: 'bar',
|
|
height: 340,
|
|
events: {
|
|
dataPointSelection: (e, ctx, cfg) => {
|
|
const r = rows[cfg.dataPointIndex];
|
|
if (r) $('#fCustomers').val([r.code]).trigger('change');
|
|
}
|
|
}
|
|
},
|
|
series: [{
|
|
name: 'Valore scaduto',
|
|
data: rows.map(r => +r.v)
|
|
}],
|
|
colors: ['#dc3545'],
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: true,
|
|
borderRadius: 4,
|
|
barHeight: '65%'
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
xaxis: {
|
|
categories: rows.map(r => (r.name || r.code).length > 26 ? (r.name || r.code).substring(0, 24) + '…' : (r.name || r.code)),
|
|
labels: {
|
|
formatter: v => fmtEurShort(v)
|
|
}
|
|
},
|
|
tooltip: {
|
|
custom: ({
|
|
dataPointIndex
|
|
}) => {
|
|
const r = rows[dataPointIndex];
|
|
return r ? `<div style="padding:8px 12px;font-size:.85rem;"><b>${esc(r.name)}</b><br>${fmtNum(r.n)} righe scadute · <b>${fmtEur(r.v)}</b></div>` : '';
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#eef2f7'
|
|
},
|
|
noData: {
|
|
text: 'Nessuna riga scaduta 🎉'
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderTable(rows) {
|
|
$('#rowsInfo').text(rows.length >= 5000 ? '(prime 5.000: restringi i filtri)' : `(${fmtNum(rows.length)})`);
|
|
|
|
const data = rows.map(r => {
|
|
const late = +r.late_days > 0;
|
|
const left = r.days_left !== null && r.days_left !== undefined ? +r.days_left : null;
|
|
return [
|
|
(CAN_STATS ?
|
|
`<a class="order-link" href="sales_orders_stats.php?q=${encodeURIComponent(r.internal_ord_no)}" target="_blank">${esc(r.internal_ord_no)}</a>` :
|
|
`<b>${esc(r.internal_ord_no)}</b>`) + `<div class="small text-muted">${fmtDate(r.order_date)}</div>`,
|
|
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}${CAN_STATS ? `<a class="sc-link" href="sales_customer.php?c=${encodeURIComponent(r.customer)}" title="Apri la scheda cliente" onclick="event.stopPropagation();"><i class="fa-solid fa-address-card"></i></a>` : ''}</div>`,
|
|
`<b>${esc(r.item)}</b><div class="small text-muted" style="white-space:normal;max-width:300px;">${esc(r.description)}</div>`,
|
|
`<span data-order="${+r.residual_qty}" class="amount"><b>${fmtQty(r.residual_qty)}</b> ${esc(r.uom)}</span><div class="small text-muted">di ${fmtQty(r.qty)}</div>`,
|
|
`<span data-order="${esc(r.due_date || '9999-12-31')}">${dateCell(r.zibo_date, r.client_date)}</span>`,
|
|
`<span data-order="${late ? +r.late_days : (left !== null ? -left : -99999)}">` +
|
|
(late ? `<span class="late-days">+${fmtNum(r.late_days)} gg</span>` :
|
|
(left !== null ? `<span class="days-left${left <= 7 ? ' soon' : ''}">tra ${fmtNum(left)} gg</span>` : '<span class="text-muted">—</span>')) + '</span>',
|
|
`<span data-order="${+r.residual_value}" class="amount">${fmtEur(r.residual_value, 2)}</span>`,
|
|
esc(r.salesperson)
|
|
];
|
|
});
|
|
|
|
if (table) {
|
|
table.clear().rows.add(data).draw(false);
|
|
return;
|
|
}
|
|
|
|
// ordinamento sul valore in data-order del primo elemento della cella
|
|
const sortable = (type) => (d, t) => {
|
|
if (t !== 'sort' && t !== 'type') return d;
|
|
const v = $(d).first().data('order');
|
|
return type === 'num' ? (+v || 0) : (v || '');
|
|
};
|
|
table = $('#linesTable').DataTable({
|
|
data: data,
|
|
columns: [{}, {}, {}, {
|
|
className: 'text-end',
|
|
orderSequence: ['desc', 'asc']
|
|
}, {}, {
|
|
orderSequence: ['desc', 'asc']
|
|
}, {
|
|
className: 'text-end',
|
|
orderSequence: ['desc', 'asc']
|
|
}, {}],
|
|
columnDefs: [{
|
|
targets: [3, 5, 6],
|
|
type: 'num',
|
|
render: sortable('num')
|
|
}, {
|
|
targets: [4],
|
|
type: 'string',
|
|
render: sortable('str')
|
|
}],
|
|
order: [
|
|
[4, 'asc']
|
|
],
|
|
pageLength: 25,
|
|
lengthMenu: [10, 25, 50, 100],
|
|
dom: "<'row mb-2'<'col-md-6'B><'col-md-6'f>>rt<'row mt-2'<'col-md-5'i><'col-md-7'p>>",
|
|
buttons: {
|
|
dom: {
|
|
button: {
|
|
className: 'btn'
|
|
}
|
|
},
|
|
buttons: [{
|
|
extend: 'excelHtml5',
|
|
text: '<i class="fa-solid fa-file-excel me-1"></i> Excel',
|
|
className: 'btn btn-outline-success btn-sm',
|
|
title: 'Righe da consegnare',
|
|
exportOptions: {
|
|
format: {
|
|
body: (d, row, col) => {
|
|
if (col === 6) return +$(d).data('order');
|
|
return $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, '</span> ')).text().replace(/\s+/g, ' ').trim();
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
},
|
|
language: {
|
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ---------------- EVENTI ---------------- */
|
|
$(function() {
|
|
[
|
|
['#fCustomers', 'Tutti i clienti'],
|
|
['#fItems', 'Tutti gli articoli'],
|
|
['#fSalespeople', 'Tutti'],
|
|
['#fAreas', 'Tutte']
|
|
].forEach(([id, ph]) => {
|
|
$(id).select2({
|
|
theme: 'bootstrap-5',
|
|
placeholder: ph,
|
|
allowClear: true,
|
|
closeOnSelect: false,
|
|
width: '100%'
|
|
}).on('change', () => scheduleLoad());
|
|
});
|
|
|
|
$('#horizonChips').on('click', '.chip', function() {
|
|
setHorizon($(this).data('h'));
|
|
});
|
|
$('#kpiGrid').on('click', '.kpi', function() {
|
|
const h = $(this).data('h') || '';
|
|
setHorizon(horizon === h ? '' : h);
|
|
});
|
|
$('#metricSwitch').on('click', 'button', function() {
|
|
metric = $(this).data('metric');
|
|
$('#metricSwitch button').removeClass('active');
|
|
$(this).addClass('active');
|
|
scheduleLoad(0);
|
|
});
|
|
$('#fSearch').on('input', () => scheduleLoad(450));
|
|
$('#btnReset').on('click', function() {
|
|
$('#fCustomers, #fItems, #fSalespeople, #fAreas').val(null).trigger('change.select2');
|
|
$('#fSearch').val('');
|
|
setHorizon('');
|
|
});
|
|
|
|
loadFilters().then(() => {
|
|
const s = restore();
|
|
if (s) {
|
|
metric = s.metric === 'count' ? 'count' : 'value';
|
|
$('#metricSwitch button').removeClass('active');
|
|
$(`#metricSwitch button[data-metric="${metric}"]`).addClass('active');
|
|
$('#fCustomers').val(s.customers || []).trigger('change.select2');
|
|
$('#fItems').val(s.items || []).trigger('change.select2');
|
|
$('#fSalespeople').val(s.salespeople || []).trigger('change.select2');
|
|
$('#fAreas').val(s.areas || []).trigger('change.select2');
|
|
$('#fSearch').val(s.search || '');
|
|
horizon = s.horizon || '';
|
|
}
|
|
setHorizon(horizon);
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
<script src="sales_help.js?v=3"></script>
|
|
<script src="ui_layout.js?v=1"></script>
|
|
</body>
|
|
|
|
</html>
|