Statistiche Ordini Cliente
Dati Mago4Calcolo degli indicatori…
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; } $canDeliveries = userCan('sales.deliveries.view'); /* ============================================================ * AJAX * ============================================================ */ /** Importo breve per le frasi: 1,23 M€ / 456,7 k€ / 890 € */ function eurShort($v) { $v = (float)$v; $a = abs($v); if ($a >= 1e6) return number_format($v / 1e6, 2, ',', '.') . ' M€'; if ($a >= 1e3) return number_format($v / 1e3, 1, ',', '.') . ' k€'; return number_format($v, 0, ',', '.') . ' €'; } function pctTxt($p, $dec = 0) { return ($p >= 0 ? '+' : '') . number_format($p, $dec, ',', '.') . '%'; } function pctOf($cur, $prev) { return $prev ? ($cur - $prev) / abs($prev) * 100 : null; } $action = $_GET['action'] ?? ''; if ($action === 'filters') { MagoSalesStats::jsonOut(MagoSalesStats::filterOptions($pdo)); } if ($action === 'data') { try { $curY = (int)date('Y'); $from = MagoSalesStats::baseFrom(); $val = "CASE WHEN x.status <> 'annullato' THEN x.taxable_amount ELSE 0 END"; $T = []; // tessere (indicatori) // ---------------- ORDINATO ---------------- $ordYears = MagoSalesStats::fetchAllQ($pdo, " SELECT YEAR(x.order_date) AS y, SUM($val) AS full, SUM(CASE WHEN DATE_FORMAT(x.order_date, '%m-%d') <= DATE_FORMAT(CURDATE(), '%m-%d') THEN $val ELSE 0 END) AS ytd FROM $from WHERE x.order_date >= '" . ($curY - 3) . "-01-01' GROUP BY y ", []); $oy = []; foreach ($ordYears as $r) $oy[(int)$r['y']] = $r; $ordCur = (float)($oy[$curY]['ytd'] ?? 0); $ordPrev = (float)($oy[$curY - 1]['ytd'] ?? 0); $ordFc = MagoSalesStats::yearForecast($ordYears, $curY); $since13 = date('Y-m-01', strtotime('first day of -12 months')); $ordMonths = MagoSalesStats::fetchAllQ($pdo, " SELECT DATE_FORMAT(x.order_date, '%Y-%m') AS ym, SUM($val) AS v FROM $from WHERE x.order_date >= '$since13' GROUP BY ym ORDER BY ym ", []); $ord90 = (float)MagoSalesStats::fetchAllQ($pdo, "SELECT COALESCE(SUM($val),0) AS v FROM $from WHERE x.order_date > CURDATE() - INTERVAL 90 DAY", [])[0]['v']; // ---------------- FATTURATO ---------------- $invOk = false; $invCur = $invPrev = $inv90 = $inv12 = $invP12 = $cn12 = 0.0; $invFc = null; $invMonths = []; $iFrom = MagoSalesStats::invoiceFrom(); try { $invOk = (bool)$pdo->query("SELECT 1 FROM mago_sale_doc LIMIT 1")->fetchColumn(); } catch (Throwable $e) { $invOk = false; } if ($invOk) { $invYears = MagoSalesStats::fetchAllQ($pdo, " SELECT YEAR(i.document_date) AS y, SUM(i.revenue) AS full, SUM(CASE WHEN DATE_FORMAT(i.document_date, '%m-%d') <= DATE_FORMAT(CURDATE(), '%m-%d') THEN i.revenue ELSE 0 END) AS ytd FROM $iFrom WHERE i.document_date >= '" . ($curY - 3) . "-01-01' GROUP BY y ", []); $iy = []; foreach ($invYears as $r) $iy[(int)$r['y']] = $r; $invCur = (float)($iy[$curY]['ytd'] ?? 0); $invPrev = (float)($iy[$curY - 1]['ytd'] ?? 0); $invFc = MagoSalesStats::yearForecast($invYears, $curY); $invMonths = MagoSalesStats::fetchAllQ($pdo, " SELECT DATE_FORMAT(i.document_date, '%Y-%m') AS ym, SUM(i.revenue) AS v FROM $iFrom WHERE i.document_date >= '$since13' GROUP BY ym ORDER BY ym ", []); $r = MagoSalesStats::fetchAllQ($pdo, " SELECT COALESCE(SUM(CASE WHEN i.document_date > CURDATE() - INTERVAL 90 DAY THEN i.revenue END), 0) AS v90, COALESCE(SUM(CASE WHEN i.document_date > CURDATE() - INTERVAL 365 DAY THEN i.revenue END), 0) AS v12, COALESCE(SUM(CASE WHEN i.document_date <= CURDATE() - INTERVAL 365 DAY THEN i.revenue END), 0) AS p12, COALESCE(SUM(CASE WHEN i.document_date > CURDATE() - INTERVAL 365 DAY AND i.is_credit_note = 1 THEN -i.revenue END), 0) AS cn12, COALESCE(SUM(CASE WHEN i.document_date > CURDATE() - INTERVAL 365 DAY AND i.is_credit_note = 0 THEN i.revenue END), 0) AS gross12 FROM $iFrom WHERE i.document_date > CURDATE() - INTERVAL 730 DAY ", [])[0]; $inv90 = (float)$r['v90']; $inv12 = (float)$r['v12']; $invP12 = (float)$r['p12']; $cn12 = (float)$r['cn12']; $gross12 = (float)$r['gross12']; } // 1) FATTURATO DELL'ANNO if ($invOk && $invPrev > 0) { $p = pctOf($invCur, $invPrev); $T[] = [ 'key' => 'invYtd', 'group' => 'Risultati', 'title' => "Fatturato $curY", 'value' => eurShort($invCur), 'light' => MagoSalesStats::light($p, 'growth'), 'delta' => $p, 'sub' => 'ad oggi · ' . ($curY - 1) . ' stessa data ' . eurShort($invPrev), 'verdict' => $p >= 0 ? 'Stiamo fatturando ' . pctTxt($p) . ' rispetto all\'anno scorso' : 'Fatturato sotto l\'anno scorso del ' . number_format(abs($p), 0, ',', '.') . '%', 'spark' => 'inv', 'link' => 'sales_orders_years.php' ]; } else { $T[] = [ 'key' => 'invYtd', 'group' => 'Risultati', 'title' => "Fatturato $curY", 'value' => $invOk ? eurShort($invCur) : 'n.d.', 'light' => 'grey', 'sub' => $invOk ? 'nessun confronto con l\'anno scorso' : 'fatture non ancora sincronizzate', 'verdict' => '', 'link' => 'sales_orders_years.php' ]; } // 2) ORDINATO DELL'ANNO $p = pctOf($ordCur, $ordPrev); $T[] = [ 'key' => 'ordYtd', 'group' => 'Risultati', 'title' => "Ordinato $curY", 'value' => eurShort($ordCur), 'light' => MagoSalesStats::light($p, 'growth'), 'delta' => $p, 'sub' => 'ad oggi · ' . ($curY - 1) . ' stessa data ' . eurShort($ordPrev), 'verdict' => $p === null ? '' : ($p >= 0 ? 'Entrano più ordini dell\'anno scorso (' . pctTxt($p) . ')' : 'Entrano meno ordini dell\'anno scorso (' . pctTxt($p) . ')'), 'spark' => 'ord', 'link' => 'sales_orders_years.php' ]; // 3) PREVISIONE DI FINE ANNO (fatturato, altrimenti ordinato) $fc = ($invOk && $invFc && $invFc['forecast'] !== null) ? $invFc : $ordFc; $fcWhat = ($fc === $invFc) ? 'fatturato' : 'ordinato'; if ($fc && $fc['forecast'] !== null && $fc['prev_full']) { $p = pctOf($fc['forecast'], $fc['prev_full']); $T[] = [ 'key' => 'forecast', 'group' => 'Risultati', 'title' => "Chiusura $curY prevista", 'value' => eurShort($fc['forecast']), 'light' => MagoSalesStats::light($p, 'growth'), 'delta' => $p, 'sub' => "$fcWhat · forbice " . eurShort($fc['low']) . ' – ' . eurShort($fc['high']) . ' · ' . ($curY - 1) . ': ' . eurShort($fc['prev_full']), 'verdict' => $p >= 0 ? 'Se si continua così chiudiamo sopra l\'anno scorso' : 'Se si continua così chiudiamo sotto l\'anno scorso', 'link' => 'sales_orders_years.php' ]; } else { $T[] = [ 'key' => 'forecast', 'group' => 'Risultati', 'title' => "Chiusura $curY prevista", 'value' => 'n.d.', 'light' => 'grey', 'sub' => 'servono i dati degli anni precedenti', 'verdict' => '', 'link' => 'sales_orders_years.php' ]; } // 4) BOOK-TO-BILL if ($invOk && $inv90 > 0) { $b2b = $ord90 / $inv90; $T[] = [ 'key' => 'b2b', 'group' => 'Futuro', 'title' => 'Book-to-bill 90 gg', 'value' => number_format($b2b, 2, ',', '.'), 'light' => MagoSalesStats::light($b2b, 'b2b'), 'sub' => 'ordinato ' . eurShort($ord90) . ' · fatturato ' . eurShort($inv90), 'verdict' => $b2b >= 1.05 ? 'Entra più lavoro di quanto ne esce: il portafoglio cresce' : ($b2b < 0.95 ? 'Esce più lavoro di quanto ne entra: il portafoglio si svuota' : 'Lavoro in entrata e in uscita in equilibrio'), 'spark' => 'b2b', 'link' => 'sales_orders_years.php' ]; } else { $T[] = [ 'key' => 'b2b', 'group' => 'Futuro', 'title' => 'Book-to-bill 90 gg', 'value' => 'n.d.', 'light' => 'grey', 'sub' => $invOk ? 'nessun fatturato negli ultimi 90 giorni' : 'fatture non ancora sincronizzate', 'verdict' => '', 'link' => 'sales_orders_years.php' ]; } // 5-6) PORTAFOGLIO: copertura e ritardi $bk = MagoSalesStats::fetchAllQ($pdo, " SELECT COALESCE(SUM(r.residual_value), 0) AS v, COUNT(*) AS n_lines, COALESCE(SUM(CASE WHEN r.due_date < CURDATE() THEN r.residual_value END), 0) AS late_v, SUM(r.due_date < CURDATE()) AS late_n, COUNT(DISTINCT CASE WHEN r.due_date < CURDATE() THEN r.customer END) AS late_c FROM " . MagoSalesStats::openLinesFrom() . " ", [])[0]; $weekly = ($invOk && $inv90 > 0 ? $inv90 : $ord90) / (90 / 7); $weeks = $weekly > 0 ? (float)$bk['v'] / $weekly : null; $T[] = [ 'key' => 'cover', 'group' => 'Futuro', 'title' => 'Lavoro in portafoglio', 'value' => $weeks === null ? 'n.d.' : number_format($weeks, 0, ',', '.') . ' sett.', 'light' => MagoSalesStats::light($weeks, 'cover_weeks'), 'sub' => eurShort($bk['v']) . ' da consegnare · ' . number_format((int)$bk['n_lines'], 0, ',', '.') . ' righe', 'verdict' => $weeks === null ? '' : ($weeks >= 8 ? 'Lavoro assicurato per circa ' . round($weeks) . ' settimane' : ($weeks < 4 ? 'Portafoglio corto: servono ordini nuovi' : 'Portafoglio sufficiente, da tenere d\'occhio')), 'link' => $canDeliveries ? 'sales_orders_deliveries.php' : 'sales_orders_years.php' ]; $latePct = (float)$bk['v'] > 0 ? (float)$bk['late_v'] / (float)$bk['v'] * 100 : 0; $T[] = [ 'key' => 'late', 'group' => 'Clienti e servizio', 'title' => 'Consegne in ritardo', 'value' => number_format($latePct, 0, ',', '.') . '%', 'light' => MagoSalesStats::light($latePct, 'late_pct', true), 'sub' => eurShort($bk['late_v']) . ' · ' . number_format((int)$bk['late_n'], 0, ',', '.') . ' righe · ' . (int)$bk['late_c'] . ' clienti', 'verdict' => $latePct <= 10 ? 'Consegne in linea con le date comunicate' : ($latePct > 25 ? 'Troppa merce oltre la data comunicata ai clienti' : 'Una parte delle consegne è in ritardo'), 'link' => $canDeliveries ? 'sales_orders_deliveries.php' : 'sales_orders_stats.php' ]; // 7) CLIENTI A RISCHIO (dormienti / persi) $dormRows = MagoSalesStats::fetchAllQ($pdo, " SELECT x.customer AS code, x.company_name AS name, x.order_date, x.taxable_amount AS v, x.salesperson, x.area, x.internal_ord_no AS ord_no FROM $from WHERE x.status <> 'annullato' AND x.customer <> '' AND x.order_date >= CURDATE() - INTERVAL " . (int)MagoSalesStats::DORMANT_WINDOW_YEARS . " YEAR ", []); $dormant = MagoSalesStats::dormantCustomers($dormRows); $atRisk = array_values(array_filter($dormant, function ($c) { return $c['level'] !== 'watch'; })); $riskV = array_sum(array_column($atRisk, 'value12')); $base12 = $invOk && $inv12 > 0 ? $inv12 : (float)MagoSalesStats::fetchAllQ($pdo, "SELECT COALESCE(SUM($val),0) AS v FROM $from WHERE x.order_date > CURDATE() - INTERVAL 365 DAY", [])[0]['v']; $riskPct = $base12 > 0 ? $riskV / $base12 * 100 : null; $T[] = [ 'key' => 'risk', 'group' => 'Clienti e servizio', 'title' => 'Clienti a rischio', 'value' => count($atRisk) . ' clienti', 'light' => MagoSalesStats::light($riskPct, 'risk_pct', true), 'sub' => eurShort($riskV) . ' di ordinato annuo · ' . ($riskPct === null ? '' : number_format($riskPct, 1, ',', '.') . '% del giro d\'affari') . ' · ' . (count($dormant) - count($atRisk)) . ' in attenzione', 'verdict' => !$atRisk ? 'Nessun cliente abituale si è fermato' : (MagoSalesStats::light($riskPct, 'risk_pct', true) === 'green' ? 'Pochi clienti abituali fermi, con un peso ridotto sul giro d\'affari' : 'Clienti abituali importanti hanno smesso di ordinare: da richiamare'), 'link' => 'sales_orders_years.php' ]; // 8) CONCENTRAZIONE (primi 5 clienti, ultimi 12 mesi) if ($invOk && $inv12 > 0) { $top = MagoSalesStats::fetchAllQ($pdo, " SELECT i.customer AS code, MAX(i.company_name) AS name, SUM(i.revenue) AS v FROM $iFrom WHERE i.document_date > CURDATE() - INTERVAL 365 DAY GROUP BY i.customer ORDER BY v DESC LIMIT 5 ", []); $tot12 = $inv12; } else { $top = MagoSalesStats::fetchAllQ($pdo, " SELECT x.customer AS code, MAX(x.company_name) AS name, SUM($val) AS v FROM $from WHERE x.order_date > CURDATE() - INTERVAL 365 DAY GROUP BY x.customer ORDER BY v DESC LIMIT 5 ", []); $tot12 = $base12; } $top5 = array_sum(array_map('floatval', array_column($top, 'v'))); $topPct = $tot12 > 0 ? $top5 / $tot12 * 100 : null; $T[] = [ 'key' => 'top5', 'group' => 'Clienti e servizio', 'title' => 'Peso primi 5 clienti', 'value' => $topPct === null ? 'n.d.' : number_format($topPct, 0, ',', '.') . '%', 'light' => MagoSalesStats::light($topPct, 'top5_pct', true), 'sub' => 'del ' . ($invOk ? 'fatturato' : 'ordinato') . ' degli ultimi 12 mesi' . ($top ? ' · 1° ' . $top[0]['name'] : ''), 'verdict' => $topPct === null ? '' : ($topPct <= 40 ? 'Giro d\'affari ben distribuito tra i clienti' : ($topPct > 60 ? 'Dipendiamo molto da pochi clienti' : 'Dipendenza dai clienti principali da tenere d\'occhio')), 'link' => 'sales_orders_years.php', 'list' => array_map(function ($r) use ($tot12) { return ['code' => $r['code'], 'name' => $r['name'], 'v' => (float)$r['v'], 'pct' => $tot12 > 0 ? (float)$r['v'] / $tot12 * 100 : 0]; }, $top) ]; // 9) BASE CLIENTI (attivi 12 mesi vs 12 precedenti, nuovi) $cb = MagoSalesStats::fetchAllQ($pdo, " SELECT COUNT(DISTINCT CASE WHEN x.order_date > CURDATE() - INTERVAL 365 DAY THEN x.customer END) AS cur, COUNT(DISTINCT CASE WHEN x.order_date <= CURDATE() - INTERVAL 365 DAY THEN x.customer END) AS prev FROM $from WHERE x.status <> 'annullato' AND x.customer <> '' AND x.order_date > CURDATE() - INTERVAL 730 DAY ", [])[0]; $newC = (int)MagoSalesStats::fetchAllQ($pdo, " SELECT COUNT(*) AS n FROM ( SELECT x.customer FROM $from WHERE x.status <> 'annullato' AND x.customer <> '' GROUP BY x.customer HAVING MIN(x.order_date) > CURDATE() - INTERVAL 365 DAY ) t ", [])[0]['n']; $p = pctOf((int)$cb['cur'], (int)$cb['prev']); $T[] = [ 'key' => 'customers', 'group' => 'Clienti e servizio', 'title' => 'Clienti attivi', 'value' => number_format((int)$cb['cur'], 0, ',', '.'), 'light' => MagoSalesStats::light($p, 'customers'), 'delta' => $p, 'sub' => 'ultimi 12 mesi · prima ' . (int)$cb['prev'] . ' · ' . $newC . ' nuovi', 'verdict' => $p === null ? '' : ($p >= 0 ? 'La base clienti tiene' . ($newC ? " ($newC clienti nuovi)" : '') : 'Stiamo perdendo clienti rispetto all\'anno prima'), 'link' => 'sales_orders_years.php' ]; // 10) NOTE DI CREDITO if ($invOk && !empty($gross12)) { $cnPct = $cn12 / $gross12 * 100; $T[] = [ 'key' => 'cn', 'group' => 'Clienti e servizio', 'title' => 'Note di credito', 'value' => number_format($cnPct, 1, ',', '.') . '%', 'light' => MagoSalesStats::light($cnPct, 'cn_pct', true), 'sub' => eurShort($cn12) . ' negli ultimi 12 mesi', 'verdict' => $cnPct <= 1.5 ? 'Poche contestazioni e resi' : ($cnPct > 3 ? 'Troppe note di credito: verificare resi e contestazioni' : 'Note di credito da tenere d\'occhio'), 'link' => 'sales_orders_years.php' ]; } else { $T[] = [ 'key' => 'cn', 'group' => 'Clienti e servizio', 'title' => 'Note di credito', 'value' => 'n.d.', 'light' => 'grey', 'sub' => 'fatture non ancora sincronizzate', 'verdict' => '', 'link' => 'sales_orders_years.php' ]; } // ---------------- CLIENTI CHE CRESCONO / CALANO (12 mesi vs 12 precedenti) ---------------- $mv = $invOk && $inv12 > 0 ? MagoSalesStats::fetchAllQ($pdo, " SELECT i.customer AS code, MAX(i.company_name) AS name, SUM(CASE WHEN i.document_date > CURDATE() - INTERVAL 365 DAY THEN i.revenue ELSE 0 END) AS cur, SUM(CASE WHEN i.document_date <= CURDATE() - INTERVAL 365 DAY THEN i.revenue ELSE 0 END) AS prev FROM $iFrom WHERE i.document_date > CURDATE() - INTERVAL 730 DAY AND i.customer <> '' GROUP BY i.customer", []) : MagoSalesStats::fetchAllQ($pdo, " SELECT x.customer AS code, MAX(x.company_name) AS name, SUM(CASE WHEN x.order_date > CURDATE() - INTERVAL 365 DAY THEN $val ELSE 0 END) AS cur, SUM(CASE WHEN x.order_date <= CURDATE() - INTERVAL 365 DAY THEN $val ELSE 0 END) AS prev FROM $from WHERE x.order_date > CURDATE() - INTERVAL 730 DAY AND x.customer <> '' GROUP BY x.customer", []); foreach ($mv as &$m) { $m['cur'] = (float)$m['cur']; $m['prev'] = (float)$m['prev']; $m['delta'] = $m['cur'] - $m['prev']; $m['pct'] = pctOf($m['cur'], $m['prev']); } unset($m); usort($mv, function ($a, $b) { return $b['delta'] <=> $a['delta']; }); $up = array_values(array_filter(array_slice($mv, 0, 5), function ($m) { return $m['delta'] > 0; })); $down = array_values(array_filter(array_slice(array_reverse($mv), 0, 5), function ($m) { return $m['delta'] < 0; })); // ---------------- ORDINI IN ARRIVO: oggi, 7 giorni, 30 giorni (vs periodo precedente) ---------------- $fl = MagoSalesStats::fetchAllQ($pdo, " SELECT SUM(x.order_date = CURDATE()) AS n0, SUM(CASE WHEN x.order_date = CURDATE() THEN x.taxable_amount ELSE 0 END) AS v0, SUM(x.order_date = CURDATE() - INTERVAL 7 DAY) AS n0p, SUM(CASE WHEN x.order_date = CURDATE() - INTERVAL 7 DAY THEN x.taxable_amount ELSE 0 END) AS v0p, SUM(x.order_date > CURDATE() - INTERVAL 7 DAY) AS n7, SUM(CASE WHEN x.order_date > CURDATE() - INTERVAL 7 DAY THEN x.taxable_amount ELSE 0 END) AS v7, SUM(x.order_date <= CURDATE() - INTERVAL 7 DAY AND x.order_date > CURDATE() - INTERVAL 14 DAY) AS n7p, SUM(CASE WHEN x.order_date <= CURDATE() - INTERVAL 7 DAY AND x.order_date > CURDATE() - INTERVAL 14 DAY THEN x.taxable_amount ELSE 0 END) AS v7p, SUM(x.order_date > CURDATE() - INTERVAL 30 DAY) AS n30, SUM(CASE WHEN x.order_date > CURDATE() - INTERVAL 30 DAY THEN x.taxable_amount ELSE 0 END) AS v30, SUM(x.order_date <= CURDATE() - INTERVAL 30 DAY AND x.order_date > CURDATE() - INTERVAL 60 DAY) AS n30p, SUM(CASE WHEN x.order_date <= CURDATE() - INTERVAL 30 DAY AND x.order_date > CURDATE() - INTERVAL 60 DAY THEN x.taxable_amount ELSE 0 END) AS v30p, COUNT(DISTINCT CASE WHEN x.order_date > CURDATE() - INTERVAL 30 DAY THEN x.customer END) AS c30, CURDATE() AS d0, CURDATE() - INTERVAL 6 DAY AS d7, CURDATE() - INTERVAL 29 DAY AS d30 FROM $from WHERE x.status <> 'annullato' AND x.order_date > CURDATE() - INTERVAL 60 DAY AND x.order_date <= CURDATE() ", [])[0]; $flow = [ [ 'key' => 'today', 'title' => 'Oggi', 'n' => (int)$fl['n0'], 'v' => (float)$fl['v0'], 'np' => (int)$fl['n0p'], 'vp' => (float)$fl['v0p'], 'vs' => 'stesso giorno della settimana scorsa', 'from' => (string)$fl['d0'], 'to' => (string)$fl['d0'] ], [ 'key' => 'week', 'title' => 'Ultimi 7 giorni', 'n' => (int)$fl['n7'], 'v' => (float)$fl['v7'], 'np' => (int)$fl['n7p'], 'vp' => (float)$fl['v7p'], 'vs' => '7 giorni precedenti', 'from' => (string)$fl['d7'], 'to' => (string)$fl['d0'] ], [ 'key' => 'month', 'title' => 'Ultimi 30 giorni', 'n' => (int)$fl['n30'], 'v' => (float)$fl['v30'], 'np' => (int)$fl['n30p'], 'vp' => (float)$fl['v30p'], 'vs' => '30 giorni precedenti', 'customers' => (int)$fl['c30'], 'from' => (string)$fl['d30'], 'to' => (string)$fl['d0'] ], ]; // ---------------- GIUDIZIO COMPLESSIVO ---------------- $score = ['green' => 0, 'yellow' => 0, 'red' => 0, 'grey' => 0]; foreach ($T as $t) $score[$t['light']]++; $valid = $score['green'] + $score['yellow'] + $score['red']; $points = $valid ? ($score['green'] * 100 + $score['yellow'] * 50) / $valid : null; $overall = $points === null ? 'grey' : ($points >= 70 ? 'green' : ($points >= 45 ? 'yellow' : 'red')); MagoSalesStats::jsonOut([ 'success' => true, 'today' => date('Y-m-d'), 'year' => $curY, 'inv_ok' => $invOk, 'tiles' => $T, 'flow' => $flow, 'score' => $score, 'points' => $points === null ? null : round($points), 'overall' => $overall, 'spark' => ['since' => $since13, 'ord' => $ordMonths, 'inv' => $invMonths], 'risk' => array_slice($atRisk, 0, 5), 'up' => $up, 'down' => $down, 'movers_on' => $invOk && $inv12 > 0 ? 'fatturato' : 'ordinato', 'can_deliveries' => $canDeliveries, ]); } catch (Throwable $e) { MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]); } } ?>