Files

1684 lines
65 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;
}
</style>
</style>
</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>
<!-- 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,
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>";
echo "<tr data-mescola-id='{$row['id']}'>
<td>{$badge}</td>
<td class='text-start' title=\"" . htmlspecialchars($row['nome'] ?? '', ENT_QUOTES) . "\">{$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-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">
<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"));
$("#editNomeMescola").val("");
$("#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);
});
});
// 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"
});
}
});
});
});
</script>
</body>
</html>