1289 lines
49 KiB
PHP
1289 lines
49 KiB
PHP
<?php
|
|
ob_start();
|
|
ini_set('display_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
include('include/headscript.php');
|
|
require_once __DIR__ . '/class/MagoSalesStats.php';
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
// --- Permessi: pagina visibile solo con 'sales.stats.view' ---
|
|
if (!userCan('sales.stats.view')) {
|
|
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
|
|
* ============================================================ */
|
|
|
|
$action = $_GET['action'] ?? '';
|
|
|
|
if ($action === 'filters') {
|
|
MagoSalesStats::jsonOut(MagoSalesStats::filterOptions($pdo));
|
|
}
|
|
|
|
if ($action === 'data') {
|
|
try {
|
|
$from = MagoSalesStats::baseFrom();
|
|
|
|
// Filtri cliente / agente / area (le date qui non si usano: si ragiona per anni)
|
|
[$where, $params] = MagoSalesStats::buildWhere(['from' => '', 'to' => '']);
|
|
|
|
// "A pari data": ogni anno considerato solo fino al giorno/mese di oggi
|
|
$ytd = !empty($_GET['ytd']);
|
|
$ytdCond = $ytd ? " AND DATE_FORMAT(x.order_date, '%m-%d') <= DATE_FORMAT(CURDATE(), '%m-%d')" : '';
|
|
|
|
// Metrica: numero ordini o valore (imponibile); gli annullati sono sempre esclusi
|
|
$metric = (($_GET['metric'] ?? '') === 'value') ? 'value' : 'count';
|
|
$m = $metric === 'value'
|
|
? "CASE WHEN x.status <> 'annullato' THEN x.taxable_amount ELSE 0 END"
|
|
: "CASE WHEN x.status <> 'annullato' THEN 1 ELSE 0 END";
|
|
|
|
// --- Riepilogo per TUTTI gli anni (serve anche l'anno prima del primo selezionato per la variazione) ---
|
|
$summary = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT YEAR(x.order_date) AS y,
|
|
SUM(x.status <> 'annullato') AS orders,
|
|
COALESCE(SUM(CASE WHEN x.status <> 'annullato' THEN x.taxable_amount END), 0) AS value,
|
|
COUNT(DISTINCT CASE WHEN x.status <> 'annullato' THEN x.customer END) AS customers,
|
|
SUM(x.status = 'annullato') AS cancelled,
|
|
SUM(x.summary_tb_modified IS NULL) AS missing_amounts
|
|
FROM $from
|
|
WHERE $where AND x.order_date IS NOT NULL $ytdCond
|
|
GROUP BY y
|
|
ORDER BY y
|
|
", $params);
|
|
|
|
$available = array_map(function ($r) {
|
|
return (int)$r['y'];
|
|
}, $summary);
|
|
|
|
// Anni scelti (validati); default: gli ultimi 3 disponibili
|
|
$years = array_values(array_intersect(array_map('intval', MagoSalesStats::arrParam('years')), $available));
|
|
if (!$years) $years = array_slice($available, -3);
|
|
sort($years);
|
|
|
|
$monthly = [];
|
|
$customers = [];
|
|
|
|
if ($years) {
|
|
$ph = [];
|
|
$yp = $params;
|
|
foreach ($years as $i => $y) {
|
|
$ph[] = ':yy' . $i;
|
|
$yp['yy' . $i] = $y;
|
|
}
|
|
$yearIn = 'YEAR(x.order_date) IN (' . implode(',', $ph) . ')';
|
|
|
|
// --- Mensile per anno ---
|
|
$monthly = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT YEAR(x.order_date) AS y, MONTH(x.order_date) AS m, SUM($m) AS v
|
|
FROM $from
|
|
WHERE $where AND $yearIn $ytdCond
|
|
GROUP BY y, m
|
|
ORDER BY y, m
|
|
", $yp);
|
|
|
|
// --- Clienti per anno ---
|
|
$rows = MagoSalesStats::fetchAllQ($pdo, "
|
|
SELECT x.customer AS code, x.company_name AS name, YEAR(x.order_date) AS y, SUM($m) AS v
|
|
FROM $from
|
|
WHERE $where AND $yearIn $ytdCond AND x.customer <> ''
|
|
GROUP BY x.customer, x.company_name, y
|
|
", $yp);
|
|
|
|
foreach ($rows as $r) {
|
|
$k = $r['code'];
|
|
if (!isset($customers[$k])) {
|
|
$customers[$k] = ['code' => $r['code'], 'name' => $r['name'], 'v' => []];
|
|
}
|
|
$customers[$k]['v'][(int)$r['y']] = (float)$r['v'];
|
|
}
|
|
$customers = array_values($customers);
|
|
}
|
|
|
|
MagoSalesStats::jsonOut([
|
|
'success' => true,
|
|
'metric' => $metric,
|
|
'ytd' => $ytd,
|
|
'today' => date('Y-m-d'),
|
|
'available' => $available,
|
|
'years' => $years,
|
|
'summary' => $summary,
|
|
'monthly' => $monthly,
|
|
'customers' => $customers,
|
|
]);
|
|
} 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>Confronto anni - <?= 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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
</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">
|
|
|
|
<!-- HEADER -->
|
|
<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-chart-column"></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">
|
|
<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" class="active"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
|
|
<?php if (userCan('sales.deliveries.view')) : ?>
|
|
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- FILTRI -->
|
|
<div class="filters-box mb-4">
|
|
<div class="row g-3 align-items-end">
|
|
<div class="col-12 col-xl-6">
|
|
<label class="flabel">Anni da confrontare</label>
|
|
<div class="chip-row" id="yearChips"></div>
|
|
</div>
|
|
<div class="col-12 col-xl-6 d-flex gap-2 flex-wrap justify-content-xl-end">
|
|
<label class="ytd-switch" for="ytdToggle" title="Ogni anno viene considerato solo fino al giorno di oggi, per confronti equi con l'anno in corso">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="ytdToggle" checked>
|
|
<span id="ytdLabel">A pari data</span>
|
|
</label>
|
|
<div class="metric-switch" id="metricSwitch">
|
|
<button type="button" data-metric="value" class="active">€ Valore</button>
|
|
<button type="button" data-metric="count"># N° ordini</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 align-items-end mt-1">
|
|
<div class="col-12 col-md-5">
|
|
<label class="flabel" for="fCustomers">Clienti</label>
|
|
<select id="fCustomers" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="flabel" for="fSalespeople">Agente</label>
|
|
<select id="fSalespeople" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="flabel" for="fAreas">Area</label>
|
|
<select id="fAreas" multiple style="width:100%;"></select>
|
|
</div>
|
|
<div class="col-12 col-md-1 d-grid">
|
|
<button type="button" class="btn btn-outline-secondary" id="btnReset" title="Azzera filtri" style="border-radius:12px;min-height:42px;">
|
|
<i class="fa-solid fa-eraser"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="emptyState" class="empty-state" style="display:none;">
|
|
<div style="font-size:3rem;">📊</div>
|
|
<h5 class="mt-2" style="color:var(--c-ink);">Nessun dato da confrontare</h5>
|
|
<p>Sincronizza gli ordini da Mago dalla pagina <a href="sales_orders_stats.php">Andamento ordini</a>.</p>
|
|
</div>
|
|
|
|
<div id="content">
|
|
<div class="warn-strip mb-3" id="missingAmounts" style="display:none;"></div>
|
|
|
|
<!-- RIEPILOGO ANNI -->
|
|
<div class="year-grid mb-4" id="yearCards"></div>
|
|
|
|
<!-- GRAFICI -->
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-xl-6">
|
|
<div class="chart-card">
|
|
<div class="chart-title" id="ttlMonthly">Andamento mensile</div>
|
|
<div class="chart-hint">Stagionalità: un colore per anno</div>
|
|
<div id="chMonthly" style="min-height:340px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-6">
|
|
<div class="chart-card">
|
|
<div class="chart-title" id="ttlCumul">Progressivo dell'anno</div>
|
|
<div class="chart-hint">Somma da gennaio: dice se si è avanti o indietro rispetto agli anni scorsi</div>
|
|
<div id="chCumul" style="min-height:340px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CLIENTI -->
|
|
<div class="chart-card">
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
|
<div>
|
|
<div class="chart-title" id="ttlCust">Clienti a confronto</div>
|
|
<div class="chart-hint" id="custHint">Clicca un cliente per filtrare tutta la pagina</div>
|
|
</div>
|
|
<div class="chip-row" id="trendChips">
|
|
<button type="button" class="chip active" data-trend="" style="--yc:#198754;">Tutti <span class="badge bg-light text-dark" id="cntAll">0</span></button>
|
|
<button type="button" class="chip" data-trend="new" style="--yc:#0369a1;">Nuovi <span class="badge bg-light text-dark" id="cntNew">0</span></button>
|
|
<button type="button" class="chip" data-trend="lost" style="--yc:#b02a37;">Persi <span class="badge bg-light text-dark" id="cntLost">0</span></button>
|
|
<button type="button" class="chip" data-trend="up" style="--yc:#15803d;">In crescita <span class="badge bg-light text-dark" id="cntUp">0</span></button>
|
|
<button type="button" class="chip" data-trend="down" style="--yc:#c2410c;">In calo <span class="badge bg-light text-dark" id="cntDown">0</span></button>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table id="custTable" class="table table-striped align-middle w-100"></table>
|
|
</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 (come dopo il click sulla freccia)
|
|
$(function() {
|
|
$('.sidebar-wrapper').hover(
|
|
function() {
|
|
$('.wrapper.toggled').addClass('sidebar-hovered');
|
|
},
|
|
function() {
|
|
$('.wrapper').removeClass('sidebar-hovered');
|
|
}
|
|
);
|
|
});
|
|
</script>
|
|
<script>
|
|
(function() {
|
|
const MONTHS = ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'];
|
|
const PALETTE = ['#198754', '#0d6efd', '#fd7e14', '#6f42c1', '#0ea5e9', '#d63384', '#64748b', '#a16207'];
|
|
const STORE_KEY = 'zibo_sales_years_filters_v1';
|
|
const CUR_Y = new Date().getFullYear();
|
|
|
|
const charts = {};
|
|
let table = null;
|
|
let metric = 'value';
|
|
let selectedYears = [];
|
|
let trendFilter = '';
|
|
let lastData = null;
|
|
let loadTimer = null;
|
|
let reqSeq = 0;
|
|
|
|
/* ---------------- UTILS ---------------- */
|
|
const yearColor = y => PALETTE[Math.max(0, CUR_Y - y) % PALETTE.length];
|
|
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
} [c]));
|
|
}
|
|
|
|
const fmtNum = n => Number(n || 0).toLocaleString('it-IT');
|
|
const fmtEur = (n, dec) => Number(n || 0).toLocaleString('it-IT', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: dec || 0,
|
|
maximumFractionDigits: dec || 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);
|
|
}
|
|
|
|
const fmtMetric = v => v === null || v === undefined ? '—' : (metric === 'value' ? fmtEur(v) : fmtNum(Math.round(v)));
|
|
const fmtAxis = v => metric === 'value' ? fmtEurShort(v) : fmtNum(Math.round(v));
|
|
|
|
function pct(cur, prev) {
|
|
if (!prev) return null;
|
|
return (cur - prev) * 100 / prev;
|
|
}
|
|
|
|
function deltaSpan(p, label) {
|
|
if (p === null) return '';
|
|
const r = Math.round(p);
|
|
return `<span><span class="${r >= 0 ? 'delta-up' : 'delta-down'}">${r >= 0 ? '▲' : '▼'} ${Math.abs(r)}%</span> ${label}</span>`;
|
|
}
|
|
|
|
function todayLabel() {
|
|
const d = new Date();
|
|
return `${d.getDate()} ${MONTHS[d.getMonth()]}`;
|
|
}
|
|
|
|
/* ---------------- STATO FILTRI ---------------- */
|
|
function save() {
|
|
try {
|
|
localStorage.setItem(STORE_KEY, JSON.stringify({
|
|
years: selectedYears,
|
|
metric: metric,
|
|
ytd: $('#ytdToggle').is(':checked'),
|
|
customers: $('#fCustomers').val() || [],
|
|
salespeople: $('#fSalespeople').val() || [],
|
|
areas: $('#fAreas').val() || []
|
|
}));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function restore() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(STORE_KEY) || 'null');
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function scheduleLoad(delay) {
|
|
clearTimeout(loadTimer);
|
|
loadTimer = setTimeout(loadData, delay === undefined ? 300 : delay);
|
|
}
|
|
|
|
function renderYearChips(available) {
|
|
const years = available.slice().sort((a, b) => b - a);
|
|
$('#yearChips').html(years.map(y =>
|
|
`<button type="button" class="chip${selectedYears.includes(y) ? ' active' : ''}" data-year="${y}" style="--yc:${yearColor(y)};"><span class="dot"></span>${y}</button>`
|
|
).join(''));
|
|
}
|
|
|
|
function renderSyncInfo(s) {
|
|
const $p = $('#syncPill').removeClass('ok err');
|
|
const o = s && s.sale_ord;
|
|
if (!s || s.error || !o || !o.last_run_at) {
|
|
$('#syncText').text(s && s.error ? s.error : 'Mai sincronizzato');
|
|
if (s && s.error) $p.addClass('err');
|
|
return;
|
|
}
|
|
$p.addClass(o.last_status === 'ok' ? 'ok' : 'err');
|
|
$('#syncText').text(`Aggiornato il ${o.last_run_at} · ${fmtNum(s.total_orders)} ordini in archivio`);
|
|
}
|
|
|
|
/* ---------------- CARICAMENTO ---------------- */
|
|
function loadFilters() {
|
|
return fetch('?action=filters').then(r => r.json()).then(d => {
|
|
renderSyncInfo(d.sync);
|
|
if (!d.success) return d;
|
|
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('#fSalespeople', d.salespeople || [], o => o.code);
|
|
fill('#fAreas', d.areas || [], o => o.code);
|
|
return d;
|
|
});
|
|
}
|
|
|
|
function loadData() {
|
|
save();
|
|
const p = new URLSearchParams();
|
|
p.append('action', 'data');
|
|
p.append('metric', metric);
|
|
if ($('#ytdToggle').is(':checked')) p.append('ytd', '1');
|
|
selectedYears.forEach(y => p.append('years[]', y));
|
|
['customers', 'salespeople', 'areas'].forEach(k => {
|
|
const id = {
|
|
customers: '#fCustomers',
|
|
salespeople: '#fSalespeople',
|
|
areas: '#fAreas'
|
|
} [k];
|
|
($(id).val() || []).forEach(v => p.append(k + '[]', v));
|
|
});
|
|
|
|
const seq = ++reqSeq;
|
|
$('#content').css('opacity', .55);
|
|
|
|
fetch('?' + p.toString()).then(r => r.json()).then(d => {
|
|
if (seq !== reqSeq) return;
|
|
$('#content').css('opacity', 1);
|
|
if (!d.success) {
|
|
alert(d.message || 'Errore nel caricamento');
|
|
return;
|
|
}
|
|
lastData = d;
|
|
selectedYears = d.years.slice();
|
|
renderYearChips(d.available);
|
|
|
|
const empty = !d.available.length;
|
|
$('#emptyState').toggle(empty);
|
|
$('#content').toggle(!empty);
|
|
if (empty) return;
|
|
|
|
updateTitles();
|
|
renderCards(d);
|
|
renderMonthly(d);
|
|
renderCumul(d);
|
|
renderCustomers(d);
|
|
}).catch(e => {
|
|
$('#content').css('opacity', 1);
|
|
console.error(e);
|
|
});
|
|
}
|
|
|
|
function updateTitles() {
|
|
const v = metric === 'value';
|
|
const ytd = $('#ytdToggle').is(':checked');
|
|
$('#ytdLabel').text(`A pari data (fino al ${todayLabel()})`);
|
|
$('#metricSwitch button').removeClass('active');
|
|
$(`#metricSwitch button[data-metric="${metric}"]`).addClass('active');
|
|
$('#ttlMonthly').text(v ? 'Valore ordinato per mese' : 'Ordini per mese');
|
|
$('#ttlCumul').text(v ? 'Valore progressivo dell\'anno' : 'Ordini progressivi dell\'anno');
|
|
$('#ttlCust').text(v ? 'Clienti a confronto · valore ordinato' : 'Clienti a confronto · numero ordini');
|
|
}
|
|
|
|
/* ---------------- CARTE ANNO ---------------- */
|
|
function renderCards(d) {
|
|
const byYear = {};
|
|
d.summary.forEach(r => byYear[+r.y] = r);
|
|
const ytd = !!d.ytd;
|
|
|
|
const missing = d.summary.filter(r => d.years.includes(+r.y)).reduce((a, r) => a + (+r.missing_amounts || 0), 0);
|
|
$('#missingAmounts').toggle(missing > 0).html(missing > 0 ?
|
|
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(missing)} ordini negli anni scelti non hanno ancora gli importi: lancia una sincronizzazione.` : '');
|
|
|
|
$('#yearCards').html(d.years.slice().reverse().map(y => {
|
|
const r = byYear[y] || {
|
|
orders: 0,
|
|
value: 0,
|
|
customers: 0
|
|
};
|
|
const prev = byYear[y - 1];
|
|
const orders = +r.orders,
|
|
value = +r.value,
|
|
avg = orders ? value / orders : 0;
|
|
const scope = ytd ? `al ${todayLabel()}` : (y === CUR_Y ? 'in corso' : 'anno intero');
|
|
const deltas = prev ? [
|
|
deltaSpan(pct(value, +prev.value), `valore vs ${y - 1}`),
|
|
deltaSpan(pct(orders, +prev.orders), `ordini`)
|
|
].join('') : `<span>nessun dato ${y - 1}</span>`;
|
|
|
|
return `
|
|
<div class="year-card" style="--yc:${yearColor(y)};">
|
|
<div class="y">${y} <small>${scope}</small></div>
|
|
<div class="big" title="${esc(fmtEur(value, 2))}">${fmtEurShort(value)}</div>
|
|
<div class="stats">
|
|
<div><span>Ordini</span><b>${fmtNum(orders)}</b></div>
|
|
<div><span>Medio</span><b>${fmtEurShort(avg)}</b></div>
|
|
<div><span>Clienti</span><b>${fmtNum(r.customers)}</b></div>
|
|
</div>
|
|
<div class="deltas">${deltas}</div>
|
|
</div>`;
|
|
}).join(''));
|
|
}
|
|
|
|
/* ---------------- GRAFICI ---------------- */
|
|
function seriesByYear(d) {
|
|
const map = {};
|
|
d.years.forEach(y => map[y] = Array(12).fill(0));
|
|
d.monthly.forEach(r => {
|
|
if (map[+r.y]) map[+r.y][+r.m - 1] = +r.v;
|
|
});
|
|
// mesi futuri: vuoti (non zero) per l'anno in corso, e per tutti se "a pari data"
|
|
const curM = new Date().getMonth();
|
|
d.years.forEach(y => {
|
|
if (y === CUR_Y || d.ytd) {
|
|
for (let i = curM + 1; i < 12; i++) map[y][i] = null;
|
|
}
|
|
});
|
|
return map;
|
|
}
|
|
|
|
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 renderMonthly(d) {
|
|
const map = seriesByYear(d);
|
|
upsert('monthly', 'chMonthly', {
|
|
chart: {
|
|
...base,
|
|
type: 'line',
|
|
height: 340
|
|
},
|
|
series: d.years.map(y => ({
|
|
name: String(y),
|
|
data: map[y]
|
|
})),
|
|
colors: d.years.map(yearColor),
|
|
stroke: {
|
|
width: d.years.map(y => y === Math.max(...d.years) ? 4 : 2.5),
|
|
curve: 'smooth'
|
|
},
|
|
markers: {
|
|
size: 4
|
|
},
|
|
xaxis: {
|
|
categories: MONTHS
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: fmtAxis
|
|
}
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
intersect: false,
|
|
y: {
|
|
formatter: fmtMetric
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'left'
|
|
},
|
|
grid: {
|
|
borderColor: '#eef2f7'
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderCumul(d) {
|
|
const map = seriesByYear(d);
|
|
const cum = {};
|
|
d.years.forEach(y => {
|
|
let s = 0;
|
|
cum[y] = map[y].map(v => v === null ? null : (s += v));
|
|
});
|
|
upsert('cumul', 'chCumul', {
|
|
chart: {
|
|
...base,
|
|
type: 'area',
|
|
height: 340
|
|
},
|
|
series: d.years.map(y => ({
|
|
name: String(y),
|
|
data: cum[y]
|
|
})),
|
|
colors: d.years.map(yearColor),
|
|
fill: {
|
|
type: 'gradient',
|
|
gradient: {
|
|
opacityFrom: .25,
|
|
opacityTo: .02
|
|
}
|
|
},
|
|
stroke: {
|
|
width: d.years.map(y => y === Math.max(...d.years) ? 4 : 2.5),
|
|
curve: 'smooth'
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
xaxis: {
|
|
categories: MONTHS
|
|
},
|
|
yaxis: {
|
|
labels: {
|
|
formatter: fmtAxis
|
|
}
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
intersect: false,
|
|
y: {
|
|
formatter: fmtMetric
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'left'
|
|
},
|
|
grid: {
|
|
borderColor: '#eef2f7'
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ---------------- CLIENTI ---------------- */
|
|
function classify(c, last, prev) {
|
|
const a = c.v[last] || 0,
|
|
b = prev ? (c.v[prev] || 0) : 0;
|
|
if (!prev) return {
|
|
t: 'flat',
|
|
p: null
|
|
};
|
|
if (b === 0 && a > 0) return {
|
|
t: 'new',
|
|
p: null
|
|
};
|
|
if (b > 0 && a === 0) return {
|
|
t: 'lost',
|
|
p: -100
|
|
};
|
|
const p = pct(a, b);
|
|
if (p === null) return {
|
|
t: 'flat',
|
|
p: null
|
|
};
|
|
if (p >= 10) return {
|
|
t: 'up',
|
|
p
|
|
};
|
|
if (p <= -10) return {
|
|
t: 'down',
|
|
p
|
|
};
|
|
return {
|
|
t: 'flat',
|
|
p
|
|
};
|
|
}
|
|
|
|
function trendBadge(c) {
|
|
const map = {
|
|
new: ['trend-new', '★ Nuovo'],
|
|
lost: ['trend-lost', '✕ Perso'],
|
|
up: ['trend-up', '▲ Crescita'],
|
|
down: ['trend-down', '▼ Calo'],
|
|
flat: ['trend-flat', '= Stabile']
|
|
};
|
|
const [cls, txt] = map[c.t];
|
|
return `<span class="trend ${cls}">${txt}</span>`;
|
|
}
|
|
|
|
function renderCustomers(d) {
|
|
const years = d.years;
|
|
const last = years[years.length - 1];
|
|
const prev = years.length > 1 ? years[years.length - 2] : null;
|
|
|
|
const all = d.customers.map(c => Object.assign({}, c, classify(c, last, prev)));
|
|
const counts = {
|
|
new: 0,
|
|
lost: 0,
|
|
up: 0,
|
|
down: 0
|
|
};
|
|
all.forEach(c => {
|
|
if (counts[c.t] !== undefined) counts[c.t]++;
|
|
});
|
|
$('#cntAll').text(fmtNum(all.length));
|
|
$('#cntNew').text(counts.new);
|
|
$('#cntLost').text(counts.lost);
|
|
$('#cntUp').text(counts.up);
|
|
$('#cntDown').text(counts.down);
|
|
$('#trendChips .chip').removeClass('active');
|
|
$(`#trendChips .chip[data-trend="${trendFilter}"]`).addClass('active');
|
|
$('#custHint').text(prev ?
|
|
`Andamento: ${last} rispetto a ${prev} (soglia ±10%) · clicca un cliente per filtrare tutta la pagina` :
|
|
'Seleziona almeno due anni per vedere nuovi, persi, crescita e calo');
|
|
|
|
const rows = all.filter(c => !trendFilter || c.t === trendFilter);
|
|
|
|
const columns = [{
|
|
title: 'Cliente',
|
|
data: null,
|
|
render: (c, type) => type === 'display' ?
|
|
`<div class="fw-semibold">${esc(c.name)}</div><div class="small text-muted">${esc(c.code)}</div>` : (type === 'export' ? `${c.name} (${c.code})` : `${c.name} ${c.code}`)
|
|
}];
|
|
years.slice().reverse().forEach(y => columns.push({
|
|
title: `<span style="color:${yearColor(y)};">●</span> ${y}`,
|
|
data: null,
|
|
className: 'text-end amount',
|
|
render: (c, type) => {
|
|
const v = c.v[y] || 0;
|
|
if (type !== 'display') return v;
|
|
return v ? fmtMetric(v) : '<span class="text-muted">—</span>';
|
|
}
|
|
}));
|
|
columns.push({
|
|
title: prev ? `Var. ${last} vs ${prev}` : 'Var.',
|
|
data: null,
|
|
className: 'text-end',
|
|
render: (c, type) => {
|
|
if (type === 'export') return c.p === null ? '' : Math.round(c.p) / 100;
|
|
if (type !== 'display') return c.p === null ? (c.t === 'new' ? 1e9 : -1e9) : c.p;
|
|
if (c.p === null) return '<span class="text-muted">—</span>';
|
|
const r = Math.round(c.p);
|
|
return `<span class="${r >= 0 ? 'delta-up' : 'delta-down'}">${r >= 0 ? '+' : ''}${r}%</span>`;
|
|
}
|
|
}, {
|
|
title: 'Andamento',
|
|
data: null,
|
|
render: (c, type) => type === 'display' ? trendBadge(c) : (type === 'export' ? $('<div>').html(trendBadge(c)).text().replace(/^[^A-Za-zÀ-ú]+/, '') : c.t)
|
|
});
|
|
|
|
if (table) {
|
|
table.destroy();
|
|
$('#custTable').empty();
|
|
}
|
|
|
|
table = $('#custTable').DataTable({
|
|
data: rows,
|
|
columns: columns,
|
|
order: [
|
|
[1, 'desc']
|
|
],
|
|
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: 'Confronto clienti per anno',
|
|
exportOptions: {
|
|
orthogonal: 'export',
|
|
format: {
|
|
header: h => $('<div>').html(h).text().replace('●', '').trim()
|
|
}
|
|
}
|
|
}]
|
|
},
|
|
language: {
|
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
|
|
}
|
|
});
|
|
|
|
$('#custTable tbody').off('click').on('click', 'tr', function() {
|
|
const c = table.row(this).data();
|
|
if (!c) return;
|
|
$('#fCustomers').val([c.code]).trigger('change');
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ---------------- EVENTI ---------------- */
|
|
$(function() {
|
|
['#fCustomers', '#fSalespeople', '#fAreas'].forEach((id, i) => {
|
|
$(id).select2({
|
|
theme: 'bootstrap-5',
|
|
placeholder: ['Tutti i clienti', 'Tutti', 'Tutte'][i],
|
|
allowClear: true,
|
|
closeOnSelect: false,
|
|
width: '100%'
|
|
}).on('change', () => scheduleLoad());
|
|
});
|
|
|
|
$('#yearChips').on('click', '.chip', function() {
|
|
const y = +$(this).data('year');
|
|
if (selectedYears.includes(y)) {
|
|
if (selectedYears.length === 1) return; // almeno un anno
|
|
selectedYears = selectedYears.filter(x => x !== y);
|
|
} else {
|
|
selectedYears.push(y);
|
|
}
|
|
selectedYears.sort();
|
|
$(this).toggleClass('active');
|
|
scheduleLoad(0);
|
|
});
|
|
|
|
$('#ytdToggle').on('change', () => scheduleLoad(0));
|
|
|
|
$('#metricSwitch').on('click', 'button', function() {
|
|
metric = $(this).data('metric');
|
|
scheduleLoad(0);
|
|
});
|
|
|
|
$('#trendChips').on('click', '.chip', function() {
|
|
trendFilter = $(this).data('trend') || '';
|
|
if (lastData) renderCustomers(lastData);
|
|
});
|
|
|
|
$('#btnReset').on('click', function() {
|
|
$('#fCustomers, #fSalespeople, #fAreas').val(null).trigger('change.select2');
|
|
trendFilter = '';
|
|
selectedYears = [];
|
|
$('#ytdToggle').prop('checked', true);
|
|
scheduleLoad(0);
|
|
});
|
|
|
|
loadFilters().then(() => {
|
|
const s = restore();
|
|
if (s) {
|
|
selectedYears = (s.years || []).map(Number);
|
|
metric = s.metric === 'count' ? 'count' : 'value';
|
|
$('#ytdToggle').prop('checked', s.ytd !== false);
|
|
$('#fCustomers').val(s.customers || []).trigger('change.select2');
|
|
$('#fSalespeople').val(s.salespeople || []).trigger('change.select2');
|
|
$('#fAreas').val(s.areas || []).trigger('change.select2');
|
|
}
|
|
updateTitles();
|
|
loadData();
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|