$(document).ready(function () { console.log("parts.js caricato correttamente"); // =================== // GLOBAL STATE (NEW) // =================== let photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; // markers keyed by photo src => [{ partNumber, x, y } using NATURAL coords] let photoMarkers = {}; // selection & descriptions let selectedPartNumber = null; let descriptionPosition = {x: 10, y: 10}; // NATURAL coords let hasDescriptions = false; // =================== // 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}`); // display option with photo name if available 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"); // Real image size const naturalWidth = img[0].naturalWidth; const naturalHeight = img[0].naturalHeight; // Compute scale to FIT inside its parent without distorting aspect ratio const parent = $(canvas).parent(); const maxW = parent.width(); const maxH = parent.height(); const scale = Math.min(maxW / naturalWidth, maxH / naturalHeight); // Display size on screen const displayWidth = Math.max(1, Math.round(naturalWidth * scale)); const displayHeight = Math.max(1, Math.round(naturalHeight * scale)); // Save globally photoData = {naturalWidth, naturalHeight, displayWidth, displayHeight, scale}; // Canvas in REAL size (so saving uses natural coords 1:1) canvas.width = naturalWidth; canvas.height = naturalHeight; // Visual size on screen canvas.style.width = `${displayWidth}px`; canvas.style.height = `${displayHeight}px`; // Also size/align the overlay containers to match the canvas $("#markerContainer").css({width: `${displayWidth}px`, height: `${displayHeight}px`}); $("#descriptionList").css({maxWidth: `${Math.max(200, Math.round(displayWidth * 0.35))}px`}); // Draw fresh image at full resolution ctx.clearRect(0, 0, naturalWidth, naturalHeight); ctx.drawImage(img.get(0), 0, 0, naturalWidth, naturalHeight); updateMarkers(); if (hasDescriptions) drawDescriptions(descriptionPosition.x, descriptionPosition.y); }); } // =================== // PARTS TABLE // =================== function addNewRow(nextPartNumber, isMix = false) { const description = isMix ? "Mix" : ""; const newRow = ` `; $("#partsTableBody").append(newRow); updateRowButtons(); } 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"); 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(); updateRowButtons(); updatePartsList(); clearCanvasMarkers(); } 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(); 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"; // არსებული part-id row-დან (თუ უკვე არსებობს) 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-ის ID (თუ არის) part_number: partNumber, part_description: partDescription, mix: isMix, }, ], }), contentType: "application/json", success: function (response) { if (response.success) { $saveLoading.hide(); $saveStatus.show(); updatePartsList(); // თუ ახალია, backend-მა მოგვცა ახალი ID 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(); }, }); } }); 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 newRow = ` `; $("#partsTableBody").append(newRow); }); } 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() { $("#partsList").empty(); $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); if (partNumber && partDescription && !partDescription.startsWith("Mix")) { const listItem = `
  • ${partNumber} - ${partDescription}
  • `; $("#partsList").append(listItem); } }); } $(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")) return; selectedPartNumber = $(this).data("part-number"); $(this).addClass("active").siblings().removeClass("active"); }); // =================== // 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 (!photoMarkers[currentPhoto]) photoMarkers[currentPhoto] = []; const existingMarker = photoMarkers[currentPhoto].find(m => m.partNumber == selectedPartNumber); if (existingMarker) { existingMarker.x = x; existingMarker.y = y; } else { photoMarkers[currentPhoto].push({partNumber: selectedPartNumber, x, y}); } updateMarkers(); if (hasDescriptions) drawDescriptions(descriptionPosition.x, descriptionPosition.y); selectedPartNumber = null; $("#partsList li").removeClass("active"); }); function updateMarkers() { const markerContainer = $("#markerContainer"); markerContainer.empty(); // keep overlay sized to canvas display markerContainer.css({width: `${photoData.displayWidth}px`, height: `${photoData.displayHeight}px`}); const currentPhoto = $("#samplePhoto").attr("src"); const markers = photoMarkers[currentPhoto] || []; markers.forEach((marker) => { const scaledX = marker.x * photoData.scale; const scaledY = marker.y * photoData.scale; 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; } else { // draggable description panel descriptionPosition.x = (currentX + 5) / photoData.scale; descriptionPosition.y = (currentY + 5) / photoData.scale; } }); $(document).on("mouseup.dragMarker", function () { if (!isDragging) return; isDragging = false; $element.css("z-index", 1000); $(document).off("mousemove.dragMarker mouseup.dragMarker"); }); } function drawDescriptions(x, y) { const partsList = []; $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); if (partNumber && partDescription && !partDescription.startsWith("Mix")) { partsList.push(`${partNumber} ${partDescription}`); } }); const descriptionList = $("#descriptionList"); descriptionList.empty(); descriptionList.css({ display: "block", top: y * photoData.scale + "px", left: x * photoData.scale + "px", }); partsList.forEach((part) => descriptionList.append(`
    ${part}
    `)); updateMarkers(); } function clearCanvasMarkers() { const currentPhoto = $("#samplePhoto").attr("src"); photoMarkers[currentPhoto] = []; hasDescriptions = false; $("#descriptionList").css("display", "none"); $("#markerContainer").empty(); const canvas = document.getElementById("photoCanvas"); const ctx = canvas.getContext("2d"); // reset canvas to current image (keeps proportions) 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); } } $("#addDescriptionsBtn").on("click", function () { hasDescriptions = true; descriptionPosition = {x: 10, y: 10}; drawDescriptions(descriptionPosition.x, descriptionPosition.y); makeDraggable($("#descriptionList")); }); $("#removeAnnotationsBtn").on("click", function () { clearCanvasMarkers(); }); 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 --- // როცა ვცვლით input-ს ცხრილში $(document).on("input change", "#partsTableBody input", markUnsaved); // როცა ვამატებთ/ვშლით რიგს $(document).on("click", ".add-row, .add-mix-row, .remove-row", markUnsaved); // თუ გაქვს draggable marker ან description list $(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"); // Ensure canvas is real size const naturalWidth = img.get(0).naturalWidth; const naturalHeight = img.get(0).naturalHeight; canvas.width = naturalWidth; canvas.height = naturalHeight; // Redraw base image ctx.drawImage(img.get(0), 0, 0, naturalWidth, naturalHeight); // Descriptions box const partsList = []; $("#partsTableBody tr").each(function () { const partNumber = $(this).find(".part-number").val(); const partDescription = $(this).find(".part-description").val(); if (partNumber && partDescription && !partDescription.startsWith("Mix")) { partsList.push(`${partNumber} ${partDescription}`); } }); if (hasDescriptions && 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 = descriptionPosition.x; const y = 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) => { const domWidth = $("#samplePhoto").width(); const domHeight = $("#samplePhoto").height(); // NATURAL ზომა (ფაილის რეალური ზომა) const naturalWidth = photoData.naturalWidth; const naturalHeight = photoData.naturalHeight; // მასშტაბები const scaleX = naturalWidth / domWidth; const scaleY = naturalHeight / domHeight; // გადაყვანილი კოორდინატები const x = descriptionPosition.x * scaleX; const y = descriptionPosition.y * scaleY; ctx.fillText(part, x + 15, y + 35 + index * textHeight); }); } // Markers const currentPhoto = $("#samplePhoto").attr("src"); const markers = photoMarkers[currentPhoto] || []; markers.forEach((marker) => { const x = marker.x; // already NATURAL coords const y = marker.y; const radius = Math.max(5, Math.round(naturalWidth * 0.025)); const fontSize = Math.max(8, Math.round(radius * 0.9)); ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.fillStyle = "rgba(255,0,0,0.85)"; ctx.fill(); ctx.lineWidth = 3; ctx.strokeStyle = "#ffffff"; 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(); clearUnsaved(); // ✅ reset unsaved status } 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"); }); });