$(document).ready(function () { console.log("parts.js caricato correttamente"); // =================== // GLOBAL STATE // =================== let photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; // annotations keyed by photo src let photoAnnotations = {}; // colors keyed by part number let partColors = {}; // selection let selectedPartNumber = null; // =================== // POPUP HANDLING // =================== const partsButtons = document.querySelectorAll(".parts-btn"); const partsModal = document.getElementById("partsModal"); const closeBtn = document.querySelector("#partsModal .close-btn"); const overlay = document.querySelector(".overlay"); partsButtons.forEach((button) => { button.addEventListener("click", function () { console.log("Pulsante Parts cliccato"); const iddatadb = $(this).data("iddatadb"); const rowIndex = $(this).data("row"); const importRef = $("table tbody tr") .eq(rowIndex) .find("td") .eq(1) .text(); const description = $("table tbody tr").eq(rowIndex).find("td").eq(2).text() || "Sconosciuto"; $("#trfHeader").text(`${iddatadb} - ${importRef} - ${description}`); $("#partsModal").data("iddatadb", iddatadb); loadPhoto(iddatadb); loadExistingParts(iddatadb); if (partsModal) { const modal = new bootstrap.Modal(partsModal); modal.show(); } else { console.error("Modal Parts non trovato"); } }); }); if (closeBtn) { closeBtn.addEventListener("click", function () { partsModal.style.display = "none"; overlay.style.display = "none"; document.body.style.pointerEvents = "auto"; }); } if (partsModal) { window.addEventListener("click", function (event) { if (event.target === partsModal) { partsModal.style.display = "none"; overlay.style.display = "none"; document.body.style.pointerEvents = "auto"; } }); } // =================== // PHOTO LOADERS // =================== function loadPhoto(iddatadb) { $.ajax({ url: "load_photo.php", method: "GET", data: { iddatadb: iddatadb }, success: function (response) { if (response.success) { if (response.photos && response.photos.length > 1) { showPhotoSelector(response.photos); } else if ( response.photos && response.photos.length === 1 ) { loadSinglePhoto(response.photos[0]); } else { $("#samplePhoto").attr("src", ""); alert("Nessuna foto trovata per questo TRF."); } } else { alert( response.message || "Errore nel caricamento della foto.", ); } }, error: function (xhr, status, error) { alert("Errore nel caricamento della foto: " + error); }, }); } function showPhotoSelector(photos) { const selectorContainer = $("#photoSelectorContainer"); selectorContainer.empty(); const selector = $(''); photos.forEach((photo, index) => { const option = $("") .val(photo) .text(`Photo ${index + 1}`); if (photo.includes("/")) { const photoName = photo.split("/").pop(); option.text(`Photo ${index + 1} - ${photoName}`); } selector.append(option); }); selector.on("change", function () { const selectedPhoto = $(this).val(); loadSinglePhoto(selectedPhoto); }); selectorContainer.append(selector); selectorContainer.show(); if (photos.length > 0) { selector.val(photos[0]); loadSinglePhoto(photos[0]); } } function loadSinglePhoto(photoPath) { const img = $("#samplePhoto"); img.off("load"); // avoid stacking multiple handlers img.attr("src", photoPath); img.on("load", function () { const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); const naturalWidth = img[0].naturalWidth; const naturalHeight = img[0].naturalHeight; const parent = $(canvas).parent(); const maxW = parent.width(); const maxH = parent.height(); const scale = Math.min(maxW / naturalWidth, maxH / naturalHeight); const displayWidth = Math.max(1, Math.round(naturalWidth * scale)); const displayHeight = Math.max( 1, Math.round(naturalHeight * scale), ); photoData = { naturalWidth, naturalHeight, displayWidth, displayHeight, scale, }; canvas.width = naturalWidth; canvas.height = naturalHeight; canvas.style.width = `${displayWidth}px`; canvas.style.height = `${displayHeight}px`; $("#markerContainer").css({ width: `${displayWidth}px`, height: `${displayHeight}px`, }); $("#descriptionList").css({ maxWidth: `${Math.max(200, Math.round(displayWidth * 0.35))}px`, }); ctx.clearRect(0, 0, naturalWidth, naturalHeight); ctx.drawImage(img.get(0), 0, 0, naturalWidth, naturalHeight); updateMarkers(); updateDescriptions(); }); } // =================== // PARTS TABLE // =================== function addNewRow(nextPartNumber, isMix = false) { const description = isMix ? "Mix" : ""; const defaultColor = isMix ? "#0000ff" : "#ff0000"; const newRow = ` `; $("#partsTableBody").append(newRow); updateRowButtons(); // Initialize color for the new part const partNumber = nextPartNumber || 1; partColors[partNumber] = defaultColor; } function updateRowButtons() { const rowCount = $("#partsTableBody tr").length; $("#partsTableBody tr").each(function () { const $removeBtn = $(this).find(".remove-row"); if (rowCount > 1) $removeBtn.show(); else $removeBtn.hide(); }); } $(document).on("click", ".add-row", function (e) { e.preventDefault(); const maxPartNumber = Math.max( ...$("#partsTableBody tr") .map(function () { return parseInt($(this).find(".part-number").val()) || 0; }) .get(), ); addNewRow(maxPartNumber + 1); updatePartsList(); }); $(document).on("click", ".add-mix-row", function (e) { e.preventDefault(); const maxPartNumber = Math.max( ...$("#partsTableBody tr") .map(function () { return parseInt($(this).find(".part-number").val()) || 0; }) .get(), ); addNewRow(maxPartNumber + 1, true); updatePartsList(); }); $(document).on("click", ".remove-row", function (e) { e.preventDefault(); const $row = $(this).closest("tr"); const partId = $row.data("part-id"); const partNumber = $row.find(".part-number").val(); if (partId !== "new" && partId !== undefined && partId !== null) { $.ajax({ url: "delete_part.php", method: "POST", data: JSON.stringify({ part_id: partId }), contentType: "application/json", success: function (response) { if (response.success) { $row.remove(); delete partColors[partNumber]; updateRowButtons(); updatePartsList(); clearCanvasMarkers(false); // Preserve descriptions } else { alert("Errore nell'eliminazione: " + response.message); } }, error: function (xhr, status, error) { alert( "Errore nell'eliminazione: " + error + ". Stato: " + xhr.status + " - " + xhr.responseText, ); }, }); } else { $row.remove(); delete partColors[partNumber]; updateRowButtons(); updatePartsList(); } }); $(document).on("blur", ".part-description, .part-number", function () { const $input = $(this); const $row = $input.closest("tr"); const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val().trim(); const $saveStatus = $row.find(".save-status"); const $saveLoading = $row.find(".save-loading"); const iddatadb = $("#partsModal").data("iddatadb"); const isMix = partDescription.startsWith("Mix") ? "Y" : "N"; const partId = $row.data("part-id") || null; if (partDescription && iddatadb) { $saveLoading.show(); $saveStatus.hide(); $.ajax({ url: "save_parts.php", method: "POST", data: JSON.stringify({ iddatadb: iddatadb, parts: [ { id: partId, part_number: partNumber, part_description: partDescription, mix: isMix, }, ], }), contentType: "application/json", success: function (response) { if (response.success) { $saveLoading.hide(); $saveStatus.show(); updatePartsList(); if (response.part_id) { $row.attr("data-part-id", response.part_id); $row.data("part-id", response.part_id); } setTimeout(() => $saveStatus.hide(), 2000); } else { alert("Errore nel salvataggio: " + response.message); $saveLoading.hide(); } }, error: function (xhr, status, error) { alert("Errore nel salvataggio delle parti: " + error); $saveLoading.hide(); }, }); } }); $(document).on("change", ".part-color", function () { const partNumber = $(this).closest("li").data("part-number"); const partColor = $(this).val(); partColors[partNumber] = partColor; updateMarkers(); markUnsaved(); }); function loadExistingParts(iddatadb) { $.ajax({ url: "load_parts.php", method: "GET", data: { iddatadb: iddatadb }, success: function (response) { if (response.success) { $("#partsTableBody").empty(); if (response.parts.length > 0) { response.parts.forEach((part) => { const defaultColor = part.part_description.startsWith("Mix") ? "#0000ff" : "#ff0000"; const newRow = ` `; $("#partsTableBody").append(newRow); partColors[part.part_number] = defaultColor; }); } else { addNewRow(1); } updateRowButtons(); updatePartsList(); } else { alert( "Errore nel caricamento delle parti: " + response.message, ); addNewRow(1); } }, error: function (xhr, status, error) { alert("Errore nel caricamento delle parti: " + error); addNewRow(1); }, }); } function updatePartsList() { const showMixParts = $("#showMixParts").is(":checked"); $("#partsList").empty(); $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); const partColor = partColors[partNumber] || (partDescription.startsWith("Mix") ? "#0000ff" : "#ff0000"); if ( partNumber && partDescription && (showMixParts || !partDescription.startsWith("Mix")) ) { const listItem = `
  • ${partNumber} - ${partDescription}
  • `; $("#partsList").append(listItem); } }); updateMarkers(); } function renumberParts() { const $rows = $("#partsTableBody tr"); const iddatadb = $("#partsModal").data("iddatadb"); let newPartColors = {}; // Raccogli tutte le righe con i loro dati attuali let partsData = $rows .map(function (index) { const $row = $(this); const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val(); const partId = $row.data("part-id"); return { partNumber, partDescription, partId }; }) .get(); // Rinumera in modo sequenziale partsData.forEach((part, index) => { const newNumber = index + 1; newPartColors[newNumber] = partColors[part.partNumber] || "#ff0000"; part.partNumber = newNumber; }); // Aggiorna i valori nella tabella $rows.each(function (index) { const $row = $(this); $row.find(".part-number").val(index + 1); }); // Aggiorna partColors partColors = newPartColors; // Aggiorna i marker nelle annotazioni const currentPhoto = $("#samplePhoto").attr("src"); if (photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto].markers.forEach((marker) => { const oldPartNumber = marker.partNumber; const newPartNumber = partsData.find( (p) => p.partNumber == oldPartNumber, )?.partNumber; if (newPartNumber) { marker.partNumber = newPartNumber; marker.color = partColors[newPartNumber]; } }); } // Salva le modifiche nel database const partsToSave = partsData.map((part) => ({ id: part.partId || null, part_number: part.partNumber, part_description: part.partDescription, mix: part.partDescription.startsWith("Mix") ? "Y" : "N", })); console.log( "Dati inviati a renumber_parts.php:", JSON.stringify({ iddatadb: iddatadb, parts: partsToSave }), ); $.ajax({ url: "renumber_parts.php", method: "POST", data: JSON.stringify({ iddatadb: iddatadb, parts: partsToSave, }), contentType: "application/json", success: function (response) { console.log("Risposta da renumber_parts.php:", response); if (response.success) { $rows.each(function (index) { const $row = $(this); const newPartId = response.part_ids && response.part_ids[index] ? response.part_ids[index] : $row.data("part-id"); if (newPartId) { $row.attr("data-part-id", newPartId); $row.data("part-id", newPartId); } const $saveStatus = $row.find(".save-status"); const $saveLoading = $row.find(".save-loading"); $saveLoading.hide(); $saveStatus.show(); setTimeout(() => $saveStatus.hide(), 2000); }); updatePartsList(); updateMarkers(); updateDescriptions(); markUnsaved(); } else { console.error("Errore dal server:", response.message); alert( "Errore nella rinumerazione delle parti: " + response.message, ); } }, error: function (xhr, status, error) { console.error("Errore AJAX:", status, error, xhr.responseText); alert( "Errore nella rinumerazione delle parti: " + error + " - " + xhr.responseText, ); }, }); } $(document).on("click", ".add-to-mix-btn", function () { const $listItem = $(this).closest("li"); const partDescription = $listItem.text().split(" - ")[1].trim(); const $mixRow = $("#partsTableBody tr") .filter(function () { return $(this) .find(".part-description") .val() .startsWith("Mix"); }) .last(); if ($mixRow.length === 0) { alert("Crea prima una riga Mix usando il pulsante 'M'."); return; } const $descriptionInput = $mixRow.find(".part-description"); let currentDescription = $descriptionInput.val().trim(); if (currentDescription === "Mix") { currentDescription = `Mix ${partDescription}`; } else if (!currentDescription.includes(partDescription)) { currentDescription += ` + ${partDescription}`; } else { return; } $descriptionInput.val(currentDescription); $descriptionInput.trigger("blur"); updatePartsList(); }); $("#partsList").on("click", "li", function (e) { if ( $(e.target).hasClass("add-to-mix-btn") || $(e.target).hasClass("part-color") ) return; selectedPartNumber = $(this).data("part-number"); $(this).addClass("active").siblings().removeClass("active"); }); $("#showMixParts").on("change", function () { updatePartsList(); }); $("#renumberPartsBtn").on("click", function () { renumberParts(); }); // =================== // MARKERS & DESCRIPTIONS // =================== const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); $("#markerContainer").on("click", function (e) { if (selectedPartNumber === null) return; const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; const x = clickX / photoData.scale; // convert to NATURAL coords const y = clickY / photoData.scale; const currentPhoto = $("#samplePhoto").attr("src"); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, }; } const partColor = partColors[selectedPartNumber] || "#ff0000"; const existingMarker = photoAnnotations[currentPhoto].markers.find( (m) => m.partNumber == selectedPartNumber, ); if (existingMarker) { existingMarker.x = x; existingMarker.y = y; existingMarker.color = partColor; } else { photoAnnotations[currentPhoto].markers.push({ partNumber: selectedPartNumber, x, y, color: partColor, }); } updateMarkers(); updateDescriptions(); markUnsaved(); selectedPartNumber = null; $("#partsList li").removeClass("active"); }); function updateMarkers() { const markerContainer = $("#markerContainer"); markerContainer.empty(); markerContainer.css({ width: `${photoData.displayWidth}px`, height: `${photoData.displayHeight}px`, }); const currentPhoto = $("#samplePhoto").attr("src"); const annotations = photoAnnotations[currentPhoto] || { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, }; const markers = annotations.markers; const showMixParts = $("#showMixParts").is(":checked"); markers.forEach((marker) => { const partRow = $("#partsTableBody tr").filter(function () { return $(this).find(".part-number").val() == marker.partNumber; }); const partDescription = partRow.find(".part-description").val(); if ( !showMixParts && partDescription && partDescription.startsWith("Mix") ) { return; } const scaledX = marker.x * photoData.scale; const scaledY = marker.y * photoData.scale; const markerColor = marker.color || partColors[marker.partNumber] || "#ff0000"; const $marker = $( `
    ${marker.partNumber}
    `, ).css({ left: scaledX - 8 + "px", top: scaledY - 8 + "px", }); markerContainer.append($marker); makeDraggable($marker, marker); }); } function makeDraggable($element, item) { let isDragging = false; let startLeft = 0; let startTop = 0; let initialX = 0; let initialY = 0; $element.on("mousedown", function (e) { e.preventDefault(); isDragging = true; startLeft = parseFloat($element.css("left")) || 0; startTop = parseFloat($element.css("top")) || 0; initialX = e.clientX - startLeft; initialY = e.clientY - startTop; $element.css("z-index", 1001); }); $(document).on("mousemove.dragMarker", function (e) { if (!isDragging) return; let currentX = e.clientX - initialX; let currentY = e.clientY - initialY; const maxX = photoData.displayWidth - $element.width(); const maxY = photoData.displayHeight - $element.height(); currentX = Math.max(0, Math.min(currentX, maxX)); currentY = Math.max(0, Math.min(currentY, maxY)); $element.css({ left: currentX + "px", top: currentY + "px" }); if (item && item.partNumber) { item.x = (currentX + 8) / photoData.scale; item.y = (currentY + 8) / photoData.scale; markUnsaved(); } else { const currentPhoto = $("#samplePhoto").attr("src"); if (photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto].descriptionPosition.x = (currentX + 5) / photoData.scale; photoAnnotations[currentPhoto].descriptionPosition.y = (currentY + 5) / photoData.scale; markUnsaved(); } } }); $(document).on("mouseup.dragMarker", function () { if (!isDragging) return; isDragging = false; $element.css("z-index", 1000); $(document).off("mousemove.dragMarker mouseup.dragMarker"); }); } function updateDescriptions() { const currentPhoto = $("#samplePhoto").attr("src"); const annotations = photoAnnotations[currentPhoto] || { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, }; const showMixParts = $("#showMixParts").is(":checked"); const descriptionList = $("#descriptionList"); descriptionList.empty(); if (!annotations.hasDescriptions) { descriptionList.css("display", "none"); return; } const partsList = []; $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); if ( partNumber && partDescription && (showMixParts || !partDescription.startsWith("Mix")) ) { partsList.push(`${partNumber} ${partDescription}`); } }); descriptionList.css({ display: "block", top: annotations.descriptionPosition.y * photoData.scale + "px", left: annotations.descriptionPosition.x * photoData.scale + "px", }); partsList.forEach((part) => descriptionList.append(`
    ${part}
    `), ); updateMarkers(); } function clearCanvasMarkers(clearDescriptions = true) { const currentPhoto = $("#samplePhoto").attr("src"); if (clearDescriptions) { if (photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto].hasDescriptions = false; photoAnnotations[currentPhoto].descriptionPosition = { x: 10, y: 10, }; } $("#descriptionList").css("display", "none"); } $("#markerContainer").empty(); const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); canvas.width = photoData.naturalWidth; canvas.height = photoData.naturalHeight; canvas.style.width = `${photoData.displayWidth}px`; canvas.style.height = `${photoData.displayHeight}px`; const img = $("#samplePhoto"); ctx.clearRect(0, 0, canvas.width, canvas.height); if (img[0].naturalWidth) { ctx.drawImage(img.get(0), 0, 0, canvas.width, canvas.height); } markUnsaved(); updateMarkers(); } function undoLastMarker() { const currentPhoto = $("#samplePhoto").attr("src"); if ( photoAnnotations[currentPhoto] && photoAnnotations[currentPhoto].markers.length > 0 ) { photoAnnotations[currentPhoto].markers.pop(); updateMarkers(); updateDescriptions(); markUnsaved(); } } $("#addDescriptionsBtn").on("click", function () { const currentPhoto = $("#samplePhoto").attr("src"); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, }; } photoAnnotations[currentPhoto].hasDescriptions = true; updateDescriptions(); makeDraggable($("#descriptionList")); markUnsaved(); }); $("#removeAnnotationsBtn").on("click", function () { clearCanvasMarkers(true); // Remove only descriptions }); $("#undoMarkerBtn").on("click", function () { undoLastMarker(); }); let unsavedChanges = false; // --- helper functions --- function markUnsaved() { if (!unsavedChanges) { unsavedChanges = true; $("#savePhotoBtn").addClass("unsaved").text("⚠️ Salva Modifiche"); } } function clearUnsaved() { unsavedChanges = false; $("#savePhotoBtn").removeClass("unsaved").text("Salva Foto con Nome"); } // --- event listeners --- $(document).on("input change", "#partsTableBody input", markUnsaved); $(document).on("click", ".add-row, .add-mix-row, .remove-row", markUnsaved); $(document).on("markerChanged descriptionChanged", markUnsaved); // --- modal close protection --- $("#partsModal").on("hide.bs.modal", function (e) { if (unsavedChanges) { if (!confirm("Hai modifiche non salvate. Vuoi davvero uscire?")) { e.preventDefault(); } } }); // --- SAVE BUTTON --- $("#savePhotoBtn").on("click", function () { const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); const img = $("#samplePhoto"); const naturalWidth = img.get(0).naturalWidth; const naturalHeight = img.get(0).naturalHeight; canvas.width = naturalWidth; canvas.height = naturalHeight; ctx.drawImage(img.get(0), 0, 0, naturalWidth, naturalHeight); const currentPhoto = $("#samplePhoto").attr("src"); const annotations = photoAnnotations[currentPhoto] || { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, }; const showMixParts = $("#showMixParts").is(":checked"); if (annotations.hasDescriptions) { const partsList = []; $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); if ( partNumber && partDescription && (showMixParts || !partDescription.startsWith("Mix")) ) { partsList.push(`${partNumber} ${partDescription}`); } }); if (partsList.length > 0) { const fontSize = Math.round(naturalWidth * 0.02); ctx.font = fontSize + "px Arial"; const textHeight = fontSize + 8; const boxWidth = Math.round(naturalWidth * 0.28); const boxHeight = partsList.length * textHeight + 25; const x = annotations.descriptionPosition.x; const y = annotations.descriptionPosition.y; ctx.save(); ctx.shadowColor = "rgba(0,0,0,0.3)"; ctx.shadowBlur = 8; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3; ctx.fillStyle = "rgba(255, 255, 255, 0.9)"; ctx.beginPath(); ctx.roundRect(x, y, boxWidth, boxHeight, 12); ctx.fill(); ctx.restore(); ctx.fillStyle = "#111111"; partsList.forEach((part, index) => { ctx.fillText(part, x + 15, y + 35 + index * textHeight); }); } } const markers = annotations.markers; markers.forEach((marker) => { const partRow = $("#partsTableBody tr").filter(function () { return $(this).find(".part-number").val() == marker.partNumber; }); const partDescription = partRow.find(".part-description").val(); if ( !showMixParts && partDescription && partDescription.startsWith("Mix") ) { return; } const x = marker.x; const y = marker.y; const radius = Math.max(5, Math.round(naturalWidth * 0.025)); const fontSize = Math.max(8, Math.round(radius * 0.9)); const markerColor = marker.color || partColors[marker.partNumber] || "#ff0000"; ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.fillStyle = markerColor; // Use the stored color ctx.fill(); ctx.lineWidth = 3; ctx.strokeStyle = markerColor; // Use the same color for the border ctx.stroke(); ctx.fillStyle = "#ffffff"; ctx.font = `bold ${fontSize}px Arial`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(marker.partNumber || "", x, y); }); const dataURL = canvas.toDataURL("image/png"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const iddatadb = $("#partsModal").data("iddatadb"); const defaultName = `photo_${iddatadb}_${timestamp}.png`; const newName = prompt( "Inserisci il nome del file (senza estensione):", defaultName.split(".png")[0], ); if (newName) { const finalName = newName + "_" + timestamp + ".png"; $.ajax({ url: "save_annotated_photo.php", method: "POST", data: { dataURL: dataURL, filename: finalName, iddatadb: iddatadb, }, success: function (response) { if (response.success) { alert( "Foto salvata con successo: " + response.file_path, ); $("#samplePhoto").attr("src", response.file_path); loadPhoto(iddatadb); clearCanvasMarkers(false); // Preserve descriptions clearUnsaved(); } else { alert("Errore: " + response.message); } }, error: function (xhr, status, error) { alert("Errore Ajax: " + error); }, }); } }); // =================== // DEBUG HOVER LOGS // =================== $(document).on("mouseenter", "tr", function () { // console.log("Mouse entrato su riga"); }); $(document).on("mouseleave", "tr", function () { // console.log("Mouse uscito da riga"); }); });