diff --git a/db/migrations/20261002090831_add_is_disegno_to_matrice_attachments.php b/db/migrations/20261002090831_add_is_disegno_to_matrice_attachments.php new file mode 100644 index 00000000..f3aa174b --- /dev/null +++ b/db/migrations/20261002090831_add_is_disegno_to_matrice_attachments.php @@ -0,0 +1,18 @@ +table('matrice_attachments') + ->addColumn('is_disegno', 'boolean', [ + 'default' => false, + 'null' => false, + ]) + ->update(); + } +} diff --git a/public/userarea/assets/js/worksheets-linked-modals.js b/public/userarea/assets/js/worksheets-linked-modals.js index 7da90d47..ab15de7b 100644 --- a/public/userarea/assets/js/worksheets-linked-modals.js +++ b/public/userarea/assets/js/worksheets-linked-modals.js @@ -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 = ``; + } else if (wsIsImage(ext)) { + viewer = `
${escapeHtml(name)}
`; + } else { + viewer = `
Anteprima non disponibile per questo formato: usa "Apri in nuova scheda"
`; + } + + $("#wsDisegnoToolbar").html(` +
+
${escapeHtml(name)}
+ ${f.description ? `
${escapeHtml(f.description)}
` : ""} +
+ + Apri in nuova scheda + + `); + $("#wsDisegnoViewer").html(viewer); + } + + function loadWorksheetDisegni(idmatrice) { + wsDisegniList = []; + const $c = $("#wsDisegniContainer"); + + if (!idmatrice) { + $("#wsDisegniCount").text("0"); + $c.html( + '
Nessun profilo collegato a questo foglio.
', + ); + 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( + '
Nessun disegno tecnico per questo profilo. ' + + "Si possono segnare dalla gestione matrici (graffetta → pulsante compasso).
", + ); + return; + } + + let listHtml = ""; + if (wsDisegniList.length > 1) { + listHtml = + '
' + + wsDisegniList + .map( + (f, i) => + ``, + ) + .join("") + + "
"; + } + + $c.html(` + ${listHtml} +
+
+ `); + + // 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( + '
Errore nel caricamento dei disegni: ' + + escapeHtml(err.message || "") + + "
", + ); + }); + } + window.loadWorksheetDetail = function (worksheetId, endpoint) { endpoint = endpoint || "ajax/worksheet-linked-data.php"; @@ -215,8 +325,8 @@ `; } - const html = ` -
+ const headerHtml = ` +

${escapeHtml(ws.worksheet_number_label || "—")} / Rev. ${escapeHtml(ws.revision_label || "0")}

@@ -237,6 +347,9 @@
+ `; + + const html = `
@@ -324,7 +437,31 @@
`; - $("#worksheetDetailContainer").html(html); + const fullHtml = ` + ${headerHtml} + +
+ + +
+ +
+ ${html} +
+ +
+
+
Caricamento disegni tecnici...
+
+
+ `; + + $("#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 = diff --git a/public/userarea/get_matrice_attachments.php b/public/userarea/get_matrice_attachments.php index 73339156..1f21ddb0 100644 --- a/public/userarea/get_matrice_attachments.php +++ b/public/userarea/get_matrice_attachments.php @@ -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'])) : '' ]; diff --git a/public/userarea/import_worksheet_xlsx.php b/public/userarea/import_worksheet_xlsx.php index 03b8e4ab..526b9b56 100644 --- a/public/userarea/import_worksheet_xlsx.php +++ b/public/userarea/import_worksheet_xlsx.php @@ -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, ]; } diff --git a/public/userarea/include/worksheets-linked-modals.php b/public/userarea/include/worksheets-linked-modals.php index cad9c5d1..7718274b 100644 --- a/public/userarea/include/worksheets-linked-modals.php +++ b/public/userarea/include/worksheets-linked-modals.php @@ -1,3 +1,113 @@ + +