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 '

Accesso non consentito

Non hai i permessi per visualizzare questa pagina.

Torna alla Dashboard
'; exit; } $canSync = userCan('sales.sync.manage'); /* ============================================================ * AJAX * ============================================================ */ $action = $_GET['action'] ?? ($_POST['action'] ?? ''); if ($action === 'sync' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!$canSync) { MagoSalesStats::jsonOut(['ok' => false, 'message' => 'Non hai i permessi per sincronizzare i dati da Mago']); } set_time_limit(600); try { $mode = $_POST['mode'] ?? 'incremental'; $opts = []; if ($mode === 'test') $opts['limit'] = max(1, min(500, (int)($_POST['limit'] ?? 50))); if ($mode === 'full') $opts['full'] = true; $sync = new MagoSync($pdo, MagoClient::fromEnv()); $report = $sync->run($opts); $report['info'] = MagoSalesStats::syncInfo($pdo); MagoSalesStats::jsonOut($report); } catch (Throwable $e) { MagoSalesStats::jsonOut(['ok' => false, 'message' => $e->getMessage()]); } } if ($action === 'filters') { MagoSalesStats::jsonOut(MagoSalesStats::filterOptions($pdo)); } if ($action === 'data') { try { [$where, $params] = MagoSalesStats::buildWhere(); $from = MagoSalesStats::baseFrom(); // Metrica dei grafici: numero ordini oppure valore (imponibile, annullati esclusi) $metric = (($_GET['metric'] ?? '') === 'value') ? 'value' : 'count'; $m = $metric === 'value' ? "CASE WHEN x.status <> 'annullato' THEN x.taxable_amount ELSE 0 END" : "1"; $agg = "SUM($m)"; $val = "CASE WHEN x.status <> 'annullato' THEN x.taxable_amount ELSE 0 END"; // --- KPI --- $kpi = MagoSalesStats::fetchAllQ($pdo, " SELECT COUNT(*) AS orders, SUM(x.status = 'aperto') AS open_orders, SUM(x.status = 'evaso') AS delivered, SUM(x.status = 'annullato') AS cancelled, SUM(x.status = 'bloccato') AS blocked, SUM(x.is_late) AS late, SUM(x.is_late_client) AS late_client, SUM(x.status IN ('aperto','bloccato') AND x.late_lines > 0 AND x.late_lines < x.open_lines) AS late_partial, SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN x.late_lines ELSE 0 END) AS late_lines, SUM(x.status IN ('aperto','bloccato') AND x.week_lines > 0) AS week_orders, SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN x.week_lines ELSE 0 END) AS week_lines, SUM(x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0) AS to_invoice, COUNT(DISTINCT x.customer) AS customers, AVG(CASE WHEN x.status <> 'annullato' AND x.due_date IS NOT NULL AND x.due_date >= x.order_date THEN DATEDIFF(x.due_date, x.order_date) END) AS avg_lead_days, COALESCE(SUM($val), 0) AS value_total, COALESCE(SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN x.taxable_amount ELSE 0 END), 0) AS value_open, COALESCE(SUM(CASE WHEN x.is_late = 1 THEN x.taxable_amount ELSE 0 END), 0) AS value_late, COALESCE(SUM(CASE WHEN x.delivered = 1 AND x.invoiced = 0 AND x.cancelled = 0 THEN x.taxable_amount ELSE 0 END), 0) AS value_to_invoice, AVG(CASE WHEN x.status <> 'annullato' THEN x.taxable_amount END) AS value_avg, SUM(x.summary_tb_modified IS NULL) AS missing_amounts FROM $from WHERE $where ", $params)[0]; // --- Da evadere per anno della data ZIBO: residuo delle righe ancora aperte (ritardi = anno in corso) --- $cy = (int)date('Y'); $zy = "YEAR(" . MagoSalesStats::ziboDate('d') . ")"; $rv = "CASE WHEN d.qty > 0 THEN d.taxable_amount * GREATEST(d.qty - d.delivered_qty, 0) / d.qty ELSE d.taxable_amount END"; $kpi['toship'] = MagoSalesStats::fetchAllQ($pdo, " SELECT COALESCE(SUM(CASE WHEN $zy <= $cy THEN $rv END), 0) AS y_cur, COALESCE(SUM(CASE WHEN $zy = $cy + 1 THEN $rv END), 0) AS y_next, COALESCE(SUM(CASE WHEN $zy > $cy + 1 THEN $rv END), 0) AS y_later, COALESCE(SUM(CASE WHEN $zy IS NULL THEN $rv END), 0) AS y_nodate FROM mago_sale_ord_detail d JOIN $from ON x.sale_ord_id = d.sale_ord_id WHERE $where AND x.status IN ('aperto','bloccato') AND d.delivered = 0 AND d.cancelled = 0 AND (d.item <> '' OR d.qty <> 0) ", $params)[0]; $kpi['toship']['year'] = $cy; // --- Confronto con lo STESSO PERIODO dell'anno precedente --- $kpi['prev_orders'] = null; $kpi['prev_value'] = null; $fromD = $_GET['from'] ?? ''; $toD = $_GET['to'] ?? ''; if (MagoSalesStats::isValidDate($fromD) && MagoSalesStats::isValidDate($toD) && $fromD <= $toD) { $prevFrom = date('Y-m-d', strtotime($fromD . ' -1 year')); $prevTo = date('Y-m-d', strtotime($toD . ' -1 year')); [$pw, $pp] = MagoSalesStats::buildWhere(['from' => $prevFrom, 'to' => $prevTo]); $prev = MagoSalesStats::fetchAllQ($pdo, "SELECT COUNT(*) AS n, COALESCE(SUM($val), 0) AS v FROM $from WHERE $pw", $pp)[0]; $kpi['prev_orders'] = (int)$prev['n']; $kpi['prev_value'] = (float)$prev['v']; $kpi['prev_from'] = date('d/m/Y', strtotime($prevFrom)); $kpi['prev_to'] = date('d/m/Y', strtotime($prevTo)); } // --- Andamento mensile per stato --- $monthly = MagoSalesStats::fetchAllQ($pdo, " SELECT DATE_FORMAT(x.order_date, '%Y-%m') AS ym, SUM(CASE WHEN x.status = 'aperto' THEN $m ELSE 0 END) AS aperto, SUM(CASE WHEN x.status = 'bloccato' THEN $m ELSE 0 END) AS bloccato, SUM(CASE WHEN x.status = 'evaso' THEN $m ELSE 0 END) AS evaso, SUM(CASE WHEN x.status = 'annullato' THEN " . ($metric === 'value' ? '0' : '1') . " ELSE 0 END) AS annullato, $agg AS totale FROM $from WHERE $where AND x.order_date IS NOT NULL GROUP BY ym ORDER BY ym ", $params); // --- Top clienti --- $topCustomers = MagoSalesStats::fetchAllQ($pdo, " SELECT x.customer AS code, x.company_name AS name, $agg AS n, SUM(CASE WHEN x.status IN ('aperto','bloccato') THEN $m ELSE 0 END) AS open_n FROM $from WHERE $where GROUP BY x.customer, x.company_name ORDER BY n DESC LIMIT 10 ", $params); // --- Per area / agente --- $byArea = MagoSalesStats::fetchAllQ($pdo, " SELECT CASE WHEN x.area = '' THEN '(nessuna)' ELSE x.area END AS label, x.area AS code, $agg AS n FROM $from WHERE $where GROUP BY x.area ORDER BY n DESC LIMIT 12 ", $params); $bySalesperson = MagoSalesStats::fetchAllQ($pdo, " SELECT CASE WHEN x.salesperson = '' THEN '(nessuno)' ELSE x.salesperson END AS label, x.salesperson AS code, $agg AS n FROM $from WHERE $where GROUP BY x.salesperson ORDER BY n DESC LIMIT 12 ", $params); // --- Scadenze ordini aperti --- $dueBuckets = MagoSalesStats::fetchAllQ($pdo, " SELECT bucket, SUM(mv) AS n FROM ( SELECT CASE WHEN x.is_late = 1 THEN '1_scaduti' WHEN x.open_due IS NULL THEN '6_senza' WHEN YEARWEEK(x.open_due, 3) = YEARWEEK(CURDATE(), 3) THEN '2_settimana' WHEN YEARWEEK(x.open_due, 3) = YEARWEEK(CURDATE() + INTERVAL 7 DAY, 3) THEN '3_prossima' WHEN x.open_due <= CURDATE() + INTERVAL 30 DAY THEN '4_30gg' ELSE '5_oltre' END AS bucket, $m AS mv FROM $from WHERE $where AND x.status IN ('aperto','bloccato') ) b 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.week_lines, x.is_late_client, (x.summary_tb_modified IS NULL) AS no_amount, x.is_late FROM $from WHERE $where ORDER BY x.order_date DESC, x.sale_ord_id DESC LIMIT 5000 ", $params); MagoSalesStats::jsonOut([ 'success' => true, 'metric' => $metric, 'kpi' => $kpi, 'monthly' => $monthly, 'top_customers' => $topCustomers, 'by_area' => $byArea, 'by_salesperson' => $bySalesperson, 'due_buckets' => $dueBuckets, 'top_items' => $topItems, 'rows' => $rows, ]); } catch (Throwable $e) { MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]); } } if ($action === 'order') { try { $id = (int)($_GET['id'] ?? 0); $st = $pdo->prepare("SELECT x.* FROM " . MagoSalesStats::baseFrom() . " WHERE x.sale_ord_id = ?"); $st->execute([$id]); $o = $st->fetch(PDO::FETCH_ASSOC); if (!$o) MagoSalesStats::jsonOut(['success' => false, 'message' => 'Ordine non trovato']); $raw = json_decode((string)$o['raw_json'], true) ?: []; unset($o['raw_json']); // righe + (se presenti) matrice, coda "Nuovi da Mago" e lavoro di produzione collegato $has = function ($t) use ($pdo) { $q = $pdo->prepare("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"); $q->execute([$t]); return (bool)$q->fetchColumn(); }; $colQ = $pdo->prepare("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mago_sale_ord_detail' AND COLUMN_NAME = 'matrice_misura'"); $colQ->execute(); $hasMM = (bool)$colQ->fetchColumn(); $hasQ = $hasMM && $has('mago_prod_queue') && $has('productiondata'); $sel = "d.sub_id, 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"; $join = ''; if ($hasMM) $sel .= ", d.matrice_misura, d.cod_art_cli"; if ($hasQ) { $sel .= ", q.queue_status, q.match_status, q.match_info, q.discard_reason, q.mago_changed, q.mago_changes, q.productiondata_id, COALESCE(p.idmatrice, q.idmatrice) AS idmatrice, m.nome AS matrice, m.photo AS matrice_photo, p.id_status AS prod_status, s.nome AS prod_status_name, COALESCE(s.badge_color, '#6c757d') AS prod_status_color, p.id_linea, l.name AS linea, l.color AS linea_color, p.metri AS prod_metri, p.data_produzione, (SELECT GROUP_CONCAT(ms.nome ORDER BY ms.nome SEPARATOR ', ') FROM productiondata_mescole pm JOIN mescole ms ON ms.id = pm.id_mescola WHERE pm.id_productiondata = p.id) AS mescole"; $join = "LEFT JOIN mago_prod_queue q ON q.sale_ord_id = d.sale_ord_id AND q.sub_id = d.sub_id LEFT JOIN productiondata p ON p.id = q.productiondata_id LEFT JOIN matrice m ON m.id = COALESCE(p.idmatrice, q.idmatrice) LEFT JOIN production_status s ON s.id = p.id_status LEFT JOIN production_lines l ON l.id = p.id_linea"; } $stL = $pdo->prepare("SELECT $sel FROM mago_sale_ord_detail d $join WHERE d.sale_ord_id = ? ORDER BY d.position, d.line, d.sub_id"); $stL->execute([$id]); $lines = $stL->fetchAll(PDO::FETCH_ASSOC); // righe non (ancora) in coda: provo comunque a riconoscere la matrice dal valore Mago if ($hasMM) { if (!class_exists('MagoProduction') && is_file(__DIR__ . '/class/MagoProduction.php')) require_once __DIR__ . '/class/MagoProduction.php'; foreach ($lines as &$l) { $l['mm'] = null; if (!class_exists('MagoProduction') || trim((string)$l['matrice_misura']) === '') continue; $l['mm'] = MagoProduction::parse($l['matrice_misura']); $l['mm']['tipo_label'] = MagoProduction::TIPI[$l['mm']['tipo']] ?? ''; if (empty($l['idmatrice'])) { [$idm, $ms, $mi] = MagoProduction::lookup($pdo, $l['matrice_misura']); if ($idm) { $mt = $pdo->prepare("SELECT nome, photo FROM matrice WHERE id = ?"); $mt->execute([$idm]); $r = $mt->fetch(PDO::FETCH_ASSOC) ?: []; $l['idmatrice'] = $idm; $l['matrice'] = $r['nome'] ?? ''; $l['matrice_photo'] = $r['photo'] ?? ''; } if (empty($l['match_status'])) { $l['match_status'] = $ms; $l['match_info'] = $mi; } } } unset($l); // linee e mescole associate alla matrice in anagrafica (scheda di lavoro attiva, altrimenti matrici_mescole) if ($has('matrici_lines') && class_exists('MagoProduction')) { $lq = $pdo->prepare("SELECT l.name, l.color FROM matrici_lines ml JOIN production_lines l ON l.id = ml.idlinea WHERE ml.idmatrice = ? ORDER BY l.line_number, l.id"); $mq = $pdo->prepare("SELECT nome FROM mescole WHERE id = ?"); $cache = []; foreach ($lines as &$l) { $l['matrice_lines'] = []; $l['matrice_mescole'] = []; $idm = (int)($l['idmatrice'] ?? 0); if (!$idm) continue; if (!isset($cache[$idm])) { $lq->execute([$idm]); $ml = $lq->fetchAll(PDO::FETCH_ASSOC); $mx = []; try { $info = MagoProduction::matriceInfo($pdo, $idm); foreach ($info['mescole'] as $idmx) { $mq->execute([$idmx]); if (($n = $mq->fetchColumn()) !== false) $mx[] = $n; } } catch (Throwable $e) { } $cache[$idm] = [$ml, array_values(array_unique($mx))]; } [$l['matrice_lines'], $l['matrice_mescole']] = $cache[$idm]; } unset($l); } } MagoSalesStats::jsonOut([ 'success' => true, 'lines' => $lines, 'order' => $o, 'extra' => [ 'Cliente fatturazione' => $raw['InvoicingCustomer'] ?? '', 'Pagamento' => $raw['Payment'] ?? '', 'Listino' => $raw['PriceList'] ?? '', 'Trasporto' => $raw['Transport'] ?? '', 'Porto' => $raw['Port'] ?? '', 'Imballo' => $raw['Package'] ?? '', 'Magazzino' => $raw['StoragePhase1'] ?? '', 'Nostro riferimento' => $raw['OurReference'] ?? '', 'Vostro riferimento' => $raw['YourReference'] ?? '', 'Commessa' => $raw['Job'] ?? '', 'Stampato' => !empty($raw['Printed']) ? 'Sì' : 'No', 'Fatturato' => !empty($raw['Invoiced']) ? 'Sì' : 'No', 'Consegna tassativa' => MagoClient::toMysqlDate($raw['CompulsoryDeliveryDate'] ?? null), 'Creato in Mago' => MagoClient::toMysqlDate($raw['TbCreated'] ?? null, true), 'Modificato in Mago' => MagoClient::toMysqlDate($raw['TbModified'] ?? null, true), ], ]); } catch (Throwable $e) { MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]); } } ?> Statistiche Ordini Cliente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
Statistiche Ordini Cliente
Dati Mago4
Filtri
Ordini
—
 
Valore ordinato
—
 
Da evadere
—
 
Aperti
—
 
In ritardo
—
data ZIBO superata
 
Evasi
—
 
Da fatturare
—
evasi non fatturati
Clienti attivi
—
 
Grafici in
>
Elenco ordini
Clicca una riga per il dettaglio
N° ordine Data Cliente Rif. cliente Consegna (ZIBO / cliente) Imponibile Stato Fatt. Agente Area
Andamento mensile ordini
Clicca un mese per filtrarlo
Ripartizione per stato
Clicca una fetta per filtrare
Top 10 clienti per numero ordini
Clicca un cliente per vedere solo i suoi ordini
Scadenze ordini aperti
In base alla data di consegna prevista
Top 10 articoli per numero ordini
Dalle righe ordine · passa sopra una barra per quantità e valore
Ordini per area
Ordini per agente
Caricamento…