added order name supplier

This commit is contained in:
2026-10-27 11:50:50 +01:00
parent a7c224fa10
commit 8502295342
5 changed files with 1091 additions and 4 deletions
+415 -4
View File
@@ -395,8 +395,106 @@
background: #fee2e2;
color: #991b1b;
}
</style>
</style>
/* ---------- 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;
}
</style>
</head>
@@ -430,6 +528,22 @@
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addMescolaModal">➕ Aggiungi Mescola</button>
</div>
<!-- NOMI D'ACQUISTO DA ABBINARE -->
<div class="card nm-card mb-3" id="nmUnmatchedCard" style="display:none;">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<div class="fw-bold">🧾 Nomi d'acquisto Mago da abbinare <span class="badge bg-warning text-dark" id="nmUnmatchedCount">0</span></div>
<div class="small text-muted">Righe delle fatture mescole che non corrispondono a nessuna mescola: senza abbinamento non c'è il prezzo €/kg. <span id="nmSynced"></span></div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="nmToggle">Mostra</button>
</div>
<div class="card-body" id="nmUnmatchedBody" style="display:none;">
<div id="nmDuplicates"></div>
<div class="small text-muted mb-2">Scegli la mescola, controlla il nome (basta la parte che la identifica) e premi <b>Abbina</b>. Il nome viene aggiunto alla mescola e vale anche per gli acquisti futuri.</div>
<div id="nmUnmatchedList"></div>
</div>
</div>
<!-- TABELLA -->
<div class="table-responsive">
<table id="tabellaMescole" class="table table-striped align-middle text-center" style="width:100%;">
@@ -474,6 +588,7 @@
IFNULL(q.qty_totale, 0) AS qty_totale,
(SELECT COUNT(*) FROM mescole_files mf
WHERE mf.idmescola = m.id AND mf.categoria = 'certificati_analisi') AS n_certificati,
(SELECT COUNT(*) FROM mescole_supplier_names sn WHERE sn.idmescola = m.id) AS n_nomi_acq,
GROUP_CONCAT(DISTINCT pl.name SEPARATOR ', ') AS linee
FROM mescole m
LEFT JOIN (
@@ -525,9 +640,14 @@
? "<span class='badge-files badge-files-ok' title='{$nCert} certificati di analisi caricati'>📄 {$nCert}</span>"
: "<span class='badge-files badge-files-none' title='Nessun certificato di analisi'>0</span>";
$nNomi = (int)($row['n_nomi_acq'] ?? 0);
$nomiBadge = "<span class='nacq " . ($nNomi > 0 ? "nacq-ok" : "nacq-no") . "' title='"
. ($nNomi > 0 ? "{$nNomi} nomi d&#39;acquisto Mago" : "Nessun nome d&#39;acquisto Mago: il prezzo €/kg non si può calcolare")
. "'>🧾 {$nNomi}</span>";
echo "<tr data-mescola-id='{$row['id']}'>
<td>{$badge}</td>
<td class='text-start' title=\"" . htmlspecialchars($row['nome'] ?? '', ENT_QUOTES) . "\">{$nomeSafe}</td>
<td class='text-start' title=\"" . htmlspecialchars($row['nome'] ?? '', ENT_QUOTES) . "\">{$nomiBadge}{$nomeSafe}</td>
<td>
<a href='scheda_mescola.php?id={$row['id']}' title='Apri scheda mescola (ID {$row['id']})'>
{$nomeUscitaSafe}
@@ -567,6 +687,7 @@
<button type='button' class='btn btn-sm btn-outline-secondary edit-mescola'
data-id='{$row['id']}'
data-nome='" . htmlspecialchars($row['nome'] ?? '', ENT_QUOTES) . "'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'] ?? '', ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-placement='top' title='Modifica'>
✏️
@@ -699,6 +820,48 @@
<input type="hidden" id="afIdMescola">
<input type="hidden" id="afEditId">
<!-- NOMI D'ACQUISTO (MAGO) -->
<div class="nm-section">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-2">
<div>
<h6 class="mb-1">🧾 Nomi d'acquisto (Mago)</h6>
<div class="small text-muted">Come questa mescola compare nelle fatture dei fornitori in Mago. Basta la parte che la identifica: "ATES71" vale anche per "ATES71 M100 X 9mm spessore".</div>
</div>
<div class="nm-price" id="nmPrice"></div>
</div>
<input type="hidden" id="nmEditId">
<div class="row g-2 align-items-end mb-2">
<div class="col-md-4">
<label class="form-label fw-semibold">Fornitore</label>
<select class="form-select" id="nmSupplier"></select>
</div>
<div class="col-md-5">
<label class="form-label fw-semibold">Nome in Mago</label>
<input type="text" class="form-control" id="nmName" placeholder="es. ATES71 M100">
</div>
<div class="col-md-3 text-end">
<button type="button" class="btn btn-add" id="nmSaveBtn">➕ Aggiungi nome</button>
<button type="button" class="btn btn-outline-secondary btn-sm mt-1" id="nmCancel" style="display:none;">Annulla</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0" id="nmTable">
<thead class="table-light">
<tr>
<th>Fornitore</th>
<th>Nome in Mago</th>
<th class="text-end">Acquistato</th>
<th>Ultimo acquisto</th>
<th style="width:90px;"></th>
</tr>
</thead>
<tbody id="nmBody"></tbody>
</table>
</div>
</div>
<h6 class="fw-bold">📦 Lotti</h6>
<div class="row g-2 align-items-end mb-3">
<div class="col-md-3">
<label class="form-label fw-semibold">Fornitore</label>
@@ -935,7 +1098,8 @@
$(document).on("click", ".edit-mescola", function() {
$("#editIdMescola").val($(this).data("id"));
$("#editNomeUscita").val($(this).data("nomeuscita"));
$("#editNomeMescola").val("");
// carico il nome interno: prima veniva svuotato e al salvataggio si perdeva
$("#editNomeMescola").val($(this).attr("data-nome") || "");
$("#editMescolaModal").modal("show");
});
@@ -1217,6 +1381,7 @@
.then(() => {
$("#associaFornitoriModal").modal("show");
afLoadRows(idMescola);
nmLoad(idMescola);
});
});
@@ -1677,6 +1842,252 @@
});
});
});
// ============================
// NOMI D'ACQUISTO (MAGO)
// ============================
const NM_URL = "mescole_nomi_acquisto.php";
let nmOpts = null;
function nmOptions() {
if (nmOpts) return Promise.resolve(nmOpts);
return fetch(NM_URL + "?action=options").then(r => r.json()).then(d => (nmOpts = d));
}
function nmNum(v, d) {
return (+v || 0).toLocaleString("it-IT", {
minimumFractionDigits: d || 0,
maximumFractionDigits: d || 0
});
}
function nmDate(s) {
return s ? String(s).substring(0, 10).split("-").reverse().join("/") : "";
}
function nmSupplierOptions(selected) {
return '<option value="">Fornitore…</option>' + (nmOpts.suppliers || []).map(s =>
`<option value="${s.idsupplier}" ${String(s.idsupplier) === String(selected || "") ? "selected" : ""}>${fEscape(s.supplier_name)}</option>`).join("");
}
function nmPost(params) {
return fetch(NM_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(params).toString()
}).then(r => r.json());
}
function nmSetBadge(idMescola, n) {
const $b = $(`tr[data-mescola-id="${idMescola}"] .nacq`);
$b.text("🧾 " + n).toggleClass("nacq-ok", n > 0).toggleClass("nacq-no", n === 0);
}
function nmResetForm() {
$("#nmEditId").val("");
$("#nmName").val("");
$("#nmSupplier").val("").trigger("change");
$("#nmSaveBtn").text("➕ Aggiungi nome");
$("#nmCancel").hide();
}
function nmLoad(idMescola) {
$("#nmBody").html('<tr><td colspan="5" class="text-muted">Carico…</td></tr>');
$("#nmPrice").html("");
nmOptions().then(() => {
const $s = $("#nmSupplier");
if ($s.hasClass("select2-hidden-accessible")) $s.select2("destroy");
$s.html(nmSupplierOptions("")).select2({
theme: "bootstrap-5",
width: "100%",
dropdownParent: $("#associaFornitoriModal")
});
nmResetForm();
return fetch(NM_URL + "?action=list&id=" + encodeURIComponent(idMescola)).then(r => r.json());
}).then(d => {
if (!d || !d.success) {
$("#nmBody").html(`<tr><td colspan="5" class="text-danger">${fEscape((d && d.message) || "Errore")}</td></tr>`);
return;
}
const p = d.price;
$("#nmPrice").html(p ?
`Prezzo <b>${nmNum(p.price, 3)} €/kg</b> <span class="text-muted">(media ${d.months} mesi${p.old ? "" : ", " + nmNum(p.kg_window) + " kg"})</span>
${p.old ? '<span class="nm-old">nessun acquisto recente</span>' : ""}<br>
<span class="small text-muted">ultimo ${nmNum(p.last_price, 3)} €/kg del ${nmDate(p.last_date)}</span>` :
'<span class="text-muted">Nessun acquisto abbinato: prezzo non disponibile</span>');
nmSetBadge(idMescola, d.rows.length);
if (!d.rows.length) {
$("#nmBody").html('<tr><td colspan="5" class="text-muted">Nessun nome d\'acquisto: aggiungi come compare nelle fatture Mago</td></tr>');
return;
}
$("#nmBody").html(d.rows.map(r => `
<tr>
<td>${fEscape(r.supplier_name || "")}</td>
<td><b>${fEscape(r.supplier_mix_name)}</b>${r.note ? `<div class="small text-muted">${fEscape(r.note)}</div>` : ""}</td>
<td class="text-end">${r.n ? nmNum(r.kg) + " kg<div class=\"small text-muted\">" + r.n + " righe</div>" : '<span class="text-muted">mai</span>'}</td>
<td>${r.last_date ? nmNum(r.last_price, 3) + " €/kg · " + nmDate(r.last_date) + `<div class="small text-muted">${fEscape(r.last_desc || "")}</div>` : ""}</td>
<td class="text-nowrap">
<button type="button" class="btn btn-sm btn-outline-secondary nm-edit" data-id="${r.id}" data-sup="${r.idsupplier}" data-name="${fEscape(r.supplier_mix_name)}">✏️</button>
<button type="button" class="btn btn-sm btn-outline-danger nm-del" data-id="${r.id}">🗑️</button>
</td>
</tr>`).join(""));
});
}
$(document).on("click", ".nm-edit", function() {
$("#nmEditId").val($(this).attr("data-id"));
$("#nmSupplier").val($(this).attr("data-sup")).trigger("change");
$("#nmName").val($(this).attr("data-name")).trigger("focus");
$("#nmSaveBtn").text("💾 Salva nome");
$("#nmCancel").show();
});
$("#nmCancel").on("click", nmResetForm);
$("#nmSaveBtn").on("click", function(e) {
e.preventDefault();
const idMescola = $("#afIdMescola").val();
const params = {
action: "save",
id: $("#nmEditId").val(),
idmescola: idMescola,
idsupplier: $("#nmSupplier").val() || "",
name: $("#nmName").val().trim()
};
if (!params.idsupplier || !params.name) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Fornitore e nome sono obbligatori"
});
return;
}
nmPost(params).then(d => {
if (!d.success) {
Swal.fire({
icon: "error",
title: "Non salvato",
text: d.message || "Errore"
});
return;
}
nmLoad(idMescola);
nmLoadUnmatched();
});
});
$(document).on("click", ".nm-del", function() {
const id = $(this).data("id");
const idMescola = $("#afIdMescola").val();
Swal.fire({
title: "Togliere questo nome?",
text: "Gli acquisti con questo nome non saranno più collegati alla mescola.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, togli",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33"
}).then(res => {
if (!res.isConfirmed) return;
nmPost({
action: "delete",
id
}).then(d => {
if (d.success) {
nmLoad(idMescola);
nmLoadUnmatched();
}
});
});
});
/* ---- riquadro "da abbinare" ---- */
function nmLoadUnmatched() {
Promise.all([nmOptions(), fetch(NM_URL + "?action=unmatched").then(r => r.json())]).then(([o, d]) => {
if (!d || !d.success) return;
const rows = d.rows || [];
$("#nmUnmatchedCount").text(rows.length);
$("#nmSynced").text(d.synced_at ? "Acquisti aggiornati il " + nmDate(d.synced_at) + " " + String(d.synced_at).substring(11, 16) + "." : "");
$("#nmUnmatchedCard").toggle(rows.length > 0 || (d.duplicates || []).length > 0);
$("#nmDuplicates").html((d.duplicates || []).map(x =>
`<div class="alert alert-warning py-2 mb-2">⚠ Lo stesso nome d'acquisto <b>${fEscape(x.name)}</b> è su più mescole: ${x.mescole.map(fEscape).join(", ")}. Toglilo da quella sbagliata.</div>`).join(""));
const mesOpts = '<option value="">Scegli la mescola…</option>' + o.mescole.map(m =>
`<option value="${m.id}">${fEscape(m.nome)}${m.nomeuscita ? " — " + fEscape(m.nomeuscita) : ""}</option>`).join("");
const $list = $("#nmUnmatchedList");
$list.find("select").each(function() {
if ($(this).hasClass("select2-hidden-accessible")) $(this).select2("destroy");
});
$list.html(rows.map((r, i) => `
<div class="nm-row" data-i="${i}">
<div>
<div class="nm-desc">${fEscape(r.description)}</div>
<div class="nm-meta">${fEscape(r.supplier_name || r.supplier_code || "")} · ${nmNum(r.kg)} kg · ${r.price !== null ? nmNum(r.price, 3) + " €/kg" : "—"} · ultimo ${nmDate(r.last_date)}
${r.status === "ambiguous" ? ' · <span class="text-danger fw-bold">vale per più mescole</span>' : ""}</div>
</div>
<select class="form-select nm-u-mescola">${mesOpts}</select>
<input type="text" class="form-control nm-u-name" value="${fEscape(r.suggest)}" title="Nome da salvare sulla mescola">
<select class="form-select nm-u-sup">${nmSupplierOptions(r.idsupplier)}</select>
<button type="button" class="btn btn-add nm-u-save">Abbina</button>
</div>`).join(""));
$list.find(".nm-u-mescola").select2({
theme: "bootstrap-5",
width: "100%"
});
});
}
$("#nmToggle").on("click", function() {
const $b = $("#nmUnmatchedBody").toggle();
$(this).text($b.is(":visible") ? "Nascondi" : "Mostra");
});
$(document).on("click", ".nm-u-save", function() {
const $r = $(this).closest(".nm-row");
const idmescola = $r.find(".nm-u-mescola").val();
const params = {
action: "save",
idmescola,
idsupplier: $r.find(".nm-u-sup").val() || "",
name: $r.find(".nm-u-name").val().trim()
};
if (!idmescola || !params.idsupplier || !params.name) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Scegli mescola e fornitore e controlla il nome"
});
return;
}
nmPost(params).then(d => {
if (!d.success) {
Swal.fire({
icon: "error",
title: "Non abbinato",
text: d.message || "Errore"
});
return;
}
const $b = $(`tr[data-mescola-id="${idmescola}"] .nacq`);
const n = (parseInt(($b.text() || "").replace(/\D/g, ""), 10) || 0) + 1;
nmSetBadge(idmescola, n);
Swal.fire({
icon: "success",
title: "Abbinato",
timer: 1100,
showConfirmButton: false
});
nmLoadUnmatched();
});
});
$(function() {
nmLoadUnmatched();
});
</script>
</body>