prepare("SELECT m.id, m.nome, m.photo, (SELECT COUNT(*) FROM work_sheets ws JOIN work_sheet_mescole wm ON wm.worksheet_id = ws.id WHERE ws.idmatrice = m.id AND wm.mix_weight_g_m > 0) AS n_gm FROM matrice m $w ORDER BY (m.nome LIKE :start) DESC, m.nome LIMIT " . (int)$limit); $p['start'] = ($q !== '' ? preg_replace('/^M(?=\d)/i', '', $q) : '') . '%'; $st->execute($p); $out = []; foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) { $out[] = ['id' => (int)$r['id'], 'text' => $r['nome'], 'photo' => MagoMatriceStats::photoUrl($r['photo']), 'has_gm' => (int)$r['n_gm'] > 0]; } return $out; } /** Valori Mago del campo Matrice/Misura che portano a questa matrice */ public static function rawsFor(PDO $pdo, int $id): array { $raws = []; foreach ($pdo->query("SELECT DISTINCT matrice_misura FROM mago_sale_ord_detail WHERE matrice_misura <> ''")->fetchAll(PDO::FETCH_COLUMN) as $raw) { if (MagoMatriceStats::resolve($pdo, $raw)['id'] === $id) $raws[] = $raw; } return $raws; } /** * Tutti i dati per il preventivo. * @param string $kind m | kg */ public static function data(PDO $pdo, int $id, string $kind, ?string $customer): array { $kind = $kind === 'kg' ? 'kg' : 'm'; $st = $pdo->prepare("SELECT id, nome, photo FROM matrice WHERE id = ?"); $st->execute([$id]); $mat = $st->fetch(PDO::FETCH_ASSOC); if (!$mat) return ['success' => false, 'message' => 'Matrice non trovata']; $mi = MagoMatriceStats::mixInfo($pdo, [$id])[$id] ?? null; $prices = MagoMatriceStats::mixPrices($pdo); $cost = MagoMatriceStats::costOf($mi, $kind, $prices); // mescole con tutti i dati per il calcolo modificabile nella pagina $mix = []; foreach ($mi['mix'] ?? [] as $x) { $p = $prices[$x['id']] ?? null; $mix[] = [ 'id' => $x['id'], 'name' => $x['name'], 'out' => $x['out'], 'gm' => $x['gm'] !== null && $x['gm'] > 0 ? (float)$x['gm'] : null, 'eur_kg' => $p ? $p['price'] : null, 'last_price' => $p['last_price'] ?? null, 'last_date' => $p['last_date'] ?? null, 'old' => $p ? $p['old'] : null, 'supplier' => $p['last_supplier'] ?? null, 'check' => $p['check'] ?? null, ]; } // costo mese per mese $months = []; for ($i = self::HISTORY_MONTHS - 1; $i >= 0; $i--) $months[] = date('Y-m', strtotime("first day of -$i months")); $costMonths = []; if ($cost['cost'] !== null) { foreach ($months as $ym) { $c = MagoMatriceStats::costOf($mi, $kind, $prices, $ym); if ($c['cost'] !== null) $costMonths[$ym] = $c['cost']; } } // vendite della matrice nell'unità scelta $raws = self::rawsFor($pdo, $id); $since = $months[0] . '-01'; $d12 = date('Y-m-d', strtotime('-365 days')); $lines = []; foreach ($raws ? MagoMatriceStats::fetchLines($pdo, $since, [], $raws) : [] as $l) { if ($l['order_date'] < $since) continue; if (MagoMatriceStats::uomKind($l['uom']) !== $kind) continue; if ((float)$l['value'] <= 0 || (float)$l['qty'] <= 0) continue; $lines[] = $l; } $names = MagoMatriceStats::customerNames($pdo, array_column($lines, 'customer')); $all = ['m' => [], 'v12' => 0, 'q12' => 0, 'min' => null, 'max' => null, 'clients' => []]; $cus = ['m' => [], 'v12' => 0, 'q12' => 0, 'last_price' => null, 'last_date' => null, 'lines' => []]; foreach ($lines as $l) { $q = (float)$l['qty']; $v = (float)$l['value']; $pr = $v / $q; $ym = substr($l['order_date'], 0, 7); $all['m'][$ym]['v'] = ($all['m'][$ym]['v'] ?? 0) + $v; $all['m'][$ym]['q'] = ($all['m'][$ym]['q'] ?? 0) + $q; if ($l['order_date'] > $d12) { $all['v12'] += $v; $all['q12'] += $q; $all['min'] = $all['min'] === null ? $pr : min($all['min'], $pr); $all['max'] = $all['max'] === null ? $pr : max($all['max'], $pr); $c = &$all['clients'][$l['customer']]; $c['v'] = ($c['v'] ?? 0) + $v; $c['q'] = ($c['q'] ?? 0) + $q; unset($c); } if ($customer !== null && $l['customer'] === $customer) { $cus['m'][$ym]['v'] = ($cus['m'][$ym]['v'] ?? 0) + $v; $cus['m'][$ym]['q'] = ($cus['m'][$ym]['q'] ?? 0) + $q; if ($l['order_date'] > $d12) { $cus['v12'] += $v; $cus['q12'] += $q; } $cus['last_price'] = $pr; $cus['last_date'] = $l['order_date']; $cus['lines'][] = ['date' => $l['order_date'], 'ord' => $l['internal_ord_no'], 'qty' => round($q, 3), 'price' => round($pr, 4)]; } } $avgM = function (array $m) { $o = []; ksort($m); foreach ($m as $ym => $x) $o[$ym] = round($x['v'] / $x['q'], 4); return $o; }; $clients = []; foreach ($all['clients'] as $code => $c) $clients[] = ['code' => $code, 'name' => $names[$code] ?? $code, 'price' => round($c['v'] / $c['q'], 4), 'qty' => round($c['q'], 1)]; usort($clients, fn($a, $b) => $b['qty'] <=> $a['qty']); $avg12 = $all['q12'] > 0 ? $all['v12'] / $all['q12'] : null; return [ 'success' => true, 'matrice' => [ 'id' => (int)$mat['id'], 'name' => $mat['nome'], 'photo' => MagoMatriceStats::photoUrl($mat['photo']), 'raws' => $raws, 'ws' => $mi['ws'] ?? null, 'mix_source' => $mi['source'] ?? 'nessuna' ], 'kind' => $kind, 'uom' => $kind === 'kg' ? 'KG' : 'MT', 'price_months' => MagoMixPurchases::PRICE_MONTHS, 'cost' => ['cost' => $cost['cost'], 'status' => $cost['status'], 'missing' => $cost['missing'], 'check' => $cost['check']], 'mix' => $mix, 'months' => $months, 'cost_months' => $costMonths, 'all' => [ 'months' => $avgM($all['m']), 'avg12' => $avg12 !== null ? round($avg12, 4) : null, 'min12' => $all['min'] !== null ? round($all['min'], 4) : null, 'max12' => $all['max'] !== null ? round($all['max'], 4) : null, 'clients' => array_slice($clients, 0, 12), 'n_clients' => count($clients), 'markup' => ($avg12 && $cost['cost'] && $cost['status'] === 'ok') ? round(($avg12 / $cost['cost'] - 1) * 100, 0) : null, ], 'customer' => $customer === null ? null : [ 'code' => $customer, 'name' => $names[$customer] ?? self::customerName($pdo, $customer), 'months' => $avgM($cus['m']), 'avg12' => $cus['q12'] > 0 ? round($cus['v12'] / $cus['q12'], 4) : null, 'last_price' => $cus['last_price'] !== null ? round($cus['last_price'], 4) : null, 'last_date' => $cus['last_date'], 'lines' => array_slice(array_reverse($cus['lines']), 0, 12), ], 'quotes' => self::quotes($pdo, $id, null), ]; } public static function customerName(PDO $pdo, string $code): string { return MagoMatriceStats::customerNames($pdo, [$code])[$code] ?? $code; } /** Preventivi salvati di una matrice (tutti i clienti), più recenti prima */ public static function quotes(PDO $pdo, ?int $idmatrice, ?string $customer, int $limit = 50): array { $w = []; $p = []; if ($idmatrice) { $w[] = 'q.idmatrice = :m'; $p['m'] = $idmatrice; } if ($customer) { $w[] = 'q.customer_code = :c'; $p['c'] = $customer; } $user = "TRIM(CONCAT(IFNULL(u.first_name, ''), ' ', IFNULL(u.last_name, '')))"; try { $st = $pdo->prepare("SELECT q.*, NULLIF($user, '') AS user_name, u.username FROM sales_quotes q LEFT JOIN auth_users u ON u.id = q.user_id " . ($w ? 'WHERE ' . implode(' AND ', $w) : '') . " ORDER BY q.created_at DESC, q.id DESC LIMIT " . (int)$limit); $st->execute($p); return $st->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) { return []; } } /** Salva un preventivo. I numeri arrivano dalla pagina; il costo è ricalcolato dalle mescole inviate. */ public static function save(PDO $pdo, array $in, ?int $userId): array { $id = (int)($in['idmatrice'] ?? 0); $st = $pdo->prepare("SELECT nome FROM matrice WHERE id = ?"); $st->execute([$id]); $name = $st->fetchColumn(); if (!$name) return ['success' => false, 'message' => 'Matrice non trovata']; $kind = ($in['kind'] ?? 'm') === 'kg' ? 'kg' : 'm'; $num = function ($v) { if ($v === null || $v === '') return null; return (float)str_replace(',', '.', (string)$v); }; // mescole inviate dalla pagina (g/m e €/kg eventualmente corretti a mano) $mix = json_decode((string)($in['mix'] ?? '[]'), true) ?: []; $cost = 0.0; $ok = 0; $gmTot = 0.0; foreach ($mix as $x) $gmTot += max(0, (float)($x['gm'] ?? 0)); foreach ($mix as &$x) { $gm = $num($x['gm'] ?? null); $eur = $num($x['eur_kg'] ?? null); $x = ['id' => (int)($x['id'] ?? 0), 'name' => (string)($x['name'] ?? ''), 'gm' => $gm, 'eur_kg' => $eur, 'edited' => !empty($x['edited'])]; if ($eur === null) continue; if ($kind === 'm') { if ($gm) { $cost += $gm / 1000 * $eur; $ok++; } } else { $share = $gmTot > 0 ? max(0, (float)$gm) / $gmTot : 1 / max(1, count($mix)); $cost += $share * $eur; $ok++; } } unset($x); $costMix = $ok ? $cost : null; $scrap = $num($in['scrap_pct'] ?? 0) ?? 0; $other = $num($in['other_costs'] ?? 0) ?? 0; $markup = $num($in['markup_pct'] ?? null); $costTotal = $costMix !== null ? $costMix * (1 + $scrap / 100) + $other : ($other ?: null); $suggested = ($costTotal !== null && $markup !== null) ? $costTotal * (1 + $markup / 100) : null; $offered = $num($in['price_offered'] ?? null) ?? $suggested; if ($offered === null) return ['success' => false, 'message' => 'Manca il prezzo: indica il ricarico o il prezzo proposto']; $cust = trim((string)($in['customer'] ?? '')); $st = $pdo->prepare("INSERT INTO sales_quotes (created_at, user_id, idmatrice, matrice_name, customer_code, customer_name, uom, cost_mix, scrap_pct, other_costs, cost_total, markup_pct, price_suggested, price_offered, ref_last_price, ref_avg_price, mix_detail, comment) VALUES (NOW(), :u, :m, :mn, :cc, :cn, :uom, :cm, :sp, :oc, :ct, :mk, :ps, :po, :rl, :ra, :md, :cmt)"); $st->execute([ 'u' => $userId ?: null, 'm' => $id, 'mn' => $name, 'cc' => $cust !== '' ? $cust : null, 'cn' => $cust !== '' ? self::customerName($pdo, $cust) : null, 'uom' => $kind === 'kg' ? 'KG' : 'MT', 'cm' => $costMix, 'sp' => $scrap, 'oc' => $other, 'ct' => $costTotal, 'mk' => $markup, 'ps' => $suggested, 'po' => $offered, 'rl' => $num($in['ref_last_price'] ?? null), 'ra' => $num($in['ref_avg_price'] ?? null), 'md' => json_encode($mix, JSON_UNESCAPED_UNICODE), 'cmt' => trim((string)($in['comment'] ?? '')) ?: null, ]); return ['success' => true, 'id' => (int)$pdo->lastInsertId()]; } public static function delete(PDO $pdo, int $id): bool { $st = $pdo->prepare("DELETE FROM sales_quotes WHERE id = ?"); $st->execute([$id]); return $st->rowCount() > 0; } }