added mescole files drag drop
This commit is contained in:
@@ -70,3 +70,8 @@ public/userarea/logsapi/last_auth_url.txt
|
|||||||
|
|
||||||
# User uploaded files
|
# User uploaded files
|
||||||
/public/userarea/files/
|
/public/userarea/files/
|
||||||
|
|
||||||
|
# Documenti e allegati caricati dagli utenti (mescole, dipendenti, formazione, DPI)
|
||||||
|
/public/userarea/uploads/
|
||||||
|
!/public/userarea/uploads/.gitkeep
|
||||||
|
|
||||||
|
|||||||
+489
-87
@@ -131,16 +131,23 @@
|
|||||||
max-width: 260px;
|
max-width: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stato */
|
/* Certificati */
|
||||||
#tabellaMescole th:nth-child(5),
|
#tabellaMescole th:nth-child(5),
|
||||||
#tabellaMescole td:nth-child(5) {
|
#tabellaMescole td:nth-child(5) {
|
||||||
width: 140px;
|
width: 130px;
|
||||||
max-width: 140px;
|
max-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stato */
|
||||||
|
#tabellaMescole th:nth-child(6),
|
||||||
|
#tabellaMescole td:nth-child(6) {
|
||||||
|
width: 130px;
|
||||||
|
max-width: 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Azioni */
|
/* Azioni */
|
||||||
#tabellaMescole th:nth-child(6),
|
#tabellaMescole th:nth-child(7),
|
||||||
#tabellaMescole td:nth-child(6) {
|
#tabellaMescole td:nth-child(7) {
|
||||||
width: 190px;
|
width: 190px;
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
@@ -163,6 +170,193 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1;
|
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>
|
</head>
|
||||||
|
|
||||||
@@ -205,6 +399,7 @@
|
|||||||
<th>Nome Uscita</th>
|
<th>Nome Uscita</th>
|
||||||
<th>Q.tà Totale</th>
|
<th>Q.tà Totale</th>
|
||||||
<th>Linee Associate</th>
|
<th>Linee Associate</th>
|
||||||
|
<th>Certificati</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th>Azioni</th>
|
<th>Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -236,6 +431,8 @@
|
|||||||
m.nomeuscita,
|
m.nomeuscita,
|
||||||
m.is_active,
|
m.is_active,
|
||||||
IFNULL(q.qty_totale, 0) AS qty_totale,
|
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
|
GROUP_CONCAT(DISTINCT pl.name SEPARATOR ', ') AS linee
|
||||||
FROM mescole m
|
FROM mescole m
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -262,6 +459,7 @@
|
|||||||
<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>
|
<td class='text-muted'>-</td>
|
||||||
|
<td class='text-muted'>-</td>
|
||||||
</tr>";
|
</tr>";
|
||||||
} else {
|
} else {
|
||||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
@@ -278,6 +476,11 @@
|
|||||||
|
|
||||||
$nomeUscitaSafe = htmlspecialchars($row['nomeuscita']);
|
$nomeUscitaSafe = htmlspecialchars($row['nomeuscita']);
|
||||||
|
|
||||||
|
$nCert = (int)($row['n_certificati'] ?? 0);
|
||||||
|
$certBadge = $nCert > 0
|
||||||
|
? "<span class='badge-files badge-files-ok' title='Certificati di analisi caricati'>📄 {$nCert}</span>"
|
||||||
|
: "<span class='badge-files badge-files-none' title='Nessun certificato di analisi'>— nessuno</span>";
|
||||||
|
|
||||||
echo "<tr data-mescola-id='{$row['id']}'>
|
echo "<tr data-mescola-id='{$row['id']}'>
|
||||||
<td>{$row['id']}</td>
|
<td>{$row['id']}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -287,6 +490,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><span class='fw-semibold'>{$qtyTot}</span></td>
|
<td><span class='fw-semibold'>{$qtyTot}</span></td>
|
||||||
<td>{$linee}</td>
|
<td>{$linee}</td>
|
||||||
|
<td>{$certBadge}</td>
|
||||||
<td>{$badge}</td>
|
<td>{$badge}</td>
|
||||||
<td class='azioni-cell'>
|
<td class='azioni-cell'>
|
||||||
<button type='button' class='btn btn-sm btn-outline-dark associa-fornitori'
|
<button type='button' class='btn btn-sm btn-outline-dark associa-fornitori'
|
||||||
@@ -493,7 +697,7 @@
|
|||||||
|
|
||||||
<!-- MODALE FILE / REPOSITORY -->
|
<!-- MODALE FILE / REPOSITORY -->
|
||||||
<div class="modal fade" id="associaFileModal" tabindex="-1">
|
<div class="modal fade" id="associaFileModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header" style="background-color:#cfe3ff;">
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
<h5 class="modal-title">📁 File - <span id="fNomeUscita"></span></h5>
|
<h5 class="modal-title">📁 File - <span id="fNomeUscita"></span></h5>
|
||||||
@@ -503,22 +707,24 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="fIdMescola">
|
<input type="hidden" id="fIdMescola">
|
||||||
|
|
||||||
<div class="row g-2 align-items-end mb-3">
|
<div class="mb-3">
|
||||||
<div class="col-md-4">
|
<label class="form-label fw-semibold">Categoria del caricamento</label>
|
||||||
<label class="form-label fw-semibold">Categoria</label>
|
|
||||||
<select class="form-select" id="fCategoria"></select>
|
<select class="form-select" id="fCategoria"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-semibold">Titolo File</label>
|
<div class="dropzone" id="fDropzone">
|
||||||
<input type="text" class="form-control" id="fTitolo">
|
<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>
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-semibold">File</label>
|
|
||||||
<input type="file" class="form-control" id="fFileInput">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 text-end">
|
|
||||||
<button class="btn btn-add" id="fSaveBtn">➕ Carica File</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input type="file" id="fFileInput" multiple style="display:none;">
|
||||||
|
|
||||||
|
<div class="dz-queue" id="fQueue"></div>
|
||||||
|
|
||||||
|
<hr class="my-3">>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -997,45 +1203,293 @@
|
|||||||
// FILE / REPOSITORY
|
// FILE / REPOSITORY
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
|
function fEscape(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/[<>&"]/g, function(c) {
|
||||||
|
return ({
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'&': '&',
|
||||||
|
'"': '"'
|
||||||
|
})[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) {
|
function fLoadRows(idMescola) {
|
||||||
fetch("get_mescola_files.php?id=" + encodeURIComponent(idMescola))
|
fetch("get_mescola_files.php?id=" + encodeURIComponent(idMescola))
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// popola dropdown categorie (solo la prima volta che arrivano)
|
// popola dropdown categorie (solo la prima volta)
|
||||||
const sel = $("#fCategoria");
|
const sel = $("#fCategoria");
|
||||||
if (sel.children().length === 0 && data.categorie) {
|
if (sel.children().length === 0 && data.categorie) {
|
||||||
Object.keys(data.categorie).forEach(slug => {
|
Object.keys(data.categorie).forEach(slug => {
|
||||||
sel.append(`<option value="${slug}">${data.categorie[slug]}</option>`);
|
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");
|
const tbody = $("#fTable tbody");
|
||||||
tbody.empty();
|
tbody.empty();
|
||||||
|
|
||||||
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
|
if (!data.success || !Array.isArray(data.rows) || data.rows.length === 0) {
|
||||||
tbody.append(`<tr>
|
tbody.append(`<tr class="f-empty-row">
|
||||||
<td class="text-muted" colspan="5">Nessun file caricato</td>
|
<td class="text-muted" colspan="5">Nessun file caricato</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.rows.forEach(f => {
|
data.rows.forEach(f => tbody.append(fRenderRow(f)));
|
||||||
tbody.append(`
|
|
||||||
<tr>
|
|
||||||
<td>${f.categoria_label}</td>
|
|
||||||
<td>${f.titolo}</td>
|
|
||||||
<td><a href="${f.url}" target="_blank" rel="noopener">${f.original_filename}</a></td>
|
|
||||||
<td>${f.uploaded_at}</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-sm btn-outline-primary" href="${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>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 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
|
// Open modal
|
||||||
$(document).on("click", ".associa-file", function() {
|
$(document).on("click", ".associa-file", function() {
|
||||||
const idMescola = $(this).data("id");
|
const idMescola = $(this).data("id");
|
||||||
@@ -1043,65 +1497,13 @@
|
|||||||
|
|
||||||
$("#fIdMescola").val(idMescola);
|
$("#fIdMescola").val(idMescola);
|
||||||
$("#fNomeUscita").text(nomeUscita);
|
$("#fNomeUscita").text(nomeUscita);
|
||||||
$("#fTitolo").val("");
|
$("#fQueue").empty();
|
||||||
$("#fFileInput").val("");
|
$("#fFileInput").val("");
|
||||||
|
|
||||||
$("#associaFileModal").modal("show");
|
$("#associaFileModal").modal("show");
|
||||||
fLoadRows(idMescola);
|
fLoadRows(idMescola);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upload file
|
|
||||||
$("#fSaveBtn").on("click", function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const idMescola = $("#fIdMescola").val();
|
|
||||||
const categoria = $("#fCategoria").val();
|
|
||||||
const titolo = $("#fTitolo").val().trim();
|
|
||||||
const fileInput = document.getElementById("fFileInput");
|
|
||||||
|
|
||||||
if (!titolo) {
|
|
||||||
Swal.fire({
|
|
||||||
icon: "warning",
|
|
||||||
title: "Attenzione",
|
|
||||||
text: "Il titolo del file è obbligatorio"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!fileInput.files.length) {
|
|
||||||
Swal.fire({
|
|
||||||
icon: "warning",
|
|
||||||
title: "Attenzione",
|
|
||||||
text: "Seleziona un file da caricare"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("idmescola", idMescola);
|
|
||||||
formData.append("categoria", categoria);
|
|
||||||
formData.append("titolo", titolo);
|
|
||||||
formData.append("file", fileInput.files[0]);
|
|
||||||
|
|
||||||
fetch("save_mescola_file.php", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
$("#fTitolo").val("");
|
|
||||||
$("#fFileInput").val("");
|
|
||||||
fLoadRows(idMescola);
|
|
||||||
} else {
|
|
||||||
Swal.fire({
|
|
||||||
icon: "error",
|
|
||||||
title: "Errore",
|
|
||||||
text: data.message || "Upload non riuscito"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete file
|
// Delete file
|
||||||
$(document).on("click", ".f-del", function() {
|
$(document).on("click", ".f-del", function() {
|
||||||
const id = $(this).data("id");
|
const id = $(this).data("id");
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||||
|
$titolo = trim($_POST['titolo'] ?? '');
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($titolo === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il titolo non può essere vuoto']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titolo = mb_substr($titolo, 0, 255);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE mescole_files SET titolo = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$titolo, $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'titolo' => $titolo]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -16,10 +16,6 @@ if (!array_key_exists($categoria, $categorie)) {
|
|||||||
echo json_encode(['success' => false, 'message' => 'Categoria non valida']);
|
echo json_encode(['success' => false, 'message' => 'Categoria non valida']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($titolo === '') {
|
|
||||||
echo json_encode(['success' => false, 'message' => 'Il titolo del file è obbligatorio']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
echo json_encode(['success' => false, 'message' => 'File mancante o upload non riuscito']);
|
echo json_encode(['success' => false, 'message' => 'File mancante o upload non riuscito']);
|
||||||
exit;
|
exit;
|
||||||
@@ -34,6 +30,16 @@ $tmpPath = $_FILES['file']['tmp_name'];
|
|||||||
$size = (int)$_FILES['file']['size'];
|
$size = (int)$_FILES['file']['size'];
|
||||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Titolo automatico dal nome file se non fornito
|
||||||
|
if ($titolo === '') {
|
||||||
|
$titolo = pathinfo($originalName, PATHINFO_FILENAME);
|
||||||
|
$titolo = trim(preg_replace('/[_\-]+/', ' ', $titolo));
|
||||||
|
if ($titolo === '') {
|
||||||
|
$titolo = 'Documento del ' . date('d/m/Y');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$titolo = mb_substr($titolo, 0, 255);
|
||||||
|
|
||||||
if (!in_array($ext, $allowedExt, true)) {
|
if (!in_array($ext, $allowedExt, true)) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Estensione file non consentita (' . $ext . ')']);
|
echo json_encode(['success' => false, 'message' => 'Estensione file non consentita (' . $ext . ')']);
|
||||||
exit;
|
exit;
|
||||||
@@ -72,7 +78,25 @@ try {
|
|||||||
");
|
");
|
||||||
$stmt->execute([$idmescola, $categoria, $titolo, $storedName, $originalName, $mimeType, $size]);
|
$stmt->execute([$idmescola, $categoria, $titolo, $storedName, $originalName, $mimeType, $size]);
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $newId,
|
||||||
|
'row' => [
|
||||||
|
'id' => $newId,
|
||||||
|
'idmescola' => $idmescola,
|
||||||
|
'categoria' => $categoria,
|
||||||
|
'categoria_label' => $categorie[$categoria],
|
||||||
|
'titolo' => $titolo,
|
||||||
|
'filename' => $storedName,
|
||||||
|
'original_filename' => $originalName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'filesize' => $size,
|
||||||
|
'uploaded_at' => date('Y-m-d H:i:s'),
|
||||||
|
'url' => 'uploads/mescole/' . $idmescola . '/' . $storedName,
|
||||||
|
],
|
||||||
|
]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// rollback file se il DB fallisce
|
// rollback file se il DB fallisce
|
||||||
@unlink($destPath);
|
@unlink($destPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user