fixed stas3
This commit is contained in:
@@ -17,9 +17,27 @@ class MagoSalesStats
|
||||
*
|
||||
* Per invertirle basta scambiare i due valori qui sotto.
|
||||
*/
|
||||
// Verificato sui dati reali (13/10/2026): la data ZIBO è ExpectedDeliveryDate, la data cliente è ConfirmedDeliveryDate
|
||||
const ZIBO_DATE_COL = 'expected_delivery_date'; // Mago: ExpectedDeliveryDate ("Data ZIBO")
|
||||
const CLIENT_DATE_COL = 'confirmed_delivery_date'; // Mago: ConfirmedDeliveryDate ("DATA CLIENTE")
|
||||
|
||||
/*
|
||||
* DATA ZIBO = SETTIMANA DI CONSEGNA
|
||||
* In Mago la data ZIBO è il primo giorno della settimana: per il cliente significa "entro quella settimana".
|
||||
* La scadenza reale è quindi la fine di quella settimana; durante la settimana compare un avviso.
|
||||
* ZIBO_IS_WEEK = false → la data ZIBO torna a valere come giorno preciso
|
||||
* ZIBO_WEEK_END_DAYS = giorni dopo il lunedì: 6 = domenica, 4 = venerdì
|
||||
*/
|
||||
const ZIBO_IS_WEEK = true;
|
||||
const ZIBO_WEEK_END_DAYS = 6;
|
||||
|
||||
/*
|
||||
* ORDINI ESCLUSI da tutte le statistiche: parole cercate (senza distinzione maiuscole/minuscole) in
|
||||
* numero ordine esterno, nostro/vostro riferimento, note di testata, descrizioni e note delle righe.
|
||||
* Lasciare l'elenco vuoto per non escludere nulla.
|
||||
*/
|
||||
const EXCLUDE_KEYWORDS = ['listino'];
|
||||
|
||||
/** Data ZIBO "valida" di una riga/testata: in Mago, se non compilata, resta uguale alla data ordine. */
|
||||
public static function ziboDate($a)
|
||||
{
|
||||
@@ -33,10 +51,40 @@ class MagoSalesStats
|
||||
return "CASE WHEN $c > $a.order_date THEN $c END";
|
||||
}
|
||||
|
||||
/** Scadenza effettiva di una riga: data ZIBO, se manca la data cliente. */
|
||||
/** Scadenza della data ZIBO: fine della settimana indicata (o il giorno stesso se ZIBO_IS_WEEK = false). */
|
||||
public static function ziboDeadline($a)
|
||||
{
|
||||
$z = self::ziboDate($a);
|
||||
if (!self::ZIBO_IS_WEEK) return $z;
|
||||
return "DATE_ADD($z, INTERVAL (" . (int)self::ZIBO_WEEK_END_DAYS . " - WEEKDAY($z)) DAY)";
|
||||
}
|
||||
|
||||
/** Scadenza effettiva di una riga: fine settimana ZIBO, se manca la data cliente. */
|
||||
public static function lineDue($a)
|
||||
{
|
||||
return "COALESCE(" . self::ziboDate($a) . ", " . self::clientDate($a) . ")";
|
||||
return "COALESCE(" . self::ziboDeadline($a) . ", " . self::clientDate($a) . ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Condizione SQL che ESCLUDE gli ordini contenenti una delle parole in EXCLUDE_KEYWORDS
|
||||
* (alias della testata: $a). Stringa vuota se non c'è nulla da escludere.
|
||||
*/
|
||||
public static function excludeCondition($a)
|
||||
{
|
||||
$words = array_filter(array_map('trim', self::EXCLUDE_KEYWORDS));
|
||||
if (!$words) return '1=1';
|
||||
|
||||
$parts = [];
|
||||
foreach ($words as $w) {
|
||||
$like = "'%" . str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $w) . "%'";
|
||||
foreach (['external_ord_no', 'our_reference', 'your_reference', 'notes'] as $col) {
|
||||
$parts[] = "COALESCE($a.$col, '') LIKE $like"; // COALESCE: un campo NULL non deve escludere l'ordine
|
||||
}
|
||||
$parts[] = "EXISTS (SELECT 1 FROM mago_sale_ord_detail ex
|
||||
WHERE ex.sale_ord_id = $a.sale_ord_id
|
||||
AND (COALESCE(ex.description, '') LIKE $like OR COALESCE(ex.notes, '') LIKE $like))";
|
||||
}
|
||||
return 'NOT (' . implode(' OR ', $parts) . ')';
|
||||
}
|
||||
|
||||
/** Aggregati per ordine calcolati dalle righe (alias l). */
|
||||
@@ -56,7 +104,8 @@ class MagoSalesStats
|
||||
MIN($due) AS first_due,
|
||||
COUNT(DISTINCT CASE WHEN $open THEN $due END) AS open_dates,
|
||||
SUM($open AND $due < CURDATE()) AS late_lines,
|
||||
SUM($open AND " . self::clientDate('d') . " < CURDATE()) AS late_lines_client
|
||||
SUM($open AND " . self::clientDate('d') . " < CURDATE()) AS late_lines_client,
|
||||
SUM($open AND $due >= CURDATE() AND YEARWEEK($due, 3) = YEARWEEK(CURDATE(), 3)) AS week_lines
|
||||
FROM mago_sale_ord_detail d
|
||||
WHERE d.item <> '' OR d.qty <> 0
|
||||
GROUP BY d.sale_ord_id
|
||||
@@ -110,13 +159,14 @@ class MagoSalesStats
|
||||
-- (o alla data cliente se la data ZIBO non è compilata)
|
||||
COALESCE(
|
||||
l.next_due, l.first_due,
|
||||
" . self::ziboDate('o') . ", " . self::clientDate('o') . ", o.compulsory_delivery_date
|
||||
" . self::ziboDeadline('o') . ", " . self::clientDate('o') . ", o.compulsory_delivery_date
|
||||
) AS due_date,
|
||||
COALESCE(l.lines_total, 0) AS lines_total,
|
||||
COALESCE(l.open_lines, 0) AS open_lines,
|
||||
COALESCE(l.open_dates, 0) AS open_dates,
|
||||
COALESCE(l.late_lines, 0) AS late_lines,
|
||||
COALESCE(l.late_lines_client, 0) AS late_lines_client,
|
||||
COALESCE(l.week_lines, 0) AS week_lines,
|
||||
CASE
|
||||
WHEN o.cancelled = 1 THEN 'annullato'
|
||||
WHEN o.delivered = 1 THEN 'evaso'
|
||||
@@ -127,6 +177,7 @@ class MagoSalesStats
|
||||
LEFT JOIN mago_cust_supp c
|
||||
ON c.cust_supp_type = " . self::CUSTOMER_TYPE . " AND c.cust_supp = o.customer
|
||||
LEFT JOIN " . self::lineAggregates() . " ON l.sale_ord_id = o.sale_ord_id
|
||||
WHERE " . self::excludeCondition('o') . "
|
||||
) x";
|
||||
}
|
||||
|
||||
@@ -201,6 +252,7 @@ class MagoSalesStats
|
||||
'bloccato' => "x.status = 'bloccato'",
|
||||
'ritardo' => "(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE())",
|
||||
'ritardo_cliente' => "(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE())",
|
||||
'settimana' => "(x.status IN ('aperto','bloccato') AND x.week_lines > 0)", // righe da consegnare questa settimana
|
||||
'da_fatturare' => "(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0)",
|
||||
'fatturato' => "x.invoiced = 1",
|
||||
];
|
||||
|
||||
@@ -501,6 +501,18 @@ if ($action === 'data') {
|
||||
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;
|
||||
@@ -628,7 +640,7 @@ if ($action === 'data') {
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-dates">
|
||||
<span><b class="z">Data ZIBO</b> = scadenza comunicata (rossa se superata)</span>
|
||||
<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>
|
||||
@@ -826,11 +838,37 @@ if ($action === 'data') {
|
||||
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 ?
|
||||
`<div class="dt-zibo${zibo < today ? ' late' : ''}"><span class="dt-tag">ZIBO</span>${fmtDate(zibo)}</div>` :
|
||||
'<div class="dt-fallback">data ZIBO non indicata</div>';
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ if ($action === 'data') {
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS late,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE()) AS late_client,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.late_lines > 0 AND x.late_lines < x.open_lines) AS late_partial,
|
||||
SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN x.late_lines ELSE 0 END) AS late_lines,
|
||||
SUM(x.status IN ('aperto','bloccato') AND x.week_lines > 0) AS week_orders,
|
||||
SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN x.week_lines ELSE 0 END) AS week_lines,
|
||||
SUM(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0) AS to_invoice,
|
||||
COUNT(DISTINCT x.customer) AS customers,
|
||||
AVG(CASE WHEN x.status <> 'annullato' AND x.due_date IS NOT NULL AND x.due_date >= x.order_date
|
||||
@@ -184,7 +187,7 @@ if ($action === 'data') {
|
||||
x.order_date, x.due_date, x.customer, x.company_name,
|
||||
x.salesperson, x.area, x.status, x.invoiced, x.your_reference,
|
||||
x.taxable_amount, x.total_amount,
|
||||
x.zibo_date, x.client_date, x.open_lines, x.late_lines, x.late_lines_client, x.open_dates,
|
||||
x.zibo_date, x.client_date, x.open_lines, x.late_lines, x.late_lines_client, x.open_dates, x.week_lines,
|
||||
(x.status IN ('aperto','bloccato') AND x.client_date < CURDATE()) AS is_late_client,
|
||||
(x.summary_tb_modified IS NULL) AS no_amount,
|
||||
(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS is_late
|
||||
@@ -727,6 +730,43 @@ if ($action === 'order') {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: .9rem;
|
||||
font-weight: 700;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
|
||||
.kpi-week {
|
||||
margin: 10px -18px -16px;
|
||||
padding: 7px 18px;
|
||||
background: #fef3c7;
|
||||
border-top: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
font-size: .8rem;
|
||||
transition: background .15s ease;
|
||||
}
|
||||
|
||||
.kpi-week:hover {
|
||||
background: #fde68a;
|
||||
}
|
||||
|
||||
.kpi-week.selected {
|
||||
background: #fcd34d;
|
||||
box-shadow: inset 0 0 0 2px #b45309;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.more-dates {
|
||||
display: inline-block;
|
||||
background: #eef2ff;
|
||||
@@ -932,6 +972,7 @@ if ($action === 'order') {
|
||||
<div class="chip-row" id="statusChips">
|
||||
<button type="button" class="chip" data-status="aperto" style="--chip-c:var(--c-open);">Aperti</button>
|
||||
<button type="button" class="chip" data-status="ritardo" style="--chip-c:var(--c-late);">In ritardo (ZIBO)</button>
|
||||
<button type="button" class="chip" data-status="settimana" style="--chip-c:#b45309;">In consegna questa sett.</button>
|
||||
<button type="button" class="chip" data-status="ritardo_cliente" style="--chip-c:#ea580c;">Oltre data cliente</button>
|
||||
<button type="button" class="chip" data-status="bloccato" style="--chip-c:var(--c-block);">Bloccati</button>
|
||||
<button type="button" class="chip" data-status="evaso" style="--chip-c:var(--c-done);">Evasi</button>
|
||||
@@ -993,6 +1034,7 @@ if ($action === 'order') {
|
||||
<div class="kpi-label">In ritardo</div>
|
||||
<div class="kpi-value" id="kLate">—</div>
|
||||
<div class="kpi-sub" id="kLateSub">data ZIBO superata</div>
|
||||
<div class="kpi-week" id="kWeek" data-status="settimana" title="Ordini con righe da consegnare entro questa settimana (non ancora in ritardo)"> </div>
|
||||
</div>
|
||||
<div class="kpi" data-status="evaso" style="--k:var(--c-done);">
|
||||
<div class="kpi-icon"><i class="fa-solid fa-truck"></i></div>
|
||||
@@ -1321,7 +1363,8 @@ if ($action === 'order') {
|
||||
|
||||
// evidenzio il KPI corrispondente allo stato filtrato
|
||||
$('.kpi').removeClass('selected');
|
||||
if (f.statuses.length === 1) $(`.kpi[data-status="${f.statuses[0]}"]`).addClass('selected');
|
||||
$('.kpi-week').removeClass('selected');
|
||||
if (f.statuses.length === 1) $(`.kpi[data-status="${f.statuses[0]}"], .kpi-week[data-status="${f.statuses[0]}"]`).addClass('selected');
|
||||
}
|
||||
|
||||
// aperta da link (?q=numero ordine): non sovrascrivo i filtri salvati dell'utente
|
||||
@@ -1479,10 +1522,13 @@ if ($action === 'order') {
|
||||
const orders = +k.orders || 0;
|
||||
$('#kOrders').text(fmtNum(orders));
|
||||
$('#kOpen').text(fmtNum(k.open_orders));
|
||||
$('#kLate').text(fmtNum(k.late));
|
||||
$('#kLateSub').html(`data ZIBO superata` +
|
||||
$('#kLate').html(`${fmtNum(k.late)} <span class="kpi-unit">ordini</span>`);
|
||||
$('#kLateSub').html(`<b style="color:var(--c-late);">${fmtNum(k.late_lines)}</b> righe in ritardo` +
|
||||
(+k.late_partial ? ` · ${fmtNum(k.late_partial)} parziali` : '') +
|
||||
`<br><span style="color:#ea580c;font-weight:700;">${fmtNum(k.late_client)}</span> oltre data cliente`);
|
||||
$('#kWeek').html(+k.week_orders ?
|
||||
`<i class="fa-solid fa-calendar-week"></i> <b>${fmtNum(k.week_orders)}</b> in consegna questa sett. · ${fmtNum(k.week_lines)} righe` :
|
||||
'<i class="fa-solid fa-calendar-check"></i> nessuna consegna in scadenza questa settimana');
|
||||
$('#kDelivered').text(fmtNum(k.delivered));
|
||||
$('#kToInvoice').text(fmtNum(k.to_invoice));
|
||||
$('#kCustomers').text(fmtNum(k.customers));
|
||||
@@ -1898,16 +1944,43 @@ if ($action === 'order') {
|
||||
});
|
||||
}
|
||||
|
||||
// cella consegna: data ZIBO (rossa se superata) + data cliente (arancione se superata)
|
||||
// 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>';
|
||||
}
|
||||
|
||||
// cella consegna: data ZIBO (rossa a settimana finita, avviso in settimana) + data cliente (arancione se superata)
|
||||
function dateCell(zibo, client, opts) {
|
||||
opts = opts || {};
|
||||
const today = iso(new Date());
|
||||
const open = opts.open !== false;
|
||||
const zLate = open && zibo && zibo < today;
|
||||
const cLate = open && client && client < today;
|
||||
let html = '';
|
||||
if (zibo) {
|
||||
html += `<div class="dt-zibo${zLate ? ' late' : ''}"><span class="dt-tag">ZIBO</span>${fmtDate(zibo)}${opts.more ? `<span class="more-dates" title="Le righe aperte hanno date di consegna diverse">+${opts.more} date</span>` : ''}</div>`;
|
||||
html += ziboHtml(zibo, open, opts.more ? `<span class="more-dates" title="Le righe aperte hanno date di consegna diverse">+${opts.more} date</span>` : '');
|
||||
} else {
|
||||
html += `<div class="dt-fallback">data ZIBO non indicata</div>`;
|
||||
}
|
||||
@@ -2082,7 +2155,7 @@ if ($action === 'order') {
|
||||
const today = iso(new Date());
|
||||
const rows = lines.map(l => {
|
||||
const open = !+l.delivered && !+l.cancelled;
|
||||
const due = l.zibo_date || l.client_date;
|
||||
const due = l.zibo_date ? ziboEnd(l.zibo_date) : l.client_date;
|
||||
const late = open && due && due < today;
|
||||
const days = late ? Math.round((new Date(today) - new Date(due)) / 86400000) : 0;
|
||||
const st = +l.cancelled ? '<span class="st-badge st-annullato">Annullata</span>' :
|
||||
@@ -2187,6 +2260,12 @@ if ($action === 'order') {
|
||||
});
|
||||
$('#fSearch').on('input', () => scheduleLoad(450));
|
||||
|
||||
// striscia "in consegna questa settimana" dentro il box rosso: filtra solo quegli ordini
|
||||
$('#kpiGrid').on('click', '.kpi-week', function(e) {
|
||||
e.stopPropagation();
|
||||
toggleStatus('settimana', true);
|
||||
});
|
||||
|
||||
$('#kpiGrid').on('click', '.kpi', function() {
|
||||
if ($(this).data('nofilter')) return;
|
||||
const st = $(this).data('status');
|
||||
|
||||
Reference in New Issue
Block a user