added matrici to orders

This commit is contained in:
2026-10-22 14:12:42 +02:00
parent 570094ab21
commit c14cc4a354
2 changed files with 458 additions and 32 deletions
+15
View File
@@ -332,6 +332,21 @@ class MagoProduction
return rtrim(rtrim(number_format((float)$v, 3, ',', '.'), '0'), ','); return rtrim(rtrim(number_format((float)$v, 3, ',', '.'), '0'), ',');
} }
/** Riconosce la matrice di un valore Matrice/Misura (indice caricato una volta per richiesta). [idmatrice|null, match_status, match_info] */
public static function lookup(PDO $pdo, $raw)
{
static $idx = null;
if ($idx === null) {
try {
$idx = self::matriceIndex($pdo);
} catch (Throwable $e) {
$idx = ['exact' => [], 'base' => [], 'num' => [], 'map' => []];
}
}
[$id, $ms, $mi] = self::resolve($idx, $raw);
return [$id, $ms, $mi];
}
private static function dt($v) private static function dt($v)
{ {
return $v ? date('d/m/Y', strtotime($v)) : '—'; return $v ? date('d/m/Y', strtotime($v)) : '—';
+443 -32
View File
@@ -226,19 +226,98 @@ if ($action === 'order') {
$raw = json_decode((string)$o['raw_json'], true) ?: []; $raw = json_decode((string)$o['raw_json'], true) ?: [];
unset($o['raw_json']); unset($o['raw_json']);
$stL = $pdo->prepare(" // righe + (se presenti) matrice, coda "Nuovi da Mago" e lavoro di produzione collegato
SELECT d.line, d.item, d.description, d.uom, d.qty, d.delivered_qty, d.invoiced_qty, d.unit_value, $has = function ($t) use ($pdo) {
GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty, $q = $pdo->prepare("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?");
d.taxable_amount, d.delivered, d.invoiced, d.cancelled, d.notes, $q->execute([$t]);
" . MagoSalesStats::ziboDate('d') . " AS zibo_date, return (bool)$q->fetchColumn();
" . MagoSalesStats::clientDate('d') . " AS client_date };
FROM mago_sale_ord_detail d $colQ = $pdo->prepare("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mago_sale_ord_detail' AND COLUMN_NAME = 'matrice_misura'");
WHERE d.sale_ord_id = ? $colQ->execute();
ORDER BY d.position, d.line, d.sub_id $hasMM = (bool)$colQ->fetchColumn();
"); $hasQ = $hasMM && $has('mago_prod_queue') && $has('productiondata');
$sel = "d.sub_id, d.line, d.item, d.description, d.uom, d.qty, d.delivered_qty, d.invoiced_qty, d.unit_value,
GREATEST(d.qty - d.delivered_qty, 0) AS residual_qty,
d.taxable_amount, d.delivered, d.invoiced, d.cancelled, d.notes,
" . MagoSalesStats::ziboDate('d') . " AS zibo_date,
" . MagoSalesStats::clientDate('d') . " AS client_date";
$join = '';
if ($hasMM) $sel .= ", d.matrice_misura, d.cod_art_cli";
if ($hasQ) {
$sel .= ", q.queue_status, q.match_status, q.match_info, q.discard_reason, q.mago_changed, q.mago_changes,
q.productiondata_id, COALESCE(p.idmatrice, q.idmatrice) AS idmatrice,
m.nome AS matrice, m.photo AS matrice_photo,
p.id_status AS prod_status, s.nome AS prod_status_name, COALESCE(s.badge_color, '#6c757d') AS prod_status_color,
p.id_linea, l.name AS linea, l.color AS linea_color, p.metri AS prod_metri, p.data_produzione,
(SELECT GROUP_CONCAT(ms.nome ORDER BY ms.nome SEPARATOR ', ') FROM productiondata_mescole pm
JOIN mescole ms ON ms.id = pm.id_mescola WHERE pm.id_productiondata = p.id) AS mescole";
$join = "LEFT JOIN mago_prod_queue q ON q.sale_ord_id = d.sale_ord_id AND q.sub_id = d.sub_id
LEFT JOIN productiondata p ON p.id = q.productiondata_id
LEFT JOIN matrice m ON m.id = COALESCE(p.idmatrice, q.idmatrice)
LEFT JOIN production_status s ON s.id = p.id_status
LEFT JOIN production_lines l ON l.id = p.id_linea";
}
$stL = $pdo->prepare("SELECT $sel FROM mago_sale_ord_detail d $join WHERE d.sale_ord_id = ? ORDER BY d.position, d.line, d.sub_id");
$stL->execute([$id]); $stL->execute([$id]);
$lines = $stL->fetchAll(PDO::FETCH_ASSOC); $lines = $stL->fetchAll(PDO::FETCH_ASSOC);
// righe non (ancora) in coda: provo comunque a riconoscere la matrice dal valore Mago
if ($hasMM) {
if (!class_exists('MagoProduction') && is_file(__DIR__ . '/class/MagoProduction.php')) require_once __DIR__ . '/class/MagoProduction.php';
foreach ($lines as &$l) {
$l['mm'] = null;
if (!class_exists('MagoProduction') || trim((string)$l['matrice_misura']) === '') continue;
$l['mm'] = MagoProduction::parse($l['matrice_misura']);
$l['mm']['tipo_label'] = MagoProduction::TIPI[$l['mm']['tipo']] ?? '';
if (empty($l['idmatrice'])) {
[$idm, $ms, $mi] = MagoProduction::lookup($pdo, $l['matrice_misura']);
if ($idm) {
$mt = $pdo->prepare("SELECT nome, photo FROM matrice WHERE id = ?");
$mt->execute([$idm]);
$r = $mt->fetch(PDO::FETCH_ASSOC) ?: [];
$l['idmatrice'] = $idm;
$l['matrice'] = $r['nome'] ?? '';
$l['matrice_photo'] = $r['photo'] ?? '';
}
if (empty($l['match_status'])) {
$l['match_status'] = $ms;
$l['match_info'] = $mi;
}
}
}
unset($l);
// linee e mescole associate alla matrice in anagrafica (scheda di lavoro attiva, altrimenti matrici_mescole)
if ($has('matrici_lines') && class_exists('MagoProduction')) {
$lq = $pdo->prepare("SELECT l.name, l.color FROM matrici_lines ml JOIN production_lines l ON l.id = ml.idlinea WHERE ml.idmatrice = ? ORDER BY l.line_number, l.id");
$mq = $pdo->prepare("SELECT nome FROM mescole WHERE id = ?");
$cache = [];
foreach ($lines as &$l) {
$l['matrice_lines'] = [];
$l['matrice_mescole'] = [];
$idm = (int)($l['idmatrice'] ?? 0);
if (!$idm) continue;
if (!isset($cache[$idm])) {
$lq->execute([$idm]);
$ml = $lq->fetchAll(PDO::FETCH_ASSOC);
$mx = [];
try {
$info = MagoProduction::matriceInfo($pdo, $idm);
foreach ($info['mescole'] as $idmx) {
$mq->execute([$idmx]);
if (($n = $mq->fetchColumn()) !== false) $mx[] = $n;
}
} catch (Throwable $e) {
}
$cache[$idm] = [$ml, array_values(array_unique($mx))];
}
[$l['matrice_lines'], $l['matrice_mescole']] = $cache[$idm];
}
unset($l);
}
}
MagoSalesStats::jsonOut([ MagoSalesStats::jsonOut([
'success' => true, 'success' => true,
'lines' => $lines, 'lines' => $lines,
@@ -877,6 +956,272 @@ if ($action === 'order') {
} }
} }
/* modale larga: quasi tutto lo schermo su PC, a tutto schermo su tablet in verticale e telefono */
.od-dialog.modal-xl {
--bs-modal-width: min(1480px, 96vw);
}
@media (max-width: 991.98px) {
.od-dialog .modal-content {
border-radius: 0 !important;
}
}
@media (min-width: 768px) {
.od-dialog .ro-grid {
grid-template-columns: 150px 1fr 150px 1fr;
}
}
@media (min-width: 1200px) {
.od-dialog .ro-grid {
grid-template-columns: 170px 1fr 170px 1fr;
}
}
/* righe ordine */
.ol-head,
.ol-row {
display: grid;
grid-template-columns: minmax(190px, 1.6fr) minmax(200px, 1.5fr) minmax(150px, 1fr) minmax(150px, 1fr) minmax(120px, .8fr) minmax(200px, 1.4fr);
gap: 10px 16px;
align-items: start;
}
.ol-head {
background: #f1f5f9;
border-radius: 10px;
padding: 8px 14px;
font-size: .78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .03em;
color: #475569;
}
.ol-row {
border: 1px solid #e2e8f0;
border-left: 5px solid #3b82f6;
border-radius: 12px;
padding: 12px 14px;
margin-top: 8px;
background: #fff;
font-size: .9rem;
}
.ol-row.done {
border-left-color: #22c55e;
}
.ol-row.cancel {
border-left-color: #94a3b8;
opacity: .7;
}
.ol-row.late {
border-left-color: #dc3545;
background: #fff7f7;
}
.ol-k {
display: none;
font-size: .7rem;
font-weight: 700;
text-transform: uppercase;
color: #64748b;
letter-spacing: .03em;
margin-bottom: 2px;
}
.ol-n {
display: inline-block;
min-width: 26px;
padding: 1px 6px;
margin-right: 6px;
border-radius: 6px;
background: #e2e8f0;
color: #334155;
font-size: .75rem;
font-weight: 700;
text-align: center;
}
.ol-item {
font-weight: 800;
color: #0f172a;
font-size: .98rem;
}
.ol-sub {
font-size: .8rem;
color: #64748b;
white-space: normal;
}
.ol-mat {
display: flex;
gap: 10px;
align-items: flex-start;
}
.ol-mat img {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 10px;
border: 1px solid #e2e8f0;
background: #f8fafc;
cursor: zoom-in;
flex: 0 0 auto;
}
.ol-mname {
font-weight: 800;
color: #0f172a;
}
.ol-raw {
display: inline-block;
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: .78rem;
background: #f1f5f9;
border-radius: 6px;
padding: 1px 6px;
color: #334155;
}
.ol-chip {
display: inline-flex;
align-items: center;
gap: 4px;
border-radius: 999px;
padding: 2px 9px;
font-size: .74rem;
font-weight: 700;
line-height: 1.5;
margin-top: 3px;
background: color-mix(in srgb, var(--c, #64748b) 14%, #fff);
color: var(--c, #64748b);
border: 1px solid color-mix(in srgb, var(--c, #64748b) 35%, #fff);
}
.ol-qty b {
font-size: 1rem;
}
.ol-bar {
height: 6px;
border-radius: 99px;
background: #e2e8f0;
overflow: hidden;
margin: 5px 0 3px;
}
.ol-bar>span {
display: block;
height: 100%;
background: #22c55e;
border-radius: 99px;
}
.ol-amt {
text-align: right;
}
.ol-amt .amount {
font-weight: 800;
font-size: 1rem;
}
.ol-lpill {
display: inline-block;
border-radius: 8px;
padding: 2px 8px;
margin: 2px 4px 0 0;
font-size: .76rem;
font-weight: 700;
background: color-mix(in srgb, var(--c, #64748b) 18%, #fff);
color: #1e293b;
border: 1px solid color-mix(in srgb, var(--c, #64748b) 45%, #fff);
}
.ol-mrow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px 0;
margin-top: 5px;
}
.ol-mrow>span:first-child {
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
color: #64748b;
letter-spacing: .03em;
margin-right: 6px;
}
.ol-mx {
display: inline-block;
border-radius: 8px;
padding: 2px 8px;
margin: 2px 4px 0 0;
font-size: .76rem;
font-weight: 600;
background: #f5f3ff;
color: #5b21b6;
border: 1px solid #ddd6fe;
}
.ol-note {
grid-column: 1 / -1;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 6px 10px;
font-size: .84rem;
white-space: pre-wrap;
color: #713f12;
}
.ol-warn {
grid-column: 1 / -1;
font-size: .82rem;
color: #b45309;
font-weight: 600;
}
@media (max-width: 1199.98px) {
.ol-head {
display: none;
}
.ol-row {
grid-template-columns: 1fr 1fr;
}
.ol-k {
display: block;
}
.ol-art,
.ol-mat,
.ol-prod {
grid-column: 1 / -1;
}
.ol-amt {
text-align: left;
}
}
@media (max-width: 575.98px) {
.ol-row {
grid-template-columns: 1fr;
}
}
.sc-link { .sc-link {
color: #198754; color: #198754;
margin-left: 6px; margin-left: 6px;
@@ -1179,7 +1524,7 @@ if ($action === 'order') {
<!-- MODALE DETTAGLIO ORDINE --> <!-- MODALE DETTAGLIO ORDINE -->
<div class="modal fade" id="orderModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="orderModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-dialog-centered modal-xl modal-fullscreen-lg-down modal-dialog-scrollable od-dialog">
<div class="modal-content" style="border-radius:16px;"> <div class="modal-content" style="border-radius:16px;">
<div class="modal-header" style="background-color:#b9ebc7;"> <div class="modal-header" style="background-color:#b9ebc7;">
<h5 class="modal-title">Dettaglio ordine cliente</h5> <h5 class="modal-title">Dettaglio ordine cliente</h5>
@@ -2167,11 +2512,61 @@ if ($action === 'order') {
}); });
} }
const MATCH = {
esatta: ['Matrice riconosciuta', '#15803d'],
revisione: ['Altra revisione in anagrafica', '#b45309'],
mappa: ['Abbinata a mano', '#0f766e'],
manuale: ['Scelta a mano', '#0f766e'],
da_creare: ['Da creare in anagrafica', '#dc2626'],
profilo: ['Profilo: matrice da scegliere', '#7c3aed'],
anomalia: ['Da verificare', '#dc2626']
};
function lineMatrix(l) {
const raw = (l.matrice_misura || '').trim();
const mm = l.mm || {};
if (!raw && !l.matrice) return '<span class="text-muted">—</span>';
const misura = mm.misura ? `${esc(mm.tipo_label || '')} ${esc(mm.misura)}`.trim() : '';
const m = MATCH[l.match_status];
const img = l.matrice_photo ? `<img src="photos/matrici/${encodeURIComponent(l.matrice_photo)}" alt="" loading="lazy" class="ol-photo" title="Ingrandisci">` : '';
return `${img}<div>
${l.matrice ? `<div class="ol-mname">${esc(l.matrice)}</div>` : ''}
${raw ? `<div class="ol-sub">Mago <span class="ol-raw">${esc(raw)}</span>${misura ? ' · ' + misura : ''}</div>` : ''}
${m ? `<span class="ol-chip" style="--c:${m[1]};" title="${esc(l.match_info || '')}">${m[0]}</span>` : ''}
${(l.matrice_lines || []).length ? `<div class="ol-mrow"><span>Linee</span>${l.matrice_lines.map(x => `<span class="ol-lpill" style="--c:${esc(x.color || '#64748b')};">${esc(x.name)}</span>`).join('')}</div>` : ''}
${(l.matrice_mescole || []).length ? `<div class="ol-mrow"><span>Mescole</span>${l.matrice_mescole.map(x => `<span class="ol-mx">${esc(x)}</span>`).join('')}</div>` : ''}
</div>`;
}
function lineProduction(l, open) {
const pills = arr => arr.map(x => `<span class="ol-lpill" style="--c:${esc(x.color || '#64748b')};">${esc(x.name)}</span>`).join('');
let h = '';
if (l.prod_status) {
h += `<span class="ol-chip" style="--c:${esc(l.prod_status_color)};"><i class="fa-solid fa-industry"></i> ${esc(l.prod_status_name)}</span>`;
if (l.linea) h += `<div>${pills([{ name: l.linea, color: l.linea_color }])}</div>`;
const bits = [];
if (+l.prod_metri) bits.push(fmtNum(Math.round(+l.prod_metri)) + ' m');
if (l.data_produzione) bits.push('prod. ' + fmtDate(l.data_produzione));
if (l.mescole) bits.push(esc(l.mescole));
if (bits.length) h += `<div class="ol-sub">${bits.join(' · ')}</div>`;
} else if (l.queue_status === 'scartato') {
h += `<span class="ol-chip" style="--c:#64748b;">Non da produrre</span>${l.discard_reason ? `<div class="ol-sub">${esc(l.discard_reason)}</div>` : ''}`;
} else if (l.queue_status === 'chiuso_mago') {
h += `<span class="ol-chip" style="--c:#64748b;">Chiusa in Mago</span>`;
} else if (l.queue_status === 'importato') {
h += `<span class="ol-chip" style="--c:#64748b;">Importata, lavoro non più presente</span>`;
} else {
h += `<span class="ol-sub">${open ? 'Non ancora in produzione' : '—'}</span>`;
}
return h;
}
function renderLines(lines) { function renderLines(lines) {
if (!lines.length) { if (!lines.length) {
return '<div class="warn-strip">Righe non ancora sincronizzate: premi <b>Aggiorna da Mago</b>.</div>'; return '<div class="warn-strip">Righe non ancora sincronizzate: premi <b>Aggiorna da Mago</b>.</div>';
} }
const today = iso(new Date()); const today = iso(new Date());
const qtyTxt = n => fmtNum(Math.round(+n * 100) / 100);
const rows = lines.map(l => { const rows = lines.map(l => {
const open = !+l.delivered && !+l.cancelled; const open = !+l.delivered && !+l.cancelled;
const due = l.zibo_date ? ziboEnd(l.zibo_date) : l.client_date; const due = l.zibo_date ? ziboEnd(l.zibo_date) : l.client_date;
@@ -2180,32 +2575,48 @@ if ($action === 'order') {
const st = +l.cancelled ? '<span class="st-badge st-annullato">Annullata</span>' : const st = +l.cancelled ? '<span class="st-badge st-annullato">Annullata</span>' :
(+l.delivered ? '<span class="st-badge st-evaso">Consegnata</span>' : (+l.delivered ? '<span class="st-badge st-evaso">Consegnata</span>' :
'<span class="st-badge st-aperto">Aperta</span>' + (late ? `<span class="late-badge">+${days} gg</span>` : '')); '<span class="st-badge st-aperto">Aperta</span>' + (late ? `<span class="late-badge">+${days} gg</span>` : ''));
const qtyTxt = n => fmtNum(Math.round(+n * 100) / 100); const pct = +l.qty > 0 ? Math.min(100, Math.round(+l.delivered_qty / +l.qty * 100)) : 0;
return `<tr> const cls = +l.cancelled ? 'cancel' : (+l.delivered ? 'done' : (late ? 'late' : ''));
<td><b>${esc(l.item)}</b><div class="small text-muted" style="white-space:normal;max-width:320px;">${esc(l.description)}</div></td> return `<div class="ol-row ${cls}">
<td class="text-end amount">${qtyTxt(l.qty)} ${esc(l.uom)}</td> <div class="ol-art">
<td class="text-end amount">${qtyTxt(l.delivered_qty)}</td> <div><span class="ol-n">${esc(l.line)}</span><span class="ol-item">${esc(l.item)}</span></div>
<td class="text-end amount">${open ? '<b>' + qtyTxt(l.residual_qty) + '</b>' : '<span class="text-muted">—</span>'}</td> <div class="ol-sub">${esc(l.description)}</div>
<td>${dateCell(l.zibo_date, l.client_date, {open: open})}</td> ${l.cod_art_cli ? `<div class="ol-sub">Cod. cliente <b>${esc(l.cod_art_cli)}</b></div>` : ''}
<td class="text-end amount">${fmtEur(l.taxable_amount, 2)}</td> </div>
<td>${st}</td> <div><div class="ol-k">Matrice</div><div class="ol-mat">${lineMatrix(l)}</div></div>
</tr>`; <div class="ol-qty">
<div class="ol-k">Quantità</div>
<b class="amount">${qtyTxt(l.qty)}</b> ${esc(l.uom)}
<div class="ol-bar"><span style="width:${pct}%;"></span></div>
<div class="ol-sub">consegnati ${qtyTxt(l.delivered_qty)}${open ? ` · residuo <b>${qtyTxt(l.residual_qty)}</b>` : ''}</div>
</div>
<div><div class="ol-k">Consegna</div>${dateCell(l.zibo_date, l.client_date, {open: open})}</div>
<div class="ol-amt"><div class="ol-k">Imponibile</div><div class="amount">${fmtEur(l.taxable_amount, 2)}</div><div class="mt-1">${st}</div></div>
<div class="ol-prod"><div class="ol-k">Produzione</div>${lineProduction(l, open)}</div>
${+l.mago_changed ? `<div class="ol-warn"><i class="fa-solid fa-triangle-exclamation me-1"></i>Cambiata in Mago dopo l'invio in produzione${l.mago_changes ? ': ' + esc(l.mago_changes) : ''}</div>` : ''}
${l.notes ? `<div class="ol-note"><b>Note riga:</b> ${esc(l.notes)}</div>` : ''}
</div>`;
}).join(''); }).join('');
return ` return `
<div class="fw-bold mb-2" style="color:var(--c-ink);">Righe ordine (${lines.length})</div> <div class="fw-bold mb-2" style="color:var(--c-ink);">Righe ordine (${lines.length})</div>
<div class="table-responsive"> <div class="ol-head"><div>Articolo</div><div>Matrice</div><div>Quantità</div><div>Consegna (ZIBO / cliente)</div><div class="text-end">Imponibile · stato</div><div>Produzione</div></div>
<table class="table table-sm align-middle" style="font-size:.9rem;"> ${rows}`;
<thead style="background:#f1f5f9;">
<tr>
<th>Articolo</th><th class="text-end">Qtà</th><th class="text-end">Consegnata</th><th class="text-end">Residuo</th>
<th>Consegna (ZIBO / cliente)</th><th class="text-end">Imponibile</th><th>Stato</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>`;
} }
// foto matrice a tutto schermo
$(document).on('click', '#orderModalBody .ol-photo', function() {
Swal.fire({
imageUrl: this.src,
imageAlt: 'Matrice',
showConfirmButton: false,
showCloseButton: true,
width: 'auto',
customClass: {
image: 'm-0'
}
});
});
/* ---------------- SINCRONIZZAZIONE ---------------- */ /* ---------------- SINCRONIZZAZIONE ---------------- */
function runSync(mode) { function runSync(mode) {
const labels = { const labels = {