fixed stats

This commit is contained in:
2026-10-13 16:30:47 +02:00
parent bb3d1b1aa1
commit c96717b051
7 changed files with 1894 additions and 20 deletions
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Righe ordini cliente da MA_SaleOrdDetails (Mago4) e date di consegna
* calcolate dalle righe sulla testata.
*/
final class CreateMagoSaleOrdDetail extends AbstractMigration
{
public function change(): void
{
$this->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();
}
}
+100 -7
View File
@@ -9,6 +9,60 @@ class MagoSalesStats
{ {
const CUSTOMER_TYPE = 3211264; 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). */ /** Risposta JSON e fine script (pulisce l'output di headscript). */
public static function jsonOut(array $data) public static function jsonOut(array $data)
{ {
@@ -36,8 +90,13 @@ class MagoSalesStats
} }
/** /**
* Vista base: ordini + nome cliente + stato calcolato + data consegna di riferimento. * Vista base: ordini + nome cliente + stato + date di consegna (alias x).
* Le statistiche filtrano su questa vista (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() public static function baseFrom()
{ {
@@ -45,13 +104,19 @@ class MagoSalesStats
SELECT SELECT
o.*, o.*,
COALESCE(NULLIF(c.company_name, ''), o.customer) AS company_name, COALESCE(NULLIF(c.company_name, ''), o.customer) AS company_name,
-- Data consegna di riferimento: prevista; la 'confermata' solo se successiva all'ordine COALESCE(l.next_zibo, l.first_zibo, " . self::ziboDate('o') . ") AS zibo_date,
-- (in Mago spesso coincide con la data ordine e farebbe risultare tutto in ritardo) 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( COALESCE(
o.expected_delivery_date, l.next_due, l.first_due,
CASE WHEN o.confirmed_delivery_date > o.order_date THEN o.confirmed_delivery_date END, " . self::ziboDate('o') . ", " . self::clientDate('o') . ", o.compulsory_delivery_date
o.compulsory_delivery_date
) AS due_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 CASE
WHEN o.cancelled = 1 THEN 'annullato' WHEN o.cancelled = 1 THEN 'annullato'
WHEN o.delivered = 1 THEN 'evaso' WHEN o.delivered = 1 THEN 'evaso'
@@ -61,9 +126,36 @@ class MagoSalesStats
FROM mago_sale_ord o FROM mago_sale_ord o
LEFT JOIN mago_cust_supp c LEFT JOIN mago_cust_supp c
ON c.cust_supp_type = " . self::CUSTOMER_TYPE . " AND c.cust_supp = o.customer 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"; ) 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. * WHERE + parametri dai filtri in querystring.
* $override sostituisce from/to (stringa vuota = nessun filtro data). * $override sostituisce from/to (stringa vuota = nessun filtro data).
@@ -108,6 +200,7 @@ class MagoSalesStats
'annullato' => "x.status = 'annullato'", 'annullato' => "x.status = 'annullato'",
'bloccato' => "x.status = 'bloccato'", 'bloccato' => "x.status = 'bloccato'",
'ritardo' => "(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE())", '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)", 'da_fatturare' => "(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0)",
'fatturato' => "x.invoiced = 1", 'fatturato' => "x.invoiced = 1",
]; ];
+188
View File
@@ -20,6 +20,7 @@ class MagoSync
const ENTITY_ORDERS = 'sale_ord'; const ENTITY_ORDERS = 'sale_ord';
const ENTITY_CUSTOMER = 'cust_supp'; const ENTITY_CUSTOMER = 'cust_supp';
const ENTITY_SUMMARY = 'sale_ord_summary'; const ENTITY_SUMMARY = 'sale_ord_summary';
const ENTITY_DETAILS = 'sale_ord_details';
/** @var PDO */ /** @var PDO */
private $pdo; private $pdo;
@@ -62,6 +63,7 @@ class MagoSync
$report['clienti'] = $this->syncCustomers($full); $report['clienti'] = $this->syncCustomers($full);
} }
$report['ordini'] = $this->syncSaleOrders($from, $limit, $full); $report['ordini'] = $this->syncSaleOrders($from, $limit, $full);
$report['righe'] = $this->syncSaleOrderDetails($from, $full, (bool)$limit);
$report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit); $report['importi'] = $this->syncSaleOrderSummaries($from, $full, (bool)$limit);
} catch (Throwable $e) { } catch (Throwable $e) {
$report['ok'] = false; $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) * IMPORTI (MA_SaleOrdSummary → colonne importo di mago_sale_ord)
* ========================================================= */ * ========================================================= */
+5
View File
@@ -235,6 +235,11 @@
<i class='bx bx-radio-circle'></i>Confronto anni <i class='bx bx-radio-circle'></i>Confronto anni
</a> </a>
</li> </li>
<li>
<a href="sales_orders_deliveries.php">
<i class='bx bx-radio-circle'></i>Consegne
</a>
</li>
</ul> </ul>
</li> </li>
<?php endif; ?> <?php endif; ?>
File diff suppressed because it is too large Load Diff
+296 -13
View File
@@ -73,6 +73,8 @@ if ($action === 'data') {
SUM(x.status = 'annullato') AS cancelled, SUM(x.status = 'annullato') AS cancelled,
SUM(x.status = 'bloccato') AS blocked, 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.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, SUM(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0) AS to_invoice,
COUNT(DISTINCT x.customer) AS customers, 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 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 GROUP BY bucket ORDER BY bucket
", $params); ", $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 --- // --- Elenco ordini ---
$rows = MagoSalesStats::fetchAllQ($pdo, " $rows = MagoSalesStats::fetchAllQ($pdo, "
SELECT x.sale_ord_id, x.internal_ord_no, x.external_ord_no, 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.order_date, x.due_date, x.customer, x.company_name,
x.salesperson, x.area, x.status, x.invoiced, x.your_reference, x.salesperson, x.area, x.status, x.invoiced, x.your_reference,
x.taxable_amount, x.total_amount, 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.summary_tb_modified IS NULL) AS no_amount,
(x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS is_late (x.status IN ('aperto','bloccato') AND x.due_date < CURDATE()) AS is_late
FROM $from FROM $from
@@ -183,6 +203,7 @@ if ($action === 'data') {
'by_area' => $byArea, 'by_area' => $byArea,
'by_salesperson' => $bySalesperson, 'by_salesperson' => $bySalesperson,
'due_buckets' => $dueBuckets, 'due_buckets' => $dueBuckets,
'top_items' => $topItems,
'rows' => $rows, 'rows' => $rows,
]); ]);
} catch (Throwable $e) { } catch (Throwable $e) {
@@ -201,8 +222,22 @@ if ($action === 'order') {
$raw = json_decode((string)$o['raw_json'], true) ?: []; $raw = json_decode((string)$o['raw_json'], true) ?: [];
unset($o['raw_json']); 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([ MagoSalesStats::jsonOut([
'success' => true, 'success' => true,
'lines' => $lines,
'order' => $o, 'order' => $o,
'extra' => [ 'extra' => [
'Cliente fatturazione' => $raw['InvoicingCustomer'] ?? '', 'Cliente fatturazione' => $raw['InvoicingCustomer'] ?? '',
@@ -654,6 +689,83 @@ if ($action === 'order') {
margin-left: 6px; 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 { .inv-badge {
background: #efe9fb; background: #efe9fb;
color: var(--c-invoice); color: var(--c-invoice);
@@ -771,6 +883,7 @@ if ($action === 'order') {
<div class="stats-tabs"> <div class="stats-tabs">
<a href="sales_orders_stats.php" class="active"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a> <a href="sales_orders_stats.php" class="active"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a> <a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
</div> </div>
<!-- FILTRI --> <!-- FILTRI -->
@@ -816,7 +929,8 @@ if ($action === 'order') {
<label>Stato</label> <label>Stato</label>
<div class="chip-row" id="statusChips"> <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="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</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="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="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> <button type="button" class="chip" data-status="evaso" style="--chip-c:var(--c-done);">Evasi</button>
<button type="button" class="chip" data-status="da_fatturare" style="--chip-c:var(--c-invoice);">Da fatturare</button> <button type="button" class="chip" data-status="da_fatturare" style="--chip-c:var(--c-invoice);">Da fatturare</button>
@@ -876,7 +990,7 @@ if ($action === 'order') {
<div class="kpi-icon"><i class="fa-solid fa-triangle-exclamation"></i></div> <div class="kpi-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="kpi-label">In ritardo</div> <div class="kpi-label">In ritardo</div>
<div class="kpi-value" id="kLate">—</div> <div class="kpi-value" id="kLate">—</div>
<div class="kpi-sub">consegna prevista superata</div> <div class="kpi-sub" id="kLateSub">data ZIBO superata</div>
</div> </div>
<div class="kpi" data-status="evaso" style="--k:var(--c-done);"> <div class="kpi" data-status="evaso" style="--k:var(--c-done);">
<div class="kpi-icon"><i class="fa-solid fa-truck"></i></div> <div class="kpi-icon"><i class="fa-solid fa-truck"></i></div>
@@ -947,6 +1061,16 @@ if ($action === 'order') {
</div> </div>
</div> </div>
<div class="row g-3 mb-3">
<div class="col-12">
<div class="chart-card">
<div class="chart-title" id="ttlItems">Top 10 articoli per numero ordini</div>
<div class="chart-hint">Dalle righe ordine · passa sopra una barra per quantità e valore</div>
<div id="chItems" style="min-height:380px;"></div>
</div>
</div>
</div>
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="chart-card"> <div class="chart-card">
@@ -976,7 +1100,7 @@ if ($action === 'order') {
<th>Data</th> <th>Data</th>
<th>Cliente</th> <th>Cliente</th>
<th>Rif. cliente</th> <th>Rif. cliente</th>
<th>Consegna prev.</th> <th>Consegna <span class="small fw-normal">(ZIBO / cliente)</span></th>
<th class="text-end">Imponibile</th> <th class="text-end">Imponibile</th>
<th>Stato</th> <th>Stato</th>
<th>Fatt.</th> <th>Fatt.</th>
@@ -1198,7 +1322,11 @@ if ($action === 'order') {
if (f.statuses.length === 1) $(`.kpi[data-status="${f.statuses[0]}"]`).addClass('selected'); 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) { function saveFilters(f) {
if (URL_Q) return;
try { try {
localStorage.setItem(STORE_KEY, JSON.stringify(Object.assign({}, f, { localStorage.setItem(STORE_KEY, JSON.stringify(Object.assign({}, f, {
metric: metric metric: metric
@@ -1319,7 +1447,13 @@ if ($action === 'order') {
renderDue(d.due_buckets); renderDue(d.due_buckets);
renderSimpleBar('chArea', d.by_area, '#0ea5e9', 'areas'); renderSimpleBar('chArea', d.by_area, '#0ea5e9', 'areas');
renderSimpleBar('chSalesperson', d.by_salesperson, '#6f42c1', 'salespeople'); renderSimpleBar('chSalesperson', d.by_salesperson, '#6f42c1', 'salespeople');
renderItems(d.top_items || []);
renderTable(d.rows); 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 => { .catch(err => {
$('#statsContent').css('opacity', 1); $('#statsContent').css('opacity', 1);
@@ -1336,6 +1470,7 @@ if ($action === 'order') {
$('#ttlDue').text(v ? 'Scadenze ordini aperti (valore)' : 'Scadenze ordini aperti'); $('#ttlDue').text(v ? 'Scadenze ordini aperti (valore)' : 'Scadenze ordini aperti');
$('#ttlArea').text(v ? 'Valore per area' : 'Ordini per area'); $('#ttlArea').text(v ? 'Valore per area' : 'Ordini per area');
$('#ttlSalesperson').text(v ? 'Valore per agente' : 'Ordini per agente'); $('#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) { function renderKpi(k) {
@@ -1343,6 +1478,9 @@ if ($action === 'order') {
$('#kOrders').text(fmtNum(orders)); $('#kOrders').text(fmtNum(orders));
$('#kOpen').text(fmtNum(k.open_orders)); $('#kOpen').text(fmtNum(k.open_orders));
$('#kLate').text(fmtNum(k.late)); $('#kLate').text(fmtNum(k.late));
$('#kLateSub').html(`data ZIBO superata` +
(+k.late_partial ? ` · ${fmtNum(k.late_partial)} parziali` : '') +
`<br><span style="color:#ea580c;font-weight:700;">${fmtNum(k.late_client)}</span> oltre data cliente`);
$('#kDelivered').text(fmtNum(k.delivered)); $('#kDelivered').text(fmtNum(k.delivered));
$('#kToInvoice').text(fmtNum(k.to_invoice)); $('#kToInvoice').text(fmtNum(k.to_invoice));
$('#kCustomers').text(fmtNum(k.customers)); $('#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 `<div style="padding:8px 12px;font-size:.85rem;">
<div style="font-weight:800;">${esc(r.code)}</div>
<div style="color:#64748b;max-width:320px;white-space:normal;">${esc(r.name)}</div>
<div style="margin-top:6px;">Ordini: <b>${fmtNum(r.orders)}</b></div>
<div>Quantità: <b>${fmtNum(Math.round(r.qty))} ${esc(r.uom)}</b></div>
<div>Valore: <b>${fmtEur(r.value)}</b></div>
</div>`;
}
},
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 += `<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>`;
} else {
html += `<div class="dt-fallback">data ZIBO non indicata</div>`;
}
if (client) {
html += `<div class="dt-client${cLate ? ' late' : ''}"><span class="dt-tag">Cliente</span>${fmtDate(client)}</div>`;
}
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 ?
`<span class="partial-badge" title="Righe aperte in ritardo sulla data ZIBO">PARZIALE ${lateL}/${openL} righe</span>` :
'<span class="late-badge">IN RITARDO</span>';
}
return +r.is_late_client ? '<span class="late-client-badge" title="Data ZIBO non ancora superata">OLTRE DATA CLIENTE</span>' : '';
}
function renderTable(rows) { function renderTable(rows) {
const data = rows.map(r => [ const data = rows.map(r => [
`<b>${esc(r.internal_ord_no)}</b>${r.external_ord_no ? `<div class="small text-muted">${esc(r.external_ord_no)}</div>` : ''}`, `<b>${esc(r.internal_ord_no)}</b>${r.external_ord_no ? `<div class="small text-muted">${esc(r.external_ord_no)}</div>` : ''}`,
`<span data-order="${esc(r.order_date || '')}">${fmtDate(r.order_date)}</span>`, `<span data-order="${esc(r.order_date || '')}">${fmtDate(r.order_date)}</span>`,
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}</div>`, `<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}</div>`,
esc(r.your_reference), esc(r.your_reference),
`<span data-order="${esc(r.due_date || '')}">${fmtDate(r.due_date) || '—'}</span>`, `<span data-order="${esc(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
})}</span>`,
+r.no_amount ? '<span data-order="-1" class="text-muted">n.d.</span>' : +r.no_amount ? '<span data-order="-1" class="text-muted">n.d.</span>' :
`<span data-order="${+r.taxable_amount}" class="amount${r.status === 'annullato' ? ' text-muted text-decoration-line-through' : ''}">${fmtEur(r.taxable_amount, 2)}</span>`, `<span data-order="${+r.taxable_amount}" class="amount${r.status === 'annullato' ? ' text-muted text-decoration-line-through' : ''}">${fmtEur(r.taxable_amount, 2)}</span>`,
`<span class="st-badge st-${esc(r.status)}">${STATUS_LABEL[r.status] || esc(r.status)}</span>${+r.is_late ? '<span class="late-badge">IN RITARDO</span>' : ''}`, `<span class="st-badge st-${esc(r.status)}">${STATUS_LABEL[r.status] || esc(r.status)}</span>` + lateBadges(r),
+r.invoiced ? '<span class="inv-badge">Sì</span>' : '<span class="text-muted">—</span>', +r.invoiced ? '<span class="inv-badge">Sì</span>' : '<span class="text-muted">—</span>',
esc(r.salesperson), esc(r.salesperson),
esc(r.area), esc(r.area),
@@ -1756,7 +1991,7 @@ if ($action === 'order') {
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
format: { format: {
// importo esportato come numero, il resto come testo // importo esportato come numero, il resto come testo
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').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') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, '</span> ')).text().replace(/\s+/g, ' ').trim()
} }
} }
}, },
@@ -1769,7 +2004,7 @@ if ($action === 'order') {
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
format: { format: {
// importo esportato come numero, il resto come testo // importo esportato come numero, il resto come testo
body: (d, row, col) => col === 5 ? (+$(d).data('order') >= 0 ? +$(d).data('order') : '') : $('<div>').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') : '') : $('<div>').html(String(d).replace(/<(div|span)/g, ' <$1').replace(/<\/span>/g, '</span> ')).text().replace(/\s+/g, ' ').trim()
} }
} }
} }
@@ -1825,19 +2060,58 @@ if ($action === 'order') {
</div>` : '<div class="warn-strip mb-3">Importi non ancora letti da Mago.</div>'} </div>` : '<div class="warn-strip mb-3">Importi non ancora letti da Mago.</div>'}
<div class="ro-grid mb-3"> <div class="ro-grid mb-3">
<div class="l">Data ordine</div><div>${fmtDate(o.order_date) || '—'}</div> <div class="l">Data ordine</div><div>${fmtDate(o.order_date) || '—'}</div>
<div class="l">Consegna prevista</div><div>${fmtDate(o.expected_delivery_date) || '—'}</div> <div class="l">Consegna</div><div>${dateCell(o.zibo_date, o.client_date, {open: o.status === 'aperto' || o.status === 'bloccato'})}${+o.open_lines ? ` <span class="small text-muted">prima riga da consegnare · ${fmtNum(o.open_lines)} righe aperte${+o.late_lines ? `, <b style="color:#dc3545;">${fmtNum(o.late_lines)} in ritardo</b>` : ''}</span>` : ''}</div>
<div class="l">Consegna confermata</div><div>${fmtDate(o.confirmed_delivery_date) || '—'}</div>
<div class="l">N° ordine cliente</div><div>${esc(o.external_ord_no) || '—'}</div> <div class="l">N° ordine cliente</div><div>${esc(o.external_ord_no) || '—'}</div>
<div class="l">Agente</div><div>${esc(o.salesperson) || '—'}</div> <div class="l">Agente</div><div>${esc(o.salesperson) || '—'}</div>
<div class="l">Area</div><div>${esc(o.area) || '—'}</div> <div class="l">Area</div><div>${esc(o.area) || '—'}</div>
${extra} ${extra}
</div> </div>
${o.notes ? `<div class="p-3 rounded" style="background:#f8fafc;border:1px solid #e2e8f0;white-space:pre-wrap;"><b>Note:</b><br>${esc(o.notes)}</div>` : ''} ${renderLines(d.lines || [])}
<div class="mt-3 small text-muted"><i class="fa-solid fa-circle-info me-1"></i>Le righe articolo non sono disponibili: la tabella righe ordine non è esposta dalle API di Mago.</div> ${o.notes ? `<div class="p-3 rounded mt-3" style="background:#f8fafc;border:1px solid #e2e8f0;white-space:pre-wrap;"><b>Note:</b><br>${esc(o.notes)}</div>` : ''}
`); `);
}); });
} }
function renderLines(lines) {
if (!lines.length) {
return '<div class="warn-strip">Righe non ancora sincronizzate: premi <b>Aggiorna da Mago</b>.</div>';
}
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 ? '<span class="st-badge st-annullato">Annullata</span>' :
(+l.delivered ? '<span class="st-badge st-evaso">Consegnata</span>' :
'<span class="st-badge st-aperto">Aperta</span>' + (late ? `<span class="late-badge">+${days} gg</span>` : ''));
const qtyTxt = n => fmtNum(Math.round(+n * 100) / 100);
return `<tr>
<td><b>${esc(l.item)}</b><div class="small text-muted" style="white-space:normal;max-width:320px;">${esc(l.description)}</div></td>
<td class="text-end amount">${qtyTxt(l.qty)} ${esc(l.uom)}</td>
<td class="text-end amount">${qtyTxt(l.delivered_qty)}</td>
<td class="text-end amount">${open ? '<b>' + qtyTxt(l.residual_qty) + '</b>' : '<span class="text-muted">—</span>'}</td>
<td>${dateCell(l.zibo_date, l.client_date, {open: open})}</td>
<td class="text-end amount">${fmtEur(l.taxable_amount, 2)}</td>
<td>${st}</td>
</tr>`;
}).join('');
return `
<div class="fw-bold mb-2" style="color:var(--c-ink);">Righe ordine (${lines.length})</div>
<div class="table-responsive">
<table class="table table-sm align-middle" style="font-size:.9rem;">
<thead style="background:#f1f5f9;">
<tr>
<th>Articolo</th><th class="text-end">Qtà</th><th class="text-end">Consegnata</th><th class="text-end">Residuo</th>
<th>Consegna (ZIBO / cliente)</th><th class="text-end">Imponibile</th><th>Stato</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>`;
}
/* ---------------- SINCRONIZZAZIONE ---------------- */ /* ---------------- SINCRONIZZAZIONE ---------------- */
function runSync(mode) { function runSync(mode) {
const labels = { const labels = {
@@ -1875,6 +2149,7 @@ if ($action === 'order') {
icon: 'success', icon: 'success',
title: 'Dati aggiornati', title: 'Dati aggiornati',
html: `Ordini letti: <b>${fmtNum(o.records)}</b>` + html: `Ordini letti: <b>${fmtNum(o.records)}</b>` +
(d.righe ? `<br>Righe lette: <b>${fmtNum(d.righe.records)}</b>` : '') +
(d.importi ? `<br>Totali letti: <b>${fmtNum(d.importi.records)}</b>` : '') + (d.importi ? `<br>Totali letti: <b>${fmtNum(d.importi.records)}</b>` : '') +
(c.records !== undefined ? `<br>Clienti aggiornati: <b>${fmtNum(c.records)}</b>` : '') + (c.records !== undefined ? `<br>Clienti aggiornati: <b>${fmtNum(c.records)}</b>` : '') +
(d.logoff !== 'OK' ? `<div class="small text-muted mt-2">Logoff Mago: ${esc(d.logoff)}</div>` : ''), (d.logoff !== 'OK' ? `<div class="small text-muted mt-2">Logoff Mago: ${esc(d.logoff)}</div>` : ''),
@@ -1958,8 +2233,16 @@ if ($action === 'order') {
loadFilterOptions(false).then(d => { loadFilterOptions(false).then(d => {
window.__totalOrders = d && d.sync ? d.sync.total_orders : 0; 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 (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) { if (saved && saved.from) {
$('#fFrom').val(saved.from); $('#fFrom').val(saved.from);
$('#fTo').val(saved.to); $('#fTo').val(saved.to);
@@ -1968,7 +2251,7 @@ if ($action === 'order') {
$('#fAreas').val(saved.areas || []).trigger('change.select2'); $('#fAreas').val(saved.areas || []).trigger('change.select2');
(saved.statuses || []).forEach(s => $(`#statusChips .chip[data-status="${s}"]`).addClass('active')); (saved.statuses || []).forEach(s => $(`#statusChips .chip[data-status="${s}"]`).addClass('active'));
$('#fSearch').val(saved.q || ''); $('#fSearch').val(saved.q || '');
} else { } else if (!URL_Q) {
setPeriod('y' + new Date().getFullYear(), true); setPeriod('y' + new Date().getFullYear(), true);
} }
loadData(); loadData();
+1
View File
@@ -584,6 +584,7 @@ if ($action === 'data') {
<div class="stats-tabs"> <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_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> <a href="sales_orders_years.php" class="active"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
</div> </div>
<!-- FILTRI --> <!-- FILTRI -->