diff --git a/db/migrations/20261013135023_create_mago_sale_ord_detail.php b/db/migrations/20261013135023_create_mago_sale_ord_detail.php
new file mode 100644
index 00000000..e25b97e7
--- /dev/null
+++ b/db/migrations/20261013135023_create_mago_sale_ord_detail.php
@@ -0,0 +1,59 @@
+table('mago_sale_ord_detail', [
+ 'id' => false,
+ 'primary_key' => ['sale_ord_id', 'sub_id'],
+ 'comment' => 'Copia locale MA_SaleOrdDetails (Mago4) - sola lettura',
+ ])
+ ->addColumn('sale_ord_id', 'integer', ['signed' => false])
+ ->addColumn('sub_id', 'integer')
+ ->addColumn('line', 'integer', ['default' => 0])
+ ->addColumn('position', 'integer', ['default' => 0])
+ ->addColumn('line_type', 'integer', ['default' => 0])
+ ->addColumn('item', 'string', ['limit' => 30, 'default' => ''])
+ ->addColumn('description', 'string', ['limit' => 255, 'default' => ''])
+ ->addColumn('uom', 'string', ['limit' => 10, 'default' => ''])
+ ->addColumn('qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
+ ->addColumn('delivered_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
+ ->addColumn('invoiced_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
+ ->addColumn('unit_value', 'decimal', ['precision' => 15, 'scale' => 5, 'default' => 0])
+ ->addColumn('discount_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
+ ->addColumn('taxable_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
+ ->addColumn('total_amount', 'decimal', ['precision' => 15, 'scale' => 2, 'default' => 0])
+ ->addColumn('expected_delivery_date', 'date', ['null' => true])
+ ->addColumn('confirmed_delivery_date', 'date', ['null' => true])
+ ->addColumn('delivered', 'boolean', ['default' => false])
+ ->addColumn('invoiced', 'boolean', ['default' => false])
+ ->addColumn('cancelled', 'boolean', ['default' => false])
+ ->addColumn('customer', 'string', ['limit' => 20, 'default' => ''])
+ ->addColumn('order_date', 'date', ['null' => true])
+ ->addColumn('internal_ord_no', 'string', ['limit' => 30, 'default' => ''])
+ ->addColumn('notes', 'text', ['null' => true])
+ ->addColumn('tb_modified', 'datetime', ['null' => true])
+ ->addColumn('synced_at', 'datetime', ['null' => true])
+ ->addIndex(['item'])
+ ->addIndex(['order_date'])
+ ->addIndex(['expected_delivery_date'])
+ ->addIndex(['customer'])
+ ->create();
+
+ // Date di consegna ricavate dalle righe (la data in testata spesso resta = data ordine)
+ $this->table('mago_sale_ord')
+ ->addColumn('line_expected_date', 'date', ['null' => true, 'after' => 'compulsory_delivery_date', 'comment' => 'Prima data prevista tra le righe aperte'])
+ ->addColumn('line_confirmed_date', 'date', ['null' => true, 'after' => 'line_expected_date', 'comment' => 'Prima data confermata tra le righe aperte'])
+ ->addColumn('lines_count', 'integer', ['default' => 0, 'after' => 'line_confirmed_date'])
+ ->update();
+ }
+}
diff --git a/public/userarea/class/MagoSalesStats.php b/public/userarea/class/MagoSalesStats.php
index 76f50669..f761940c 100644
--- a/public/userarea/class/MagoSalesStats.php
+++ b/public/userarea/class/MagoSalesStats.php
@@ -9,6 +9,60 @@ class MagoSalesStats
{
const CUSTOMER_TYPE = 3211264;
+ /*
+ * MAPPATURA DATE DI CONSEGNA (righe e testata usano gli stessi nomi colonna)
+ *
+ * Data ZIBO = data comunicata da Zibo al cliente → è la SCADENZA che conta (ritardo in rosso)
+ * Data cliente = data desiderata dal cliente → riferimento (ritardo in arancione)
+ *
+ * Per invertirle basta scambiare i due valori qui sotto.
+ */
+ const ZIBO_DATE_COL = 'confirmed_delivery_date'; // Mago: ConfirmedDeliveryDate
+ const CLIENT_DATE_COL = 'expected_delivery_date'; // Mago: ExpectedDeliveryDate
+
+ /** Data ZIBO "valida" di una riga/testata: in Mago, se non compilata, resta uguale alla data ordine. */
+ public static function ziboDate($a)
+ {
+ $c = $a . '.' . self::ZIBO_DATE_COL;
+ return "CASE WHEN $c > $a.order_date THEN $c END";
+ }
+
+ public static function clientDate($a)
+ {
+ $c = $a . '.' . self::CLIENT_DATE_COL;
+ return "CASE WHEN $c > $a.order_date THEN $c END";
+ }
+
+ /** Scadenza effettiva di una riga: data ZIBO, se manca la data cliente. */
+ public static function lineDue($a)
+ {
+ return "COALESCE(" . self::ziboDate($a) . ", " . self::clientDate($a) . ")";
+ }
+
+ /** Aggregati per ordine calcolati dalle righe (alias l). */
+ public static function lineAggregates()
+ {
+ $open = "(d.delivered = 0 AND d.cancelled = 0)";
+ $due = self::lineDue('d');
+ return "(
+ SELECT d.sale_ord_id,
+ COUNT(*) AS lines_total,
+ SUM($open) AS open_lines,
+ MIN(CASE WHEN $open THEN " . self::ziboDate('d') . " END) AS next_zibo,
+ MIN(CASE WHEN $open THEN " . self::clientDate('d') . " END) AS next_client,
+ MIN(" . self::ziboDate('d') . ") AS first_zibo,
+ MIN(" . self::clientDate('d') . ") AS first_client,
+ MIN(CASE WHEN $open THEN $due END) AS next_due,
+ 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
+ FROM mago_sale_ord_detail d
+ WHERE d.item <> '' OR d.qty <> 0
+ GROUP BY d.sale_ord_id
+ ) l";
+ }
+
/** Risposta JSON e fine script (pulisce l'output di headscript). */
public static function jsonOut(array $data)
{
@@ -36,8 +90,13 @@ class MagoSalesStats
}
/**
- * Vista base: ordini + nome cliente + stato calcolato + data consegna di riferimento.
- * Le statistiche filtrano su questa vista (alias x).
+ * Vista base: ordini + nome cliente + stato + date di consegna (alias x).
+ *
+ * zibo_date = prima data ZIBO tra le righe ancora da consegnare (se tutte consegnate: la prima in assoluto);
+ * senza righe: data ZIBO di testata
+ * client_date = stessa logica con la data cliente
+ * due_date = scadenza dell'ordine: la più vicina tra le righe aperte (riga: data ZIBO, se manca data cliente)
+ * late_lines / open_lines = righe aperte in ritardo / righe aperte (per il "ritardo parziale")
*/
public static function baseFrom()
{
@@ -45,13 +104,19 @@ class MagoSalesStats
SELECT
o.*,
COALESCE(NULLIF(c.company_name, ''), o.customer) AS company_name,
- -- Data consegna di riferimento: prevista; la 'confermata' solo se successiva all'ordine
- -- (in Mago spesso coincide con la data ordine e farebbe risultare tutto in ritardo)
+ COALESCE(l.next_zibo, l.first_zibo, " . self::ziboDate('o') . ") AS zibo_date,
+ COALESCE(l.next_client, l.first_client, " . self::clientDate('o') . ") AS client_date,
+ -- scadenza = la più vicina tra le righe aperte, dove ogni riga scade alla sua data ZIBO
+ -- (o alla data cliente se la data ZIBO non è compilata)
COALESCE(
- o.expected_delivery_date,
- CASE WHEN o.confirmed_delivery_date > o.order_date THEN o.confirmed_delivery_date END,
- o.compulsory_delivery_date
+ l.next_due, l.first_due,
+ " . self::ziboDate('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,
CASE
WHEN o.cancelled = 1 THEN 'annullato'
WHEN o.delivered = 1 THEN 'evaso'
@@ -61,9 +126,36 @@ class MagoSalesStats
FROM mago_sale_ord o
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
) x";
}
+ /**
+ * Vista righe aperte (alias r) per la pagina Consegne: riga + ordine + date effettive.
+ */
+ public static function openLinesFrom()
+ {
+ return "(
+ SELECT
+ d.sale_ord_id, d.sub_id, d.line, d.position, d.item, d.description, d.uom,
+ d.qty, d.delivered_qty,
+ GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty,
+ CASE WHEN d.qty > 0 THEN ROUND(d.taxable_amount * GREATEST(d.qty - d.delivered_qty, 0) / d.qty, 2)
+ ELSE d.taxable_amount END AS residual_value,
+ d.taxable_amount,
+ " . self::ziboDate('d') . " AS zibo_date,
+ " . self::clientDate('d') . " AS client_date,
+ " . self::lineDue('d') . " AS due_date,
+ x.internal_ord_no, x.order_date, x.customer, x.company_name,
+ x.salesperson, x.area, x.status, x.your_reference, x.external_ord_no
+ FROM mago_sale_ord_detail d
+ JOIN " . self::baseFrom() . " ON x.sale_ord_id = d.sale_ord_id
+ WHERE d.delivered = 0 AND d.cancelled = 0
+ AND x.status IN ('aperto','bloccato')
+ AND (d.item <> '' OR d.qty <> 0)
+ ) r";
+ }
+
/**
* WHERE + parametri dai filtri in querystring.
* $override sostituisce from/to (stringa vuota = nessun filtro data).
@@ -108,6 +200,7 @@ class MagoSalesStats
'annullato' => "x.status = 'annullato'",
'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())",
'da_fatturare' => "(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0)",
'fatturato' => "x.invoiced = 1",
];
diff --git a/public/userarea/class/MagoSync.php b/public/userarea/class/MagoSync.php
index 56f87304..2435f569 100644
--- a/public/userarea/class/MagoSync.php
+++ b/public/userarea/class/MagoSync.php
@@ -20,6 +20,7 @@ class MagoSync
const ENTITY_ORDERS = 'sale_ord';
const ENTITY_CUSTOMER = 'cust_supp';
const ENTITY_SUMMARY = 'sale_ord_summary';
+ const ENTITY_DETAILS = 'sale_ord_details';
/** @var PDO */
private $pdo;
@@ -62,6 +63,7 @@ class MagoSync
$report['clienti'] = $this->syncCustomers($full);
}
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
+ $report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
$report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
} catch (Throwable $e) {
$report['ok'] = false;
@@ -240,6 +242,192 @@ class MagoSync
];
}
+ /* =========================================================
+ * RIGHE ORDINE (MA_SaleOrdDetails → mago_sale_ord_detail)
+ * ========================================================= */
+
+ /**
+ * Le righe hanno OrderDate, quindi si filtrano come le testate.
+ * Vengono salvate solo le righe di ordini già presenti in locale.
+ * Nelle letture complete le righe degli ordini del periodo vengono
+ * prima cancellate, così spariscono anche le righe eliminate in Mago.
+ */
+ public function syncSaleOrderDetails($from, $full = false, $testMode = false)
+ {
+ $t0 = microtime(true);
+ $state = $this->getState(self::ENTITY_DETAILS);
+
+ // ordini locali del periodo (per scartare righe di ordini non sincronizzati)
+ $st = $this->pdo->prepare("SELECT sale_ord_id FROM mago_sale_ord WHERE order_date >= ?");
+ $st->execute([$from]);
+ $localIds = array_flip(array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN)));
+ if (!$localIds) {
+ return ['records' => 0, 'salvate' => 0, 'incrementale' => false, 'ms' => $this->ms($t0)];
+ }
+
+ $incremental = !$full && !$testMode && !empty($state['last_tb_modified']);
+
+ $filters = [['key' => 'OrderDate', 'value' => $from . 'T00:00:00', 'operator' => '>=']];
+ if ($incremental) {
+ $filters[] = [
+ 'key' => 'TbModified',
+ 'value' => date('Y-m-d\TH:i:s', strtotime($state['last_tb_modified'])),
+ 'operator' => '>=',
+ ];
+ }
+ if ($testMode) {
+ // prova: leggo solo le righe nel periodo degli ordini importati
+ $range = $this->pdo->query("SELECT MIN(order_date), MAX(order_date) FROM mago_sale_ord WHERE order_date >= " . $this->pdo->quote($from))->fetch(PDO::FETCH_NUM);
+ $filters = [
+ ['key' => 'OrderDate', 'value' => $range[0] . 'T00:00:00', 'operator' => '>='],
+ ['key' => 'OrderDate', 'value' => $range[1] . 'T00:00:00', 'operator' => '<='],
+ ];
+ }
+
+ $this->log('Righe: ' . ($incremental ? 'incrementale da ' . $state['last_tb_modified'] : 'completa dal ' . $from));
+
+ $stmt = $this->pdo->prepare("
+ INSERT INTO mago_sale_ord_detail (
+ sale_ord_id, sub_id, line, position, line_type, item, description, uom,
+ qty, delivered_qty, invoiced_qty, unit_value, discount_amount, taxable_amount, total_amount,
+ expected_delivery_date, confirmed_delivery_date, delivered, invoiced, cancelled,
+ customer, order_date, internal_ord_no, notes, tb_modified, synced_at
+ ) VALUES (
+ :sale_ord_id, :sub_id, :line, :position, :line_type, :item, :description, :uom,
+ :qty, :delivered_qty, :invoiced_qty, :unit_value, :discount_amount, :taxable_amount, :total_amount,
+ :expected, :confirmed, :delivered, :invoiced, :cancelled,
+ :customer, :order_date, :internal_ord_no, :notes, :tb_modified, NOW()
+ )
+ ON DUPLICATE KEY UPDATE
+ line = VALUES(line), position = VALUES(position), line_type = VALUES(line_type),
+ item = VALUES(item), description = VALUES(description), uom = VALUES(uom),
+ qty = VALUES(qty), delivered_qty = VALUES(delivered_qty), invoiced_qty = VALUES(invoiced_qty),
+ unit_value = VALUES(unit_value), discount_amount = VALUES(discount_amount),
+ taxable_amount = VALUES(taxable_amount), total_amount = VALUES(total_amount),
+ expected_delivery_date = VALUES(expected_delivery_date),
+ confirmed_delivery_date = VALUES(confirmed_delivery_date),
+ delivered = VALUES(delivered), invoiced = VALUES(invoiced), cancelled = VALUES(cancelled),
+ customer = VALUES(customer), order_date = VALUES(order_date),
+ internal_ord_no = VALUES(internal_ord_no), notes = VALUES(notes),
+ tb_modified = VALUES(tb_modified), synced_at = NOW()
+ ");
+
+ $read = 0;
+ $saved = 0;
+ $maxModified = $state['last_tb_modified'] ?? null;
+
+ $this->pdo->beginTransaction();
+ try {
+ if (!$incremental) {
+ // lettura completa: riparto pulito per gli ordini del periodo
+ $this->pdo->prepare("
+ DELETE d FROM mago_sale_ord_detail d
+ JOIN mago_sale_ord o ON o.sale_ord_id = d.sale_ord_id
+ WHERE o.order_date >= ?
+ ")->execute([$from]);
+ }
+
+ foreach ($this->mago->iterate('MA_SaleOrdDetails', ['filters' => $filters]) as $r) {
+ $read++;
+ $id = (int)($r['SaleOrdId'] ?? 0);
+ $mod = MagoClient::toMysqlDate($r['TbModified'] ?? null, true);
+ if ($mod && (!$maxModified || $mod > $maxModified)) $maxModified = $mod;
+
+ if (!isset($localIds[$id])) continue;
+
+ $s = function ($k, $len) use ($r) {
+ return mb_substr(trim((string)($r[$k] ?? '')), 0, $len);
+ };
+ $d = function ($k, $dec = 2) use ($r) {
+ return round(MagoClient::toDecimal($r[$k] ?? 0), $dec);
+ };
+
+ $stmt->execute([
+ 'sale_ord_id' => $id,
+ 'sub_id' => (int)($r['SubId'] ?? $r['Line'] ?? 0),
+ 'line' => (int)($r['Line'] ?? 0),
+ 'position' => (int)($r['Position'] ?? 0),
+ 'line_type' => (int)($r['LineType'] ?? 0),
+ 'item' => $s('Item', 30),
+ 'description' => $s('Description', 255),
+ 'uom' => $s('UoM', 10),
+ 'qty' => $d('Qty', 3),
+ 'delivered_qty' => $d('DeliveredQty', 3),
+ 'invoiced_qty' => $d('InvoicedQty', 3),
+ 'unit_value' => $d('UnitValue', 5),
+ 'discount_amount' => $d('DiscountAmount'),
+ 'taxable_amount' => $d('TaxableAmount'),
+ 'total_amount' => $d('TotalAmount'),
+ 'expected' => MagoClient::toMysqlDate($r['ExpectedDeliveryDate'] ?? null),
+ 'confirmed' => MagoClient::toMysqlDate($r['ConfirmedDeliveryDate'] ?? null),
+ 'delivered' => MagoClient::toBool($r['Delivered'] ?? false),
+ 'invoiced' => MagoClient::toBool($r['Invoiced'] ?? false),
+ 'cancelled' => MagoClient::toBool($r['Cancelled'] ?? false),
+ 'customer' => $s('Customer', 20),
+ 'order_date' => MagoClient::toMysqlDate($r['OrderDate'] ?? null),
+ 'internal_ord_no' => $s('InternalOrdNo', 30),
+ 'notes' => trim((string)($r['Notes'] ?? '')) ?: null,
+ 'tb_modified' => $mod,
+ ]);
+ $saved++;
+
+ if ($read % 200 === 0) {
+ $this->pdo->commit();
+ $this->pdo->beginTransaction();
+ $this->log(" righe lette: $read");
+ }
+ }
+
+ $this->refreshLineDates($from);
+ $this->pdo->commit();
+ } catch (Throwable $e) {
+ if ($this->pdo->inTransaction()) $this->pdo->rollBack();
+ $this->saveState(self::ENTITY_DETAILS, null, 'errore', $e->getMessage(), $read, $t0, false);
+ if (!$incremental) {
+ // lettura completa interrotta: al prossimo giro si riparte da capo
+ $this->pdo->prepare("UPDATE mago_sync_state SET last_tb_modified = NULL WHERE entity = ?")
+ ->execute([self::ENTITY_DETAILS]);
+ }
+ throw $e;
+ }
+
+ $this->saveState(self::ENTITY_DETAILS, $testMode ? null : $maxModified, 'ok', "Lette $read righe, salvate $saved", $read, $t0, !$testMode);
+ $this->log("Righe: lette $read, salvate $saved");
+
+ return ['records' => $read, 'salvate' => $saved, 'incrementale' => $incremental, 'ms' => $this->ms($t0)];
+ }
+
+ /**
+ * Riporta sulla testata la prima data di consegna delle righe ancora da consegnare
+ * (se tutte consegnate: la prima data tra tutte le righe).
+ * La data confermata vale solo se successiva alla data ordine.
+ */
+ private function refreshLineDates($from)
+ {
+ $this->pdo->prepare("
+ UPDATE mago_sale_ord o
+ LEFT JOIN (
+ SELECT sale_ord_id,
+ COALESCE(
+ MIN(CASE WHEN delivered = 0 AND cancelled = 0 THEN expected_delivery_date END),
+ MIN(expected_delivery_date)
+ ) AS exp_date,
+ COALESCE(
+ MIN(CASE WHEN delivered = 0 AND cancelled = 0 AND confirmed_delivery_date > order_date THEN confirmed_delivery_date END),
+ MIN(CASE WHEN confirmed_delivery_date > order_date THEN confirmed_delivery_date END)
+ ) AS conf_date,
+ COUNT(*) AS n
+ FROM mago_sale_ord_detail
+ WHERE item <> '' OR qty <> 0
+ GROUP BY sale_ord_id
+ ) d ON d.sale_ord_id = o.sale_ord_id
+ SET o.line_expected_date = d.exp_date,
+ o.line_confirmed_date = d.conf_date,
+ o.lines_count = COALESCE(d.n, 0)
+ WHERE o.order_date >= ?
+ ")->execute([$from]);
+ }
+
/* =========================================================
* IMPORTI (MA_SaleOrdSummary → colonne importo di mago_sale_ord)
* ========================================================= */
diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php
index 6cbc2a62..0c8920a5 100644
--- a/public/userarea/include/navbar.php
+++ b/public/userarea/include/navbar.php
@@ -235,6 +235,11 @@
Confronto anni
+
+
+ Consegne
+
+
diff --git a/public/userarea/sales_orders_deliveries.php b/public/userarea/sales_orders_deliveries.php
new file mode 100644
index 00000000..f284bcfb
--- /dev/null
+++ b/public/userarea/sales_orders_deliveries.php
@@ -0,0 +1,1245 @@
+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 '';
+ 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)',
+ '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()]);
+ }
+}
+
+?>
+
+
+
+
+
+
+
+
+ Consegne - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Scadenza
+
+
+
+
+
+
+
+
+
+
+
+ Data ZIBO = scadenza comunicata (rossa se superata)
+ Data cliente = desiderata (arancione se superata)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Da consegnare
+
—
+
+
+
+
+
Scadute (data ZIBO)
+
—
+
+
+
+
+
Oltre data cliente
+
—
+
+
+
+
+
Prossimi 7 giorni
+
—
+
+
+
+
+
Senza data
+
—
+
righe senza data di consegna
+
+
+
+
+
+
+
+
+
+
Carico consegne per settimana
+
Per data ZIBO (se manca: data cliente) · clicca una barra per filtrare
+
+
+
+
+
+
+
+
+
+
+
+
Clienti con più ritardo
+
Valore residuo scaduto (data ZIBO) · clicca per filtrare
+
+
+
+
+
+
+
+
+
Righe da consegnare
+
Ordinate per scadenza · clicca il numero ordine per aprirlo
+
+
+
+
+
+ | Ordine |
+ Cliente |
+ Articolo |
+ Residuo |
+ Consegna (ZIBO / cliente) |
+ Ritardo |
+ Valore residuo |
+ Agente |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/userarea/sales_orders_stats.php b/public/userarea/sales_orders_stats.php
index 3ca72e46..a48b8a0c 100644
--- a/public/userarea/sales_orders_stats.php
+++ b/public/userarea/sales_orders_stats.php
@@ -73,6 +73,8 @@ if ($action === 'data') {
SUM(x.status = 'annullato') AS cancelled,
SUM(x.status = 'bloccato') AS blocked,
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(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
@@ -160,12 +162,30 @@ if ($action === 'data') {
GROUP BY bucket ORDER BY bucket
", $params);
+ // --- Top articoli (dalle righe, annullati esclusi) ---
+ $topItems = MagoSalesStats::fetchAllQ($pdo, "
+ SELECT d.item AS code,
+ MAX(d.description) AS name,
+ MAX(d.uom) AS uom,
+ SUM(d.qty) AS qty,
+ SUM(d.taxable_amount) AS value,
+ COUNT(DISTINCT d.sale_ord_id) AS orders
+ FROM mago_sale_ord_detail d
+ JOIN $from ON x.sale_ord_id = d.sale_ord_id
+ WHERE $where AND d.item <> '' AND d.cancelled = 0 AND x.status <> 'annullato'
+ GROUP BY d.item
+ ORDER BY " . ($metric === 'value' ? 'value' : 'orders') . " DESC
+ LIMIT 10
+ ", $params);
+
// --- Elenco ordini ---
$rows = MagoSalesStats::fetchAllQ($pdo, "
SELECT x.sale_ord_id, x.internal_ord_no, x.external_ord_no,
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.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
FROM $from
@@ -183,6 +203,7 @@ if ($action === 'data') {
'by_area' => $byArea,
'by_salesperson' => $bySalesperson,
'due_buckets' => $dueBuckets,
+ 'top_items' => $topItems,
'rows' => $rows,
]);
} catch (Throwable $e) {
@@ -201,8 +222,22 @@ if ($action === 'order') {
$raw = json_decode((string)$o['raw_json'], true) ?: [];
unset($o['raw_json']);
+ $stL = $pdo->prepare("
+ SELECT d.line, d.item, d.description, d.uom, d.qty, d.delivered_qty, d.invoiced_qty, d.unit_value,
+ GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty,
+ d.taxable_amount, d.delivered, d.invoiced, d.cancelled, d.notes,
+ " . MagoSalesStats::ziboDate('d') . " AS zibo_date,
+ " . MagoSalesStats::clientDate('d') . " AS client_date
+ FROM mago_sale_ord_detail d
+ WHERE d.sale_ord_id = ?
+ ORDER BY d.position, d.line, d.sub_id
+ ");
+ $stL->execute([$id]);
+ $lines = $stL->fetchAll(PDO::FETCH_ASSOC);
+
MagoSalesStats::jsonOut([
'success' => true,
+ 'lines' => $lines,
'order' => $o,
'extra' => [
'Cliente fatturazione' => $raw['InvoicingCustomer'] ?? '',
@@ -654,6 +689,83 @@ if ($action === 'order') {
margin-left: 6px;
}
+ /* date di consegna: ZIBO (scadenza) rossa se superata, cliente (desiderata) arancione */
+ .dt-zibo {
+ font-weight: 700;
+ color: var(--c-ink);
+ white-space: nowrap;
+ }
+
+ .dt-zibo.late {
+ color: #dc3545;
+ }
+
+ .dt-client {
+ font-size: .82rem;
+ color: #64748b;
+ white-space: nowrap;
+ }
+
+ .dt-client.late {
+ color: #ea580c;
+ 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;
+ }
+
+ .more-dates {
+ display: inline-block;
+ background: #eef2ff;
+ color: #4338ca;
+ font-size: .7rem;
+ font-weight: 800;
+ border-radius: 999px;
+ padding: 1px 7px;
+ margin-left: 4px;
+ }
+
+ .partial-badge {
+ background: #fff1e6;
+ color: #c2410c;
+ font-weight: 800;
+ font-size: .75rem;
+ padding: 3px 8px;
+ border-radius: 999px;
+ margin-left: 6px;
+ }
+
+ .late-badge,
+ .partial-badge,
+ .late-client-badge {
+ white-space: nowrap;
+ display: inline-block;
+ margin-top: 2px;
+ }
+
+ .late-client-badge {
+ background: #fff7ed;
+ color: #ea580c;
+ font-weight: 800;
+ font-size: .72rem;
+ padding: 3px 8px;
+ border-radius: 999px;
+ margin-left: 6px;
+ }
+
.inv-badge {
background: #efe9fb;
color: var(--c-invoice);
@@ -771,6 +883,7 @@ if ($action === 'order') {
@@ -816,7 +929,8 @@ if ($action === 'order') {
-
+
+
@@ -876,7 +990,7 @@ if ($action === 'order') {
In ritardo
—
-
consegna prevista superata
+
data ZIBO superata
@@ -947,6 +1061,16 @@ if ($action === 'order') {
+
+
+
+
Top 10 articoli per numero ordini
+
Dalle righe ordine · passa sopra una barra per quantità e valore
+
+
+
+
+
@@ -976,7 +1100,7 @@ if ($action === 'order') {
Data |
Cliente |
Rif. cliente |
-
Consegna prev. |
+
Consegna (ZIBO / cliente) |
Imponibile |
Stato |
Fatt. |
@@ -1198,7 +1322,11 @@ if ($action === 'order') {
if (f.statuses.length === 1) $(`.kpi[data-status="${f.statuses[0]}"]`).addClass('selected');
}
+ // aperta da link (?q=numero ordine): non sovrascrivo i filtri salvati dell'utente
+ const URL_Q = new URLSearchParams(location.search).get('q');
+
function saveFilters(f) {
+ if (URL_Q) return;
try {
localStorage.setItem(STORE_KEY, JSON.stringify(Object.assign({}, f, {
metric: metric
@@ -1319,7 +1447,13 @@ if ($action === 'order') {
renderDue(d.due_buckets);
renderSimpleBar('chArea', d.by_area, '#0ea5e9', 'areas');
renderSimpleBar('chSalesperson', d.by_salesperson, '#6f42c1', 'salespeople');
+ renderItems(d.top_items || []);
renderTable(d.rows);
+ if (window.__autoOpen) {
+ const hit = d.rows.find(r => r.internal_ord_no === window.__autoOpen) || (d.rows.length === 1 ? d.rows[0] : null);
+ window.__autoOpen = null;
+ if (hit) openOrder(hit.sale_ord_id);
+ }
})
.catch(err => {
$('#statsContent').css('opacity', 1);
@@ -1336,6 +1470,7 @@ if ($action === 'order') {
$('#ttlDue').text(v ? 'Scadenze ordini aperti (valore)' : 'Scadenze ordini aperti');
$('#ttlArea').text(v ? 'Valore per area' : 'Ordini per area');
$('#ttlSalesperson').text(v ? 'Valore per agente' : 'Ordini per agente');
+ $('#ttlItems').text(v ? 'Top 10 articoli per valore ordinato' : 'Top 10 articoli per numero ordini');
}
function renderKpi(k) {
@@ -1343,6 +1478,9 @@ if ($action === 'order') {
$('#kOrders').text(fmtNum(orders));
$('#kOpen').text(fmtNum(k.open_orders));
$('#kLate').text(fmtNum(k.late));
+ $('#kLateSub').html(`data ZIBO superata` +
+ (+k.late_partial ? ` · ${fmtNum(k.late_partial)} parziali` : '') +
+ `
${fmtNum(k.late_client)} oltre data cliente`);
$('#kDelivered').text(fmtNum(k.delivered));
$('#kToInvoice').text(fmtNum(k.to_invoice));
$('#kCustomers').text(fmtNum(k.customers));
@@ -1695,16 +1833,113 @@ if ($action === 'order') {
});
}
+ function renderItems(rows) {
+ const v = metric === 'value';
+ const label = r => {
+ const n = (r.name || '').trim();
+ const t = n ? `${r.code} · ${n}` : r.code;
+ return t.length > 60 ? t.substring(0, 58) + '…' : t;
+ };
+ upsertChart('items', 'chItems', {
+ chart: {
+ ...baseChart,
+ type: 'bar',
+ height: 380
+ },
+ series: [{
+ name: v ? 'Valore' : 'Ordini',
+ data: rows.map(r => v ? +r.value : +r.orders)
+ }],
+ colors: ['#0f766e'],
+ xaxis: {
+ categories: rows.map(label),
+ labels: {
+ formatter: fmtMetricAxis
+ }
+ },
+ yaxis: {
+ labels: {
+ maxWidth: 380
+ }
+ },
+ plotOptions: {
+ bar: {
+ horizontal: true,
+ borderRadius: 4,
+ barHeight: '65%'
+ }
+ },
+ dataLabels: {
+ enabled: false
+ },
+ grid: {
+ borderColor: '#eef2f7'
+ },
+ tooltip: {
+ custom: ({
+ dataPointIndex
+ }) => {
+ const r = rows[dataPointIndex];
+ if (!r) return '';
+ return `
+
${esc(r.code)}
+
${esc(r.name)}
+
Ordini: ${fmtNum(r.orders)}
+
Quantità: ${fmtNum(Math.round(r.qty))} ${esc(r.uom)}
+
Valore: ${fmtEur(r.value)}
+
`;
+ }
+ },
+ noData: {
+ text: 'Nessuna riga articolo nel periodo'
+ }
+ });
+ }
+
+ // cella consegna: data ZIBO (rossa se superata) + 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` : ''}
`;
+ } else {
+ html += `
data ZIBO non indicata
`;
+ }
+ if (client) {
+ html += `
Cliente${fmtDate(client)}
`;
+ }
+ return html || '—';
+ }
+
+ function lateBadges(r) {
+ if (r.status !== 'aperto' && r.status !== 'bloccato') return '';
+ const lateL = +r.late_lines,
+ openL = +r.open_lines;
+ if (+r.is_late) {
+ return lateL > 0 && openL > lateL ?
+ `
PARZIALE ${lateL}/${openL} righe` :
+ '
IN RITARDO';
+ }
+ return +r.is_late_client ? '
OLTRE DATA CLIENTE' : '';
+ }
+
function renderTable(rows) {
const data = rows.map(r => [
`
${esc(r.internal_ord_no)}${r.external_ord_no ? `
${esc(r.external_ord_no)}
` : ''}`,
`
${fmtDate(r.order_date)}`,
`
${esc(r.company_name)}
${esc(r.customer)}
`,
esc(r.your_reference),
- `
${fmtDate(r.due_date) || '—'}`,
+ `
${dateCell(r.zibo_date, r.client_date, {
+ open: r.status === 'aperto' || r.status === 'bloccato',
+ more: +r.open_dates > 1 ? (+r.open_dates - 1) : 0
+ })}`,
+r.no_amount ? '
n.d.' :
`
${fmtEur(r.taxable_amount, 2)}`,
- `
${STATUS_LABEL[r.status] || esc(r.status)}${+r.is_late ? '
IN RITARDO' : ''}`,
+ `
${STATUS_LABEL[r.status] || esc(r.status)}` + lateBadges(r),
+r.invoiced ? '
Sì' : '
—',
esc(r.salesperson),
esc(r.area),
@@ -1756,7 +1991,7 @@ if ($action === 'order') {
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
format: {
// importo esportato come numero, il resto come testo
- body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('
').html(String(d).replace(/<(div|span)/g, ' <$1')).text().replace(/\s+/g, ' ').trim()
+ body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('
').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, ' ')).text().replace(/\s+/g, ' ').trim()
}
}
},
@@ -1769,7 +2004,7 @@ if ($action === 'order') {
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
format: {
// importo esportato come numero, il resto come testo
- body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('
').html(String(d).replace(/<(div|span)/g, ' <$1')).text().replace(/\s+/g, ' ').trim()
+ body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('
').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, ' ')).text().replace(/\s+/g, ' ').trim()
}
}
}
@@ -1825,19 +2060,58 @@ if ($action === 'order') {
` : '
Importi non ancora letti da Mago.
'}
Data ordine
${fmtDate(o.order_date) || '—'}
-
Consegna prevista
${fmtDate(o.expected_delivery_date) || '—'}
-
Consegna confermata
${fmtDate(o.confirmed_delivery_date) || '—'}
+
Consegna
${dateCell(o.zibo_date, o.client_date, {open: o.status === 'aperto' || o.status === 'bloccato'})}${+o.open_lines ? ` prima riga da consegnare · ${fmtNum(o.open_lines)} righe aperte${+o.late_lines ? `, ${fmtNum(o.late_lines)} in ritardo` : ''}` : ''}
N° ordine cliente
${esc(o.external_ord_no) || '—'}
Agente
${esc(o.salesperson) || '—'}
Area
${esc(o.area) || '—'}
${extra}
- ${o.notes ? `
Note:
${esc(o.notes)}
` : ''}
-
Le righe articolo non sono disponibili: la tabella righe ordine non è esposta dalle API di Mago.
+ ${renderLines(d.lines || [])}
+ ${o.notes ? `
Note:
${esc(o.notes)}
` : ''}
+
`);
});
}
+ function renderLines(lines) {
+ if (!lines.length) {
+ return '
Righe non ancora sincronizzate: premi Aggiorna da Mago.
';
+ }
+ 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 late = open && due && due < today;
+ const days = late ? Math.round((new Date(today) - new Date(due)) / 86400000) : 0;
+ const st = +l.cancelled ? '
Annullata' :
+ (+l.delivered ? '
Consegnata' :
+ '
Aperta' + (late ? `
+${days} gg` : ''));
+ const qtyTxt = n => fmtNum(Math.round(+n * 100) / 100);
+ return `
+ | ${esc(l.item)} ${esc(l.description)} |
+ ${qtyTxt(l.qty)} ${esc(l.uom)} |
+ ${qtyTxt(l.delivered_qty)} |
+ ${open ? '' + qtyTxt(l.residual_qty) + '' : '—'} |
+ ${dateCell(l.zibo_date, l.client_date, {open: open})} |
+ ${fmtEur(l.taxable_amount, 2)} |
+ ${st} |
+
`;
+ }).join('');
+ return `
+
Righe ordine (${lines.length})
+
+
+
+
+ | Articolo | Qtà | Consegnata | Residuo |
+ Consegna (ZIBO / cliente) | Imponibile | Stato |
+
+
+ ${rows}
+
+
`;
+ }
+
/* ---------------- SINCRONIZZAZIONE ---------------- */
function runSync(mode) {
const labels = {
@@ -1875,6 +2149,7 @@ if ($action === 'order') {
icon: 'success',
title: 'Dati aggiornati',
html: `Ordini letti:
${fmtNum(o.records)}` +
+ (d.righe ? `
Righe lette:
${fmtNum(d.righe.records)}` : '') +
(d.importi ? `
Totali letti:
${fmtNum(d.importi.records)}` : '') +
(c.records !== undefined ? `
Clienti aggiornati:
${fmtNum(c.records)}` : '') +
(d.logoff !== 'OK' ? `
Logoff Mago: ${esc(d.logoff)}
` : ''),
@@ -1958,8 +2233,16 @@ if ($action === 'order') {
loadFilterOptions(false).then(d => {
window.__totalOrders = d && d.sync ? d.sync.total_orders : 0;
- const saved = restoreFilters();
+ const saved = URL_Q ? null : restoreFilters();
if (saved && saved.metric === 'value') metric = 'value';
+ if (URL_Q) {
+ // tutto lo storico, solo l'ordine cercato; se è uno solo apro il dettaglio
+ $('#fFrom').val((d && d.range && d.range.min_date) || '2020-01-01');
+ $('#fTo').val(iso(new Date()));
+ $('#periodChips .chip').removeClass('active');
+ $('#fSearch').val(URL_Q);
+ window.__autoOpen = URL_Q;
+ }
if (saved && saved.from) {
$('#fFrom').val(saved.from);
$('#fTo').val(saved.to);
@@ -1968,7 +2251,7 @@ if ($action === 'order') {
$('#fAreas').val(saved.areas || []).trigger('change.select2');
(saved.statuses || []).forEach(s => $(`#statusChips .chip[data-status="${s}"]`).addClass('active'));
$('#fSearch').val(saved.q || '');
- } else {
+ } else if (!URL_Q) {
setPeriod('y' + new Date().getFullYear(), true);
}
loadData();
diff --git a/public/userarea/sales_orders_years.php b/public/userarea/sales_orders_years.php
index 6d91f07b..bc90a47e 100644
--- a/public/userarea/sales_orders_years.php
+++ b/public/userarea/sales_orders_years.php
@@ -584,6 +584,7 @@ if ($action === 'data') {