Files
2026-10-27 11:50:50 +01:00

2095 lines
84 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Mescole - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- jQuery e Bootstrap -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- DataTables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!-- Select2 -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.modal-content {
border-radius: 16px;
}
#tabellaMescole thead th {
text-align: center;
vertical-align: middle;
}
/* --- FIX colonne lunghe: tronca con ellissi --- */
#tabellaMescole {
table-layout: fixed;
width: 100% !important;
}
#tabellaMescole th,
#tabellaMescole td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#tabellaMescole td:nth-child(1) {
overflow: visible;
}
/* ID */
#tabellaMescole th:nth-child(1),
#tabellaMescole td:nth-child(1) {
width: 70px;
max-width: 70px;
}
/* Stato (pallino) */
#tabellaMescole th:nth-child(1),
#tabellaMescole td:nth-child(1) {
width: 42px;
max-width: 42px;
padding-left: 4px;
padding-right: 4px;
}
/* Nome (interno) */
#tabellaMescole th:nth-child(2),
#tabellaMescole td:nth-child(2) {
width: 22%;
}
/* Nome Uscita */
#tabellaMescole th:nth-child(3),
#tabellaMescole td:nth-child(3) {
width: 26%;
}
#tabellaMescole td:nth-child(3) a {
color: #1f2d3d;
text-decoration: none;
font-weight: 600;
}
#tabellaMescole td:nth-child(3) a:hover {
text-decoration: underline;
color: #0d6efd;
}
/* Q.tà totale */
#tabellaMescole th:nth-child(4),
#tabellaMescole td:nth-child(4) {
width: 110px;
max-width: 110px;
}
/* Linee */
#tabellaMescole th:nth-child(5),
#tabellaMescole td:nth-child(5) {
width: auto;
}
/* Certificati */
#tabellaMescole th:nth-child(6),
#tabellaMescole td:nth-child(6) {
width: 80px;
max-width: 80px;
}
/* Azioni */
#tabellaMescole th:nth-child(7),
#tabellaMescole td:nth-child(7) {
width: 230px;
max-width: 230px;
white-space: normal;
}
/* ---------- PALLINO STATO ---------- */
.dot-status {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
vertical-align: middle;
}
.dot-active {
background: #16a34a;
box-shadow: 0 0 0 3px rgba(22, 163, 74, .18);
}
.dot-inactive {
background: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, .18);
}
/* Testo dello stato: invisibile ma ricercabile da DataTables */
.dot-sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.azioni-cell {
display: flex;
justify-content: center;
gap: 4px;
flex-wrap: nowrap;
}
.azioni-cell .btn {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
line-height: 1;
}
/* ---------- DROP ZONE FILE ---------- */
.dropzone {
border: 2px dashed #bcd4f4;
background: #f8fbff;
border-radius: 14px;
padding: 28px 20px;
text-align: center;
cursor: pointer;
transition: all .18s ease-in-out;
user-select: none;
}
.dropzone:hover {
background: #eef5ff;
border-color: #0d6efd;
}
.dropzone.dragover {
background: #dbeafe;
border-color: #0d6efd;
border-style: solid;
transform: scale(1.01);
}
.dropzone-icon {
font-size: 2.4rem;
line-height: 1;
margin-bottom: 6px;
}
.dropzone-main {
font-weight: 600;
color: #1f2d3d;
font-size: 1.05rem;
}
.dropzone-sub {
color: #64748b;
font-size: .87rem;
margin-top: 4px;
}
.dz-queue {
margin-top: 14px;
}
.dz-item {
display: flex;
align-items: center;
gap: 10px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 8px 12px;
margin-bottom: 8px;
font-size: .92rem;
}
.dz-item-name {
flex: 1;
min-width: 0;
text-align: left;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.3;
}
.dz-item-state {
font-size: .85rem;
font-weight: 600;
white-space: nowrap;
}
.dz-ok {
color: #166534;
}
.dz-err {
color: #991b1b;
}
.dz-progress {
width: 110px;
height: 6px;
background: #e2e8f0;
border-radius: 999px;
overflow: hidden;
}
.dz-progress-bar {
height: 100%;
width: 0;
background: #0d6efd;
transition: width .15s;
}
/* ---------- TABELLA FILE NELLA MODALE ---------- */
#fTable {
table-layout: fixed;
width: 100%;
}
#fTable th,
#fTable td {
vertical-align: middle;
word-break: break-word;
overflow-wrap: anywhere;
white-space: normal;
}
/* Categoria */
#fTable th:nth-child(1),
#fTable td:nth-child(1) {
width: 150px;
}
/* Titolo */
#fTable th:nth-child(2),
#fTable td:nth-child(2) {
width: auto;
}
/* Nome File */
#fTable th:nth-child(3),
#fTable td:nth-child(3) {
width: 30%;
}
/* Caricato il */
#fTable th:nth-child(4),
#fTable td:nth-child(4) {
width: 140px;
}
/* Azioni */
#fTable th:nth-child(5),
#fTable td:nth-child(5) {
width: 110px;
white-space: nowrap;
}
#fTable td a {
color: #0d6efd;
text-decoration: none;
}
#fTable td a:hover {
text-decoration: underline;
}
.file-title-cell {
cursor: text;
text-align: left;
line-height: 1.35;
}
.file-title-cell:hover {
background: #f1f5f9;
}
.file-title-input {
width: 100%;
border: 1px solid #0d6efd;
border-radius: 6px;
padding: 3px 6px;
font-size: .95rem;
}
.badge-files {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: .8rem;
font-weight: 600;
}
.badge-files-ok {
background: #dcfce7;
color: #166534;
}
.badge-files-none {
background: #fee2e2;
color: #991b1b;
}
/* ---------- NOMI D'ACQUISTO (MAGO) ---------- */
.nacq {
display: inline-block;
min-width: 38px;
margin-right: 6px;
padding: 1px 7px;
border-radius: 999px;
font-size: .75rem;
font-weight: 700;
text-align: center;
vertical-align: middle;
}
.nacq-ok {
background: #e0f2fe;
color: #075985;
}
.nacq-no {
background: #fef3c7;
color: #92400e;
}
.nm-card {
border: 1px solid #fde68a;
background: #fffbeb;
box-shadow: none;
}
.nm-card .card-header {
background: transparent;
border-bottom: 1px solid #fde68a;
}
.nm-row {
display: grid;
grid-template-columns: minmax(220px, 2fr) minmax(200px, 1.4fr) minmax(170px, 1fr) minmax(150px, .9fr) auto;
gap: 8px;
align-items: center;
background: #fff;
border: 1px solid #fde68a;
border-radius: 12px;
padding: 10px 12px;
margin-bottom: 8px;
}
@media (max-width: 1200px) {
.nm-row {
grid-template-columns: 1fr 1fr;
}
}
.nm-desc {
font-weight: 700;
color: #1f2d3d;
line-height: 1.25;
word-break: break-word;
}
.nm-meta {
font-size: .82rem;
color: #64748b;
}
.nm-section {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 14px;
padding: 14px;
margin-bottom: 18px;
}
.nm-section h6 {
font-weight: 800;
color: #075985;
}
.nm-price {
background: #fff;
border: 1px solid #bae6fd;
border-radius: 10px;
padding: 8px 12px;
font-size: .92rem;
}
.nm-old {
display: inline-block;
background: #fee2e2;
color: #991b1b;
border-radius: 999px;
padding: 1px 8px;
font-size: .75rem;
font-weight: 700;
}
#nmTable td {
text-align: left;
word-break: break-word;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Gestione Mescole</h5>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h6 class="fw-semibold mb-0">Elenco Completo</h6>
<select id="filterActive" class="form-select form-select-sm" style="width:220px;">
<option value="all">Tutte</option>
<option value="1" selected>Solo Attive</option>
<option value="0">Solo Inattive</option>
</select>
</div>
<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%;">
<thead>
<tr>
<th title="Stato">●</th>
<th>Nome</th>
<th>Nome Uscita</th>
<th>Q.tà Totale</th>
<th>Linee Associate</th>
<th>Cert.</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// filtro: all / 1 / 0
$activeFilter = $_GET['active'] ?? '1';
if (!in_array($activeFilter, ['all', '0', '1'], true)) {
$activeFilter = '1';
}
$where = "";
$params = [];
if ($activeFilter !== 'all') {
$where = "WHERE m.is_active = ?";
$params[] = (int)$activeFilter;
}
// QTY IN SUBQUERY per evitare raddoppi dovuti alle linee
$sql = "
SELECT
m.id,
m.nome,
m.nomeuscita,
m.is_active,
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 (
SELECT idmescola, SUM(qty) AS qty_totale
FROM mescole_supplier_lots
GROUP BY idmescola
) q ON q.idmescola = m.id
LEFT JOIN mescole_lines ml ON m.id = ml.idmescola
LEFT JOIN production_lines pl ON ml.idlinea = pl.id
$where
GROUP BY m.id
ORDER BY m.id DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
if ($stmt->rowCount() === 0) {
// DataTables-friendly: 6 TD reali
echo "<tr>
<td class='text-muted'>-</td>
<td class='text-muted'>Nessuna mescola presente</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
<td class='text-muted'>-</td>
</tr>";
} else {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$linee = $row['linee'] ? htmlspecialchars($row['linee']) : '<span class="text-muted">Nessuna</span>';
$qtyTot = number_format((float)$row['qty_totale'], 3, ',', '.');
$isActive = (int)$row['is_active'] === 1;
$statoLabel = $isActive ? 'Attiva' : 'Inattiva';
$badge = "<span class='dot-status " . ($isActive ? 'dot-active' : 'dot-inactive') . "' title='{$statoLabel}'></span>"
. "<span class='dot-sr'>{$statoLabel}</span>";
$toggleText = $isActive ? "Disattiva" : "Attiva";
$toggleClass = $isActive ? "btn-outline-warning" : "btn-outline-success";
$nomeUscitaSafe = htmlspecialchars($row['nomeuscita'] ?? '');
$nomeSafe = trim((string)($row['nome'] ?? '')) !== ''
? htmlspecialchars($row['nome'])
: "<span class='text-muted'>—</span>";
$nCert = (int)($row['n_certificati'] ?? 0);
$certBadge = $nCert > 0
? "<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) . "\">{$nomiBadge}{$nomeSafe}</td>
<td>
<a href='scheda_mescola.php?id={$row['id']}' title='Apri scheda mescola (ID {$row['id']})'>
{$nomeUscitaSafe}
</a>
</td>
<td><span class='fw-semibold'>{$qtyTot}</span></td>
<td>{$linee}</td>
<td>{$certBadge}</td>
<td class='azioni-cell'>
<button type='button' class='btn btn-sm btn-outline-dark associa-fornitori'
data-id='{$row['id']}'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-placement='top' title='Fornitori / Lotti'>
🧾
</button>
<button type='button' class='btn btn-sm btn-outline-info associa-file'
data-id='{$row['id']}'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'], ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-placement='top' title='File'>
📁
</button>
<button type='button' class='btn btn-sm btn-outline-primary associa-linee'
data-id='{$row['id']}'
data-bs-toggle='tooltip' data-bs-placement='top' title='Linee Associate'>
⚙️
</button>
<button type='button' class='btn btn-sm {$toggleClass} toggle-active'
data-id='{$row['id']}'
data-active='" . ($isActive ? '1' : '0') . "'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'] ?? '', ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-placement='top' title='{$toggleText}'>
🔁
</button>
<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'>
✏️
</button>
<button type='button' class='btn btn-sm btn-outline-danger delete-mescola'
data-id='{$row['id']}'
data-nomeuscita='" . htmlspecialchars($row['nomeuscita'] ?? '', ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-placement='top' title='Elimina'>
🗑️
</button>
</td>
</tr>";
}
}
?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- MODALE AGGIUNTA MESCOLA -->
<div class="modal fade" id="addMescolaModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Aggiungi Nuova Mescola</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addMescolaForm">
<div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="nomeMescola" placeholder="opzionale / interno">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Nome Uscita</label>
<input type="text" class="form-control" id="nomeUscita" required>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- MODALE EDIT -->
<div class="modal fade" id="editMescolaModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Modifica Mescola</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editMescolaForm">
<input type="hidden" id="editIdMescola">
<div class="mb-3">
<label class="form-label fw-semibold">Nome Mescola (interno)</label>
<input type="text" class="form-control" id="editNomeMescola" placeholder="opzionale / interno">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Nome Uscita</label>
<input type="text" class="form-control" id="editNomeUscita" required>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- MODALE ASSOCIA LINEE -->
<div class="modal fade" id="associaLineeModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Associa Linee</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="associaLineeForm">
<input type="hidden" id="idMescolaLinee">
<div class="mb-3">
<label class="form-label fw-semibold">Linee</label>
<select id="lineeSelect" class="form-select" multiple style="width:100%;"></select>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- MODALE ASSOCIA FORNITORI / LOTTI -->
<div class="modal fade" id="associaFornitoriModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">Fornitori / Lotti - <span id="afNomeUscita"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<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>
<select class="form-select" id="afIdSupplier" style="width:100%;"></select>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Nome mescola fornitore</label>
<input type="text" class="form-control" id="afSupplierMixName" placeholder="Nome specifico fornitore">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Lotto</label>
<input type="text" class="form-control" id="afLotCode" placeholder="LOT-...">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Scadenza</label>
<input type="date" class="form-control" id="afExpiryDate">
</div>
<div class="col-md-2">
<label class="form-label fw-semibold">Q.tà</label>
<input type="number" step="0.001" class="form-control" id="afQty" value="0">
</div>
<div class="col-md-12 text-end">
<button class="btn btn-add" id="afSaveBtn">➕ Aggiungi Riga</button>
<button class="btn btn-outline-secondary ms-2" id="afCancelEdit" type="button" style="display:none;">Annulla modifica</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped align-middle text-center" id="afTable">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Fornitore</th>
<th>Nome mescola fornitore</th>
<th>Lotto</th>
<th>Scadenza</th>
<th>Q.tà</th>
<th>Azioni</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- MODALE FILE / REPOSITORY -->
<div class="modal fade" id="associaFileModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background-color:#cfe3ff;">
<h5 class="modal-title">📁 File - <span id="fNomeUscita"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="fIdMescola">
<div class="mb-3">
<label class="form-label fw-semibold">Categoria del caricamento</label>
<select class="form-select" id="fCategoria"></select>
</div>
<div class="dropzone" id="fDropzone">
<div class="dropzone-icon">📥</div>
<div class="dropzone-main">Trascina qui i file dalla mail</div>
<div class="dropzone-sub">
oppure clicca per selezionarli · <kbd>Ctrl</kbd>+<kbd>V</kbd> per incollare<br>
Il titolo viene creato dal nome del file — PDF, Word, Excel, immagini · max 15MB
</div>
</div>
<input type="file" id="fFileInput" multiple style="display:none;">
<div class="dz-queue" id="fQueue"></div>
<hr class="my-3">
<div class="table-responsive">
<table class="table table-striped align-middle text-center" id="fTable">
<thead class="table-light">
<tr>
<th>Categoria</th>
<th>Titolo</th>
<th>Nome File</th>
<th>Caricato il</th>
<th>Azioni</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
/* ----------------- DATATABLE ----------------- */
$(document).ready(function() {
// init dropdown from URL
const urlParams = new URLSearchParams(window.location.search);
const activeParam = urlParams.get('active') || '1';
$('#filterActive').val(activeParam);
// datatable
$('#tabellaMescole').DataTable({
order: [
[2, 'asc']
],
columnDefs: [{
targets: [6],
orderable: false,
searchable: false
}],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
}).on('draw.dt', function() {
initTooltips();
});
// filter reload
$('#filterActive').on('change', function() {
const v = $(this).val();
const url = new URL(window.location.href);
url.searchParams.set('active', v);
window.location.href = url.toString();
});
initTooltips();
});
/* -------- TOOLTIP BOOTSTRAP SUI PULSANTI ICONA -------- */
function initTooltips() {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
const existing = bootstrap.Tooltip.getInstance(el);
if (existing) existing.dispose();
new bootstrap.Tooltip(el);
});
}
/* -------- TOGGLE ACTIVE -------- */
$(document).on('click', '.toggle-active', function() {
const id = $(this).data('id');
const isActive = String($(this).data('active')) === '1';
const nome = $(this).data('nomeuscita') || '';
const azione = isActive ? 'Disattivare' : 'Attivare';
const testo = isActive ?
'La mescola non comparirà più nella lista delle attive.' :
'La mescola tornerà disponibile fra le attive.';
Swal.fire({
title: azione + ' la mescola?',
html: (nome ? '<b>' + nome + '</b><br>' : '') + testo,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Sì, ' + (isActive ? 'disattiva' : 'attiva'),
cancelButtonText: 'Annulla',
confirmButtonColor: isActive ? '#e8930c' : '#16a34a',
cancelButtonColor: '#6c757d'
}).then((res) => {
if (!res.isConfirmed) return;
fetch('toggle_mescola_active.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
location.reload();
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
}
})
.catch(function() {
Swal.fire({
icon: "error",
title: "Errore",
text: "Errore di comunicazione."
});
});
});
});
/* -------- AGGIUNTA MESCOLA -------- */
$("#addMescolaForm").on("submit", function(e) {
e.preventDefault();
let nome = $("#nomeMescola").val().trim();
let nomeuscita = $("#nomeUscita").val().trim();
fetch("save_mescola.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `nome=${encodeURIComponent(nome)}&nomeuscita=${encodeURIComponent(nomeuscita)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Salvato!",
confirmButtonColor: "#3085d6"
})
.then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Errore salvataggio"
});
}
});
});
/* -------- APERTURA MODALE EDIT -------- */
$(document).on("click", ".edit-mescola", function() {
$("#editIdMescola").val($(this).data("id"));
$("#editNomeUscita").val($(this).data("nomeuscita"));
// carico il nome interno: prima veniva svuotato e al salvataggio si perdeva
$("#editNomeMescola").val($(this).attr("data-nome") || "");
$("#editMescolaModal").modal("show");
});
/* -------- SALVATAGGIO EDIT -------- */
$("#editMescolaForm").on("submit", function(e) {
e.preventDefault();
let id = $("#editIdMescola").val();
let nome = $("#editNomeMescola").val().trim();
let nomeuscita = $("#editNomeUscita").val().trim();
fetch("update_mescola.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}&nome=${encodeURIComponent(nome)}&nomeuscita=${encodeURIComponent(nomeuscita)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Aggiornato!",
confirmButtonColor: "#3085d6"
})
.then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Errore aggiornamento"
});
}
});
});
/* -------- CANCELLAZIONE MESCOLA -------- */
$(document).on("click", ".delete-mescola", function() {
const id = $(this).data("id");
const nome = $(this).data("nomeuscita") || '';
Swal.fire({
title: "Eliminare definitivamente?",
html: (nome ? '<b>' + nome + '</b><br>' : '') +
"L'operazione non può essere annullata.<br>" +
"<span class='text-muted small'>Se la mescola ha lotti o file, disattivala invece di eliminarla.</span>",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, elimina",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33",
cancelButtonColor: "#6c757d"
}).then((res) => {
if (!res.isConfirmed) return;
fetch("delete_mescola.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Eliminata!",
timer: 1400,
showConfirmButton: false
})
.then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Impossibile eliminare",
text: data.message || "Operazione non riuscita"
});
}
})
.catch(function() {
Swal.fire({
icon: "error",
title: "Errore",
text: "Errore di comunicazione."
});
});
});
});
/* -------- MODALE ASSOCIA LINEE -------- */
$(document).on("click", ".associa-linee", function() {
const idMescola = $(this).data("id");
$("#idMescolaLinee").val(idMescola);
fetch("get_linee_mescola.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json())
.then(data => {
const select = $("#lineeSelect");
select.empty();
data.tutte_linee.forEach(l => {
const selected = data.associate.includes(l.id.toString()) ? "selected" : "";
select.append(`<option value="${l.id}" ${selected}>${l.name}</option>`);
});
select.select2({
theme: "bootstrap-5",
width: "100%"
});
$("#associaLineeModal").modal("show");
});
});
/* -------- SALVATAGGIO LINEE -------- */
$("#associaLineeForm").on("submit", function(e) {
e.preventDefault();
const idMescola = $("#idMescolaLinee").val();
const linee = $("#lineeSelect").val() || [];
fetch("save_mescola_linee.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
idMescola,
linee
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: "success",
title: "Aggiornato!"
})
.then(() => location.reload());
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Errore salvataggio"
});
}
});
});
// ============================
// FORNITORI / LOTTI
// ============================
function afResetForm() {
$("#afEditId").val("");
$("#afIdSupplier").val("").trigger("change");
$("#afSupplierMixName").val("");
$("#afLotCode").val("");
$("#afExpiryDate").val("");
$("#afQty").val("0");
$("#afSaveBtn").text("➕ Aggiungi Riga");
$("#afCancelEdit").hide();
}
function afSetSupplierValue(supplierId) {
const sel = $("#afIdSupplier");
const normalizedId = String(supplierId || "").trim();
if (normalizedId === "") {
sel.val("").trigger("change");
return;
}
if (sel.find('option[value="' + normalizedId + '"]').length > 0) {
sel.val(normalizedId).trigger("change");
} else {
sel.val("").trigger("change");
}
}
function afLoadSuppliers(selectedId = "") {
return fetch("get_suppliers.php")
.then(r => r.json())
.then(data => {
const sel = $("#afIdSupplier");
if (sel.hasClass("select2-hidden-accessible")) {
sel.select2("destroy");
}
sel.empty();
sel.append(`<option value="">Seleziona...</option>`);
if (data.success && Array.isArray(data.rows)) {
data.rows.forEach(s => {
const value = String(s.idsupplier);
sel.append(`<option value="${value}">${s.supplier_name}</option>`);
});
}
sel.select2({
theme: "bootstrap-5",
width: "100%",
dropdownParent: $("#associaFornitoriModal")
});
if (selectedId !== "") {
sel.val(String(selectedId)).trigger("change");
} else {
sel.val("").trigger("change");
}
});
}
function afLoadRows(idMescola) {
fetch("get_mescola_supplier_lots.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json())
.then(data => {
console.log("ROWS LOTTI:", data);
const tbody = $("#afTable tbody");
tbody.empty();
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
tbody.append(`<tr>
<td class="text-muted">-</td>
<td class="text-muted">Nessuna associazione presente</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
<td class="text-muted">-</td>
</tr>`);
return;
}
data.rows.forEach(row => {
const exp = row.expiry_date ? row.expiry_date : "";
const supplierId = String(row.idsupplier ?? row.id_supplier ?? row.supplier_id ?? row.supplierId ?? "");
tbody.append(`
<tr>
<td>${row.id}</td>
<td>${row.supplier_name}</td>
<td>${row.supplier_mix_name}</td>
<td>${row.lot_code ?? ""}</td>
<td>${exp}</td>
<td>${row.qty}</td>
<td>
<button type="button" class="btn btn-sm btn-outline-secondary af-edit"
data-id="${row.id}"
data-idsupplier="${supplierId}"
data-mix="${String(row.supplier_mix_name).replace(/"/g, '&quot;')}"
data-lot="${String(row.lot_code ?? '').replace(/"/g, '&quot;')}"
data-exp="${exp}"
data-qty="${row.qty}">
✏️
</button>
<button type="button" class="btn btn-sm btn-outline-danger af-del" data-id="${row.id}">🗑️</button>
</td>
</tr>
`);
});
});
}
// Open modal
$(document).on("click", ".associa-fornitori", function() {
const idMescola = $(this).data("id");
const nomeUscita = $(this).data("nomeuscita");
$("#afIdMescola").val(idMescola);
$("#afNomeUscita").text(nomeUscita);
afResetForm();
afLoadSuppliers("")
.then(() => {
$("#associaFornitoriModal").modal("show");
afLoadRows(idMescola);
nmLoad(idMescola);
});
});
// Edit row in modal
$(document).on("click", ".af-edit", function() {
const editId = $(this).attr("data-id");
const supplierId = $(this).attr("data-idsupplier");
const mix = $(this).attr("data-mix");
const lot = $(this).attr("data-lot");
const exp = $(this).attr("data-exp");
const qty = $(this).attr("data-qty");
$("#afEditId").val(editId);
afSetSupplierValue(supplierId);
$("#afSupplierMixName").val(mix);
$("#afLotCode").val(lot);
$("#afExpiryDate").val(exp);
$("#afQty").val(qty);
$("#afSaveBtn").text("💾 Salva Modifica");
$("#afCancelEdit").show();
});
$("#afCancelEdit").on("click", function() {
afResetForm();
});
// Save (insert/update)
$("#afSaveBtn").on("click", function(e) {
e.preventDefault();
const idmescola = $("#afIdMescola").val();
const editId = $("#afEditId").val();
const idsupplier = String($("#afIdSupplier").val() || "").trim();
const supplier_mix_name = $("#afSupplierMixName").val().trim();
const lot_code = $("#afLotCode").val().trim();
const expiry_date = $("#afExpiryDate").val();
const qty = $("#afQty").val();
if (!idsupplier || !supplier_mix_name) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Fornitore e Nome mescola fornitore sono obbligatori"
});
return;
}
const actionUrl = editId ? "update_mescola_supplier_lot.php" : "save_mescola_supplier_lot.php";
const body =
(editId ? `id=${encodeURIComponent(editId)}&` : ``) +
`idmescola=${encodeURIComponent(idmescola)}` +
`&idsupplier=${encodeURIComponent(idsupplier)}` +
`&supplier_mix_name=${encodeURIComponent(supplier_mix_name)}` +
`&lot_code=${encodeURIComponent(lot_code)}` +
`&expiry_date=${encodeURIComponent(expiry_date)}` +
`&qty=${encodeURIComponent(qty)}`;
fetch(actionUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
})
.then(r => r.json())
.then(data => {
if (data.success) {
afResetForm();
afLoadRows(idmescola);
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Operazione non riuscita"
});
}
});
});
// Delete
$(document).on("click", ".af-del", function() {
const id = $(this).data("id");
const idmescola = $("#afIdMescola").val();
Swal.fire({
title: "Eliminare la riga?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, elimina",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33"
}).then((res) => {
if (!res.isConfirmed) return;
fetch("delete_mescola_supplier_lot.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
afLoadRows(idmescola);
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Cancellazione non riuscita"
});
}
});
});
});
// ============================
// FILE / REPOSITORY
// ============================
function fEscape(s) {
return String(s == null ? '' : s).replace(/[<>&"]/g, function(c) {
return ({
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
})[c];
});
}
function fFmtDate(s) {
if (!s) return '—';
var d = new Date(String(s).replace(' ', 'T'));
if (isNaN(d.getTime())) return s;
var p = function(n) {
return String(n).padStart(2, '0');
};
return p(d.getDate()) + '/' + p(d.getMonth() + 1) + '/' + d.getFullYear() +
' ' + p(d.getHours()) + ':' + p(d.getMinutes());
}
function fRenderRow(f) {
return `
<tr data-file-id="${f.id}">
<td>${fEscape(f.categoria_label)}</td>
<td class="file-title-cell" title="Doppio click per rinominare">${fEscape(f.titolo)}</td>
<td><a href="${fEscape(f.url)}" target="_blank" rel="noopener">${fEscape(f.original_filename)}</a></td>
<td>${fFmtDate(f.uploaded_at)}</td>
<td>
<a class="btn btn-sm btn-outline-primary" href="${fEscape(f.url)}" target="_blank" rel="noopener">⬇️</a>
<button type="button" class="btn btn-sm btn-outline-danger f-del" data-id="${f.id}">🗑️</button>
</td>
</tr>`;
}
function fLoadRows(idMescola) {
fetch("get_mescola_files.php?id=" + encodeURIComponent(idMescola))
.then(r => r.json())
.then(data => {
// popola dropdown categorie (solo la prima volta)
const sel = $("#fCategoria");
if (sel.children().length === 0 && data.categorie) {
Object.keys(data.categorie).forEach(slug => {
sel.append(`<option value="${slug}">${data.categorie[slug]}</option>`);
});
// preseleziona i certificati di analisi: è il caso d'uso più frequente
if (data.categorie['certificati_analisi']) {
sel.val('certificati_analisi');
}
}
const tbody = $("#fTable tbody");
tbody.empty();
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
tbody.append(`<tr class="f-empty-row">
<td class="text-muted" colspan="5">Nessun file caricato</td>
</tr>`);
return;
}
data.rows.forEach(f => tbody.append(fRenderRow(f)));
});
}
// ---------- UPLOAD CON CODA E PROGRESSO ----------
function fUploadOne(file, idMescola, categoria) {
return new Promise(function(resolve) {
const $item = $(`
<div class="dz-item">
<span>📄</span>
<span class="dz-item-name">${fEscape(file.name)}</span>
<div class="dz-progress"><div class="dz-progress-bar"></div></div>
<span class="dz-item-state">0%</span>
</div>`);
$("#fQueue").append($item);
const $bar = $item.find('.dz-progress-bar');
const $state = $item.find('.dz-item-state');
const fd = new FormData();
fd.append("idmescola", idMescola);
fd.append("categoria", categoria);
fd.append("titolo", ""); // il server genera il titolo dal nome file
fd.append("file", file);
const xhr = new XMLHttpRequest();
xhr.open("POST", "save_mescola_file.php", true);
xhr.upload.onprogress = function(e) {
if (!e.lengthComputable) return;
const pct = Math.round((e.loaded / e.total) * 100);
$bar.css('width', pct + '%');
$state.text(pct + '%');
};
xhr.onload = function() {
let data = null;
try {
data = JSON.parse(xhr.responseText);
} catch (err) {
data = null;
}
if (data && data.success) {
$bar.css('width', '100%');
$state.removeClass().addClass('dz-item-state dz-ok').text('✓ Caricato');
if (data.row) {
$("#fTable tbody .f-empty-row").remove();
$("#fTable tbody").prepend(fRenderRow(data.row));
}
setTimeout(function() {
$item.fadeOut(300, function() {
$(this).remove();
});
}, 2500);
} else {
$item.find('.dz-progress').remove();
$state.removeClass().addClass('dz-item-state dz-err')
.text('✗ ' + ((data && data.message) ? data.message : 'Errore'));
}
resolve();
};
xhr.onerror = function() {
$item.find('.dz-progress').remove();
$state.removeClass().addClass('dz-item-state dz-err').text('✗ Errore di rete');
resolve();
};
xhr.send(fd);
});
}
function fHandleFiles(fileList) {
const idMescola = $("#fIdMescola").val();
const categoria = $("#fCategoria").val();
if (!idMescola) return;
if (!categoria) {
Swal.fire({
icon: "warning",
title: "Attenzione",
text: "Seleziona prima una categoria"
});
return;
}
const files = Array.from(fileList || []);
if (files.length === 0) return;
// Caricamento sequenziale: più gentile col server e progresso leggibile
files.reduce(function(chain, file) {
return chain.then(function() {
return fUploadOne(file, idMescola, categoria);
});
}, Promise.resolve());
}
// Click sulla dropzone → apre il selettore file
$(document).on("click", "#fDropzone", function() {
document.getElementById("fFileInput").click();
});
$(document).on("change", "#fFileInput", function() {
fHandleFiles(this.files);
this.value = ""; // permette di ricaricare lo stesso file
});
// Drag & drop
['dragenter', 'dragover'].forEach(function(evt) {
document.addEventListener(evt, function(e) {
const dz = e.target.closest && e.target.closest('#fDropzone');
if (!dz) return;
e.preventDefault();
e.stopPropagation();
dz.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(function(evt) {
document.addEventListener(evt, function(e) {
const dz = e.target.closest && e.target.closest('#fDropzone');
if (!dz) return;
e.preventDefault();
e.stopPropagation();
dz.classList.remove('dragover');
});
});
document.addEventListener('drop', function(e) {
const dz = e.target.closest && e.target.closest('#fDropzone');
if (!dz) return;
e.preventDefault();
if (e.dataTransfer && e.dataTransfer.files) {
fHandleFiles(e.dataTransfer.files);
}
});
// Impedisce al browser di aprire il file se cade fuori dalla dropzone
['dragover', 'drop'].forEach(function(evt) {
document.addEventListener(evt, function(e) {
if (!$('#associaFileModal').hasClass('show')) return;
if (e.target.closest && e.target.closest('#fDropzone')) return;
e.preventDefault();
});
});
// Ctrl+V dentro la modale
document.addEventListener('paste', function(e) {
if (!$('#associaFileModal').hasClass('show')) return;
if (!e.clipboardData || !e.clipboardData.items) return;
const files = [];
Array.from(e.clipboardData.items).forEach(function(item) {
if (item.kind === 'file') {
const f = item.getAsFile();
if (f) files.push(f);
}
});
if (files.length > 0) {
e.preventDefault();
fHandleFiles(files);
}
});
// Rinomina il titolo con doppio click
$(document).on("dblclick", ".file-title-cell", function() {
const $td = $(this);
if ($td.find('input').length) return;
const oldVal = $td.text().trim();
const fileId = $td.closest('tr').data('file-id');
$td.html(`<input type="text" class="file-title-input" value="${fEscape(oldVal)}">`);
const $input = $td.find('input');
$input.trigger('focus').trigger('select');
function commit(save) {
const newVal = $input.val().trim();
if (!save || newVal === '' || newVal === oldVal) {
$td.text(oldVal);
return;
}
$td.text(newVal);
const p = new URLSearchParams();
p.append('id', fileId);
p.append('titolo', newVal);
fetch("rename_mescola_file.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: p.toString()
})
.then(r => r.json())
.then(data => {
if (!data.success) {
$td.text(oldVal);
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Impossibile rinominare"
});
}
})
.catch(function() {
$td.text(oldVal);
});
}
$input.on('blur', function() {
commit(true);
});
$input.on('keydown', function(ev) {
if (ev.key === 'Enter') {
ev.preventDefault();
$input.trigger('blur');
} else if (ev.key === 'Escape') {
$input.off('blur');
commit(false);
}
});
});
// Open modal
$(document).on("click", ".associa-file", function() {
const idMescola = $(this).data("id");
const nomeUscita = $(this).data("nomeuscita");
$("#fIdMescola").val(idMescola);
$("#fNomeUscita").text(nomeUscita);
$("#fQueue").empty();
$("#fFileInput").val("");
$("#associaFileModal").modal("show");
fLoadRows(idMescola);
});
// Delete file
$(document).on("click", ".f-del", function() {
const id = $(this).data("id");
const idMescola = $("#fIdMescola").val();
Swal.fire({
title: "Eliminare il file?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sì, elimina",
cancelButtonText: "Annulla",
confirmButtonColor: "#d33"
}).then((res) => {
if (!res.isConfirmed) return;
fetch("delete_mescola_file.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `id=${encodeURIComponent(id)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
fLoadRows(idMescola);
} else {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || "Cancellazione non riuscita"
});
}
});
});
});
// ============================
// 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>
</html>