diff --git a/db/migrations/20261027104527_add_mago_mix_purchases.php b/db/migrations/20261027104527_add_mago_mix_purchases.php new file mode 100644 index 00000000..9e384e9a --- /dev/null +++ b/db/migrations/20261027104527_add_mago_mix_purchases.php @@ -0,0 +1,62 @@ +table('suppliers'); + if (!$t->hasColumn('mago_code')) { + $t->addColumn('mago_code', 'string', ['limit' => 20, 'null' => true, 'after' => 'vat']) + ->addIndex(['mago_code'], ['name' => 'idx_suppliers_mago_code']) + ->update(); + } + + // codici Mago già noti dalla prova acquisti (DER-GOM e HEXPOL si collegano da soli al primo sync) + foreach (['COMET' => 'C063', 'SIGEA' => 'S022', 'TOVO' => 'T050', 'EVER COMPOUND' => 'E025'] as $name => $code) { + $this->execute("UPDATE suppliers SET mago_code = '$code' WHERE supplier_name = '$name' AND (mago_code IS NULL OR mago_code = '')"); + } + if (!$this->fetchRow("SELECT idsupplier FROM suppliers WHERE supplier_name LIKE 'HEXPOL%'")) { + $this->execute("INSERT INTO suppliers (supplier_name) VALUES ('HEXPOL')"); + } + + if (!$this->hasTable('mago_mix_purchases')) { + $this->table('mago_mix_purchases', ['id' => false, 'primary_key' => ['purchase_doc_id', 'line'], 'collation' => 'utf8mb4_general_ci']) + ->addColumn('purchase_doc_id', 'integer') + ->addColumn('line', 'integer') + ->addColumn('document_type', 'integer', ['null' => true]) + ->addColumn('doc_date', 'date', ['null' => true]) + ->addColumn('doc_no', 'string', ['limit' => 40, 'null' => true]) + ->addColumn('supplier_code', 'string', ['limit' => 20, 'null' => true]) + ->addColumn('supplier_name', 'string', ['limit' => 150, 'null' => true]) + ->addColumn('item', 'string', ['limit' => 40, 'null' => true]) + ->addColumn('description', 'string', ['limit' => 500, 'null' => true]) + ->addColumn('uom', 'string', ['limit' => 10, 'null' => true]) + ->addColumn('qty', 'decimal', ['precision' => 14, 'scale' => 3, 'default' => 0]) + ->addColumn('unit_value', 'decimal', ['precision' => 14, 'scale' => 5, 'default' => 0]) + ->addColumn('taxable_amount', 'decimal', ['precision' => 14, 'scale' => 2, 'default' => 0]) + ->addColumn('notes', 'string', ['limit' => 500, 'null' => true]) + ->addColumn('purchase_ord_no', 'string', ['limit' => 40, 'null' => true]) + ->addColumn('synced_at', 'datetime', ['null' => true]) + ->addIndex(['doc_date']) + ->addIndex(['supplier_code']) + ->create(); + } + } + + public function down(): void + { + if ($this->hasTable('mago_mix_purchases')) $this->table('mago_mix_purchases')->drop()->save(); + $t = $this->table('suppliers'); + if ($t->hasColumn('mago_code')) $t->removeIndexByName('idx_suppliers_mago_code')->removeColumn('mago_code')->update(); + } +} diff --git a/public/userarea/class/MagoMixPurchases.php b/public/userarea/class/MagoMixPurchases.php new file mode 100644 index 00000000..ae3f6dce --- /dev/null +++ b/public/userarea/class/MagoMixPurchases.php @@ -0,0 +1,408 @@ + 'IMPASTO', 'ZIBO' => 'ZB', 'MESCOLE' => 'MESCOLA', 'TMDT' => 'TMTD', 'TTMD' => 'TMTD', 'BIANCI' => 'BIANCO', 'ESCO' => 'ECO', 'PAFX' => 'PAF']; + const DROP = ['MESCOLA', 'ART', 'STRISCIA', 'PICCOLA', 'SPESSORE', 'MM', 'LP', 'SP', 'PV', 'ZBO', 'FILTRATA', 'FILTRATO', 'RILAVORATE', 'RIPRESE']; + + private static ?array $namesCache = null; + + /* ============================================================ + * NORMALIZZAZIONE + * ============================================================ */ + + /** + * Parole significative di un nome o descrizione: maiuscole, senza revisione, gradi Comet (M100/L080), + * codici (B5904), formato/imballo (STRISCIA…), punteggiatura; "ATES71" → ATES 71; "01" → 1. + */ + public static function tokens(string $s): array + { + $s = strtoupper($s); + $s = preg_replace('/\bREV\.?\s*\d+/', ' ', $s); + $s = preg_replace('/\b[ML]\d{3}\b/', ' ', $s); + $s = preg_replace('/\(B\d+\)?/', ' ', $s); + $s = preg_replace('/\b\w*(STRISC|TRISCIA|STRSC)\w*\b.*$/s', ' ', $s); + $s = preg_replace('/(?<=[A-Z])(?=\d)|(?<=\d)(?=[A-Z])/', ' ', $s); + $out = []; + foreach (preg_split('/[^A-Z0-9]+/', $s) as $t) { + if ($t === '') continue; + if (ctype_digit($t)) $t = (string)(int)$t; + $t = self::SYN[$t] ?? $t; + if (in_array($t, self::DROP, true)) continue; + $out[$t] = true; + } + return array_keys($out); + } + + public static function isExcluded(string $desc): bool + { + return (bool)preg_match(self::EXCLUDE_RE, $desc); + } + + public static function revision(string $desc): ?int + { + return preg_match('/REV\.?\s*(\d+)/i', $desc, $m) ? (int)$m[1] : null; + } + + /* ============================================================ + * NOMI D'ACQUISTO + * ============================================================ */ + + /** Tutti i nomi d'acquisto con le loro parole */ + public static function names(PDO $pdo, bool $refresh = false): array + { + if (self::$namesCache !== null && !$refresh) return self::$namesCache; + $rows = $pdo->query("SELECT sn.id, sn.idmescola, sn.idsupplier, sn.supplier_mix_name, sn.is_preferred, sn.note, + m.nome AS mescola, m.nomeuscita, m.is_active, s.supplier_name, s.mago_code + FROM mescole_supplier_names sn + JOIN mescole m ON m.id = sn.idmescola + LEFT JOIN suppliers s ON s.idsupplier = sn.idsupplier")->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as &$r) $r['tokens'] = self::tokens((string)$r['supplier_mix_name']); + unset($r); + return self::$namesCache = array_values(array_filter($rows, fn($r) => $r['tokens'])); + } + + /** + * Mescola di una descrizione d'acquisto. + * @return array status: ok | ambiguous | none | excluded ; name (riga nomi) ; candidates (id mescola) + */ + public static function matchDescription(PDO $pdo, string $desc, ?string $supplierCode = null): array + { + if (self::isExcluded($desc)) return ['status' => 'excluded', 'name' => null, 'candidates' => []]; + $T = array_flip(self::tokens($desc)); + $hits = []; + foreach (self::names($pdo) as $n) { + // nome legato a un fornitore con codice Mago diverso da quello della fattura → non vale + if ($supplierCode !== null && $n['mago_code'] && strcasecmp($n['mago_code'], $supplierCode) !== 0) continue; + foreach ($n['tokens'] as $t) if (!isset($T[$t])) continue 2; + $hits[] = $n; + } + if (!$hits) return ['status' => 'none', 'name' => null, 'candidates' => []]; + $max = max(array_map(fn($n) => count($n['tokens']), $hits)); + $best = array_values(array_filter($hits, fn($n) => count($n['tokens']) === $max)); + $ids = array_values(array_unique(array_map(fn($n) => (int)$n['idmescola'], $best))); + if (count($ids) > 1) return ['status' => 'ambiguous', 'name' => $best[0], 'candidates' => $ids]; + return ['status' => 'ok', 'name' => $best[0], 'candidates' => $ids]; + } + + /** Stesso nome (stesse parole) su mescole diverse */ + public static function duplicateNames(PDO $pdo): array + { + $by = []; + foreach (self::names($pdo) as $n) { + $k = implode(' ', $n['tokens']); + $by[$k][(int)$n['idmescola']] = $n['mescola']; + } + return array_filter($by, fn($m) => count($m) > 1); + } + + /* ============================================================ + * RIGHE D'ACQUISTO ABBINATE + * ============================================================ */ + + /** Righe locali (fatture, KG) con la mescola riconosciuta */ + public static function lines(PDO $pdo, ?string $since = null): array + { + $p = []; + $w = "item = '" . self::ITEM . "' AND document_type = " . self::DOC_INVOICE; + if ($since) { + $w .= " AND doc_date >= :since"; + $p['since'] = $since; + } + $st = $pdo->prepare("SELECT * FROM mago_mix_purchases WHERE $w ORDER BY doc_date, purchase_doc_id, line"); + $st->execute($p); + $out = []; + foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) { + $m = self::matchDescription($pdo, (string)$r['description'], $r['supplier_code']); + $r['match'] = $m['status']; + $r['idmescola'] = $m['status'] === 'ok' ? $m['candidates'][0] : null; + $r['candidates'] = $m['candidates']; + $r['qty'] = (float)$r['qty']; + $r['value'] = (float)$r['taxable_amount'] != 0 ? (float)$r['taxable_amount'] : (float)$r['qty'] * (float)$r['unit_value']; + $r['rev'] = self::revision((string)$r['description']); + $out[] = $r; + } + return $out; + } + + /** + * €/kg per mescola. + * @return array [idmescola => price, last_price, last_date, kg_window, kg_total, old (bool), months [ym => €/kg], suppliers [nome => €/kg]] + */ + public static function prices(PDO $pdo, int $months = self::PRICE_MONTHS, ?array $lines = null): array + { + $months = max(1, $months); + $from = date('Y-m-d', strtotime("-$months months")); + $lines = $lines ?? self::lines($pdo); + $acc = []; + foreach ($lines as $l) { + if (!$l['idmescola'] || $l['qty'] <= 0 || $l['value'] <= 0 || strtoupper(trim((string)$l['uom'])) !== 'KG') continue; + $a = &$acc[$l['idmescola']]; + $a['kg_total'] = ($a['kg_total'] ?? 0) + $l['qty']; + $a['val_total'] = ($a['val_total'] ?? 0) + $l['value']; + if ($l['doc_date'] >= $from) { + $a['kg_w'] = ($a['kg_w'] ?? 0) + $l['qty']; + $a['val_w'] = ($a['val_w'] ?? 0) + $l['value']; + $s = $l['supplier_name'] ?: $l['supplier_code']; + $a['sup'][$s]['kg'] = ($a['sup'][$s]['kg'] ?? 0) + $l['qty']; + $a['sup'][$s]['val'] = ($a['sup'][$s]['val'] ?? 0) + $l['value']; + } + if (($a['last_date'] ?? '') <= $l['doc_date']) { + $a['last_date'] = $l['doc_date']; + $a['last_price'] = $l['value'] / $l['qty']; + } + $ym = substr((string)$l['doc_date'], 0, 7); + $a['m'][$ym]['kg'] = ($a['m'][$ym]['kg'] ?? 0) + $l['qty']; + $a['m'][$ym]['val'] = ($a['m'][$ym]['val'] ?? 0) + $l['value']; + unset($a); + } + $out = []; + foreach ($acc as $id => $a) { + $win = !empty($a['kg_w']); + $ms = []; + ksort($a['m']); + foreach ($a['m'] as $ym => $x) $ms[$ym] = round($x['val'] / $x['kg'], 4); + $sup = []; + foreach ($a['sup'] ?? [] as $s => $x) $sup[$s] = round($x['val'] / $x['kg'], 4); + $out[$id] = [ + 'price' => round($win ? $a['val_w'] / $a['kg_w'] : $a['last_price'], 4), + 'last_price' => round($a['last_price'], 4), + 'last_date' => $a['last_date'], + 'kg_window' => round($a['kg_w'] ?? 0, 1), + 'kg_total' => round($a['kg_total'], 1), + 'old' => !$win, + 'months' => $ms, + 'suppliers' => $sup, + ]; + } + return $out; + } + + /** Descrizioni d'acquisto non abbinate (o ambigue), raggruppate per parole, ordinate per valore */ + public static function unmatched(PDO $pdo, ?array $lines = null): array + { + $lines = $lines ?? self::lines($pdo); + $g = []; + foreach ($lines as $l) { + if (!in_array($l['match'], ['none', 'ambiguous'], true)) continue; + $k = implode(' ', self::tokens((string)$l['description'])); + $x = &$g[$k]; + $x['key'] = $k; + $x['description'] = $x['description'] ?? trim(preg_replace('/\s+/', ' ', (string)$l['description'])); + $x['suggest'] = $x['suggest'] ?? self::suggestName((string)$l['description']); + $x['status'] = $l['match']; + $x['candidates'] = $l['candidates']; + $x['supplier_code'] = $l['supplier_code']; + $x['supplier_name'] = $l['supplier_name']; + $x['n'] = ($x['n'] ?? 0) + 1; + $x['kg'] = ($x['kg'] ?? 0) + $l['qty']; + $x['value'] = ($x['value'] ?? 0) + $l['value']; + $x['last_date'] = max($x['last_date'] ?? '', (string)$l['doc_date']); + unset($x); + } + foreach ($g as &$x) { + $x['price'] = $x['kg'] > 0 ? round($x['value'] / $x['kg'], 4) : null; + $x['kg'] = round($x['kg'], 1); + $x['value'] = round($x['value'], 2); + } + unset($x); + usort($g, fn($a, $b) => $b['value'] <=> $a['value']); + return $g; + } + + /** Nome d'acquisto proposto da una descrizione: senza imballo, spessore, revisione, "MESCOLA" */ + public static function suggestName(string $desc): string + { + $s = strtoupper(preg_replace('/\s+/', ' ', trim($desc))); + $s = preg_replace('/^MESCOL[AE]\s*/', '', $s); + $s = preg_replace('/^ART\.?\s*/', '', $s); + $s = preg_replace('/\s*\bREV\.?\s*\d+\b/', '', $s); + $s = preg_replace('/\s*\b\w*(STRISC|TRISCIA|STRSC)\w*\b.*$/', '', $s); + $s = preg_replace('/\s*\b(X\s*)?\d+\s*MM\b.*$/', '', $s); + $s = preg_replace('/\s*\b(PANI|FESTONI|IMPILATA|IN CASSONI|CONFEZIONE)\b.*$/', '', $s); + return trim($s, " -.,"); + } + + /** Righe e kg acquistati per ogni nome d'acquisto (per la modale della mescola) */ + public static function nameStats(PDO $pdo, int $idmescola, ?array $lines = null): array + { + $lines = $lines ?? self::lines($pdo); + $out = []; + foreach ($lines as $l) { + if ($l['match'] !== 'ok' || (int)$l['idmescola'] !== $idmescola) continue; + $m = self::matchDescription($pdo, (string)$l['description'], $l['supplier_code']); + $nid = (int)$m['name']['id']; + $o = &$out[$nid]; + $o['n'] = ($o['n'] ?? 0) + 1; + $o['kg'] = ($o['kg'] ?? 0) + $l['qty']; + $o['value'] = ($o['value'] ?? 0) + $l['value']; + if (($o['last_date'] ?? '') <= $l['doc_date']) { + $o['last_date'] = $l['doc_date']; + $o['last_price'] = $l['qty'] > 0 ? $l['value'] / $l['qty'] : null; + $o['last_desc'] = $l['description']; + $o['last_supplier'] = $l['supplier_name']; + } + unset($o); + } + return $out; + } + + /* ============================================================ + * SINCRONIZZAZIONE DA MAGO (sola lettura) + * ============================================================ */ + + /** + * Copia le righe MESCOLE delle fatture fornitore con data documento >= $from. + * Le righe del periodo che non esistono più in Mago vengono tolte. + * @return array letti, scritti, tolti, fornitori collegati + */ + public static function sync(PDO $pdo, MagoClient $mago, string $from, ?callable $log = null): array + { + $log = $log ?: function ($s) {}; + $fields = [ + 'PurchaseDocId', + 'Line', + 'DocumentType', + 'DocumentDate', + 'DocNo', + 'Supplier', + 'Item', + 'Description', + 'UoM', + 'Qty', + 'UnitValue', + 'TaxableAmount', + 'Notes', + 'PurchaseOrdNo' + ]; + $flt = [ + ['key' => 'Item', 'value' => self::ITEM, 'operator' => '='], + ['key' => 'DocumentDate', 'value' => $from, 'operator' => '>='], + ['key' => 'DocumentType', 'value' => (string)self::DOC_INVOICE, 'operator' => '='], + ]; + $rows = []; + foreach ($mago->iterate('MA_PurchaseDocDetail', ['fields' => $fields, 'filters' => $flt]) as $rec) $rows[] = $rec; + $log(count($rows) . " righe lette da Mago dal $from"); + + // nomi fornitori Mago + $names = []; + foreach (array_unique(array_map(fn($r) => (string)($r['Supplier'] ?? ''), $rows)) as $code) { + if ($code === '') continue; + try { + $r = $mago->getDataTable('MA_CustSupp', [ + 'fields' => ['CustSupp', 'CompanyName', 'CustSuppType'], + 'filters' => [['key' => 'CustSupp', 'value' => $code, 'operator' => '=']], + 'recordsPerPage' => 5 + ]); + foreach ($r['records'] as $c) { + $names[$code] = trim((string)$c['CompanyName']) ?: $code; + if ((string)$c['CustSuppType'] !== '3211264') break; // preferisco il record fornitore + } + } catch (Throwable $e) { + $names[$code] = $code; + } + } + + $now = date('Y-m-d H:i:s'); + $ins = $pdo->prepare("INSERT INTO mago_mix_purchases + (purchase_doc_id, line, document_type, doc_date, doc_no, supplier_code, supplier_name, item, description, uom, qty, unit_value, taxable_amount, notes, purchase_ord_no, synced_at) + VALUES (:id, :line, :type, :date, :no, :sc, :sn, :item, :desc, :uom, :qty, :uv, :ta, :notes, :pon, :now) + ON DUPLICATE KEY UPDATE document_type = VALUES(document_type), doc_date = VALUES(doc_date), doc_no = VALUES(doc_no), + supplier_code = VALUES(supplier_code), supplier_name = VALUES(supplier_name), item = VALUES(item), description = VALUES(description), + uom = VALUES(uom), qty = VALUES(qty), unit_value = VALUES(unit_value), taxable_amount = VALUES(taxable_amount), + notes = VALUES(notes), purchase_ord_no = VALUES(purchase_ord_no), synced_at = VALUES(synced_at)"); + $seen = []; + $pdo->beginTransaction(); + try { + foreach ($rows as $r) { + $id = (int)$r['PurchaseDocId']; + $line = (int)$r['Line']; + $seen["$id-$line"] = true; + $code = (string)($r['Supplier'] ?? ''); + $ins->execute([ + 'id' => $id, + 'line' => $line, + 'type' => (int)$r['DocumentType'], + 'date' => MagoClient::toMysqlDate($r['DocumentDate'] ?? null), + 'no' => mb_substr((string)($r['DocNo'] ?? ''), 0, 40), + 'sc' => $code, + 'sn' => mb_substr($names[$code] ?? $code, 0, 150), + 'item' => (string)($r['Item'] ?? ''), + 'desc' => mb_substr(trim((string)($r['Description'] ?? '')), 0, 500), + 'uom' => trim((string)($r['UoM'] ?? '')), + 'qty' => MagoClient::toDecimal($r['Qty'] ?? 0), + 'uv' => MagoClient::toDecimal($r['UnitValue'] ?? 0), + 'ta' => MagoClient::toDecimal($r['TaxableAmount'] ?? 0), + 'notes' => mb_substr(trim((string)($r['Notes'] ?? '')), 0, 500), + 'pon' => mb_substr((string)($r['PurchaseOrdNo'] ?? ''), 0, 40), + 'now' => $now, + ]); + } + // righe del periodo non più presenti in Mago + $removed = 0; + $st = $pdo->prepare("SELECT purchase_doc_id, line FROM mago_mix_purchases WHERE doc_date >= ?"); + $st->execute([$from]); + $del = $pdo->prepare("DELETE FROM mago_mix_purchases WHERE purchase_doc_id = ? AND line = ?"); + foreach ($st->fetchAll(PDO::FETCH_NUM) as [$id, $line]) { + if (!isset($seen["$id-$line"])) { + $del->execute([$id, $line]); + $removed++; + } + } + $pdo->commit(); + } catch (Throwable $e) { + $pdo->rollBack(); + throw $e; + } + + $linked = self::linkSuppliers($pdo, $names); + $log(count($rows) . " righe salvate, $removed tolte" . ($linked ? ", fornitori collegati: " . implode(', ', $linked) : '')); + return ['read' => count($rows), 'removed' => $removed, 'linked' => $linked]; + } + + /** Collega i fornitori del portale senza codice Mago, per nome (es. "DER-GOM" ↔ "DER-GOM S.R.L.") */ + public static function linkSuppliers(PDO $pdo, array $magoNames): array + { + $norm = fn($s) => preg_replace('/[^A-Z0-9]/', '', strtoupper((string)$s)); + $known = $pdo->query("SELECT mago_code FROM suppliers WHERE mago_code IS NOT NULL AND mago_code <> ''")->fetchAll(PDO::FETCH_COLUMN); + $free = $pdo->query("SELECT idsupplier, supplier_name FROM suppliers WHERE mago_code IS NULL OR mago_code = ''")->fetchAll(PDO::FETCH_ASSOC); + $upd = $pdo->prepare("UPDATE suppliers SET mago_code = ? WHERE idsupplier = ? AND (mago_code IS NULL OR mago_code = '')"); + $done = []; + foreach ($magoNames as $code => $name) { + if (in_array($code, $known, true)) continue; + $nm = $norm($name); + foreach ($free as $i => $s) { + $ns = $norm($s['supplier_name']); + if (strlen($ns) >= 4 && strpos($nm, $ns) === 0) { + $upd->execute([$code, $s['idsupplier']]); + $done[] = $s['supplier_name'] . " = $code"; + unset($free[$i]); + break; + } + } + } + return $done; + } +} diff --git a/public/userarea/mago_sync_mix_cli.php b/public/userarea/mago_sync_mix_cli.php new file mode 100644 index 00000000..16d1c799 --- /dev/null +++ b/public/userarea/mago_sync_mix_cli.php @@ -0,0 +1,93 @@ +> /home/ubuntu/logs/mago_sync_mix.log 2>&1 + */ + +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit("Solo da terminale\n"); +} + +set_time_limit(0); +date_default_timezone_set('Europe/Rome'); + +// --- vendor e .env: come mago_sync_cli.php (userarea e fino a 3 cartelle sopra) --- +$dirs = [__DIR__, dirname(__DIR__), dirname(__DIR__, 2), dirname(__DIR__, 3)]; +foreach ($dirs as $d) { + if (file_exists($d . '/vendor/autoload.php')) { + require_once $d . '/vendor/autoload.php'; + break; + } +} +if (class_exists('Dotenv\Dotenv')) { + foreach ($dirs as $d) { + if (file_exists($d . '/.env')) { + Dotenv\Dotenv::createImmutable($d)->safeLoad(); + break; + } + } +} + +// --- DB della piattaforma --- +require_once __DIR__ . '/class/db-functions.php'; +require_once __DIR__ . '/class/MagoClient.php'; +require_once __DIR__ . '/class/MagoMixPurchases.php'; + +$opt = getopt('', ['from::', 'prices']); +$from = (isset($opt['from']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$opt['from'])) + ? $opt['from'] + : date('Y-m-d', strtotime('-' . MagoMixPurchases::SYNC_DAYS . ' days')); + +$t0 = microtime(true); +$log = function ($s) { + echo '[' . date('Y-m-d H:i:s') . "] $s\n"; +}; + +$mago = null; +$exit = 0; +try { + $pdo = DBHandlerSelect::getInstance()->getConnection(); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $mago = MagoClient::fromEnv(); + $log("Sync acquisti mescole dal $from"); + MagoMixPurchases::sync($pdo, $mago, $from, $log); + + $lines = MagoMixPurchases::lines($pdo); + $c = array_count_values(array_column($lines, 'match')); + $log(sprintf( + "Righe in archivio: %d · abbinate %d · da abbinare %d · ambigue %d · escluse (recuperi…) %d", + count($lines), + $c['ok'] ?? 0, + $c['none'] ?? 0, + $c['ambiguous'] ?? 0, + $c['excluded'] ?? 0 + )); + + if (isset($opt['prices'])) { + $prices = MagoMixPurchases::prices($pdo, MagoMixPurchases::PRICE_MONTHS, $lines); + $names = $pdo->query("SELECT id, nome FROM mescole")->fetchAll(PDO::FETCH_KEY_PAIR); + uasort($prices, fn($a, $b) => $b['kg_total'] <=> $a['kg_total']); + echo "\nPrezzo €/kg (media " . MagoMixPurchases::PRICE_MONTHS . " mesi)\n"; + foreach ($prices as $id => $p) { + printf(" %-45s %7.3f €/kg ultimo %7.3f del %s%s\n", mb_substr($names[$id] ?? "#$id", 0, 45), $p['price'], $p['last_price'], $p['last_date'], $p['old'] ? ' (vecchio)' : ''); + } + } +} catch (Throwable $e) { + $log('ERRORE: ' . $e->getMessage()); + $exit = 1; +} finally { + if ($mago) { + $ok = $mago->logoff(); + $log('Logoff Mago: ' . ($ok ? 'OK' : 'NON riuscito → ' . $mago->getLastLogoffError())); + } + $log('Durata: ' . round(microtime(true) - $t0, 1) . ' s'); +} +exit($exit); diff --git a/public/userarea/mescole.php b/public/userarea/mescole.php index 7f4cc02f..97f2c476 100644 --- a/public/userarea/mescole.php +++ b/public/userarea/mescole.php @@ -395,8 +395,106 @@ background: #fee2e2; color: #991b1b; } - - + + /* ---------- NOMI D'ACQUISTO (MAGO) ---------- */ + .nacq { + display: inline-block; + min-width: 38px; + margin-right: 6px; + padding: 1px 7px; + border-radius: 999px; + font-size: .75rem; + font-weight: 700; + text-align: center; + vertical-align: middle; + } + + .nacq-ok { + background: #e0f2fe; + color: #075985; + } + + .nacq-no { + background: #fef3c7; + color: #92400e; + } + + .nm-card { + border: 1px solid #fde68a; + background: #fffbeb; + box-shadow: none; + } + + .nm-card .card-header { + background: transparent; + border-bottom: 1px solid #fde68a; + } + + .nm-row { + display: grid; + grid-template-columns: minmax(220px, 2fr) minmax(200px, 1.4fr) minmax(170px, 1fr) minmax(150px, .9fr) auto; + gap: 8px; + align-items: center; + background: #fff; + border: 1px solid #fde68a; + border-radius: 12px; + padding: 10px 12px; + margin-bottom: 8px; + } + + @media (max-width: 1200px) { + .nm-row { + grid-template-columns: 1fr 1fr; + } + } + + .nm-desc { + font-weight: 700; + color: #1f2d3d; + line-height: 1.25; + word-break: break-word; + } + + .nm-meta { + font-size: .82rem; + color: #64748b; + } + + .nm-section { + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 14px; + padding: 14px; + margin-bottom: 18px; + } + + .nm-section h6 { + font-weight: 800; + color: #075985; + } + + .nm-price { + background: #fff; + border: 1px solid #bae6fd; + border-radius: 10px; + padding: 8px 12px; + font-size: .92rem; + } + + .nm-old { + display: inline-block; + background: #fee2e2; + color: #991b1b; + border-radius: 999px; + padding: 1px 8px; + font-size: .75rem; + font-weight: 700; + } + + #nmTable td { + text-align: left; + word-break: break-word; + } @@ -430,6 +528,22 @@ + +
+| {$badge} | -{$nomeSafe} | +{$nomiBadge}{$nomeSafe} | {$nomeUscitaSafe} @@ -567,6 +687,7 @@ |