fixed sommario and added queue mago
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Ponte ordini Mago → programmazione produzione.
|
||||
*
|
||||
* mago_prod_queue una riga per ogni riga d'ordine Mago "da produrre" (traccia tutto il percorso:
|
||||
* nuovo → importato in produzione / scartato / chiuso in Mago)
|
||||
* matrice_mago_map corrispondenze manuali "valore Matrice/Misura di Mago" → matrice del portale
|
||||
* clients.mago_code codice cliente Mago (per collegare i clienti del portale)
|
||||
*/
|
||||
final class CreateMagoProdQueue extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('mago_prod_queue', [
|
||||
'comment' => 'Righe ordine Mago da portare in programmazione produzione',
|
||||
])
|
||||
// --- riga Mago ---
|
||||
->addColumn('sale_ord_id', 'integer', ['signed' => false])
|
||||
->addColumn('sub_id', 'integer')
|
||||
->addColumn('internal_ord_no', 'string', ['limit' => 30, 'default' => ''])
|
||||
->addColumn('line_no', 'integer', ['default' => 0])
|
||||
->addColumn('order_date', 'date', ['null' => true])
|
||||
->addColumn('customer', 'string', ['limit' => 20, 'default' => ''])
|
||||
->addColumn('company_name', 'string', ['limit' => 200, 'default' => ''])
|
||||
->addColumn('item', 'string', ['limit' => 30, 'default' => ''])
|
||||
->addColumn('description', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('cod_art_cli', 'string', ['limit' => 60, 'default' => ''])
|
||||
->addColumn('uom', 'string', ['limit' => 10, 'default' => ''])
|
||||
->addColumn('qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('delivered_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('residual_qty', 'decimal', ['precision' => 15, 'scale' => 3, 'default' => 0])
|
||||
->addColumn('zibo_date', 'date', ['null' => true])
|
||||
->addColumn('client_date', 'date', ['null' => true])
|
||||
->addColumn('mago_notes', 'text', ['null' => true])
|
||||
// --- matrice ---
|
||||
->addColumn('matrice_misura', 'string', ['limit' => 30, 'default' => '', 'comment' => 'Valore originale di Mago'])
|
||||
->addColumn('mm_tipo', 'string', ['limit' => 2, 'default' => '', 'comment' => 'M matrice, D diametro, R rettangolo, T tubo, Q quadrato, S, ? altro'])
|
||||
->addColumn('mm_codice', 'string', ['limit' => 40, 'default' => '', 'comment' => 'Codice matrice estratto (es. 00786S-1)'])
|
||||
->addColumn('mm_misura', 'string', ['limit' => 40, 'default' => '', 'comment' => 'Misura (es. 3,3 / 17X12)'])
|
||||
->addColumn('idmatrice', 'integer', ['null' => true])
|
||||
->addColumn('match_status', 'string', ['limit' => 20, 'default' => '', 'comment' => 'esatta, revisione, mappa, manuale, da_creare, profilo, anomalia'])
|
||||
->addColumn('match_info', 'string', ['limit' => 255, 'default' => ''])
|
||||
// --- percorso ---
|
||||
->addColumn('queue_status', 'string', ['limit' => 20, 'default' => 'nuovo', 'comment' => 'nuovo, importato, scartato, chiuso_mago'])
|
||||
->addColumn('discard_reason', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('productiondata_id', 'integer', ['null' => true])
|
||||
->addColumn('mago_changed', 'boolean', ['default' => false, 'comment' => 'Riga cambiata in Mago dopo l\'import'])
|
||||
->addColumn('mago_changes', 'string', ['limit' => 500, 'default' => ''])
|
||||
->addColumn('first_seen_at', 'datetime', ['null' => true])
|
||||
->addColumn('updated_at', 'datetime', ['null' => true])
|
||||
->addColumn('imported_at', 'datetime', ['null' => true])
|
||||
->addColumn('imported_by', 'string', ['limit' => 100, 'default' => ''])
|
||||
->addIndex(['sale_ord_id', 'sub_id'], ['unique' => true, 'name' => 'uk_mpq_line'])
|
||||
->addIndex(['queue_status'])
|
||||
->addIndex(['productiondata_id'])
|
||||
->addIndex(['idmatrice'])
|
||||
->create();
|
||||
|
||||
$this->table('matrice_mago_map', [
|
||||
'comment' => 'Corrispondenze manuali: valore Matrice/Misura di Mago → matrice',
|
||||
])
|
||||
->addColumn('codice_mago', 'string', ['limit' => 60])
|
||||
->addColumn('idmatrice', 'integer')
|
||||
->addColumn('note', 'string', ['limit' => 255, 'default' => ''])
|
||||
->addColumn('created_by', 'string', ['limit' => 100, 'default' => ''])
|
||||
->addColumn('created_at', 'datetime', ['null' => true])
|
||||
->addIndex(['codice_mago'], ['unique' => true])
|
||||
->addForeignKey('idmatrice', 'matrice', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->create();
|
||||
|
||||
$this->table('clients')
|
||||
->addColumn('mago_code', 'string', ['limit' => 20, 'null' => true, 'after' => 'nome', 'comment' => 'Codice cliente Mago'])
|
||||
->addIndex(['mago_code'])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MagoProduction - ponte tra le righe d'ordine Mago e la programmazione produzione.
|
||||
*
|
||||
* MagoProduction::refreshQueue($pdo) allinea la coda "Nuovi da Mago" (dopo ogni sync)
|
||||
* MagoProduction::import($pdo, $id, $data) porta una riga della coda in productiondata
|
||||
*
|
||||
* Mago resta in sola lettura: qui si legge solo dalle tabelle locali mago_*.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/MagoSalesStats.php';
|
||||
|
||||
class MagoProduction
|
||||
{
|
||||
/* ===================== IMPOSTAZIONI ===================== */
|
||||
|
||||
/** Righe d'ordine considerate: solo ordini da questa data in poi (per partire con poche righe di prova). */
|
||||
const QUEUE_FROM_DATE = '2026-10-01';
|
||||
|
||||
/** Produzione solo se la quantità è maggiore di questo valore (1 o 0 = servizio, indicazione del cliente). */
|
||||
const MIN_QTY = 1;
|
||||
|
||||
/** Articoli che non si producono mai (prefissi del codice articolo Mago). */
|
||||
const EXCLUDED_ITEM_PREFIXES = ['SPES', 'SPATT'];
|
||||
|
||||
/** Import automatico in programmazione delle righe con matrice certa e linea unica (false = solo su conferma). */
|
||||
const AUTO_IMPORT = false;
|
||||
|
||||
/** Status di productiondata assegnato all'import. */
|
||||
const STATUS_DA_PROGRAMMARE = 1;
|
||||
const STATUS_PROGRAMMATO = 6;
|
||||
|
||||
/** Etichette dei tipi di Matrice/Misura. */
|
||||
const TIPI = [
|
||||
'M' => 'Matrice',
|
||||
'D' => 'Diametro',
|
||||
'R' => 'Rettangolo',
|
||||
'T' => 'Tubo',
|
||||
'Q' => 'Quadrato',
|
||||
'S' => 'Sezione',
|
||||
'?' => 'Altro',
|
||||
];
|
||||
|
||||
/* ===================== MATRICE / MISURA ===================== */
|
||||
|
||||
/**
|
||||
* Scompone il valore "Matrice/Misura" di Mago.
|
||||
* M00786S → tipo M, codice 00786S
|
||||
* M00407P-1 → tipo M, codice 00407P-1
|
||||
* Q8X8M00936P → tipo M, codice 00936P, misura 8X8 (profilo Q)
|
||||
* D 3,3 → tipo D, misura 3,3
|
||||
* ZB00193P → tipo ?, codice ZB00193P (si cerca il valore intero tra le matrici)
|
||||
*/
|
||||
public static function parse($raw)
|
||||
{
|
||||
$v = strtoupper(trim(preg_replace('/\s+/', ' ', (string)$raw)));
|
||||
$out = ['raw' => $v, 'tipo' => '', 'codice' => '', 'misura' => ''];
|
||||
if ($v === '') return $out;
|
||||
|
||||
if (preg_match('/M(\d{4,6}[A-Z]?(?:-\d+)?)/', $v, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$out['tipo'] = 'M';
|
||||
$out['codice'] = $m[1][0];
|
||||
$before = trim(substr($v, 0, $m[0][1]));
|
||||
if (preg_match('/^[DRTQS]\s*([0-9][0-9.,X ]*)$/', $before, $mm)) $out['misura'] = trim($mm[1]);
|
||||
return $out;
|
||||
}
|
||||
if (preg_match('/^([DRTQS])\s*([0-9][0-9.,X ]*)$/', $v, $m)) {
|
||||
$out['tipo'] = $m[1];
|
||||
$out['misura'] = trim($m[2]);
|
||||
return $out;
|
||||
}
|
||||
$out['tipo'] = '?';
|
||||
$out['codice'] = $v;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Indice delle matrici del portale per il collegamento (caricato una volta). */
|
||||
private static function matriceIndex(PDO $pdo)
|
||||
{
|
||||
$idx = ['exact' => [], 'base' => [], 'num' => []];
|
||||
foreach ($pdo->query("SELECT id, nome FROM matrice WHERE TRIM(nome) <> ''") as $r) {
|
||||
$tok = strtoupper(strtok(trim($r['nome']), ' '));
|
||||
if ($tok === '' || $tok === false) continue;
|
||||
$idx['exact'][$tok] = ['id' => (int)$r['id'], 'nome' => trim($r['nome'])];
|
||||
$base = preg_replace('/-\d+$/', '', $tok);
|
||||
$idx['base'][$base][] = ['id' => (int)$r['id'], 'nome' => trim($r['nome']), 'tok' => $tok];
|
||||
if (preg_match('/^(\d+)/', $tok, $n)) $idx['num'][(int)$n[1]][] = trim($r['nome']);
|
||||
}
|
||||
foreach ($idx['base'] as &$list) {
|
||||
usort($list, function ($a, $b) {
|
||||
return strnatcmp($b['tok'], $a['tok']); // revisione più alta per prima
|
||||
});
|
||||
}
|
||||
unset($list);
|
||||
$idx['map'] = [];
|
||||
try {
|
||||
foreach ($pdo->query("SELECT codice_mago, idmatrice FROM matrice_mago_map") as $r) {
|
||||
$idx['map'][strtoupper(trim($r['codice_mago']))] = (int)$r['idmatrice'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
return $idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trova la matrice del portale per un valore Matrice/Misura.
|
||||
* Ritorna [idmatrice|null, match_status, match_info, parse]
|
||||
*/
|
||||
public static function resolve(array $idx, $raw)
|
||||
{
|
||||
$p = self::parse($raw);
|
||||
if ($p['raw'] === '') return [null, 'anomalia', 'Matrice/Misura vuoto in Mago', $p];
|
||||
|
||||
// 0) corrispondenza manuale
|
||||
if (isset($idx['map'][$p['raw']])) return [$idx['map'][$p['raw']], 'mappa', 'Corrispondenza salvata', $p];
|
||||
|
||||
if ($p['codice'] !== '') {
|
||||
$c = $p['codice'];
|
||||
// 1) esatta
|
||||
if (isset($idx['exact'][$c])) return [$idx['exact'][$c]['id'], 'esatta', '', $p];
|
||||
// 2) stessa matrice, altra revisione
|
||||
$base = preg_replace('/-\d+$/', '', $c);
|
||||
if (!empty($idx['base'][$base])) {
|
||||
$best = $idx['base'][$base][0];
|
||||
return [$best['id'], 'revisione', "Revisione diversa: Mago $c, anagrafica {$best['nome']}", $p];
|
||||
}
|
||||
// 3) stesso numero, lettera diversa: solo suggerimento
|
||||
$sim = '';
|
||||
if (preg_match('/^(\d+)/', $c, $n) && !empty($idx['num'][(int)$n[1]])) {
|
||||
$sim = ' · simili: ' . implode(', ', array_slice($idx['num'][(int)$n[1]], 0, 3));
|
||||
}
|
||||
if ($p['tipo'] === 'M') return [null, 'da_creare', "Matrice $c non presente in anagrafica" . $sim, $p];
|
||||
return [null, 'anomalia', "Codice $c non riconosciuto" . $sim, $p];
|
||||
}
|
||||
|
||||
// profilo standard (D/R/T/Q/S + misura)
|
||||
$label = self::TIPI[$p['tipo']] ?? $p['tipo'];
|
||||
return [null, 'profilo', "$label {$p['misura']}: scegliere la matrice", $p];
|
||||
}
|
||||
|
||||
/* ===================== CODA "NUOVI DA MAGO" ===================== */
|
||||
|
||||
/**
|
||||
* Allinea la coda con le righe d'ordine Mago locali.
|
||||
* Ritorna un piccolo report (nuove, aggiornate, chiuse, modificate dopo l'import, importate in automatico).
|
||||
*/
|
||||
public static function refreshQueue(PDO $pdo)
|
||||
{
|
||||
$rep = ['candidati' => 0, 'nuove' => 0, 'aggiornate' => 0, 'chiuse' => 0, 'modificate' => 0, 'auto_importate' => 0];
|
||||
$idx = self::matriceIndex($pdo);
|
||||
|
||||
$excl = '';
|
||||
foreach (self::EXCLUDED_ITEM_PREFIXES as $i => $pfx) {
|
||||
$excl .= " AND d.item NOT LIKE " . $pdo->quote($pfx . '%');
|
||||
}
|
||||
|
||||
$rows = MagoSalesStats::fetchAllQ($pdo, "
|
||||
SELECT d.sale_ord_id, d.sub_id, d.line, d.item, d.description, d.uom, d.qty, d.delivered_qty,
|
||||
d.notes, d.matrice_misura, d.cod_art_cli,
|
||||
" . MagoSalesStats::ziboDate('d') . " AS zibo_date,
|
||||
" . MagoSalesStats::clientDate('d') . " AS client_date,
|
||||
x.internal_ord_no, x.order_date, x.customer, x.company_name
|
||||
FROM mago_sale_ord_detail d
|
||||
JOIN " . MagoSalesStats::baseFrom() . " ON x.sale_ord_id = d.sale_ord_id
|
||||
WHERE x.order_date >= :from
|
||||
AND x.status IN ('aperto', 'bloccato')
|
||||
AND d.delivered = 0 AND d.cancelled = 0
|
||||
AND d.item <> '' AND d.qty > :minq
|
||||
$excl
|
||||
", ['from' => self::QUEUE_FROM_DATE, 'minq' => self::MIN_QTY]);
|
||||
$rep['candidati'] = count($rows);
|
||||
|
||||
$existing = [];
|
||||
foreach ($pdo->query("SELECT * FROM mago_prod_queue") as $q) {
|
||||
$existing[$q['sale_ord_id'] . '-' . $q['sub_id']] = $q;
|
||||
}
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO mago_prod_queue (
|
||||
sale_ord_id, sub_id, internal_ord_no, line_no, order_date, customer, company_name, item, description,
|
||||
cod_art_cli, uom, qty, delivered_qty, residual_qty, zibo_date, client_date, mago_notes,
|
||||
matrice_misura, mm_tipo, mm_codice, mm_misura, idmatrice, match_status, match_info,
|
||||
queue_status, first_seen_at, updated_at
|
||||
) VALUES (
|
||||
:sale_ord_id, :sub_id, :internal_ord_no, :line_no, :order_date, :customer, :company_name, :item, :description,
|
||||
:cod_art_cli, :uom, :qty, :delivered_qty, :residual_qty, :zibo_date, :client_date, :mago_notes,
|
||||
:matrice_misura, :mm_tipo, :mm_codice, :mm_misura, :idmatrice, :match_status, :match_info,
|
||||
'nuovo', NOW(), NOW()
|
||||
)");
|
||||
$upd = $pdo->prepare("
|
||||
UPDATE mago_prod_queue SET
|
||||
internal_ord_no = :internal_ord_no, line_no = :line_no, order_date = :order_date, customer = :customer,
|
||||
company_name = :company_name, item = :item, description = :description, cod_art_cli = :cod_art_cli,
|
||||
uom = :uom, qty = :qty, delivered_qty = :delivered_qty, residual_qty = :residual_qty,
|
||||
zibo_date = :zibo_date, client_date = :client_date, mago_notes = :mago_notes,
|
||||
matrice_misura = :matrice_misura, mm_tipo = :mm_tipo, mm_codice = :mm_codice, mm_misura = :mm_misura,
|
||||
idmatrice = :idmatrice, match_status = :match_status, match_info = :match_info,
|
||||
queue_status = :queue_status, mago_changed = :mago_changed, mago_changes = :mago_changes,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id");
|
||||
|
||||
$seen = [];
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
foreach ($rows as $r) {
|
||||
$key = $r['sale_ord_id'] . '-' . $r['sub_id'];
|
||||
$seen[$key] = true;
|
||||
[$idm, $ms, $mi, $p] = self::resolve($idx, $r['matrice_misura']);
|
||||
$residual = max(0, (float)$r['qty'] - (float)$r['delivered_qty']);
|
||||
$data = [
|
||||
'internal_ord_no' => $r['internal_ord_no'],
|
||||
'line_no' => (int)$r['line'],
|
||||
'order_date' => $r['order_date'],
|
||||
'customer' => $r['customer'],
|
||||
'company_name' => mb_substr((string)$r['company_name'], 0, 200),
|
||||
'item' => $r['item'],
|
||||
'description' => mb_substr((string)$r['description'], 0, 255),
|
||||
'cod_art_cli' => (string)$r['cod_art_cli'],
|
||||
'uom' => $r['uom'],
|
||||
'qty' => $r['qty'],
|
||||
'delivered_qty' => $r['delivered_qty'],
|
||||
'residual_qty' => $residual,
|
||||
'zibo_date' => $r['zibo_date'],
|
||||
'client_date' => $r['client_date'],
|
||||
'mago_notes' => $r['notes'],
|
||||
'matrice_misura' => (string)$r['matrice_misura'],
|
||||
'mm_tipo' => $p['tipo'],
|
||||
'mm_codice' => mb_substr($p['codice'], 0, 40),
|
||||
'mm_misura' => mb_substr($p['misura'], 0, 40),
|
||||
'idmatrice' => $idm,
|
||||
'match_status' => $ms,
|
||||
'match_info' => mb_substr($mi, 0, 255),
|
||||
];
|
||||
|
||||
if (!isset($existing[$key])) {
|
||||
$ins->execute($data + ['sale_ord_id' => $r['sale_ord_id'], 'sub_id' => $r['sub_id']]);
|
||||
$rep['nuove']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$q = $existing[$key];
|
||||
$status = $q['queue_status'];
|
||||
$changed = (int)$q['mago_changed'];
|
||||
$changes = $q['mago_changes'];
|
||||
|
||||
// scelta manuale della matrice: non la tocco
|
||||
if ($q['match_status'] === 'manuale' && $q['idmatrice']) {
|
||||
$data['idmatrice'] = $q['idmatrice'];
|
||||
$data['match_status'] = 'manuale';
|
||||
$data['match_info'] = $q['match_info'];
|
||||
}
|
||||
|
||||
if ($status === 'importato') {
|
||||
$diff = self::diff($q, $data);
|
||||
if ($diff) {
|
||||
$changed = 1;
|
||||
$changes = mb_substr(trim(($changes ? $changes . ' | ' : '') . date('d/m') . ': ' . implode(', ', $diff)), -500);
|
||||
$rep['modificate']++;
|
||||
}
|
||||
// dopo l'import la matrice resta quella scelta in produzione
|
||||
$data['idmatrice'] = $q['idmatrice'];
|
||||
$data['match_status'] = $q['match_status'];
|
||||
$data['match_info'] = $q['match_info'];
|
||||
} elseif ($status === 'chiuso_mago') {
|
||||
$status = 'nuovo'; // riaperta in Mago
|
||||
}
|
||||
|
||||
$upd->execute($data + ['queue_status' => $status, 'mago_changed' => $changed, 'mago_changes' => $changes, 'id' => $q['id']]);
|
||||
$rep['aggiornate']++;
|
||||
}
|
||||
|
||||
// righe non più "da produrre" in Mago (consegnate, annullate, cancellate, quantità ≤ 1...)
|
||||
$why = $pdo->prepare("SELECT delivered, cancelled FROM mago_sale_ord_detail WHERE sale_ord_id = ? AND sub_id = ?");
|
||||
$close = $pdo->prepare("UPDATE mago_prod_queue SET queue_status = :s, mago_changed = :c, mago_changes = :t, updated_at = NOW() WHERE id = :id");
|
||||
foreach ($existing as $key => $q) {
|
||||
if (isset($seen[$key]) || in_array($q['queue_status'], ['chiuso_mago'], true)) continue;
|
||||
$why->execute([$q['sale_ord_id'], $q['sub_id']]);
|
||||
$d = $why->fetch(PDO::FETCH_ASSOC);
|
||||
$reason = !$d ? 'riga cancellata in Mago' : ((int)$d['cancelled'] ? 'riga annullata in Mago' : ((int)$d['delivered'] ? 'riga consegnata in Mago' : 'riga non più da produrre in Mago'));
|
||||
|
||||
if ($q['queue_status'] === 'importato') {
|
||||
// già in produzione: segnalo solo se è un annullamento/cancellazione (la consegna è normale)
|
||||
if (strpos($reason, 'consegnata') === false && strpos((string)$q['mago_changes'], $reason) === false) {
|
||||
$close->execute(['s' => 'importato', 'c' => 1, 't' => mb_substr(trim(($q['mago_changes'] ? $q['mago_changes'] . ' | ' : '') . date('d/m') . ': ' . $reason), -500), 'id' => $q['id']]);
|
||||
$rep['modificate']++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($q['queue_status'] === 'nuovo' || $q['queue_status'] === 'scartato') {
|
||||
$close->execute(['s' => 'chiuso_mago', 'c' => 0, 't' => $reason, 'id' => $q['id']]);
|
||||
$rep['chiuse']++;
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// import automatico (spento finché AUTO_IMPORT = false)
|
||||
if (self::AUTO_IMPORT) {
|
||||
$ids = $pdo->query("SELECT id FROM mago_prod_queue WHERE queue_status = 'nuovo' AND match_status IN ('esatta','mappa') AND idmatrice IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($ids as $id) {
|
||||
$pre = self::prefill($pdo, (int)$id);
|
||||
if (!$pre || !$pre['id_linea']) continue; // linea non univoca: resta da completare a mano
|
||||
try {
|
||||
self::import($pdo, (int)$id, $pre, 'automatico');
|
||||
$rep['auto_importate']++;
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return $rep;
|
||||
}
|
||||
|
||||
/** Differenze rilevanti tra la riga in coda e la riga Mago aggiornata. */
|
||||
private static function diff(array $q, array $d)
|
||||
{
|
||||
$out = [];
|
||||
if (abs((float)$q['residual_qty'] - (float)$d['residual_qty']) > 0.0005) {
|
||||
$out[] = 'quantità ' . self::num($q['residual_qty']) . ' → ' . self::num($d['residual_qty']) . ' ' . $d['uom'];
|
||||
}
|
||||
if ((string)$q['zibo_date'] !== (string)$d['zibo_date']) $out[] = 'data ZIBO ' . self::dt($q['zibo_date']) . ' → ' . self::dt($d['zibo_date']);
|
||||
if ((string)$q['client_date'] !== (string)$d['client_date']) $out[] = 'data cliente ' . self::dt($q['client_date']) . ' → ' . self::dt($d['client_date']);
|
||||
if (strtoupper(trim($q['matrice_misura'])) !== strtoupper(trim($d['matrice_misura']))) $out[] = 'matrice ' . $q['matrice_misura'] . ' → ' . $d['matrice_misura'];
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function num($v)
|
||||
{
|
||||
return rtrim(rtrim(number_format((float)$v, 3, ',', '.'), '0'), ',');
|
||||
}
|
||||
|
||||
private static function dt($v)
|
||||
{
|
||||
return $v ? date('d/m/Y', strtotime($v)) : '—';
|
||||
}
|
||||
|
||||
/* ===================== PRECOMPILAZIONE ===================== */
|
||||
|
||||
/** Dati tecnici di una matrice: linee, mescole e velocità dalla scheda di lavoro. */
|
||||
public static function matriceInfo(PDO $pdo, $idmatrice)
|
||||
{
|
||||
$idmatrice = (int)$idmatrice;
|
||||
$info = ['lines' => [], 'mescole' => [], 'speed_m_h' => null, 'speed_kg_h' => null, 'g_m' => null, 'worksheet' => null];
|
||||
if (!$idmatrice) return $info;
|
||||
|
||||
$st = $pdo->prepare("SELECT idlinea FROM matrici_lines WHERE idmatrice = ? ORDER BY idlinea");
|
||||
$st->execute([$idmatrice]);
|
||||
$info['lines'] = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$st = $pdo->prepare("SELECT id, worksheet_number, speed_expected_m_h, speed_expected_kg_h
|
||||
FROM work_sheets WHERE idmatrice = ? AND worksheet_status = 'active'
|
||||
ORDER BY worksheet_date DESC, id DESC LIMIT 1");
|
||||
$st->execute([$idmatrice]);
|
||||
if ($ws = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$info['worksheet'] = (int)$ws['worksheet_number'];
|
||||
$info['speed_m_h'] = $ws['speed_expected_m_h'] !== null ? (float)$ws['speed_expected_m_h'] : null;
|
||||
$info['speed_kg_h'] = $ws['speed_expected_kg_h'] !== null ? (float)$ws['speed_expected_kg_h'] : null;
|
||||
$st = $pdo->prepare("SELECT idmescola, mix_weight_g_m FROM work_sheet_mescole WHERE worksheet_id = ? ORDER BY mix_position");
|
||||
$st->execute([$ws['id']]);
|
||||
$g = 0;
|
||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $m) {
|
||||
$info['mescole'][] = (int)$m['idmescola'];
|
||||
$g += (float)$m['mix_weight_g_m'];
|
||||
}
|
||||
$info['g_m'] = $g > 0 ? $g : null;
|
||||
}
|
||||
if (!$info['mescole']) {
|
||||
$st = $pdo->prepare("SELECT idmescola FROM matrici_mescole WHERE idmatrice = ?");
|
||||
$st->execute([$idmatrice]);
|
||||
$info['mescole'] = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/** Proposta dei campi di productiondata per una riga della coda. */
|
||||
public static function prefill(PDO $pdo, $queueId, $idmatrice = null)
|
||||
{
|
||||
$st = $pdo->prepare("SELECT * FROM mago_prod_queue WHERE id = ?");
|
||||
$st->execute([(int)$queueId]);
|
||||
$q = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$q) return null;
|
||||
|
||||
$idm = $idmatrice !== null ? (int)$idmatrice : (int)$q['idmatrice'];
|
||||
$info = self::matriceInfo($pdo, $idm);
|
||||
$uom = strtoupper(trim($q['uom']));
|
||||
$qty = (float)$q['residual_qty'];
|
||||
$metri = in_array($uom, ['MT', 'M', 'ML'], true) ? $qty : 0;
|
||||
|
||||
$ore = 0;
|
||||
if ($metri > 0 && $info['speed_m_h'] > 0) $ore = $metri / $info['speed_m_h'];
|
||||
elseif ($uom === 'KG' && $info['speed_kg_h'] > 0) $ore = $qty / $info['speed_kg_h'];
|
||||
|
||||
$note = [];
|
||||
$note[] = "Da Mago: ordine {$q['internal_ord_no']} riga {$q['line_no']} · {$q['item']}" . ($q['cod_art_cli'] ? " · cod. cliente {$q['cod_art_cli']}" : '');
|
||||
$note[] = 'Matrice/Misura Mago: ' . $q['matrice_misura'] . ($q['mm_misura'] !== '' ? ' (' . (self::TIPI[$q['mm_tipo']] ?? '') . ' ' . $q['mm_misura'] . ')' : '');
|
||||
if ($metri <= 0) $note[] = 'Quantità ordinata: ' . self::num($qty) . ' ' . $q['uom'];
|
||||
if ($metri > 0 && $info['g_m']) $note[] = 'Kg stimati: ' . self::num(round($metri * $info['g_m'] / 1000, 1)) . ' (scheda di lavoro ' . $info['worksheet'] . ')';
|
||||
if (trim((string)$q['mago_notes']) !== '') $note[] = trim($q['mago_notes']);
|
||||
|
||||
return [
|
||||
'queue' => $q,
|
||||
'idmatrice' => $idm ?: null,
|
||||
'id_linea' => count($info['lines']) === 1 ? $info['lines'][0] : null,
|
||||
'lines' => $info['lines'],
|
||||
'mescole' => $info['mescole'],
|
||||
'metri' => round($metri, 2),
|
||||
'kg_sp' => 0,
|
||||
'kg_p' => 0,
|
||||
'ore_previste' => round($ore, 2),
|
||||
'data_zibo' => $q['zibo_date'],
|
||||
'data_cliente' => $q['client_date'],
|
||||
'conferma_ordine' => $q['internal_ord_no'],
|
||||
'note_extra' => implode("\n", $note),
|
||||
'id_status' => self::STATUS_DA_PROGRAMMARE,
|
||||
'worksheet' => $info['worksheet'],
|
||||
];
|
||||
}
|
||||
|
||||
/* ===================== IMPORT IN PRODUZIONE ===================== */
|
||||
|
||||
/** Cliente del portale per un codice cliente Mago (lo crea se non esiste). */
|
||||
public static function clientId(PDO $pdo, $code, $name)
|
||||
{
|
||||
$code = trim((string)$code);
|
||||
$name = trim((string)$name) ?: $code;
|
||||
if ($code === '') return null;
|
||||
|
||||
$st = $pdo->prepare("SELECT id FROM clients WHERE mago_code = ? LIMIT 1");
|
||||
$st->execute([$code]);
|
||||
if ($id = $st->fetchColumn()) return (int)$id;
|
||||
|
||||
$st = $pdo->prepare("SELECT id FROM clients WHERE (mago_code IS NULL OR mago_code = '') AND UPPER(TRIM(nome)) = UPPER(TRIM(?)) LIMIT 1");
|
||||
$st->execute([$name]);
|
||||
if ($id = $st->fetchColumn()) {
|
||||
$pdo->prepare("UPDATE clients SET mago_code = ? WHERE id = ?")->execute([$code, $id]);
|
||||
return (int)$id;
|
||||
}
|
||||
|
||||
// nuovo cliente (il nome è unico: se esiste già con un altro codice aggiungo il codice)
|
||||
$st = $pdo->prepare("SELECT COUNT(*) FROM clients WHERE nome = ?");
|
||||
$st->execute([$name]);
|
||||
$nome = $st->fetchColumn() ? "$name ($code)" : $name;
|
||||
$pdo->prepare("INSERT INTO clients (nome, mago_code) VALUES (?, ?)")->execute([mb_substr($nome, 0, 255), $code]);
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea il lavoro in productiondata e collega la riga della coda.
|
||||
* $d: idmatrice, id_linea, mescole[], metri, kg_sp, kg_p, ore_previste, data_zibo, data_cliente,
|
||||
* note_extra, id_status, remember (salva la corrispondenza codice Mago → matrice)
|
||||
*/
|
||||
public static function import(PDO $pdo, $queueId, array $d, $user = '')
|
||||
{
|
||||
$st = $pdo->prepare("SELECT * FROM mago_prod_queue WHERE id = ? FOR UPDATE");
|
||||
$own = !$pdo->inTransaction();
|
||||
if ($own) $pdo->beginTransaction();
|
||||
try {
|
||||
$st->execute([(int)$queueId]);
|
||||
$q = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$q) throw new RuntimeException('Riga non trovata');
|
||||
if ($q['queue_status'] === 'importato') throw new RuntimeException("Ordine {$q['internal_ord_no']} riga {$q['line_no']}: già in programmazione");
|
||||
|
||||
$idm = (int)($d['idmatrice'] ?? 0);
|
||||
$lin = (int)($d['id_linea'] ?? 0);
|
||||
if (!$idm) throw new RuntimeException("Ordine {$q['internal_ord_no']} riga {$q['line_no']}: manca la matrice");
|
||||
if (!$lin) throw new RuntimeException("Ordine {$q['internal_ord_no']} riga {$q['line_no']}: manca la linea");
|
||||
$status = (int)($d['id_status'] ?? self::STATUS_DA_PROGRAMMARE);
|
||||
if (!in_array($status, [self::STATUS_DA_PROGRAMMARE, self::STATUS_PROGRAMMATO], true)) $status = self::STATUS_DA_PROGRAMMARE;
|
||||
|
||||
$idCliente = self::clientId($pdo, $q['customer'], $q['company_name']);
|
||||
$date = function ($v) {
|
||||
return MagoSalesStats::isValidDate((string)$v) ? $v : null;
|
||||
};
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO productiondata
|
||||
(Data, data_produzione, conferma_ordine, idmatrice, id_linea, id_cliente, data_zibo, data_cliente,
|
||||
metri, kg_sp, kg_p, ore_previste, note_extra, id_status,
|
||||
fermomacchina, `hour`, hourprod, kgprod, mtprod, scarto)
|
||||
VALUES
|
||||
(CURDATE(), NULL, :conferma, :idmatrice, :id_linea, :id_cliente, :data_zibo, :data_cliente,
|
||||
:metri, :kg_sp, :kg_p, :ore, :note, :status,
|
||||
0, '00:00:00', '00:00:00', 0, 0, 0)
|
||||
")->execute([
|
||||
'conferma' => mb_substr($q['internal_ord_no'], 0, 50),
|
||||
'idmatrice' => $idm,
|
||||
'id_linea' => $lin,
|
||||
'id_cliente' => $idCliente,
|
||||
'data_zibo' => $date($d['data_zibo'] ?? $q['zibo_date']),
|
||||
'data_cliente' => $date($d['data_cliente'] ?? $q['client_date']),
|
||||
'metri' => round((float)($d['metri'] ?? 0), 2),
|
||||
'kg_sp' => round((float)($d['kg_sp'] ?? 0), 2),
|
||||
'kg_p' => round((float)($d['kg_p'] ?? 0), 2),
|
||||
'ore' => min(999.99, round((float)($d['ore_previste'] ?? 0), 2)),
|
||||
'note' => (string)($d['note_extra'] ?? '') ?: null,
|
||||
'status' => $status,
|
||||
]);
|
||||
$pid = (int)$pdo->lastInsertId();
|
||||
|
||||
$mescole = array_values(array_unique(array_filter(array_map('intval', (array)($d['mescole'] ?? [])))));
|
||||
if ($mescole) {
|
||||
$insM = $pdo->prepare("INSERT INTO productiondata_mescole (id_productiondata, id_mescola) VALUES (?, ?)");
|
||||
foreach ($mescole as $m) $insM->execute([$pid, $m]);
|
||||
}
|
||||
|
||||
// la scelta della matrice fatta a mano diventa la regola per le prossime righe con lo stesso codice
|
||||
$matchStatus = $q['match_status'];
|
||||
if ((int)$q['idmatrice'] !== $idm) $matchStatus = 'manuale';
|
||||
if (!empty($d['remember']) && trim($q['matrice_misura']) !== '') {
|
||||
$pdo->prepare("INSERT INTO matrice_mago_map (codice_mago, idmatrice, note, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE idmatrice = VALUES(idmatrice), created_by = VALUES(created_by), created_at = NOW()")
|
||||
->execute([strtoupper(trim($q['matrice_misura'])), $idm, 'da import ordine ' . $q['internal_ord_no'], mb_substr((string)$user, 0, 100)]);
|
||||
$matchStatus = 'mappa';
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE mago_prod_queue SET queue_status = 'importato', productiondata_id = ?, idmatrice = ?,
|
||||
match_status = ?, imported_at = NOW(), imported_by = ?, mago_changed = 0, mago_changes = '', updated_at = NOW()
|
||||
WHERE id = ?")
|
||||
->execute([$pid, $idm, $matchStatus, mb_substr((string)$user, 0, 100), $q['id']]);
|
||||
|
||||
if ($own) $pdo->commit();
|
||||
return $pid;
|
||||
} catch (Throwable $e) {
|
||||
if ($own && $pdo->inTransaction()) $pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ class MagoSalesStats
|
||||
$parts = [];
|
||||
foreach ($words as $w) {
|
||||
$like = "'%" . str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $w) . "%'";
|
||||
foreach (['external_ord_no', 'our_reference', 'your_reference', 'notes'] as $col) {
|
||||
foreach (['internal_ord_no', 'external_ord_no', 'our_reference', 'your_reference', 'notes'] as $col) {
|
||||
$parts[] = "COALESCE($a.$col, '') LIKE $like"; // COALESCE: un campo NULL non deve escludere l'ordine
|
||||
}
|
||||
$parts[] = "EXISTS (SELECT 1 FROM mago_sale_ord_detail ex
|
||||
|
||||
@@ -76,6 +76,14 @@ class MagoSync
|
||||
$report['documenti'] = 'tabella mago_sale_doc mancante: eseguire la migrazione Phinx';
|
||||
$this->log('Documenti: tabella mago_sale_doc mancante, salto (eseguire la migrazione)');
|
||||
}
|
||||
|
||||
// Coda "Nuovi da Mago" per la programmazione produzione (solo dati locali, nessuna chiamata a Mago)
|
||||
if ($this->tableExists('mago_prod_queue')) {
|
||||
require_once __DIR__ . '/MagoProduction.php';
|
||||
$q = MagoProduction::refreshQueue($this->pdo);
|
||||
$report['coda_produzione'] = $q;
|
||||
$this->log("Coda produzione: {$q['nuove']} nuove, {$q['aggiornate']} aggiornate, {$q['chiuse']} chiuse, {$q['modificate']} modificate dopo l'import");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$report['ok'] = false;
|
||||
$report['message'] = $e->getMessage();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -250,6 +250,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$nuoviMago = 0;
|
||||
try {
|
||||
$nuoviMago = (int)$pdo->query("SELECT COUNT(*) FROM mago_prod_queue WHERE queue_status = 'nuovo' OR (queue_status = 'importato' AND mago_changed = 1)")->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
?>
|
||||
<a href="production_mago_queue.php" class="btn btn-warning fw-bold">
|
||||
🆕 Nuovi da Mago <?php if ($nuoviMago): ?><span class="badge bg-dark ms-1"><?= $nuoviMago ?></span><?php endif; ?>
|
||||
</a>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNuovo">Aggiungi</button>
|
||||
<button class="btn btn-secondary" onclick="location.href='production_dashboard.php'">Torna</button>
|
||||
</div>
|
||||
|
||||
@@ -384,6 +384,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$nuoviMago = 0;
|
||||
try {
|
||||
$nuoviMago = (int)$pdo->query("SELECT COUNT(*) FROM mago_prod_queue WHERE queue_status = 'nuovo' OR (queue_status = 'importato' AND mago_changed = 1)")->fetchColumn();
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
?>
|
||||
<a href="production_mago_queue.php" class="btn btn-warning fw-bold">
|
||||
🆕 Nuovi da Mago <?php if ($nuoviMago): ?><span class="badge bg-dark ms-1"><?= $nuoviMago ?></span><?php endif; ?>
|
||||
</a>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNuovo">Aggiungi</button>
|
||||
<button class="btn btn-secondary" onclick="location.href='production_dashboard.php'">Torna</button>
|
||||
</div>
|
||||
|
||||
@@ -1926,7 +1926,7 @@ if ($action === 'data') {
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="sales_help.js?v=2"></script>
|
||||
<script src="sales_help.js?v=3"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -70,6 +70,17 @@
|
||||
},
|
||||
|
||||
/* ===================== SOMMARIO ===================== */
|
||||
"sm.flow": {
|
||||
t: "Nuovi ordini",
|
||||
b: `<p>Quanti ordini sono entrati (per data ordine) e quanto valgono, annullati esclusi:</p>
|
||||
<ul>
|
||||
<li><b>Oggi</b>, confrontato con lo stesso giorno della settimana scorsa;</li>
|
||||
<li><b>Ultimi 7 giorni</b>, confrontati con i 7 giorni prima;</li>
|
||||
<li><b>Ultimi 30 giorni</b>, confrontati con i 30 giorni prima.</li>
|
||||
</ul>
|
||||
<p>▲ verde = più della volta prima · ▼ rosso = meno.</p>
|
||||
<p class="sh-note">"Oggi" va letto a fine giornata: al mattino è normale che sia basso.</p>`,
|
||||
},
|
||||
"sm.invYtd": {
|
||||
t: "Fatturato dell'anno",
|
||||
b: `<p>Fatturato da gennaio a oggi rispetto allo <b>stesso periodo dell'anno scorso</b>.</p>
|
||||
|
||||
@@ -1300,7 +1300,7 @@ if ($action === 'data') {
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="sales_help.js?v=2"></script>
|
||||
<script src="sales_help.js?v=3"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2354,7 +2354,7 @@ if ($action === 'order') {
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="sales_help.js?v=2"></script>
|
||||
<script src="sales_help.js?v=3"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -389,6 +389,56 @@ if ($action === 'data') {
|
||||
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
|
||||
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'
|
||||
],
|
||||
[
|
||||
'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'
|
||||
],
|
||||
[
|
||||
'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']
|
||||
],
|
||||
];
|
||||
|
||||
// ---------------- GIUDIZIO COMPLESSIVO ----------------
|
||||
$score = ['green' => 0, 'yellow' => 0, 'red' => 0, 'grey' => 0];
|
||||
foreach ($T as $t) $score[$t['light']]++;
|
||||
@@ -402,6 +452,7 @@ if ($action === 'data') {
|
||||
'year' => $curY,
|
||||
'inv_ok' => $invOk,
|
||||
'tiles' => $T,
|
||||
'flow' => $flow,
|
||||
'score' => $score,
|
||||
'points' => $points === null ? null : round($points),
|
||||
'overall' => $overall,
|
||||
@@ -1290,6 +1341,99 @@ if ($action === 'data') {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.flow-head {
|
||||
font-weight: 900;
|
||||
font-size: 1.05rem;
|
||||
color: var(--c-ink);
|
||||
margin: 4px 0 10px;
|
||||
}
|
||||
|
||||
.flow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.fl-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: linear-gradient(135deg, #eff6ff, #fff);
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, .05);
|
||||
transition: transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
|
||||
.fl-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 26px rgba(15, 23, 42, .1);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fl-t {
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.fl-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.fl-n {
|
||||
font-size: 2.1rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: var(--c-ink);
|
||||
}
|
||||
|
||||
.fl-v {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
color: var(--c-ink);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.fl-l {
|
||||
font-size: .8rem;
|
||||
color: var(--c-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.fl-d {
|
||||
font-weight: 800;
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fl-d.up {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.fl-d.down {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.fl-d.flat {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.fl-vs {
|
||||
font-size: .76rem;
|
||||
color: var(--c-muted);
|
||||
margin-top: 8px;
|
||||
border-top: 1px dashed #dbeafe;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.goodbad {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -1688,6 +1832,10 @@ if ($action === 'data') {
|
||||
<div class="ring" id="heroRing" style="--p:0;"><span id="heroPoints">—<small>PUNTEGGIO</small></span></div>
|
||||
</div>
|
||||
|
||||
<!-- ORDINI IN ARRIVO -->
|
||||
<div class="flow-head"><span data-help="sm.flow"><i class="fa-solid fa-inbox me-2"></i>Nuovi ordini</span></div>
|
||||
<div class="flow-grid mb-3" id="flow"></div>
|
||||
|
||||
<div class="goodbad mb-2">
|
||||
<div class="gb-card good">
|
||||
<h6><i class="fa-solid fa-circle-check me-1"></i> Cosa va bene</h6>
|
||||
@@ -1720,7 +1868,7 @@ if ($action === 'data') {
|
||||
<div class="legend-strip">
|
||||
<span><span class="dot green"></span> Bene</span>
|
||||
<span><span class="dot yellow"></span> Così così: da tenere d'occhio</span>
|
||||
<span><span class="dot red"></span> Da Migliorare: serve un intervento</span>
|
||||
<span><span class="dot red"></span> Male: serve un intervento</span>
|
||||
<span><span class="dot grey"></span> Dato non disponibile</span>
|
||||
<span class="ms-auto">Clicca una tessera per i dettagli · ⓘ per la spiegazione</span>
|
||||
</div>
|
||||
@@ -1754,7 +1902,7 @@ if ($action === 'data') {
|
||||
const LIGHT_LABEL = {
|
||||
green: 'Bene',
|
||||
yellow: 'Così così',
|
||||
red: 'Da Migliorare',
|
||||
red: 'Male',
|
||||
grey: 'n.d.'
|
||||
};
|
||||
const LIGHT_ICON = {
|
||||
@@ -1816,6 +1964,7 @@ if ($action === 'data') {
|
||||
}
|
||||
$('#content').show();
|
||||
renderHero(d);
|
||||
renderFlow(d);
|
||||
renderGoodBad(d);
|
||||
renderTiles(d);
|
||||
renderLists(d);
|
||||
@@ -1859,6 +2008,39 @@ if ($action === 'data') {
|
||||
$('#heroPoints').html(`${d.points === null ? '—' : d.points}<small>SU 100</small>`);
|
||||
}
|
||||
|
||||
/* ---------------- NUOVI ORDINI ---------------- */
|
||||
function arrow(cur, prev) {
|
||||
if (!prev && !cur) return '<span class="fl-d flat">= come prima</span>';
|
||||
if (!prev) return '<span class="fl-d up">▲ nuovo</span>';
|
||||
const p = (cur - prev) / Math.abs(prev) * 100;
|
||||
const r = Math.round(p);
|
||||
if (Math.abs(r) < 1) return '<span class="fl-d flat">= stabile</span>';
|
||||
return `<span class="fl-d ${r > 0 ? 'up' : 'down'}">${r > 0 ? '▲' : '▼'} ${Math.abs(r)}%</span>`;
|
||||
}
|
||||
|
||||
function renderFlow(d) {
|
||||
const icons = {
|
||||
today: 'fa-sun',
|
||||
week: 'fa-calendar-week',
|
||||
month: 'fa-calendar'
|
||||
};
|
||||
$('#flow').html((d.flow || []).map(f => `
|
||||
<a class="fl-card" href="sales_orders_stats.php" title="Apri Andamento ordini">
|
||||
<div class="fl-top"><span class="fl-t"><i class="fa-solid ${icons[f.key]} me-1"></i>${esc(f.title)}</span></div>
|
||||
<div class="fl-row">
|
||||
<div>
|
||||
<div class="fl-n">${f.n.toLocaleString('it-IT')}</div>
|
||||
<div class="fl-l">ordini ${arrow(f.n, f.np)}</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="fl-v">${fmtEurShort(f.v)}</div>
|
||||
<div class="fl-l">valore ${arrow(f.v, f.vp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fl-vs">vs ${esc(f.vs)}: ${f.np.toLocaleString('it-IT')} ordini · ${fmtEurShort(f.vp)}${f.customers !== undefined ? ` · ${f.customers} clienti nel mese` : ''}</div>
|
||||
</a>`).join(''));
|
||||
}
|
||||
|
||||
function renderGoodBad(d) {
|
||||
const ok = d.tiles.filter(t => t.light === 'green' && t.verdict);
|
||||
const ko = d.tiles.filter(t => (t.light === 'red' || t.light === 'yellow') && t.verdict)
|
||||
@@ -2019,7 +2201,7 @@ if ($action === 'data') {
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="sales_help.js?v=2"></script>
|
||||
<script src="sales_help.js?v=3"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user