added mescole files drag drop

This commit is contained in:
2026-07-24 08:53:49 +02:00
parent c11adf19e8
commit 151e3bbe95
4 changed files with 569 additions and 110 deletions
+5
View File
@@ -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
View File
@@ -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. Totale</th> <th>Q. 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 ({
'<': '&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) { 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");
+28
View File
@@ -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()]);
}
+29 -5
View File
@@ -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);