284 lines
12 KiB
PHP
284 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* MagoSalesStats - funzioni comuni alle pagine statistiche ordini cliente
|
|
* (sales_orders_stats.php e sales_orders_years.php).
|
|
* Legge SOLO dalle tabelle locali sincronizzate da Mago.
|
|
*/
|
|
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 = 'expected_delivery_date'; // Mago: ConfirmedDeliveryDate
|
|
const CLIENT_DATE_COL = 'confirmed_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)
|
|
{
|
|
while (ob_get_level()) ob_end_clean();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
public static function isValidDate($d)
|
|
{
|
|
return is_string($d) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) && strtotime($d) !== false;
|
|
}
|
|
|
|
/** Parametro GET multiplo (es. customers[]) come array pulito. */
|
|
public static function arrParam($key)
|
|
{
|
|
$v = $_GET[$key] ?? [];
|
|
if (!is_array($v)) $v = [$v];
|
|
return array_values(array_filter(array_map(function ($x) {
|
|
return trim((string)$x);
|
|
}, $v), function ($x) {
|
|
return $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()
|
|
{
|
|
return "(
|
|
SELECT
|
|
o.*,
|
|
COALESCE(NULLIF(c.company_name, ''), o.customer) AS company_name,
|
|
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(
|
|
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'
|
|
WHEN o.is_blocked = 1 THEN 'bloccato'
|
|
ELSE 'aperto'
|
|
END AS status
|
|
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).
|
|
*/
|
|
public static function buildWhere(array $override = [])
|
|
{
|
|
$w = ['1=1'];
|
|
$p = [];
|
|
|
|
$from = array_key_exists('from', $override) ? $override['from'] : ($_GET['from'] ?? '');
|
|
$to = array_key_exists('to', $override) ? $override['to'] : ($_GET['to'] ?? '');
|
|
|
|
if (self::isValidDate($from)) {
|
|
$w[] = 'x.order_date >= :from';
|
|
$p['from'] = $from;
|
|
}
|
|
if (self::isValidDate($to)) {
|
|
$w[] = 'x.order_date <= :to';
|
|
$p['to'] = $to;
|
|
}
|
|
|
|
$multi = [
|
|
'customers' => 'x.customer',
|
|
'salespeople' => 'x.salesperson',
|
|
'areas' => 'x.area',
|
|
];
|
|
foreach ($multi as $key => $col) {
|
|
$vals = self::arrParam($key);
|
|
if (!$vals) continue;
|
|
$ph = [];
|
|
foreach ($vals as $i => $v) {
|
|
$name = $key . $i;
|
|
$ph[] = ':' . $name;
|
|
$p[$name] = $v;
|
|
}
|
|
$w[] = "$col IN (" . implode(',', $ph) . ")";
|
|
}
|
|
|
|
$statusMap = [
|
|
'aperto' => "x.status = 'aperto'",
|
|
'evaso' => "x.status = 'evaso'",
|
|
'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",
|
|
];
|
|
$statuses = array_intersect(self::arrParam('statuses'), array_keys($statusMap));
|
|
if ($statuses) {
|
|
$w[] = '(' . implode(' OR ', array_map(function ($s) use ($statusMap) {
|
|
return $statusMap[$s];
|
|
}, $statuses)) . ')';
|
|
}
|
|
|
|
$q = trim((string)($_GET['q'] ?? ''));
|
|
if ($q !== '') {
|
|
$like = '%' . $q . '%';
|
|
$w[] = '(x.internal_ord_no LIKE :q1 OR x.external_ord_no LIKE :q2 OR x.your_reference LIKE :q3 OR x.company_name LIKE :q4 OR x.customer LIKE :q5)';
|
|
$p['q1'] = $p['q2'] = $p['q3'] = $p['q4'] = $p['q5'] = $like;
|
|
}
|
|
|
|
return [implode(' AND ', $w), $p];
|
|
}
|
|
|
|
public static function fetchAllQ(PDO $pdo, $sql, array $params)
|
|
{
|
|
$st = $pdo->prepare($sql);
|
|
$st->execute($params);
|
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
/** Stato dell'ultima sincronizzazione (per il badge in alto). */
|
|
public static function syncInfo(PDO $pdo)
|
|
{
|
|
try {
|
|
$rows = $pdo->query("SELECT * FROM mago_sync_state")->fetchAll(PDO::FETCH_ASSOC);
|
|
$out = [];
|
|
foreach ($rows as $r) {
|
|
$out[$r['entity']] = [
|
|
'last_run_at' => $r['last_run_at'] ? date('d/m/Y H:i', strtotime($r['last_run_at'])) : '',
|
|
'last_status' => $r['last_status'],
|
|
'last_message' => $r['last_message'],
|
|
'records' => (int)$r['records_last_run'],
|
|
];
|
|
}
|
|
$out['total_orders'] = (int)$pdo->query("SELECT COUNT(*) FROM mago_sale_ord")->fetchColumn();
|
|
return $out;
|
|
} catch (Throwable $e) {
|
|
return ['error' => 'Tabelle Mago non presenti: eseguire la migrazione Phinx', 'total_orders' => 0];
|
|
}
|
|
}
|
|
|
|
/** Opzioni dei filtri (clienti, agenti, aree, anni disponibili). */
|
|
public static function filterOptions(PDO $pdo)
|
|
{
|
|
try {
|
|
$customers = $pdo->query("
|
|
SELECT o.customer AS code, COALESCE(NULLIF(c.company_name,''), o.customer) AS name, COUNT(*) AS n
|
|
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
|
|
WHERE o.customer <> ''
|
|
GROUP BY o.customer, name
|
|
ORDER BY name
|
|
")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$salespeople = $pdo->query("SELECT salesperson AS code, COUNT(*) AS n FROM mago_sale_ord WHERE salesperson <> '' GROUP BY salesperson ORDER BY salesperson")->fetchAll(PDO::FETCH_ASSOC);
|
|
$areas = $pdo->query("SELECT area AS code, COUNT(*) AS n FROM mago_sale_ord WHERE area <> '' GROUP BY area ORDER BY area")->fetchAll(PDO::FETCH_ASSOC);
|
|
$range = $pdo->query("SELECT MIN(order_date) AS min_date, MAX(order_date) AS max_date FROM mago_sale_ord")->fetch(PDO::FETCH_ASSOC);
|
|
$years = array_map('intval', $pdo->query("SELECT DISTINCT YEAR(order_date) FROM mago_sale_ord WHERE order_date IS NOT NULL ORDER BY 1")->fetchAll(PDO::FETCH_COLUMN));
|
|
|
|
return [
|
|
'success' => true,
|
|
'customers' => $customers,
|
|
'salespeople' => $salespeople,
|
|
'areas' => $areas,
|
|
'range' => $range,
|
|
'years' => $years,
|
|
'sync' => self::syncInfo($pdo),
|
|
];
|
|
} catch (Throwable $e) {
|
|
return ['success' => false, 'message' => $e->getMessage(), 'sync' => self::syncInfo($pdo)];
|
|
}
|
|
}
|
|
}
|