diff --git a/db/migrations/20261027112137_add_sales_quotes.php b/db/migrations/20261027112137_add_sales_quotes.php new file mode 100644 index 00000000..2b14ceb5 --- /dev/null +++ b/db/migrations/20261027112137_add_sales_quotes.php @@ -0,0 +1,60 @@ +fetchRow("SELECT id FROM auth_permissions WHERE name = 'sales.quote.view'")) { + $this->table('auth_permissions')->insert([ + 'name' => 'sales.quote.view', + 'display_name' => 'Preventivi matrici', + 'description' => 'Calcolare preventivi: costo mescola, ricarico, prezzo suggerito', + 'removable' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ])->saveData(); + } + + if (!$this->hasTable('sales_quotes')) { + $this->table('sales_quotes', ['collation' => 'utf8mb4_general_ci']) + ->addColumn('created_at', 'datetime') + ->addColumn('user_id', 'integer', ['null' => true]) + ->addColumn('idmatrice', 'integer') + ->addColumn('matrice_name', 'string', ['limit' => 150]) + ->addColumn('customer_code', 'string', ['limit' => 20, 'null' => true]) + ->addColumn('customer_name', 'string', ['limit' => 200, 'null' => true]) + ->addColumn('uom', 'string', ['limit' => 4]) + ->addColumn('cost_mix', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('scrap_pct', 'decimal', ['precision' => 6, 'scale' => 2, 'default' => 0]) + ->addColumn('other_costs', 'decimal', ['precision' => 12, 'scale' => 4, 'default' => 0]) + ->addColumn('cost_total', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('markup_pct', 'decimal', ['precision' => 7, 'scale' => 2, 'null' => true]) + ->addColumn('price_suggested', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('price_offered', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('ref_last_price', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('ref_avg_price', 'decimal', ['precision' => 12, 'scale' => 4, 'null' => true]) + ->addColumn('mix_detail', 'text', ['null' => true]) + ->addColumn('comment', 'text', ['null' => true]) + ->addIndex(['idmatrice']) + ->addIndex(['customer_code']) + ->addIndex(['created_at']) + ->create(); + } + } + + public function down(): void + { + if ($this->hasTable('sales_quotes')) $this->table('sales_quotes')->drop()->save(); + $this->execute("DELETE pr FROM auth_permission_role pr JOIN auth_permissions p ON p.id = pr.permission_id WHERE p.name = 'sales.quote.view'"); + $this->execute("DELETE FROM auth_permissions WHERE name = 'sales.quote.view'"); + } +} diff --git a/public/userarea/class/MagoQuote.php b/public/userarea/class/MagoQuote.php new file mode 100644 index 00000000..0b165e3c --- /dev/null +++ b/public/userarea/class/MagoQuote.php @@ -0,0 +1,301 @@ +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; + } +} diff --git a/public/userarea/sales_help.js b/public/userarea/sales_help.js index 40e1d9b8..f8f255ac 100644 --- a/public/userarea/sales_help.js +++ b/public/userarea/sales_help.js @@ -484,6 +484,57 @@

🟢 da 60% · 🟡 da 40 a 60% · 🔴 sotto 40%.

Contano solo le matrici con costo completo. Clic su una matrice per il dettaglio con la linea del costo nel grafico.

`, }, + /* ===================== PREVENTIVO ===================== */ + "page.quote": { + t: "Preventivo: come si usa", + b: `
    +
  1. Scegli la matrice, il cliente (facoltativo) e se vendi al metro o al kg.
  2. +
  3. Costo: le mescole della scheda di lavoro con g/m ed €/kg pagati in Mago. Puoi correggere i numeri (diventano gialli) e aggiungere scarto e altri costi.
  4. +
  5. Prezzo: scegli il ricarico sul costo. Il prezzo suggerito si calcola da solo; il prezzo proposto si può arrotondare a mano.
  6. +
  7. Confronta con ultimo prezzo al cliente, media di mercato e forbice, poi scrivi un commento e Salva.
  8. +
+

I preventivi salvati restano in fondo alla pagina e si riaprono con un clic. Stampa per avere un foglio pulito.

`, + }, + "pv.cost": { + t: "Costo", + b: ` +

Le correzioni valgono solo per questo preventivo e vengono salvate con lui.

`, + }, + "pv.scrap": { + t: "Scarto %", + b: `

Percentuale di mescola persa in produzione (avvii, sfridi, scarti). Il costo della mescola viene aumentato di questa percentuale.

`, + }, + "pv.other": { + t: "Altri costi", + b: `

Costi per metro o per kg da aggiungere alla mescola: lavorazioni esterne (biadesivo, giunzione, raspatura), imballo, trasporto, quota di lavorazione.

`, + }, + "pv.price": { + t: "Prezzo suggerito", + b: `

Prezzo suggerito = costo totale × (1 + ricarico %). Es. costo 0,50 €/m con ricarico 100% → 1,00 €/m.

+

Il pulsante come mercato usa il ricarico che oggi si ottiene con il prezzo medio di vendita della matrice.

+

🟢 margine da 60% · 🟡 da 40 a 60% · 🔴 sotto 40% (margine = (prezzo − costo) ÷ prezzo).

`, + }, + "pv.offered": { + t: "Prezzo proposto", + b: `

È il prezzo che finisce nel preventivo: parte dal suggerito e si può arrotondare a mano. Accanto, il margine che ne risulta.

`, + }, + "pv.refs": { + t: "Confronto", + b: ``, + }, + "pv.chart": { + t: "Costo e prezzi nel tempo", + b: `

Mese per mese: costo della mescola (con i prezzi pagati in quel mese), prezzo medio di tutti i clienti e del cliente scelto. Le righe orizzontali sono il costo totale e il prezzo proposto.

+

Se la linea del costo sale e quella dei prezzi no, il margine si sta riducendo.

`, + }, "mx.map": { t: "Mappa dei prezzi", b: `

Ogni bolla è una matrice venduta nel periodo (le prime 150 per valore).

diff --git a/public/userarea/sales_matrici.php b/public/userarea/sales_matrici.php index 43277576..ed3c6510 100644 --- a/public/userarea/sales_matrici.php +++ b/public/userarea/sales_matrici.php @@ -2197,4 +2197,2217 @@ if ($action === 'list' || $action === 'detail') { +getConnection(); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Permesso specifico: prezzi di vendita per matrice e cliente (dati riservati) + if (!userCan('sales.matrici.view')) { + if (isset($_GET['action'])) { + MagoSalesStats::jsonOut(['success' => 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; + } + $canDeliveries = userCan('sales.deliveries.view'); + $canQuote = userCan('sales.quote.view'); + + /* ============================================================ + * AJAX + * ============================================================ */ + function mx_date($v, $def) + { + $v = (string)$v; + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : $def; + } + function mx_customers() + { + $c = trim((string)($_GET['c'] ?? '')); + return $c === '' ? [] : array_slice(array_values(array_filter(array_map('trim', explode(',', $c)))), 0, 200); + } + + $action = $_GET['action'] ?? ''; + + if ($action === 'filters') { + MagoSalesStats::jsonOut(MagoSalesStats::filterOptions($pdo)); + } + + if ($action === 'list' || $action === 'detail') { + try { + $from = mx_date($_GET['from'] ?? '', date('Y-m-d', strtotime('-365 days'))); + $to = mx_date($_GET['to'] ?? '', date('Y-m-d')); + if ($from < MagoMatriceStats::SINCE) $from = MagoMatriceStats::SINCE; + if ($to < $from) $to = $from; + if ($action === 'list') { + MagoSalesStats::jsonOut(MagoMatriceStats::buildList($pdo, $from, $to, mx_customers())); + } + MagoSalesStats::jsonOut(MagoMatriceStats::buildDetail($pdo, (string)($_GET['k'] ?? ''), $from, $to, mx_customers())); + } catch (Throwable $e) { + MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]); + } + } + ?> + + + + + + + + + Matrici: prezzi e mescole - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+
+
+
+
+
Matrici: prezzi e mescole
+ Dati Mago4 +
+
+ +
+
+ +
+ + + +
+
+
+ +
+ + + + + + + + +
+
+
+ +
+
+ +
+
+
+ +
+ + + + + + + +
+
+
+ +
+ + + + +
+
+
+
+ + + +
+
+
Carico le righe d'ordine…
+
+ + +
+
+
+
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/sales_preventivo.php b/public/userarea/sales_preventivo.php new file mode 100644 index 00000000..8be5dfb5 --- /dev/null +++ b/public/userarea/sales_preventivo.php @@ -0,0 +1,1385 @@ +getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +if (!userCan('sales.quote.view')) { + if (isset($_GET['action']) || isset($_POST['action'])) { + MagoSalesStats::jsonOut(['success' => 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; +} +$canDeliveries = userCan('sales.deliveries.view'); +$canMatrici = userCan('sales.matrici.view'); + +/* ============================================================ + * AJAX + * ============================================================ */ +$action = $_GET['action'] ?? $_POST['action'] ?? ''; +try { + if ($action === 'matrici') { + MagoSalesStats::jsonOut(['results' => MagoQuote::searchMatrici($pdo, (string)($_GET['q'] ?? ''))]); + } + if ($action === 'customers') { + $f = MagoSalesStats::filterOptions($pdo); + MagoSalesStats::jsonOut(['success' => true, 'customers' => $f['customers'] ?? []]); + } + if ($action === 'data') { + $c = trim((string)($_GET['c'] ?? '')); + MagoSalesStats::jsonOut(MagoQuote::data($pdo, (int)($_GET['m'] ?? 0), (string)($_GET['u'] ?? 'm'), $c !== '' ? $c : null)); + } + if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') { + MagoSalesStats::jsonOut(MagoQuote::save($pdo, $_POST, (int)($iduserlogin ?? 0))); + } + if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') { + MagoSalesStats::jsonOut(['success' => MagoQuote::delete($pdo, (int)($_POST['id'] ?? 0))]); + } +} catch (Throwable $e) { + MagoSalesStats::jsonOut(['success' => false, 'message' => $e->getMessage()]); +} +?> + + + + + + + + + Preventivo matrice - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + + + + + + + + +
+ + + +
+
+
+
+
+
+
+
+
Preventivo matrice
+
Costo della mescola, prezzi praticati e prezzo suggerito
+
+
+ +
+
+ +
+ + + +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ +
+ +
Scegli una matrice per calcolare il preventivo.
+
+ + + +
+
+
+
+ +
+ + + + + + + \ No newline at end of file