diff --git a/public/userarea/class/MagoSalesStats.php b/public/userarea/class/MagoSalesStats.php index 5c31c637..50504e22 100644 --- a/public/userarea/class/MagoSalesStats.php +++ b/public/userarea/class/MagoSalesStats.php @@ -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", ]; diff --git a/public/userarea/sales_orders_deliveries.php b/public/userarea/sales_orders_deliveries.php index 4a485472..7297890e 100644 --- a/public/userarea/sales_orders_deliveries.php +++ b/public/userarea/sales_orders_deliveries.php @@ -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') {
- Data ZIBO = scadenza comunicata (rossa se superata) + Data ZIBO = settimana di consegna comunicata (rossa a settimana finita) Data cliente = desiderata (arancione se superata)
@@ -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 = ; + const ZIBO_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 `
ZIBO${ZIBO_WEEK ? ' sett.' : ''}${fmtDate(z)}` + + (st === 'week' ? 'IN SETTIMANA' : '') + (extra || '') + '
'; + } + function dateCell(zibo, client) { const today = iso(new Date()); - let h = zibo ? - `
ZIBO${fmtDate(zibo)}
` : - '
data ZIBO non indicata
'; + let h = zibo ? ziboHtml(zibo, true) : '
data ZIBO non indicata
'; if (client) h += `
Cliente${fmtDate(client)}
`; return h; } diff --git a/public/userarea/sales_orders_stats.php b/public/userarea/sales_orders_stats.php index dc927d02..df69e981 100644 --- a/public/userarea/sales_orders_stats.php +++ b/public/userarea/sales_orders_stats.php @@ -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') {
+ @@ -993,6 +1034,7 @@ if ($action === 'order') {
In ritardo
—
data ZIBO superata
+
 
@@ -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)} ordini`); + $('#kLateSub').html(`${fmtNum(k.late_lines)} righe in ritardo` + (+k.late_partial ? ` · ${fmtNum(k.late_partial)} parziali` : '') + `
${fmtNum(k.late_client)} oltre data cliente`); + $('#kWeek').html(+k.week_orders ? + ` ${fmtNum(k.week_orders)} in consegna questa sett. · ${fmtNum(k.week_lines)} righe` : + ' 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 = ; + const ZIBO_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 `
ZIBO${ZIBO_WEEK ? ' sett.' : ''}${fmtDate(z)}` + + (st === 'week' ? 'IN SETTIMANA' : '') + (extra || '') + '
'; + } + + // 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 += `
ZIBO${fmtDate(zibo)}${opts.more ? `+${opts.more} date` : ''}
`; + html += ziboHtml(zibo, open, opts.more ? `+${opts.more} date` : ''); } else { html += `
data ZIBO non indicata
`; } @@ -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 ? 'Annullata' : @@ -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');