added order name supplier
This commit is contained in:
+415
-4
@@ -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'acquisto Mago" : "Nessun nome d'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>
|
||||
|
||||
Reference in New Issue
Block a user