added fiel tecnici tags additional 2 and fixed menu

This commit is contained in:
2026-10-19 10:19:15 +02:00
parent ac23738037
commit f649bc0bd5
9 changed files with 510 additions and 82 deletions
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddIsStrumentoIsCicloToMatriceAttachments extends AbstractMigration
{
public function change(): void
{
$this->table('matrice_attachments')
->addColumn('is_strumento', 'boolean', [
'default' => false,
'null' => false,
'after' => 'is_disegno',
])
->addColumn('is_ciclo', 'boolean', [
'default' => false,
'null' => false,
'after' => 'is_strumento',
])
->update();
}
}
@@ -130,7 +130,40 @@
});
};
let wsDisegniList = [];
// ================================================================
// Tab allegati della matrice: disegni tecnici, strumenti, cicli
// ================================================================
const WS_ATT_KINDS = {
disegni: {
flag: "is_disegno",
label: "Disegni tecnici",
icon: "📐",
itemLabel: "Disegno",
empty:
"Nessun disegno tecnico per questo profilo. " +
"Si possono segnare dalla gestione matrici (graffetta → pulsante compasso).",
},
strumenti: {
flag: "is_strumento",
label: "Strumenti di lavoro",
icon: "🔧",
itemLabel: "Strumento",
empty:
"Nessuno strumento di lavoro per questo profilo. " +
"Si possono segnare dalla gestione matrici (graffetta → pulsante chiave inglese).",
},
cicli: {
flag: "is_ciclo",
label: "Cicli di lavoro",
icon: "🔁",
itemLabel: "Ciclo",
empty:
"Nessun ciclo di lavoro per questo profilo. " +
"Si possono segnare dalla gestione matrici (graffetta → pulsante frecce).",
},
};
let wsAttLists = { disegni: [], strumenti: [], cicli: [] };
function wsFileExt(name) {
const p = String(name || "").split(".");
@@ -141,12 +174,15 @@
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
}
function showWorksheetDisegno(index) {
const f = wsDisegniList[index];
function showWorksheetAttachment(kind, index) {
const list = wsAttLists[kind] || [];
const f = list[index];
if (!f) return;
$(".ws-disegno-item").removeClass("active");
$(`.ws-disegno-item[data-index="${index}"]`).addClass("active");
$(`.ws-disegno-item[data-kind="${kind}"]`).removeClass("active");
$(
`.ws-disegno-item[data-kind="${kind}"][data-index="${index}"]`,
).addClass("active");
const name = f.file_name || "";
const path = f.file_url || f.file_path || "";
@@ -161,7 +197,7 @@
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(`
$(`#wsToolbar_${kind}`).html(`
<div>
<div class="fw-bold">${escapeHtml(name)}</div>
${f.description ? `<div class="small text-muted">${escapeHtml(f.description)}</div>` : ""}
@@ -170,18 +206,59 @@
<i class="fa-solid fa-arrow-up-right-from-square"></i> Apri in nuova scheda
</a>
`);
$("#wsDisegnoViewer").html(viewer);
$(`#wsViewer_${kind}`).html(viewer);
}
function loadWorksheetDisegni(idmatrice) {
wsDisegniList = [];
const $c = $("#wsDisegniContainer");
function renderWorksheetAttachmentKind(kind) {
const cfg = WS_ATT_KINDS[kind];
const list = wsAttLists[kind];
const $c = $(`#wsContainer_${kind}`);
$(`#wsCount_${kind}`).text(list.length);
if (!list.length) {
$c.html(
`<div class="text-muted p-3">${escapeHtml(cfg.empty)}</div>`,
);
return;
}
let listHtml = "";
if (list.length > 1) {
listHtml =
'<div class="ws-disegni-list">' +
list
.map(
(f, i) =>
`<button type="button" class="ws-disegno-item" data-kind="${kind}" data-index="${i}">${cfg.icon} ${escapeHtml(f.file_name || cfg.itemLabel + " " + (i + 1))}</button>`,
)
.join("") +
"</div>";
}
$c.html(`
${listHtml}
<div class="ws-disegno-toolbar" id="wsToolbar_${kind}"></div>
<div class="ws-disegno-viewer" id="wsViewer_${kind}"></div>
`);
// Se l'utente ha già aperto il tab mentre caricava, mostro subito il primo
if ($(`.ws-tab[data-tab="${kind}"]`).hasClass("active")) {
showWorksheetAttachment(kind, 0);
}
}
function loadWorksheetAttachments(idmatrice) {
wsAttLists = { disegni: [], strumenti: [], cicli: [] };
const kinds = Object.keys(WS_ATT_KINDS);
if (!idmatrice) {
$("#wsDisegniCount").text("0");
$c.html(
kinds.forEach((k) => {
$(`#wsCount_${k}`).text("0");
$(`#wsContainer_${k}`).html(
'<div class="text-muted">Nessun profilo collegato a questo foglio.</div>',
);
});
return;
}
@@ -193,51 +270,26 @@
data.message || "Errore caricamento allegati",
);
wsDisegniList = (data.attachments || []).filter(
(a) => parseInt(a.is_disegno, 10) === 1,
const all = data.attachments || [];
kinds.forEach((k) => {
const flag = WS_ATT_KINDS[k].flag;
wsAttLists[k] = all.filter(
(a) => parseInt(a[flag], 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);
}
renderWorksheetAttachmentKind(k);
});
})
.catch((err) => {
$("#wsDisegniCount").text("!");
$c.html(
'<div class="text-danger">Errore nel caricamento dei disegni: ' +
kinds.forEach((k) => {
$(`#wsCount_${k}`).text("!");
$(`#wsContainer_${k}`).html(
'<div class="text-danger">Errore nel caricamento degli allegati: ' +
escapeHtml(err.message || "") +
"</div>",
);
});
});
}
window.loadWorksheetDetail = function (worksheetId, endpoint) {
@@ -437,6 +489,28 @@
</div>
`;
const attTabsHtml = Object.keys(WS_ATT_KINDS)
.map((k) => {
const c = WS_ATT_KINDS[k];
return `
<button type="button" class="ws-tab" data-tab="${k}">
${c.icon} ${escapeHtml(c.label)} <span class="ws-tab-count" id="wsCount_${k}">…</span>
</button>`;
})
.join("");
const attPanesHtml = Object.keys(WS_ATT_KINDS)
.map((k) => {
const c = WS_ATT_KINDS[k];
return `
<div class="ws-tab-pane" data-pane="${k}">
<div id="wsContainer_${k}">
<div class="text-muted">Caricamento ${escapeHtml(c.label.toLowerCase())}...</div>
</div>
</div>`;
})
.join("");
const fullHtml = `
${headerHtml}
@@ -444,24 +518,18 @@
<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>
${attTabsHtml}
</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>
${attPanesHtml}
`;
$("#worksheetDetailContainer").html(fullHtml);
loadWorksheetDisegni(ws.idmatrice);
loadWorksheetAttachments(ws.idmatrice);
})
.catch((err) => {
console.error("Catch loadMatriceWorksheets:", err);
@@ -495,18 +563,21 @@
$(".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
// Carico il file solo la prima volta che si apre il tab
if (
tab === "disegni" &&
wsDisegniList.length &&
$("#wsDisegnoViewer").is(":empty")
WS_ATT_KINDS[tab] &&
(wsAttLists[tab] || []).length &&
$(`#wsViewer_${tab}`).is(":empty")
) {
showWorksheetDisegno(0);
showWorksheetAttachment(tab, 0);
}
});
$(document).on("click", ".ws-disegno-item", function () {
showWorksheetDisegno(parseInt($(this).data("index"), 10));
showWorksheetAttachment(
$(this).data("kind"),
parseInt($(this).data("index"), 10),
);
});
$(document).on("click", ".open-worksheet-detail", function () {
@@ -28,6 +28,8 @@ try {
description,
sort_order,
is_disegno,
is_strumento,
is_ciclo,
created_at,
updated_at
FROM matrice_attachments
@@ -52,6 +54,8 @@ try {
'description' => $row['description'] ?? '',
'sort_order' => (int)($row['sort_order'] ?? 0),
'is_disegno' => (int)($row['is_disegno'] ?? 0),
'is_strumento' => (int)($row['is_strumento'] ?? 0),
'is_ciclo' => (int)($row['is_ciclo'] ?? 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'])) : ''
];
@@ -27,6 +27,14 @@
background: #6f42c1;
}
.ws-tab[data-tab="strumenti"].active {
background: #fd7e14;
}
.ws-tab[data-tab="cicli"].active {
background: #0f9d77;
}
.ws-tab-count {
display: inline-block;
min-width: 24px;
@@ -67,6 +75,26 @@
color: #fff;
}
.ws-disegno-item[data-kind="strumenti"] {
border-color: #fd7e14;
color: #fd7e14;
}
.ws-disegno-item[data-kind="strumenti"].active {
background: #fd7e14;
color: #fff;
}
.ws-disegno-item[data-kind="cicli"] {
border-color: #0f9d77;
color: #0f9d77;
}
.ws-disegno-item[data-kind="cicli"].active {
background: #0f9d77;
color: #fff;
}
.ws-disegno-toolbar {
display: flex;
justify-content: space-between;
+129 -4
View File
@@ -577,6 +577,60 @@ function revisionLabel($rev)
background: #6f42c1;
color: #fff;
}
.attachments-existing-list .attachment-existing-row.is-ciclo:not(.is-disegno) {
border: 2px solid #0f9d77;
background: #effaf6;
}
.attachments-existing-list .attachment-existing-row.is-strumento:not(.is-disegno):not(.is-ciclo) {
border: 2px solid #fd7e14;
background: #fff6ee;
}
.badge-strumento,
.badge-ciclo {
display: inline-flex;
align-items: center;
gap: 5px;
color: #fff;
font-size: .78rem;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
margin-left: 6px;
vertical-align: middle;
}
.badge-strumento {
background: #fd7e14;
}
.badge-ciclo {
background: #0f9d77;
}
.btn-toggle-strumento {
border-color: #fd7e14;
color: #fd7e14;
}
.btn-toggle-strumento:hover,
.btn-toggle-strumento.active {
background: #fd7e14;
color: #fff;
}
.btn-toggle-ciclo {
border-color: #0f9d77;
color: #0f9d77;
}
.btn-toggle-ciclo:hover,
.btn-toggle-ciclo.active {
background: #0f9d77;
color: #fff;
}
</style>
</head>
@@ -1175,9 +1229,11 @@ function revisionLabel($rev)
return;
}
const list = (data.attachments || []).sort((a, b) =>
(parseInt(b.is_disegno, 10) || 0) - (parseInt(a.is_disegno, 10) || 0)
);
const flagScore = x =>
(parseInt(x.is_disegno, 10) || 0) * 4 +
(parseInt(x.is_ciclo, 10) || 0) * 2 +
(parseInt(x.is_strumento, 10) || 0);
const list = (data.attachments || []).sort((a, b) => flagScore(b) - flagScore(a));
const container = $("#existingAttachmentsList");
container.empty();
@@ -1196,6 +1252,8 @@ function revisionLabel($rev)
const createdAt = item.created_at || '';
const id = item.id || 0;
const isDisegno = (parseInt(item.is_disegno, 10) || 0) === 1;
const isStrumento = (parseInt(item.is_strumento, 10) || 0) === 1;
const isCiclo = (parseInt(item.is_ciclo, 10) || 0) === 1;
const previewHtml = isImageExt(ext) && filePath ?
`<img src="${escapeHtml(filePath)}" class="attachment-preview-thumb" alt="">` :
@@ -1221,7 +1279,7 @@ function revisionLabel($rev)
}
const html = `
<div class="attachment-existing-row ${isDisegno ? 'is-disegno' : ''}">
<div class="attachment-existing-row ${isDisegno ? 'is-disegno' : ''} ${isCiclo ? 'is-ciclo' : ''} ${isStrumento ? 'is-strumento' : ''}">
<div class="d-flex gap-3 align-items-start">
<div>${previewHtml}</div>
@@ -1231,6 +1289,8 @@ function revisionLabel($rev)
<div class="attachment-meta-title">
${escapeHtml(fileName)}
${isDisegno ? '<span class="badge-disegno"><i class="fa-solid fa-compass-drafting"></i> Disegno tecnico</span>' : ''}
${isStrumento ? '<span class="badge-strumento"><i class="fa-solid fa-screwdriver-wrench"></i> Strumento di lavoro</span>' : ''}
${isCiclo ? '<span class="badge-ciclo"><i class="fa-solid fa-arrows-rotate"></i> Ciclo di lavoro</span>' : ''}
</div>
<div class="attachment-meta-sub">${badge} ${createdAt ? ' • ' + escapeHtml(createdAt) : ''}</div>
</div>
@@ -1244,6 +1304,22 @@ function revisionLabel($rev)
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-toggle-strumento toggle-flag ${isStrumento ? 'active' : ''}"
data-id="${id}"
data-field="is_strumento"
data-value="${isStrumento ? 1 : 0}"
title="${isStrumento ? 'Togli da strumenti di lavoro' : 'Segna come strumento di lavoro'}">
<i class="fa-solid fa-screwdriver-wrench"></i>
</button>
<button type="button"
class="btn btn-sm btn-toggle-ciclo toggle-flag ${isCiclo ? 'active' : ''}"
data-id="${id}"
data-field="is_ciclo"
data-value="${isCiclo ? 1 : 0}"
title="${isCiclo ? 'Togli da cicli di lavoro' : 'Segna come ciclo di lavoro'}">
<i class="fa-solid fa-arrows-rotate"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-existing-file" data-id="${id}">Elimina</button>
</div>
</div>
@@ -1820,6 +1896,55 @@ function revisionLabel($rev)
});
});
$(document).on("click", ".toggle-flag", function() {
const $btn = $(this);
const id = $btn.data("id");
const field = $btn.data("field");
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) +
"&field=" + encodeURIComponent(field) +
"&value=" + 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";
+1 -1
View File
@@ -241,7 +241,7 @@
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
+1 -1
View File
@@ -245,7 +245,7 @@
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
+165 -2
View File
@@ -11,7 +11,6 @@
<!-- jQuery, Bootstrap, DataTables, SweetAlert -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet"
@@ -76,6 +75,68 @@
font-weight: 600;
padding: 10px 18px;
}
/* Tabella attrezzature: colonne fisse, testo a capo, ridimensionabili */
#tabTools {
width: 100% !important;
table-layout: fixed;
font-size: .95rem;
}
#tabTools td {
white-space: normal !important;
overflow-wrap: anywhere;
}
/* Titoli: a capo solo tra le parole, mai a metà */
#tabTools th {
position: relative;
white-space: normal !important;
word-break: normal;
overflow-wrap: normal;
overflow: hidden;
text-overflow: ellipsis;
}
/* Maniglia per ridimensionare */
.col-resizer {
position: absolute;
top: 0;
right: 0;
width: 7px;
height: 100%;
cursor: col-resize;
user-select: none;
z-index: 2;
}
.col-resizer:hover,
.col-resizer.resizing {
background: rgba(13, 110, 253, .35);
}
/* Lineetta sempre visibile tra i titoli */
.col-resizer::after {
content: "";
position: absolute;
top: 20%;
bottom: 20%;
right: 3px;
width: 2px;
border-radius: 2px;
background: #cbd5e1;
}
.col-resizer:hover::after,
.col-resizer.resizing::after {
background: #0d6efd;
}
body.col-resizing {
cursor: col-resize;
user-select: none;
}
</style>
</head>
@@ -298,11 +359,15 @@
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#tabTools').DataTable({
autoWidth: false,
initComplete: function() {
initColResize();
},
order: [
[0, 'asc']
],
@@ -312,6 +377,104 @@
}
});
// ↔️ COLONNE RIDIMENSIONABILI (salvate nel browser, doppio clic = ripristina)
function initColResize() {
const table = document.getElementById("tabTools");
const ths = Array.from(table.querySelectorAll("thead th"));
const KEY = "tabTools_colWidths_v2";
const MIN = 50;
let justResized = false;
// Nome, N.Reg, N.Seriale, Tipo, Produttore, Descrizione, Stato, Azioni (totale 100)
const DEFAULT = [30, 10, 10, 10, 10, 16, 8, 6];
function applyPct(pcts) {
ths.forEach((th, i) => th.style.width = pcts[i] + "%");
}
applyPct(DEFAULT);
try {
const saved = JSON.parse(localStorage.getItem(KEY) || "null");
if (Array.isArray(saved) && saved.length === ths.length) applyPct(saved);
} catch (e) {}
function savePct() {
const total = ths.reduce((s, th) => s + th.offsetWidth, 0);
const pcts = ths.map(th => +(th.offsetWidth / total * 100).toFixed(2));
applyPct(pcts);
try {
localStorage.setItem(KEY, JSON.stringify(pcts));
} catch (e) {}
}
// Evita che il rilascio del mouse dopo il trascinamento faccia ordinare la colonna
table.querySelector("thead").addEventListener("click", function(e) {
if (justResized) {
e.stopPropagation();
e.preventDefault();
justResized = false;
}
}, true);
ths.forEach((th, i) => {
if (i === ths.length - 1) return; // l'ultima colonna non ha maniglia
const handle = document.createElement("div");
handle.className = "col-resizer";
handle.title = "Trascina per allargare · doppio clic per ripristinare";
th.appendChild(handle);
handle.addEventListener("click", e => e.stopPropagation());
handle.addEventListener("dblclick", e => {
e.stopPropagation();
try {
localStorage.removeItem(KEY);
} catch (err) {}
applyPct(DEFAULT);
});
handle.addEventListener("mousedown", e => {
e.preventDefault();
e.stopPropagation();
const next = ths[i + 1];
const widths = ths.map(t => t.offsetWidth);
ths.forEach((t, j) => t.style.width = widths[j] + "px");
const startX = e.pageX;
const startW = widths[i];
const startN = widths[i + 1];
handle.classList.add("resizing");
document.body.classList.add("col-resizing");
function onMove(ev) {
let d = ev.pageX - startX;
d = Math.max(MIN - startW, Math.min(startN - MIN, d));
th.style.width = (startW + d) + "px";
next.style.width = (startN - d) + "px";
}
function onUp() {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
handle.classList.remove("resizing");
document.body.classList.remove("col-resizing");
justResized = true;
setTimeout(() => {
justResized = false;
}, 0);
savePct();
}
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
});
}
// ➕ ADD TOOL
$("#addToolForm").on("submit", function(e) {
e.preventDefault();
@@ -13,17 +13,30 @@ function jsonOut($success, $message = '')
exit;
}
// Campi ammessi => [messaggio attivato, messaggio disattivato]
$campi = [
'is_disegno' => ['Segnato come disegno tecnico', 'Rimosso dai disegni tecnici'],
'is_strumento' => ['Segnato come strumento di lavoro', 'Rimosso dagli strumenti di lavoro'],
'is_ciclo' => ['Segnato come ciclo di lavoro', 'Rimosso dai cicli di lavoro'],
];
$id = (int)($_POST['id'] ?? 0);
$value = ($_POST['is_disegno'] ?? '0') === '1' ? 1 : 0;
$field = (string)($_POST['field'] ?? 'is_disegno');
$raw = $_POST['value'] ?? ($_POST['is_disegno'] ?? '0');
$value = ((string)$raw === '1') ? 1 : 0;
if ($id <= 0) {
jsonOut(false, 'ID allegato non valido');
}
if (!isset($campi[$field])) {
jsonOut(false, 'Categoria non valida');
}
try {
$st = $pdo->prepare("UPDATE matrice_attachments SET is_disegno = ? WHERE id = ?");
$st = $pdo->prepare("UPDATE matrice_attachments SET `$field` = ? WHERE id = ?");
$st->execute([$value, $id]);
jsonOut(true, $value ? 'Segnato come disegno tecnico' : 'Rimosso dai disegni tecnici');
jsonOut(true, $value ? $campi[$field][0] : $campi[$field][1]);
} catch (Throwable $e) {
jsonOut(false, 'Errore: ' . $e->getMessage());
}