$(document).ready(function () { // =================== // GLOBAL STATE // =================== let photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; let photoAnnotations = {}; let partColors = {}; let partMatrice = {}; let selectedPartNumber = null; let unsavedChanges = false; let fabricCanvas = null; let descriptionTextbox = null; let markerObjects = {}; let matrici = []; // =================== // VOICE RECOGNITION SETUP // =================== const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition = null; let isVoiceActive = false; const magicWord = "salva"; if (SpeechRecognition) { recognition = new SpeechRecognition(); recognition.lang = "it-IT"; recognition.continuous = true; recognition.interimResults = false; recognition.onresult = function (event) { const transcript = event.results[ event.results.length - 1 ][0].transcript .trim() .toLowerCase(); const $currentRow = $("#partsTableBody tr:last"); const $descriptionInput = $currentRow.find(".part-description"); if (transcript.includes(magicWord)) { const cleanedTranscript = transcript .replace(magicWord, "") .trim(); if (cleanedTranscript) { $descriptionInput.val( ( $descriptionInput.val() + " " + cleanedTranscript ).trim(), ); $descriptionInput.trigger("blur"); } const maxPartNumber = Math.max( ...$("#partsTableBody tr") .map(function () { return ( parseInt($(this).find(".part-number").val()) || 0 ); }) .get(), ); addNewRow(maxPartNumber + 1); const $newRow = $("#partsTableBody tr:last"); $newRow.find(".part-description").focus(); } else { $descriptionInput.val( ($descriptionInput.val() + " " + transcript).trim(), ); $descriptionInput.trigger("blur"); } }; recognition.onerror = function (event) { if (event.error === "no-speech" || event.error === "aborted") { if (isVoiceActive) recognition.start(); } else { alert("Errore nel riconoscimento vocale: " + event.error); toggleVoiceRecognition(); } }; recognition.onend = function () { if (isVoiceActive) recognition.start(); }; } else { $("#toggleVoiceBtn").hide(); } function toggleVoiceRecognition() { if (!recognition) return; isVoiceActive = !isVoiceActive; const $btn = $("#toggleVoiceBtn"); if (isVoiceActive) { $btn.addClass("btn-danger").html( ' Stop Voce', ); recognition.start(); $("#partsTableBody tr:last").find(".part-description").focus(); } else { $btn.removeClass("btn-danger") .addClass("btn-secondary") .html(' Voce'); recognition.stop(); } } $("#toggleVoiceBtn").on("click", toggleVoiceRecognition); // =================== // MODAL HANDLING // =================== $(".parts-btn").on("click", function () { const iddatadb = $(this).data("iddatadb"); const idquotations = $(this).data("idquotations"); 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 || idquotations} - ${importRef} - ${description}`, ); $("#partsModal") .data("iddatadb", iddatadb) .data("idquotations", idquotations); // Precarica le matrici se iddatadb (assumendo matrici solo per iddatadb) if (iddatadb) { if (matrici.length === 0) { $.ajax({ url: "get_matrici_db.php", method: "GET", dataType: "json", success: function (data) { matrici = data.value || []; initializeGlobalSelect2(); loadPhoto(iddatadb, idquotations); loadExistingParts(iddatadb, idquotations); }, error: function (xhr, status, error) { matrici = []; initializeGlobalSelect2(); loadPhoto(iddatadb, idquotations); loadExistingParts(iddatadb, idquotations); const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } else { initializeGlobalSelect2(); loadPhoto(iddatadb, idquotations); loadExistingParts(iddatadb, idquotations); } } else { loadPhoto(iddatadb, idquotations); loadExistingParts(iddatadb, idquotations); } const modalElement = document.getElementById("partsModal"); if (modalElement) { // Verifica se il modale è già stato inizializzato let modal = bootstrap.Modal.getInstance(modalElement); if (!modal) { modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true, }); } modal.show(); } else { console.error("Elemento modale non trovato: #partsModal"); const errorMsg = $( '', ); $("body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }); $("#partsModal .close-btn, #partsModal").on("click", function (event) { if (event.target === this || $(event.target).hasClass("close-btn")) { const modalElement = document.getElementById("partsModal"); const modal = bootstrap.Modal.getInstance(modalElement); if (modal) { modal.hide(); } } }); $("#partsModal").on("hide.bs.modal", function (e) { if ( unsavedChanges && !confirm("Hai modifiche non salvate. Vuoi davvero uscire?") ) { e.preventDefault(); } }); $("#partsModal").on("hidden.bs.modal", function () { // Resetta lo stato photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; photoAnnotations = {}; partColors = {}; partMatrice = {}; selectedPartNumber = null; unsavedChanges = false; if (fabricCanvas) { fabricCanvas.off(); fabricCanvas.dispose(); fabricCanvas = null; } descriptionTextbox = null; markerObjects = {}; matrici = []; $("#photoSelectorContainer").empty().hide(); $("#samplePhoto").attr("src", ""); $("#partsTableBody").empty(); $("#global-matrice").empty(); $(".temp-alert").remove(); // Rimuovi manualmente il backdrop e ripristina il body const modalElement = document.getElementById("partsModal"); const modal = bootstrap.Modal.getInstance(modalElement); if (modal) { modal.dispose(); // Distrugge l'istanza del modale } $(".modal-backdrop").remove(); $("body").removeClass("modal-open"); $("body").css("padding-right", ""); $(":focus").blur(); }); // =================== // PHOTO LOADERS // =================== function loadPhoto(iddatadb, idquotations) { const currentPhoto = $("#samplePhoto").attr("src"); const endpoint = idquotations ? "load_photo_quotation.php" : "load_photo.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; $.ajax({ url: endpoint, method: "GET", data: data, success: function (response) { if (response.success) { if (response.photos && response.photos.length > 1) { showPhotoSelector(response.photos, currentPhoto); } else if ( response.photos && response.photos.length === 1 ) { loadSinglePhoto(response.photos[0]); } else { $("#samplePhoto").attr("src", ""); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } } else { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } function showPhotoSelector(photos, selected = null) { const selectorContainer = $("#photoSelectorContainer"); selectorContainer.empty().show(); const selector = $( '', ); photos.forEach((photo, index) => { const photoName = photo.split("/").pop(); const option = $("") .val(photo) .text(`Photo ${index + 1} - ${photoName}`); selector.append(option); }); selector.on("change", function () { loadSinglePhoto($(this).val()); }); selectorContainer.append(selector); const photoToSelect = selected && photos.includes(selected) ? selected : photos[0]; if (photoToSelect) { selector.val(photoToSelect); loadSinglePhoto(photoToSelect); } } function loadSinglePhoto(photoPath) { const img = $("#samplePhoto"); img.off("load").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); photoData = { naturalWidth, naturalHeight, displayWidth: Math.max(1, Math.round(naturalWidth * scale)), displayHeight: Math.max(1, Math.round(naturalHeight * scale)), scale, }; canvas.width = naturalWidth; canvas.height = naturalHeight; canvas.style.width = `${photoData.displayWidth}px`; canvas.style.height = `${photoData.displayHeight}px`; ctx.clearRect(0, 0, naturalWidth, naturalHeight); ctx.drawImage(img[0], 0, 0, naturalWidth, naturalHeight); // === FIX PER FABRIC.JS === // 1. Disponi completamente il canvas esistente if (fabricCanvas) { fabricCanvas.off(); // Rimuove tutti gli eventi esistenti fabricCanvas.dispose(); fabricCanvas = null; } // 2. Resetta completamente l'elemento canvas overlay const overlayCanvas = document.getElementById("overlayCanvas"); // Rimuovi l'elemento esistente e creane uno nuovo const canvasContainer = overlayCanvas.parentElement; const newOverlayCanvas = document.createElement("canvas"); newOverlayCanvas.id = "overlayCanvas"; newOverlayCanvas.width = photoData.displayWidth; newOverlayCanvas.height = photoData.displayHeight; newOverlayCanvas.style.width = `${photoData.displayWidth}px`; newOverlayCanvas.style.height = `${photoData.displayHeight}px`; // Mantieni gli stili CSS esistenti newOverlayCanvas.style.position = "absolute"; newOverlayCanvas.style.top = "0"; newOverlayCanvas.style.left = "0"; newOverlayCanvas.style.zIndex = "10"; // Sostituisci l'elemento canvasContainer.removeChild(overlayCanvas); canvasContainer.appendChild(newOverlayCanvas); // 3. Aspetta un tick per assicurarsi che il DOM sia aggiornato setTimeout(() => { // 4. Inizializza nuovo canvas Fabric.js fabricCanvas = new fabric.Canvas("overlayCanvas", { selection: true, preserveObjectStacking: true, width: photoData.displayWidth, height: photoData.displayHeight, }); // 5. Forza il ridimensionamento del canvas fabricCanvas.setDimensions({ width: photoData.displayWidth, height: photoData.displayHeight, }); // 6. Registra gli eventi fabricCanvas.on("mouse:down", function (options) { if (selectedPartNumber === null) { return; } if (options.target) { return; } const pointer = fabricCanvas.getPointer(options.e); const x = pointer.x / photoData.scale; const y = pointer.y / photoData.scale; const currentPhoto = $("#samplePhoto").attr("src"); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; } 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(); markUnsaved(); selectedPartNumber = null; $("#partsList li").removeClass("active"); }); // 7. Forza focus e render fabricCanvas.upperCanvasEl.focus(); fabricCanvas.renderAll(); // 8. Aggiorna marker e descrizioni esistenti per questa foto updateMarkers(); updateDescriptions(); }, 10); // Piccolo delay per assicurare che il DOM sia pronto }); } // =================== // DOWNLOAD PHOTO // =================== $("#downloadPhotoBtn").on("click", function () { const photoSrc = $("#samplePhoto").attr("src"); if (!photoSrc) { const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } const photoName = photoSrc.split("/").pop(); const link = document.createElement("a"); link.href = photoSrc; link.download = photoName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // =================== // PARTS TABLE // =================== function addNewRow(nextPartNumber, isMix = false) { const description = isMix ? "Mix" : ""; const defaultColor = isMix ? "#0000ff" : "#ff0000"; const newRow = ` `; $("#partsTableBody").append(newRow); updateRowButtons(); partColors[nextPartNumber || 1] = defaultColor; markUnsaved(); } function updateRowButtons() { const rowCount = $("#partsTableBody tr").length; $("#partsTableBody tr").each(function () { $(this) .find(".remove-row") .toggle(rowCount > 1); }); } $(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(); if (photoAnnotations[$("#samplePhoto").attr("src")]?.hasDescriptions) { updateDescriptions(); } }); $(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(); if (photoAnnotations[$("#samplePhoto").attr("src")]?.hasDescriptions) { updateDescriptions(); } }); $(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(); const iddatadb = $("#partsModal").data("iddatadb"); const idquotations = $("#partsModal").data("idquotations"); const endpoint = idquotations ? "delete_part_quotation.php" : "delete_part.php"; if (partId && partId !== "new") { $.ajax({ url: endpoint, method: "POST", data: JSON.stringify({ part_id: partId }), contentType: "application/json", success: function (response) { if (response.success) { $row.remove(); delete partColors[partNumber]; delete partMatrice[partNumber]; if (markerObjects[partNumber]) { fabricCanvas.remove(markerObjects[partNumber]); delete markerObjects[partNumber]; fabricCanvas.renderAll(); } updateRowButtons(); updatePartsList(); markUnsaved(); } else { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } else { $row.remove(); delete partColors[partNumber]; delete partMatrice[partNumber]; if (markerObjects[partNumber]) { fabricCanvas.remove(markerObjects[partNumber]); delete markerObjects[partNumber]; fabricCanvas.renderAll(); } updateRowButtons(); updatePartsList(); if ( photoAnnotations[$("#samplePhoto").attr("src")]?.hasDescriptions ) { updateDescriptions(); } markUnsaved(); } }); $(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 idquotations = $("#partsModal").data("idquotations"); const isMix = partDescription.startsWith("Mix") ? "Y" : "N"; const partId = $row.data("part-id") || null; const endpoint = idquotations ? "save_parts_quotation.php" : "save_parts.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; if (partDescription && (iddatadb || idquotations)) { $saveLoading.show(); $saveStatus.hide(); $.ajax({ url: endpoint, method: "POST", data: JSON.stringify({ ...data, parts: [ { id: partId, part_number: partNumber, part_description: partDescription, mix: isMix, }, ], }), contentType: "application/json", success: function (response) { $saveLoading.hide(); if (response.success) { $saveStatus.show(); if (response.part_id) { $row.attr("data-part-id", response.part_id).data( "part-id", response.part_id, ); } setTimeout(() => $saveStatus.hide(), 2000); updatePartsList(); if ( photoAnnotations[$("#samplePhoto").attr("src")] ?.hasDescriptions ) { updateDescriptions(); } } else { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { $saveLoading.hide(); const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } }); // =================== // LOAD EXISTING PARTS // =================== function loadExistingParts(iddatadb, idquotations) { const endpoint = idquotations ? "load_parts_quotation.php" : "load_parts.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; $.ajax({ url: endpoint, method: "GET", data: data, success: function (response) { $("#partsTableBody").empty(); if (response.success && 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; if (part.idmatrice) { partMatrice[part.part_number] = part.idmatrice; } }); } else { addNewRow(1); } updateRowButtons(); updatePartsList(); }, error: function (xhr, status, error) { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); addNewRow(1); }, }); } // Funzione per inizializzare Select2 sulle tendine delle matrici function initializeSelect2($select, partNumber, partId, idmatrice) { if (typeof $.fn.select2 === "undefined") { $select.replaceWith( '', ); return; } const options = matrici.map(function (matrice) { return { id: matrice.IdMatrice, text: matrice.NomeMatrice, // Updated to use NomeMatrice }; }); $select.select2({ placeholder: "Seleziona matrice", allowClear: true, data: options, dropdownParent: $("#partsModal"), matcher: function (params, data) { if (!params.term || params.term.length < 3) { return data; } const term = params.term.toUpperCase(); if (data.text.toUpperCase().indexOf(term) >= 0) { return data; } return null; }, }); if (partId && partId !== "new" && idmatrice) { const matrice = matrici.find((m) => m.IdMatrice == idmatrice); if (matrice) { const option = new Option( matrice.NomeMatrice, // Updated to use NomeMatrice matrice.IdMatrice, true, true, ); $select.append(option).trigger("change"); partMatrice[partNumber] = matrice.IdMatrice; } } $select.on("change", function () { const idmatrice = $(this).val(); const $listItem = $(this).closest("li"); const $saveStatus = $listItem.find(".save-status"); const $saveLoading = $listItem.find(".save-loading"); partMatrice[partNumber] = idmatrice || null; if (partId && partId !== "new") { $saveLoading.show(); $saveStatus.hide(); const iddatadb = $("#partsModal").data("iddatadb"); const idquotations = $("#partsModal").data("idquotations"); const endpoint = idquotations ? "save_matrice_quotation.php" : "save_matrice.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; $.ajax({ url: endpoint, method: "POST", data: JSON.stringify({ ...data, parts: [ { id: partId, idmatrice: idmatrice || null, }, ], }), contentType: "application/json", success: function (response) { if (response.success) { $saveLoading.hide(); $saveStatus.show(); setTimeout(() => $saveStatus.hide(), 2000); } else { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); $saveLoading.hide(); } }, error: function (xhr, status, error) { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); $saveLoading.hide(); }, }); } }); } // Funzione per inizializzare Select2 sul dropdown globale function initializeGlobalSelect2() { const $select = $("#global-matrice"); if (typeof $.fn.select2 === "undefined") { $select.replaceWith( '', ); return; } const options = matrici.map(function (matrice) { return { id: matrice.IdMatrice, text: matrice.NomeMatrice, // Updated to use NomeMatrice }; }); $select.select2({ placeholder: "Seleziona matrice globale", allowClear: true, data: options, dropdownParent: $("#partsModal"), matcher: function (params, data) { if (!params.term || params.term.length < 3) { return data; } const term = params.term.toUpperCase(); if (data.text.toUpperCase().indexOf(term) >= 0) { return data; } return null; }, }); } // =================== // PARTS LIST // =================== function updatePartsList() { const showMixParts = $("#showMixParts").is(":checked"); $("#partsList").empty(); const predefinedColors = [ "#ff0000", // Rosso "#0000ff", // Blu "#00ff00", // Verde "#01832cff", // Giallo "#ff00ff", // Magenta "#00ffff", // Ciano "#800080", // Viola "#ffa500", // Arancione ]; $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); const partId = $(this).data("part-id"); const partColor = partColors[partNumber] || (partDescription.startsWith("Mix") ? "#0000ff" : "#ff0000"); if ( partNumber && partDescription && (showMixParts || !partDescription.startsWith("Mix")) ) { const colorOptions = predefinedColors .map( (color) => `
`, ) .join(""); const listItem = `
  • ${partNumber} - ${partDescription}
    ${colorOptions}
  • `; $("#partsList").append(listItem); const $select = $("#partsList").find( `li[data-part-number="${partNumber}"] .part-matrice`, ); initializeSelect2( $select, partNumber, partId, partMatrice[partNumber], ); } }); $(".selected-color").on("click", function (e) { e.stopPropagation(); const $picker = $(this).siblings(".color-picker"); $(".color-picker").not($picker).hide(); $picker.toggle(); }); $(".color-option").on("click", function (e) { e.stopPropagation(); const $this = $(this); const color = $this.data("color"); const $listItem = $this.closest("li"); const partNumber = $listItem.data("part-number"); partColors[partNumber] = color; $listItem.find(".selected-color").css("background-color", color); $this.closest(".color-picker").hide(); updateMarkers(); markUnsaved(); }); $(document).on("click", function (e) { if (!$(e.target).closest(".color-picker-container").length) { $(".color-picker").hide(); } }); } $(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) { const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); 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(); if (photoAnnotations[$("#samplePhoto").attr("src")]?.hasDescriptions) { updateDescriptions(); } }); $(document).on("click", ".propagate-matrice-btn", function () { const $listItem = $(this).closest("li"); const globalVal = $("#global-matrice").val(); if (globalVal) { $listItem.find(".part-matrice").val(globalVal).trigger("change"); } else { const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }); $("#partsList").on("click", "li", function (e) { if ( $(e.target).hasClass("add-to-mix-btn") || $(e.target).hasClass("color-option") || $(e.target).closest(".color-picker-container").length || $(e.target).hasClass("part-matrice") || $(e.target).closest(".select2-container").length || $(e.target).hasClass("propagate-matrice-btn") ) return; selectedPartNumber = $(this).data("part-number"); $(this).addClass("active").siblings().removeClass("active"); }); $("#showMixParts").on("change", function () { updatePartsList(); updateMarkers(); if (photoAnnotations[$("#samplePhoto").attr("src")]?.hasDescriptions) { updateDescriptions(); } }); function renumberParts() { const $rows = $("#partsTableBody tr"); const iddatadb = $("#partsModal").data("iddatadb"); const idquotations = $("#partsModal").data("idquotations"); const endpoint = idquotations ? "renumber_parts_quotation.php" : "renumber_parts.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; let newPartColors = {}; let newPartMatrice = {}; let newMarkerObjects = {}; let partsData = $rows .map(function (index) { const $row = $(this); return { partNumber: $row.find(".part-number").val(), partDescription: $row.find(".part-description").val(), partId: $row.data("part-id"), }; }) .get(); partsData.forEach((part, index) => { const newNumber = index + 1; newPartColors[newNumber] = partColors[part.partNumber] || "#ff0000"; newPartMatrice[newNumber] = partMatrice[part.partNumber] || null; if (markerObjects[part.partNumber]) { newMarkerObjects[newNumber] = markerObjects[part.partNumber]; } part.partNumber = newNumber; }); $rows.each(function (index) { $(this) .find(".part-number") .val(index + 1); }); const currentPhoto = $("#samplePhoto").attr("src"); if (photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto].markers.forEach((marker) => { const newPartNumber = partsData.find( (p) => p.partNumber == marker.partNumber, )?.partNumber; if (newPartNumber) { marker.partNumber = newPartNumber; marker.color = newPartColors[newPartNumber]; } }); } partColors = newPartColors; partMatrice = newPartMatrice; markerObjects = newMarkerObjects; const partsToSave = partsData.map((part) => ({ id: part.partId || null, part_number: part.partNumber, part_description: part.partDescription, mix: part.partDescription.startsWith("Mix") ? "Y" : "N", idmatrice: partMatrice[part.partNumber] || null, })); $.ajax({ url: endpoint, method: "POST", data: JSON.stringify({ ...data, parts: partsToSave }), contentType: "application/json", success: function (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).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(); if (photoAnnotations[currentPhoto]?.hasDescriptions) { updateDescriptions(); } markUnsaved(); } else { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } $("#renumberPartsBtn").on("click", renumberParts); // =================== // MARKERS & DESCRIPTIONS // =================== function updateMarkers() { // Rimuovi marker esistenti for (let partNumber in markerObjects) { fabricCanvas.remove(markerObjects[partNumber]); delete markerObjects[partNumber]; } markerObjects = {}; const currentPhoto = $("#samplePhoto").attr("src"); const annotations = photoAnnotations[currentPhoto] || { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; const showMixParts = $("#showMixParts").is(":checked"); annotations.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 radius = 12; const fontSize = 16; const markerColor = marker.color || partColors[marker.partNumber] || "#ff0000"; // Crea cerchio const circle = new fabric.Circle({ radius: radius, fill: markerColor, stroke: markerColor, strokeWidth: 1, left: marker.x * photoData.scale, top: marker.y * photoData.scale, originX: "center", originY: "center", selectable: true, hasControls: false, lockScalingX: true, lockScalingY: true, lockRotation: true, }); // Crea testo const text = new fabric.Text(marker.partNumber.toString(), { fontFamily: "Arial", fontSize: fontSize, fontWeight: "bold", fill: "#ffffff", left: marker.x * photoData.scale, top: marker.y * photoData.scale, originX: "center", originY: "middle", selectable: true, hasControls: false, lockScalingX: true, lockScalingY: true, lockRotation: true, }); // Raggruppa cerchio e testo const group = new fabric.Group([circle, text], { left: marker.x * photoData.scale, top: marker.y * photoData.scale, originX: "center", originY: "center", selectable: true, hasControls: false, lockScalingX: true, lockScalingY: true, lockRotation: true, }); group.on("moving", function () { marker.x = this.left / photoData.scale; marker.y = this.top / photoData.scale; markUnsaved(); }); fabricCanvas.add(group); markerObjects[marker.partNumber] = group; }); fabricCanvas.renderAll(); } function updateDescriptions() { const currentPhoto = $("#samplePhoto").attr("src"); const annotations = photoAnnotations[currentPhoto] || { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; const showMixParts = $("#showMixParts").is(":checked"); if (!annotations.hasDescriptions) { if (descriptionTextbox) { fabricCanvas.remove(descriptionTextbox); descriptionTextbox = null; fabricCanvas.renderAll(); } 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}`); } }); const text = partsList.join("\n"); // Rimuovi il textbox esistente if (descriptionTextbox) { fabricCanvas.remove(descriptionTextbox); descriptionTextbox = null; } descriptionTextbox = new fabric.Textbox(text, { left: annotations.descriptionPosition.x * photoData.scale, top: annotations.descriptionPosition.y * photoData.scale, width: annotations.descriptionSize.width, scaleX: 1, scaleY: 1, backgroundColor: "rgba(255, 255, 255, 0)", // Changed to fully transparent fontFamily: "Arial", fontSize: 24, fill: "#000000", padding: 10, editable: false, selectable: true, hasControls: true, borderColor: "#ccc", cornerColor: "#888", cornerSize: 10, transparentCorners: false, lockRotation: true, lockScalingFlip: true, minScaleLimit: 0.1, }); descriptionTextbox.on("scaling", function () { fitFontToBox(this); annotations.descriptionSize.width = this.width * this.scaleX; annotations.descriptionSize.height = this.height * this.scaleY; markUnsaved(); }); descriptionTextbox.on("moving", function () { annotations.descriptionPosition.x = this.left / photoData.scale; annotations.descriptionPosition.y = this.top / photoData.scale; markUnsaved(); }); fabricCanvas.add(descriptionTextbox); fitFontToBox(descriptionTextbox); fabricCanvas.renderAll(); } function fitFontToBox(textbox) { let fontSize = 24; textbox.set("fontSize", fontSize); textbox.setCoords(); while ( (textbox.textLines.length * textbox.fontSize * 1.2 > textbox.height * textbox.scaleY || textbox._getTransformedDimensions().x > textbox.width * textbox.scaleX) && fontSize > 8 ) { fontSize -= 1; textbox.set("fontSize", fontSize); textbox.setCoords(); } while ( textbox.textLines.length * textbox.fontSize * 1.2 < textbox.height * textbox.scaleY && textbox._getTransformedDimensions().x < textbox.width * textbox.scaleX && fontSize < 32 ) { fontSize += 1; textbox.set("fontSize", fontSize); textbox.setCoords(); } } function clearCanvasMarkers(clearDescriptions = true) { const currentPhoto = $("#samplePhoto").attr("src"); if (clearDescriptions && photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto].hasDescriptions = false; photoAnnotations[currentPhoto].descriptionPosition = { x: 10, y: 10, }; photoAnnotations[currentPhoto].descriptionSize = { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }; if (descriptionTextbox) { fabricCanvas.remove(descriptionTextbox); descriptionTextbox = null; } } for (let partNumber in markerObjects) { fabricCanvas.remove(markerObjects[partNumber]); delete markerObjects[partNumber]; } markerObjects = {}; 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`; ctx.clearRect(0, 0, canvas.width, canvas.height); if ($("#samplePhoto")[0].naturalWidth) { ctx.drawImage( $("#samplePhoto")[0], 0, 0, canvas.width, canvas.height, ); } updateMarkers(); if (photoAnnotations[currentPhoto]?.hasDescriptions) { updateDescriptions(); } } function undoLastMarker() { const currentPhoto = $("#samplePhoto").attr("src"); if ( photoAnnotations[currentPhoto] && photoAnnotations[currentPhoto].markers.length > 0 ) { const lastMarker = photoAnnotations[currentPhoto].markers.pop(); if (markerObjects[lastMarker.partNumber]) { fabricCanvas.remove(markerObjects[lastMarker.partNumber]); delete markerObjects[lastMarker.partNumber]; fabricCanvas.renderAll(); } markUnsaved(); } } $("#addDescriptionsBtn").on("click", function () { const currentPhoto = $("#samplePhoto").attr("src"); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; } photoAnnotations[currentPhoto].hasDescriptions = true; updateDescriptions(); markUnsaved(); }); $("#removeAnnotationsBtn").on("click", function () { clearCanvasMarkers(true); }); $("#undoMarkerBtn").on("click", undoLastMarker); // =================== // SAVE PHOTO // =================== $("#savePhotoBtn").on("click", function () { console.log("savePhotoBtn clicked"); if (!$("#samplePhoto").attr("src")) { console.log("No image source found"); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } if (!fabricCanvas) { console.log("fabricCanvas is undefined"); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); const img = $("#samplePhoto")[0]; console.log( "Image loaded:", img.complete, "Natural width:", img.naturalWidth, ); if (!img.complete || img.naturalWidth === 0) { console.log("Image not loaded correctly"); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } console.log( "Setting canvas dimensions:", photoData.naturalWidth, photoData.naturalHeight, ); canvas.width = photoData.naturalWidth; canvas.height = photoData.naturalHeight; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); console.log("Rendering Fabric canvas"); try { const fabricDataURL = fabricCanvas.toDataURL({ format: "png", multiplier: 1 / photoData.scale, }); console.log( "Fabric DataURL generated, length:", fabricDataURL.length, ); const tempImg = new Image(); tempImg.src = fabricDataURL; tempImg.onload = function () { console.log("Drawing Fabric overlay on canvas"); ctx.drawImage(tempImg, 0, 0, canvas.width, canvas.height); fetch(fabricDataURL) .then((res) => res.blob()) .then((blob) => { console.log("Fabric blob generated, size:", blob.size); finalizeSave(blob); }) .catch((err) => { console.log("Error converting DataURL to Blob:", err); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }); }; tempImg.onerror = function () { console.log("Error loading Fabric overlay image"); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }; } catch (e) { console.log("Error generating Fabric DataURL:", e); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } function finalizeSave(fabricBlob) { console.log("Finalizing save"); canvas.toBlob( function (blob) { if (!blob) { console.log("Failed to generate final blob"); const errorMsg = $( '', ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } console.log("Final blob generated, size:", blob.size); const timestamp = new Date() .toISOString() .replace(/[:.]/g, "-"); const iddatadb = $("#partsModal").data("iddatadb"); const idquotations = $("#partsModal").data("idquotations"); const id = iddatadb || idquotations; const endpoint = idquotations ? "save_annotated_photo_quotation.php" : "save_annotated_photo.php"; const finalName = `photo_${id}_${timestamp}.png`; console.log( "Sending AJAX request to:", endpoint, "with filename:", finalName, "and ID:", id, ); const formData = new FormData(); formData.append("file", blob, finalName); formData.append("filename", finalName); formData.append( idquotations ? "idquotations" : "iddatadb", id, ); $.ajax({ url: endpoint, method: "POST", data: formData, processData: false, contentType: false, success: function (response) { console.log("AJAX success:", response); if (response.success) { const successMsg = $( '", ); $("#partsModal .modal-body").prepend( successMsg, ); setTimeout(function () { successMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); const photoSelector = $("#photoSelector"); if (photoSelector.length > 0) { const newPhotoPath = response.file_path; const newPhotoName = newPhotoPath .split("/") .pop(); const optionCount = photoSelector.find("option").length; const newOption = $("") .val(newPhotoPath) .text( `Photo ${optionCount + 1} - ${newPhotoName}`, ); photoSelector.append(newOption); const firstPhotoPath = photoSelector .find("option:first") .val(); photoSelector.val(firstPhotoPath); console.log( "Loading first photo:", firstPhotoPath, ); loadSinglePhoto(firstPhotoPath); } else { const currentPhoto = $("#samplePhoto").attr("src"); if (currentPhoto) { console.log( "Reloading current photo:", currentPhoto, ); loadSinglePhoto(currentPhoto); } } const currentPhoto = $("#samplePhoto").attr("src"); const newPhoto = response.file_path; photoAnnotations[newPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; console.log("Clearing canvas and annotations"); clearCanvasMarkers(true); clearUnsaved(); } else { console.log( "AJAX response error:", response.message, ); const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { console.log( "AJAX error:", status, error, xhr.status, ); const errorMsg = $( '", ); $("#partsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); }, "image/png", 0.9, ); } }); // =================== // HELPER FUNCTIONS // =================== function markUnsaved() { if (!unsavedChanges) { unsavedChanges = true; $("#savePhotoBtn").addClass("unsaved").text("⚠️ Salva Modifiche"); } } function clearUnsaved() { unsavedChanges = false; $("#savePhotoBtn").removeClass("unsaved").text("Salva Foto"); } $(document).on("input change", "#partsTableBody input", markUnsaved); $(document).on("click", ".add-row, .add-mix-row, .remove-row", markUnsaved); });