fixed import fogli di lavoro and disegni tecnici

This commit is contained in:
2026-10-02 11:24:51 +02:00
parent 63674ea6be
commit c2212f7022
11 changed files with 802 additions and 87 deletions
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddIsDisegnoToMatriceAttachments extends AbstractMigration
{
public function change(): void
{
$this->table('matrice_attachments')
->addColumn('is_disegno', 'boolean', [
'default' => false,
'null' => false,
])
->update();
}
}
@@ -130,6 +130,116 @@
});
};
let wsDisegniList = [];
function wsFileExt(name) {
const p = String(name || "").split(".");
return p.length > 1 ? p.pop().toLowerCase() : "";
}
function wsIsImage(ext) {
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
}
function showWorksheetDisegno(index) {
const f = wsDisegniList[index];
if (!f) return;
$(".ws-disegno-item").removeClass("active");
$(`.ws-disegno-item[data-index="${index}"]`).addClass("active");
const name = f.file_name || "";
const path = f.file_url || f.file_path || "";
const ext = wsFileExt(name);
let viewer;
if (ext === "pdf") {
viewer = `<iframe src="${escapeHtml(path)}#zoom=page-width"></iframe>`;
} else if (wsIsImage(ext)) {
viewer = `<div class="img-wrap"><img src="${escapeHtml(path)}" alt="${escapeHtml(name)}"></div>`;
} else {
viewer = `<div class="h-100 d-flex justify-content-center align-items-center text-muted">Anteprima non disponibile per questo formato: usa "Apri in nuova scheda"</div>`;
}
$("#wsDisegnoToolbar").html(`
<div>
<div class="fw-bold">${escapeHtml(name)}</div>
${f.description ? `<div class="small text-muted">${escapeHtml(f.description)}</div>` : ""}
</div>
<a href="${escapeHtml(path)}" target="_blank" class="worksheet-open-link">
<i class="fa-solid fa-arrow-up-right-from-square"></i> Apri in nuova scheda
</a>
`);
$("#wsDisegnoViewer").html(viewer);
}
function loadWorksheetDisegni(idmatrice) {
wsDisegniList = [];
const $c = $("#wsDisegniContainer");
if (!idmatrice) {
$("#wsDisegniCount").text("0");
$c.html(
'<div class="text-muted">Nessun profilo collegato a questo foglio.</div>',
);
return;
}
fetch("get_matrice_attachments.php?id=" + encodeURIComponent(idmatrice))
.then((r) => r.json())
.then((data) => {
if (!data.success)
throw new Error(
data.message || "Errore caricamento allegati",
);
wsDisegniList = (data.attachments || []).filter(
(a) => parseInt(a.is_disegno, 10) === 1,
);
$("#wsDisegniCount").text(wsDisegniList.length);
if (!wsDisegniList.length) {
$c.html(
'<div class="text-muted p-3">Nessun disegno tecnico per questo profilo. ' +
"Si possono segnare dalla gestione matrici (graffetta → pulsante compasso).</div>",
);
return;
}
let listHtml = "";
if (wsDisegniList.length > 1) {
listHtml =
'<div class="ws-disegni-list">' +
wsDisegniList
.map(
(f, i) =>
`<button type="button" class="ws-disegno-item" data-index="${i}">📐 ${escapeHtml(f.file_name || "Disegno " + (i + 1))}</button>`,
)
.join("") +
"</div>";
}
$c.html(`
${listHtml}
<div class="ws-disegno-toolbar" id="wsDisegnoToolbar"></div>
<div class="ws-disegno-viewer" id="wsDisegnoViewer"></div>
`);
// Se l'utente ha già aperto il tab mentre caricava, mostro subito il primo
if ($('.ws-tab[data-tab="disegni"]').hasClass("active")) {
showWorksheetDisegno(0);
}
})
.catch((err) => {
$("#wsDisegniCount").text("!");
$c.html(
'<div class="text-danger">Errore nel caricamento dei disegni: ' +
escapeHtml(err.message || "") +
"</div>",
);
});
}
window.loadWorksheetDetail = function (worksheetId, endpoint) {
endpoint = endpoint || "ajax/worksheet-linked-data.php";
@@ -215,8 +325,8 @@
`;
}
const html = `
<div class="worksheet-title-box mb-4">
const headerHtml = `
<div class="worksheet-title-box mb-3">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div>
<h4 class="mb-1">${escapeHtml(ws.worksheet_number_label || "—")} / Rev. ${escapeHtml(ws.revision_label || "0")}</h4>
@@ -237,6 +347,9 @@
</div>
</div>
`;
const html = `
<div class="row g-3">
<div class="col-lg-6">
<div class="readonly-card">
@@ -324,7 +437,31 @@
</div>
`;
$("#worksheetDetailContainer").html(html);
const fullHtml = `
${headerHtml}
<div class="ws-tabs">
<button type="button" class="ws-tab active" data-tab="dati">
<i class="fa-solid fa-file-lines me-1"></i> Dati foglio
</button>
<button type="button" class="ws-tab" data-tab="disegni">
📐 Disegni tecnici <span class="ws-tab-count" id="wsDisegniCount">…</span>
</button>
</div>
<div class="ws-tab-pane active" data-pane="dati">
${html}
</div>
<div class="ws-tab-pane" data-pane="disegni">
<div id="wsDisegniContainer">
<div class="text-muted">Caricamento disegni tecnici...</div>
</div>
</div>
`;
$("#worksheetDetailContainer").html(fullHtml);
loadWorksheetDisegni(ws.idmatrice);
})
.catch((err) => {
console.error("Catch loadMatriceWorksheets:", err);
@@ -349,6 +486,29 @@
).show();
});
$(document).on("click", ".ws-tab", function () {
const tab = $(this).data("tab");
$(".ws-tab").removeClass("active");
$(this).addClass("active");
$(".ws-tab-pane").removeClass("active");
$(`.ws-tab-pane[data-pane="${tab}"]`).addClass("active");
// Carico il PDF solo la prima volta che si apre il tab
if (
tab === "disegni" &&
wsDisegniList.length &&
$("#wsDisegnoViewer").is(":empty")
) {
showWorksheetDisegno(0);
}
});
$(document).on("click", ".ws-disegno-item", function () {
showWorksheetDisegno(parseInt($(this).data("index"), 10));
});
$(document).on("click", ".open-worksheet-detail", function () {
const worksheetId = $(this).data("id");
const endpoint =
@@ -27,6 +27,7 @@ try {
file_type,
description,
sort_order,
is_disegno,
created_at,
updated_at
FROM matrice_attachments
@@ -50,6 +51,7 @@ try {
'file_type' => $row['file_type'],
'description' => $row['description'] ?? '',
'sort_order' => (int)($row['sort_order'] ?? 0),
'is_disegno' => (int)($row['is_disegno'] ?? 0),
'created_at' => !empty($row['created_at']) ? date('d/m/Y H:i', strtotime($row['created_at'])) : '',
'updated_at' => !empty($row['updated_at']) ? date('d/m/Y H:i', strtotime($row['updated_at'])) : ''
];
+70 -29
View File
@@ -237,10 +237,12 @@ function extract_fields($grid)
$out['speed_expected_m_h'] = null;
$rv = row_by_label($grid, 'velocita di produzione');
if ($rv) {
$out['speed_expected_kg_h'] = num_or_null(cell($grid, $rv, 'C'));
$v = num_or_null(cell($grid, $rv, 'C'));
$out['speed_expected_kg_h'] = $v === null ? null : round($v, 2);
for ($i = $rv; $i <= $rv + 2; $i++) {
if (norm(cell($grid, $i, 'B')) === 'm/h') {
$out['speed_expected_m_h'] = num_or_null(cell($grid, $i, 'C'));
$v = num_or_null(cell($grid, $i, 'C'));
$out['speed_expected_m_h'] = $v === null ? null : round($v, 2);
break;
}
}
@@ -267,25 +269,37 @@ function extract_fields($grid)
* 3) MATCHER su DB
* ========================================================================= */
function norm_mesc($s)
function strip_code($s)
{
$s = strtolower((string)$s);
$s = preg_replace('/\s+/', ' ', $s);
return trim($s);
$s = str_replace(['à', 'è', 'é', 'ì', 'ò', 'ù'], ['a', 'e', 'e', 'i', 'o', 'u'], $s);
return preg_replace('/[^a-z0-9]+/', '', $s); // solo lettere + cifre (ignora spazi e punteggiatura)
}
/** porzione iniziale prima del primo separatore descrittivo (es. "ATES 71 - EPDM..." -> "ATES 71") */
function raw_head($s)
{
$parts = preg_split('/\s*[-–—|(:,]\s*/u', (string)$s, 2);
return trim($parts[0] ?? '');
}
function match_mescola($raw, array $mescole)
{
$r = norm_mesc($raw);
if ($r === '') return [null, 'none', null];
foreach ($mescole as $m) if (norm_mesc($m['nome']) === $r) return [(int)$m['id'], 'exact', $m['nome']];
foreach ($mescole as $m) {
foreach (explode('/', $m['nome']) as $part) {
if (norm_mesc($part) === $r) return [(int)$m['id'], 'exact-part', $m['nome']];
}
if (trim((string)$raw) === '') return [null, 'none', null];
$rawStrip = strip_code($raw);
$headStrip = strip_code(raw_head($raw));
// 1) uguaglianza ignorando spazi/punteggiatura, sul nome intero (gestisce "ATES 71" = "ATES71")
foreach ($mescole as $m) if ($rawStrip !== '' && strip_code($m['nome']) === $rawStrip) return [(int)$m['id'], 'exact', $m['nome']];
// 2) parte di nome composito (a / b / c)
foreach ($mescole as $m) foreach (explode('/', $m['nome']) as $p) if ($rawStrip !== '' && strip_code($p) === $rawStrip) return [(int)$m['id'], 'exact-part', $m['nome']];
// 3) nome in uscita
foreach ($mescole as $m) if ($rawStrip !== '' && strip_code($m['nomeuscita'] ?? '') === $rawStrip) return [(int)$m['id'], 'exact-uscita', $m['nome']];
// 4) codice iniziale prima di " - descrizione" (gestisce "ATES 71 - EPDM...")
if ($headStrip !== '' && $headStrip !== $rawStrip) {
foreach ($mescole as $m) if (strip_code($m['nome']) === $headStrip) return [(int)$m['id'], 'code', $m['nome']];
foreach ($mescole as $m) foreach (explode('/', $m['nome']) as $p) if (strip_code($p) === $headStrip) return [(int)$m['id'], 'code-part', $m['nome']];
}
foreach ($mescole as $m) if (norm_mesc($m['nomeuscita'] ?? '') === $r) return [(int)$m['id'], 'exact-uscita', $m['nome']];
foreach ($mescole as $m) if ($r !== '' && strpos(norm_mesc($m['nome']), $r) !== false) return [(int)$m['id'], 'contains', $m['nome']];
return [null, 'none', null];
}
@@ -309,24 +323,51 @@ function dim_tokens($s)
function match_packaging($raw, array $packaging, $preferCat = null)
{
$cands = dim_tokens($raw);
if (!$cands) return [null, 'none', null, null];
$best = null;
$rawStrip = strip_code($raw);
if ($rawStrip === '') return [null, 'none', null, null, true];
// 1) nome/codice ESATTO (categoria solo come spareggio) -> sicuro
$exact = [];
foreach ($packaging as $p) {
$itemForms = array_merge(dim_tokens($p['item_name']), dim_tokens($p['item_code']));
foreach ($cands as $c) {
if (in_array($c, $itemForms, true)) {
if ($preferCat && $p['category'] === $preferCat) {
return [(int)$p['id'], 'dimension', $p['item_name'], $p['category']];
}
if ($best === null) $best = $p;
if (strip_code($p['item_code']) === $rawStrip || strip_code($p['item_name']) === $rawStrip) $exact[] = $p;
}
if ($exact) {
$pick = $exact[0];
foreach ($exact as $p) if ($preferCat && $p['category'] === $preferCat) {
$pick = $p;
break;
}
return [(int)$pick['id'], 'name-code', $pick['item_name'], $pick['category'], false];
}
// 2) DIMENSIONI -> sicuro se unico, proposta se ambiguo
$rd = dim_tokens($raw);
if ($rd) {
$dm = [];
foreach ($packaging as $p) {
$pf = array_merge(dim_tokens($p['item_name']), dim_tokens($p['item_code']));
if (array_intersect($rd, $pf)) $dm[] = $p;
}
if ($dm) {
$pick = $dm[0];
foreach ($dm as $p) if ($preferCat && $p['category'] === $preferCat) {
$pick = $p;
break;
}
$ambiguous = count($dm) > 1;
return [(int)$pick['id'], 'dimension', $pick['item_name'], $pick['category'], $ambiguous];
}
}
if ($best) return [(int)$best['id'], 'dimension', $best['item_name'], $best['category']];
return [null, 'none', null, null];
}
// 3) ABBREVIAZIONE: testo corto, prefisso UNICO di un nome -> solo proposta
if (strlen($rawStrip) <= 4) {
$cand = [];
foreach ($packaging as $p) if (strpos(strip_code($p['item_name']), $rawStrip) === 0) $cand[] = $p;
if (count($cand) === 1) return [(int)$cand[0]['id'], 'abbrev', $cand[0]['item_name'], $cand[0]['category'], true];
}
return [null, 'none', null, null, true];
}
/* ---- match generico su dropdown (ws_lookup_options) ---- */
function lk_norm($s)
@@ -461,7 +502,7 @@ try {
];
foreach ($pkgSpecs as $spec) {
if ($spec['raw'] === '') continue;
[$pid, $pconf, $pname, $pcat] = match_packaging($spec['raw'], $packaging, $spec['cat']);
[$pid, $pconf, $pname, $pcat, $pneeds] = match_packaging($spec['raw'], $packaging, $spec['cat']);
$packagesOut[] = [
'source' => $spec['source'],
'raw' => $spec['raw'],
@@ -469,7 +510,7 @@ try {
'matched_name' => $pname,
'category' => $pcat,
'confidence' => $pconf,
'needs_review' => ($pid === null),
'needs_review' => ($pid === null) ? true : $pneeds,
];
}
@@ -1,3 +1,113 @@
<style>
.ws-tabs {
display: flex;
gap: 8px;
border-bottom: 2px solid #dbeafe;
margin-bottom: 16px;
flex-wrap: wrap;
}
.ws-tab {
border: 0;
background: #f1f5f9;
color: #475569;
font-weight: 700;
padding: 10px 18px;
border-radius: 12px 12px 0 0;
cursor: pointer;
font-size: 1rem;
}
.ws-tab.active {
background: #0d6efd;
color: #fff;
}
.ws-tab[data-tab="disegni"].active {
background: #6f42c1;
}
.ws-tab-count {
display: inline-block;
min-width: 24px;
padding: 1px 7px;
margin-left: 6px;
border-radius: 999px;
background: rgba(0, 0, 0, .12);
font-size: .8rem;
}
.ws-tab-pane {
display: none;
}
.ws-tab-pane.active {
display: block;
}
.ws-disegni-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.ws-disegno-item {
border: 2px solid #6f42c1;
background: #fff;
color: #6f42c1;
font-weight: 700;
padding: 8px 14px;
border-radius: 10px;
cursor: pointer;
}
.ws-disegno-item.active {
background: #6f42c1;
color: #fff;
}
.ws-disegno-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.ws-disegno-viewer {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: #f8fafc;
height: 70vh;
}
.ws-disegno-viewer iframe {
width: 100%;
height: 100%;
border: 0;
background: #fff;
display: block;
}
.ws-disegno-viewer .img-wrap {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #111;
}
.ws-disegno-viewer img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>
<!-- MODALE LISTA FOGLI DI LAVORO -->
<div class="modal fade" id="worksheetsListModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-worksheet-list">
+147 -21
View File
@@ -985,6 +985,22 @@ $isEdit = ($worksheet_id > 0);
margin-bottom: 16px;
font-size: 0.9rem;
}
tr.import-pending td {
background: #fff8db !important;
}
.pending-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fde68a;
color: #92400e;
border-radius: 999px;
padding: 3px 10px;
font-size: 0.75rem;
font-weight: 800;
}
</style>
</head>
@@ -1892,6 +1908,69 @@ $isEdit = ($worksheet_id > 0);
$(function() {
window.importStaged = null;
function renderPendingPackages(packages) {
$('#packageRowsBody tr.import-pending').remove();
$('#packageImportNote').remove();
const $tb = $('#packageRowsBody');
const nonAbbinati = [];
(packages || []).forEach(function(p) {
// righe solo per gli imballi ABBINATI (sicuri o proposti)
if (!p.package_code) {
nonAbbinati.push('“' + (p.raw || '') + '” (' + (p.source || '') + ')');
return;
}
const stato = p.needs_review ?
'<span class="pending-badge">❓ proposto — verifica</span>' :
'<span class="pending-badge">⏳ verrà aggiunto al salvataggio</span>';
const nome = p.matched_name || '—';
const $tr = $(
'<tr class="import-pending">' +
'<td>—</td>' +
'<td>' + (p.category || '—') + '</td>' +
'<td><div class="fw-bold">' + nome + '</div>' +
'<div class="text-muted small">dal file: ' + (p.raw || '') + ' (' + (p.source || '') + ')</div></td>' +
'<td>' + stato + '</td>' +
'</tr>'
);
$tb.prepend($tr);
});
// nota per gli imballi NON abbinati (niente righe vuote)
if (nonAbbinati.length) {
const $note = $(
'<div id="packageImportNote" class="import-hint" style="margin-bottom:10px;">' +
'⚠ Dal file non abbinati automaticamente: ' + nonAbbinati.join(', ') +
'. Aggiungili a mano qui sotto se servono.' +
'</div>'
);
$('#packageRowsBody').closest('.table-wrap').before($note);
}
}
function renderPendingMescole(mescole) {
// rimuove eventuali righe provvisorie precedenti
$('#tabellaMixRows tbody tr.import-pending').remove();
const $tb = $('#tabellaMixRows tbody');
(mescole || []).forEach(function(m) {
if (!m.idmescola) return; // le non riconosciute passano dal modal, non qui
const nome = m.matched_name || m.raw || '-';
const $tr = $(
'<tr class="import-pending">' +
'<td>—</td>' +
'<td><div class="fw-bold">' + nome + '</div>' +
'<div class="text-muted small">dal file: ' + (m.raw || '') + '</div></td>' +
'<td>' + (m.mix_weight_g_m ?? '-') + '</td>' +
'<td>' + (m.required_density ?? '-') + '</td>' +
'<td>' + (m.required_hardness_shore_a ?? '-') + '</td>' +
'<td>' + (m.lubrication_type ?? '-') + '</td>' +
'<td><span class="pending-badge">⏳ verrà aggiunta al salvataggio</span></td>' +
'</tr>'
);
$tb.prepend($tr);
});
}
function postForm(obj) {
return fetch('', {
method: 'POST',
@@ -1949,41 +2028,51 @@ $isEdit = ($worksheet_id > 0);
window.finalizeImportIfAny = async function(newId) {
const staged = window.importStaged;
if (!staged) return false;
const unresolved = [];
const unresolvedPkgs = [];
const matchedMescole = [];
const unmatchedMescole = [];
// imballi: salva solo quelli SICURI; i proposti li segnaliamo da confermare
for (const p of (staged.packages || [])) {
if (p.package_code) {
if (p.package_code && !p.needs_review) {
await postForm({
ajax: 1,
action: 'save_package_row',
worksheet_id: newId,
package_code: p.package_code
});
} else if (p.package_code && p.needs_review) {
unresolvedPkgs.push('Imballo “' + p.raw + '” (' + p.source + ') → proposto: ' + (p.matched_name || '') + ' — verifica e aggiungi a mano');
} else {
unresolved.push('Imballo “' + p.raw + '” (' + p.source + ')');
unresolvedPkgs.push('Imballo “' + p.raw + '” (' + p.source + ') → nessun abbinamento, aggiungi a mano');
}
}
// mescole: separa risolte / non risolte
for (const m of (staged.mescole || [])) {
if (m.idmescola) {
await postForm({
ajax: 1,
action: 'save_mix_row',
worksheet_id: newId,
idmescola: m.idmescola,
mix_weight_g_m: m.mix_weight_g_m ?? '',
required_density: m.required_density ?? '',
required_hardness_shore_a: m.required_hardness_shore_a ?? '',
lubrication_type: m.lubrication_type ?? '',
lubrication_notes: ''
});
} else {
unresolved.push('Mescola “' + m.raw + '”');
}
if (m.idmescola) matchedMescole.push(m);
else unmatchedMescole.push(m);
}
// salva automaticamente le mescole riconosciute
for (const m of matchedMescole) {
await postForm({
ajax: 1,
action: 'save_mix_row',
worksheet_id: newId,
idmescola: m.idmescola,
mix_weight_g_m: m.mix_weight_g_m ?? '',
required_density: m.required_density ?? '',
required_hardness_shore_a: m.required_hardness_shore_a ?? '',
lubrication_type: m.lubrication_type ?? '',
lubrication_notes: ''
});
}
window.importStaged = null;
// Allega il file Excel di origine al foglio appena creato
// allega il file Excel di origine al foglio appena creato
if (window.importFile) {
try {
const fd = new FormData();
@@ -1999,10 +2088,44 @@ $isEdit = ($worksheet_id > 0);
window.importFile = null;
}
// mescola NON riconosciuta -> apri il modal già precompilato (il foglio ora esiste)
if (unmatchedMescole.length) {
const m = unmatchedMescole[0]; // il modulo ha 1 mescola per foglio
$('#wsId').val(newId);
$('#mixWorksheetId').val(newId);
if (!window.location.search.includes('id=')) {
window.history.replaceState({}, '', 'manage-worksheet.php?id=' + newId);
}
const extra = unresolvedPkgs.length ? ('\n\nDa aggiungere a mano: ' + unresolvedPkgs.join(', ')) : '';
await Swal.fire({
icon: 'info',
title: 'Mescola da abbinare',
text: 'La mescola “' + m.raw + '” non è stata riconosciuta. Selezionala dal menu: peso, densità, durezza e lubrificazione sono già compilati.' + extra
});
// precompila i campi dopo che il modal (e select2) è pronto
$('#mixModal').one('shown.bs.modal', function() {
$('#mixModalTitle').text('Mescola dal file: ' + m.raw + ' — seleziona la corrispondenza');
$('#mixRowId').val(0);
$('#mixWorksheetId').val(newId);
$('#mix_weight_g_m').val(m.mix_weight_g_m ?? '');
$('#required_density').val(m.required_density ?? '');
$('#required_hardness_shore_a').val(m.required_hardness_shore_a ?? '');
$('#lubrication_type').val(m.lubrication_type ?? '');
$('#idmescola').val('').trigger('change');
});
const modal = new bootstrap.Modal(document.getElementById('mixModal'));
modal.show();
return true; // niente reload: ci pensa il salvataggio del modal (save_mix_row + reload)
}
// tutto risolto: messaggio + reload
let msg = 'Testata e righe importate salvate correttamente.';
if (unresolved.length) msg += '\n\nDa aggiungere a mano dopo il ricaricamento:\n• ' + unresolved.join('\n• ');
if (unresolvedPkgs.length) msg += '\n\nDa aggiungere a mano dopo il ricaricamento:\n• ' + unresolvedPkgs.join('\n• ');
await Swal.fire({
icon: unresolved.length ? 'warning' : 'success',
icon: unresolvedPkgs.length ? 'warning' : 'success',
title: 'Import completato',
text: msg
});
@@ -2031,6 +2154,7 @@ $isEdit = ($worksheet_id > 0);
const fd = new FormData();
fd.append('xlsx_file', file);
$('#packageImportNote').remove(); // azzera nota import precedente
Swal.fire({
title: 'Lettura Excel…',
allowOutsideClick: false,
@@ -2083,6 +2207,8 @@ $isEdit = ($worksheet_id > 0);
warnings: data.warnings || []
};
renderBanner(data);
renderPendingMescole(data.mescole || []);
renderPendingPackages(data.packages || []);
Swal.fire({
icon: 'success',
+159 -11
View File
@@ -547,6 +547,36 @@ function revisionLabel($rev)
color: #fff;
opacity: .95;
}
.attachments-existing-list .attachment-existing-row.is-disegno {
border: 2px solid #6f42c1;
background: #f7f3ff;
}
.badge-disegno {
display: inline-flex;
align-items: center;
gap: 5px;
background: #6f42c1;
color: #fff;
font-size: .78rem;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
margin-left: 6px;
vertical-align: middle;
}
.btn-toggle-disegno {
border-color: #6f42c1;
color: #6f42c1;
}
.btn-toggle-disegno:hover,
.btn-toggle-disegno.active {
background: #6f42c1;
color: #fff;
}
</style>
</head>
@@ -713,6 +743,7 @@ function revisionLabel($rev)
data-nome='" . htmlspecialchars($row['nome'], ENT_QUOTES) . "'
data-cliente='" . htmlspecialchars($row['cliente'], ENT_QUOTES) . "'
data-data='{$dataIT}'
data-photo='" . htmlspecialchars($hasPhoto ? $thumb : '', ENT_QUOTES) . "'
data-bs-toggle='tooltip' data-bs-title='Modifica'>
<i class='fa-solid fa-pen-to-square'></i>
</button>
@@ -797,6 +828,19 @@ function revisionLabel($rev)
<input type="text" class="form-control" id="dataMatrice" placeholder="dd/mm/yyyy">
</div>
<div class="mb-3">
<label class="fw-semibold">Foto</label>
<input type="file" class="form-control" id="fotoMatrice" accept=".jpg,.jpeg,.png,.gif,.webp,image/*">
<input type="hidden" id="removeFotoMatrice" value="0">
<div id="fotoMatricePreviewWrap" class="mt-2 text-center" style="display:none;">
<img id="fotoMatricePreview" src="" style="max-width:100%; max-height:200px; border-radius:10px; border:1px solid #dee2e6;">
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-danger" id="btnRimuoviFoto">🗑️ Rimuovi foto</button>
</div>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-add">💾 Salva</button>
</div>
@@ -1131,7 +1175,9 @@ function revisionLabel($rev)
return;
}
const list = data.attachments || [];
const list = (data.attachments || []).sort((a, b) =>
(parseInt(b.is_disegno, 10) || 0) - (parseInt(a.is_disegno, 10) || 0)
);
const container = $("#existingAttachmentsList");
container.empty();
@@ -1149,6 +1195,7 @@ function revisionLabel($rev)
const badge = getFileTypeBadge(ext);
const createdAt = item.created_at || '';
const id = item.id || 0;
const isDisegno = (parseInt(item.is_disegno, 10) || 0) === 1;
const previewHtml = isImageExt(ext) && filePath ?
`<img src="${escapeHtml(filePath)}" class="attachment-preview-thumb" alt="">` :
@@ -1174,19 +1221,29 @@ function revisionLabel($rev)
}
const html = `
<div class="attachment-existing-row">
<div class="attachment-existing-row ${isDisegno ? 'is-disegno' : ''}">
<div class="d-flex gap-3 align-items-start">
<div>${previewHtml}</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start gap-2">
<div>
<div class="attachment-meta-title">${escapeHtml(fileName)}</div>
<div class="attachment-meta-title">
${escapeHtml(fileName)}
${isDisegno ? '<span class="badge-disegno"><i class="fa-solid fa-compass-drafting"></i> Disegno tecnico</span>' : ''}
</div>
<div class="attachment-meta-sub">${badge} ${createdAt ? ' • ' + escapeHtml(createdAt) : ''}</div>
</div>
<div class="d-flex gap-1">
${openButtonHtml}
<button type="button"
class="btn btn-sm btn-toggle-disegno toggle-disegno ${isDisegno ? 'active' : ''}"
data-id="${id}"
data-value="${isDisegno ? 1 : 0}"
title="${isDisegno ? 'Togli da disegni tecnici' : 'Segna come disegno tecnico'}">
<i class="fa-solid fa-compass-drafting"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-existing-file" data-id="${id}">Elimina</button>
</div>
</div>
@@ -1272,12 +1329,49 @@ function revisionLabel($rev)
.attr('placeholder', 'Nome matrice, cliente…')
.addClass('form-control');
function resetFotoMatrice(photoUrl) {
$("#fotoMatrice").val('');
$("#removeFotoMatrice").val('0');
if (photoUrl) {
$("#fotoMatricePreview").attr("src", photoUrl);
$("#fotoMatricePreviewWrap").show();
} else {
$("#fotoMatricePreview").attr("src", "");
$("#fotoMatricePreviewWrap").hide();
}
}
$(document).on("click", ".edit", function() {
$("#idMatriceEdit").val($(this).data("id"));
$("#nomeMatrice").val($(this).data("nome"));
$("#clienteMatrice").val($(this).data("cliente"));
$("#dataMatrice").val($(this).data("data"));
new bootstrap.Modal(document.getElementById('addMatriceModal')).show();
resetFotoMatrice($(this).data("photo") || "");
bootstrap.Modal.getOrCreateInstance(document.getElementById('addMatriceModal')).show();
});
// Anteprima quando si sceglie un file
$("#fotoMatrice").on("change", function() {
const file = this.files && this.files[0];
if (!file) return;
$("#removeFotoMatrice").val('0');
$("#fotoMatricePreview").attr("src", URL.createObjectURL(file));
$("#fotoMatricePreviewWrap").show();
});
// Rimuovi foto
$("#btnRimuoviFoto").on("click", function() {
$("#fotoMatrice").val('');
$("#removeFotoMatrice").val('1');
$("#fotoMatricePreview").attr("src", "");
$("#fotoMatricePreviewWrap").hide();
});
// Alla chiusura del modale svuota tutto (così "Aggiungi" parte pulito)
document.getElementById('addMatriceModal').addEventListener('hidden.bs.modal', function() {
$("#addMatriceForm")[0].reset();
$("#idMatriceEdit").val('');
resetFotoMatrice('');
});
$("#addMatriceForm").on("submit", function(e) {
@@ -1289,15 +1383,21 @@ function revisionLabel($rev)
let dataIT = $("#dataMatrice").val().trim();
let dataSQL = convertToMySQLDate(dataIT);
const formData = new FormData();
formData.append("id", id);
formData.append("nome", nome);
formData.append("cliente", cliente);
formData.append("data", dataSQL);
formData.append("remove_photo", $("#removeFotoMatrice").val());
const fotoFile = $("#fotoMatrice")[0].files[0];
if (fotoFile) {
formData.append("photo", fotoFile);
}
fetch("save_matrice.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "id=" + encodeURIComponent(id) +
"&nome=" + encodeURIComponent(nome) +
"&cliente=" + encodeURIComponent(cliente) +
"&data=" + encodeURIComponent(dataSQL)
body: formData
})
.then(r => r.json())
.then(data => {
@@ -1672,6 +1772,54 @@ function revisionLabel($rev)
});
});
$(document).on("click", ".toggle-disegno", function() {
const $btn = $(this);
const id = $btn.data("id");
const newValue = parseInt($btn.data("value"), 10) === 1 ? 0 : 1;
const idmatrice = $("#mf_idmatrice").val();
$btn.prop("disabled", true);
fetch("toggle_matrice_attachment_disegno.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "id=" + encodeURIComponent(id) + "&is_disegno=" + newValue
})
.then(r => r.json())
.then(data => {
if (!data.success) {
Swal.fire({
icon: "error",
title: "Errore",
text: data.message || ""
});
$btn.prop("disabled", false);
return;
}
Swal.fire({
toast: true,
position: "top-end",
icon: "success",
title: data.message,
showConfirmButton: false,
timer: 1500
});
loadExistingAttachments(idmatrice);
})
.catch(() => {
Swal.fire({
icon: "error",
title: "Errore",
text: "Errore di comunicazione con il server"
});
$btn.prop("disabled", false);
});
});
$(document).on("click", ".preview-existing-file", function() {
const file = $(this).data("file") || "";
const name = $(this).data("name") || "Anteprima file";
Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

+11 -3
View File
@@ -1912,7 +1912,9 @@ if (!empty($_GET['ajax'])) {
return;
}
const files = r.attachments || [];
const files = (r.attachments || []).sort((a, b) =>
(parseInt(b.is_disegno, 10) || 0) - (parseInt(a.is_disegno, 10) || 0)
);
if (!files.length) {
$("#matriceFilesList").html('<div style="color:#64748b;">Nessun allegato disponibile per questa matrice.</div>');
@@ -1929,6 +1931,7 @@ if (!empty($_GET['ajax'])) {
const ext = getFileExt(fileName);
const icon = getFileIcon(ext);
const badge = getFileBadge(ext);
const isDisegno = (parseInt(f.is_disegno, 10) || 0) === 1;
let actionHtml = '';
if (isImageExt(ext) || ext === 'pdf') {
@@ -1966,7 +1969,9 @@ if (!empty($_GET['ajax'])) {
}
html += `
<div style="border:1px solid #e2e8f0; border-radius:14px; padding:12px; background:#fff;">
<div style="${isDisegno
? 'border:2px solid #6f42c1; background:#f7f3ff;'
: 'border:1px solid #e2e8f0; background:#fff;'} border-radius:14px; padding:12px;">
<div style="display:flex; gap:14px; align-items:flex-start;">
<div>${previewHtml}</div>
@@ -1974,7 +1979,10 @@ if (!empty($_GET['ajax'])) {
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<div style="font-weight:700; color:#1f2d3d;">${escapeHtml(fileName)}</div>
<div style="margin-top:4px;">${badge}</div>
<div style="margin-top:4px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
${badge}
${isDisegno ? '<span style="background:#6f42c1;color:#fff;padding:3px 10px;border-radius:999px;font-size:0.78rem;font-weight:700;">📐 Disegno tecnico</span>' : ''}
</div>
${createdAt ? `<div style="margin-top:6px; color:#64748b; font-size:0.88rem;">${escapeHtml(createdAt)}</div>` : ''}
</div>
<div>${actionHtml}</div>
+93 -20
View File
@@ -1,28 +1,101 @@
<?php
require_once("include/headscript.php");
$db = DBHandlerSelect::getInstance();
ob_start();
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$id = $_POST['id'] ?? null;
$nome = $_POST['nome'] ?? '';
$cliente = $_POST['cliente'] ?? '';
$data = $_POST['data'] ?? null; // formato YYYY-MM-DD
if (!$nome) {
echo json_encode(["success" => false, "message" => "Nome obbligatorio"]);
function jsonOut($success, $message = '', $extra = [])
{
if (ob_get_length()) ob_clean();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array_merge(['success' => $success, 'message' => $message], $extra));
exit;
}
if ($id) {
// UPDATE
$sql = "UPDATE matrice SET nome = ?, cliente = ?, data_produzione = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([$nome, $cliente, $data, $id]);
} else {
// INSERT
$sql = "INSERT INTO matrice (nome, cliente, data_produzione) VALUES (?,?,?)";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([$nome, $cliente, $data]);
$id = (int)($_POST['id'] ?? 0);
$nome = trim($_POST['nome'] ?? '');
$cliente = trim($_POST['cliente'] ?? '');
$data = trim($_POST['data'] ?? '');
$removePhoto = ($_POST['remove_photo'] ?? '0') === '1';
if ($nome === '') {
jsonOut(false, 'Il nome matrice è obbligatorio');
}
echo json_encode(["success" => $ok]);
$data = ($data !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $data)) ? $data : null;
$cliente = $cliente !== '' ? $cliente : null;
$photoDir = __DIR__ . '/photos/matrici/';
if (!is_dir($photoDir)) {
mkdir($photoDir, 0775, true);
}
// Validazione file (se presente)
$hasUpload = isset($_FILES['photo']) && $_FILES['photo']['error'] !== UPLOAD_ERR_NO_FILE;
$ext = '';
if ($hasUpload) {
if ($_FILES['photo']['error'] !== UPLOAD_ERR_OK) {
jsonOut(false, 'Errore upload foto (codice ' . $_FILES['photo']['error'] . ')');
}
if ($_FILES['photo']['size'] > 10 * 1024 * 1024) {
jsonOut(false, 'La foto supera i 10 MB');
}
$ext = strtolower(pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
jsonOut(false, 'Formato foto non consentito (jpg, jpeg, png, gif, webp)');
}
if (@getimagesize($_FILES['photo']['tmp_name']) === false) {
jsonOut(false, 'Il file caricato non è un\'immagine valida');
}
}
try {
$pdo->beginTransaction();
$oldPhoto = null;
if ($id > 0) {
$st = $pdo->prepare("SELECT photo FROM matrice WHERE id = ?");
$st->execute([$id]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$pdo->rollBack();
jsonOut(false, 'Matrice non trovata');
}
$oldPhoto = $row['photo'];
$st = $pdo->prepare("UPDATE matrice SET nome = ?, cliente = ?, data_produzione = ? WHERE id = ?");
$st->execute([$nome, $cliente, $data, $id]);
} else {
$st = $pdo->prepare("INSERT INTO matrice (nome, cliente, data_produzione) VALUES (?, ?, ?)");
$st->execute([$nome, $cliente, $data]);
$id = (int)$pdo->lastInsertId();
}
// Nuova foto
if ($hasUpload) {
$newName = 'matrice_' . $id . '_' . time() . '.' . $ext;
if (!move_uploaded_file($_FILES['photo']['tmp_name'], $photoDir . $newName)) {
$pdo->rollBack();
jsonOut(false, 'Impossibile salvare la foto sul server');
}
$pdo->prepare("UPDATE matrice SET photo = ? WHERE id = ?")->execute([$newName, $id]);
if ($oldPhoto && is_file($photoDir . basename($oldPhoto))) {
@unlink($photoDir . basename($oldPhoto));
}
} elseif ($removePhoto && $oldPhoto) {
// Rimozione foto
$pdo->prepare("UPDATE matrice SET photo = NULL WHERE id = ?")->execute([$id]);
if (is_file($photoDir . basename($oldPhoto))) {
@unlink($photoDir . basename($oldPhoto));
}
}
$pdo->commit();
jsonOut(true, 'Matrice salvata correttamente', ['id' => $id]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
jsonOut(false, 'Errore: ' . $e->getMessage());
}
@@ -0,0 +1,29 @@
<?php
ob_start();
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
function jsonOut($success, $message = '')
{
if (ob_get_length()) ob_clean();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $success, 'message' => $message]);
exit;
}
$id = (int)($_POST['id'] ?? 0);
$value = ($_POST['is_disegno'] ?? '0') === '1' ? 1 : 0;
if ($id <= 0) {
jsonOut(false, 'ID allegato non valido');
}
try {
$st = $pdo->prepare("UPDATE matrice_attachments SET is_disegno = ? WHERE id = ?");
$st->execute([$value, $id]);
jsonOut(true, $value ? 'Segnato come disegno tecnico' : 'Rimosso dai disegni tecnici');
} catch (Throwable $e) {
jsonOut(false, 'Errore: ' . $e->getMessage());
}