theloftstore/public/userarea/annotationsModal.js
2025-10-31 10:16:57 +01:00

1556 lines
61 KiB
JavaScript

$(document).ready(function () {
// ===================
// GLOBAL STATE
// ===================
let photoData = {
naturalWidth: 0,
naturalHeight: 0,
displayWidth: 0,
displayHeight: 0,
scale: 1,
};
let photoAnnotations = {};
let partColors = {};
let partSizes = {}; // memorizza la dimensione specifica per parte
let selectedPartNumber = null;
let unsavedChanges = false;
let fabricCanvas = null;
let descriptionTextbox = null;
let markerObjects = {};
let nextMarkerId = 1;
let partsListData = [];
// DIMENSIONE GLOBALE MARKER
let globalMarkerSize = 16;
// ===================
// MODAL INITIALIZATION
// ===================
window.initAnnotationsModal = function (iddatadb, idquotations, trfHeader) {
console.log("initAnnotationsModal chiamato con:", {
iddatadb,
idquotations,
trfHeader,
});
$("#annotationsModal").attr("data-iddatadb", iddatadb);
if (!iddatadb && !idquotations) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante. Impossibile inizializzare il modale delle annotazioni.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
$("#trfHeaderAnnotations").text(trfHeader || "N/D");
$("#annotationsModal")
.data("iddatadb", iddatadb || null)
.data("idquotations", idquotations || null);
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
const modalElement = document.getElementById("annotationsModal");
if (!modalElement) {
console.error("Elemento #annotationsModal non trovato nel DOM.");
alert(
"Errore: Il modale delle annotazioni non è presente nel DOM.",
);
return;
}
let modal = bootstrap.Modal.getInstance(modalElement);
if (!modal) {
modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
}
modal.show();
// Inizializza slider dimensione marker
$("#markerSizeSlider").val(globalMarkerSize);
$("#markerSizeValue").text(globalMarkerSize + "px");
// Debug: Verifica presenza elementi DOM
console.log(
"Presenza #partsListAnnotations:",
$("#partsListAnnotations").length,
);
console.log(
"Presenza #showMixPartsAnnotations:",
$("#showMixPartsAnnotations").length,
);
console.log(
"Presenza #addDescriptionsBtnAnnotations:",
$("#addDescriptionsBtnAnnotations").length,
);
console.log(
"Presenza #removeAnnotationsBtnAnnotations:",
$("#removeAnnotationsBtnAnnotations").length,
);
console.log(
"Presenza #downloadPhotoBtnAnnotations:",
$("#downloadPhotoBtnAnnotations").length,
);
console.log(
"Presenza #backToPartsBtnAnnotations:",
$("#backToPartsBtnAnnotations").length,
);
console.log(
"Presenza #overlayCanvasAnnotations:",
$("#overlayCanvasAnnotations").length,
);
};
$("#annotationsModal").on("hide.bs.modal", function (e) {
if (
unsavedChanges &&
!confirm("Hai modifiche non salvate. Vuoi davvero uscire?")
) {
e.preventDefault();
}
});
$("#annotationsModal").on("hidden.bs.modal", function () {
photoData = {
naturalWidth: 0,
naturalHeight: 0,
displayWidth: 0,
displayHeight: 0,
scale: 1,
};
photoAnnotations = {};
partColors = {};
selectedPartNumber = null;
unsavedChanges = false;
partsListData = [];
if (fabricCanvas) {
fabricCanvas.off();
fabricCanvas.dispose();
fabricCanvas = null;
}
descriptionTextbox = null;
markerObjects = {};
globalMarkerSize = 16;
$("#photoSelectorContainerAnnotations").empty().hide();
$("#samplePhotoAnnotations").attr("src", "");
$("#partsListAnnotations").empty();
$(".temp-alert").remove();
const modalElement = document.getElementById("annotationsModal");
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.dispose();
}
$(".modal-backdrop").remove();
$("body").removeClass("modal-open");
$("body").css("padding-right", "");
$(":focus").blur();
});
// SLIDER DIMENSIONE MARKER
$(document)
.off("input", "#markerSizeSlider")
.on("input", "#markerSizeSlider", function () {
globalMarkerSize = parseInt($(this).val());
$("#markerSizeValue").text(globalMarkerSize + "px");
partsListData.forEach(function (part) {
if (part && part.part_number != null) {
partSizes[part.part_number] = globalMarkerSize;
}
});
$("#partsListAnnotations .marker-size-slider").val(
globalMarkerSize,
);
$("#partsListAnnotations .marker-size-value").text(
globalMarkerSize + "px",
);
updateMarkers();
markUnsaved();
});
// ===================
// PHOTO LOADERS
// ===================
function loadPhoto(iddatadb, idquotations) {
const currentPhoto = $("#samplePhotoAnnotations").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) {
console.log("Risposta da load_photo:", 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 {
$("#samplePhotoAnnotations").attr("src", "");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto trovata per questo elemento.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">' +
(response.message ||
"Errore nel caricamento della foto.") +
"</div>",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
console.error("Errore AJAX in loadPhoto:", {
status,
error,
responseText: xhr.responseText,
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento della foto: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
}
function showPhotoSelector(photos, selected = null) {
const selectorContainer = $("#photoSelectorContainerAnnotations");
selectorContainer.empty().show();
const selector = $(
'<select id="photoSelectorAnnotations" class="form-control"></select>',
);
photos.forEach((photo, index) => {
const photoName = photo.split("/").pop();
const option = $("<option></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 = $("#samplePhotoAnnotations");
img.off("load").attr("src", photoPath);
img.on("load", function () {
console.log("Foto caricata:", photoPath);
const canvas = document.getElementById("photoCanvasAnnotations");
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);
if (fabricCanvas) {
fabricCanvas.off();
fabricCanvas.dispose();
fabricCanvas = null;
}
const overlayCanvas = document.getElementById(
"overlayCanvasAnnotations",
);
const canvasContainer = overlayCanvas.parentElement;
const newOverlayCanvas = document.createElement("canvas");
newOverlayCanvas.id = "overlayCanvasAnnotations";
newOverlayCanvas.width = photoData.displayWidth;
newOverlayCanvas.height = photoData.displayHeight;
newOverlayCanvas.style.width = `${photoData.displayWidth}px`;
newOverlayCanvas.style.height = `${photoData.displayHeight}px`;
newOverlayCanvas.style.position = "absolute";
newOverlayCanvas.style.top = "0";
newOverlayCanvas.style.left = "0";
newOverlayCanvas.style.zIndex = "1000";
canvasContainer.removeChild(overlayCanvas);
canvasContainer.appendChild(newOverlayCanvas);
setTimeout(() => {
fabricCanvas = new fabric.Canvas("overlayCanvasAnnotations", {
selection: true,
preserveObjectStacking: true,
width: photoData.displayWidth,
height: photoData.displayHeight,
});
fabricCanvas.setDimensions({
width: photoData.displayWidth,
height: photoData.displayHeight,
});
fabricCanvas.on("mouse:down", function (options) {
console.log(
"Evento mouse:down su canvas, selectedPartNumber:",
selectedPartNumber,
);
if (selectedPartNumber === null) {
console.log(
"Nessuna parte selezionata, ignoro il click.",
);
return;
}
if (options.target) {
console.log("Click su un oggetto esistente, ignoro.");
return;
}
const pointer = fabricCanvas.getPointer(options.e);
const x = pointer.x / photoData.scale;
const y = pointer.y / photoData.scale;
const currentPhoto = $("#samplePhotoAnnotations").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";
photoAnnotations[currentPhoto].markers.push({
id: nextMarkerId++,
partNumber: selectedPartNumber,
x,
y,
color: partColor,
});
console.log("Marker aggiunto/spostato:", {
partNumber: selectedPartNumber,
x,
y,
color: partColor,
});
updateMarkers();
markUnsaved();
});
fabricCanvas.upperCanvasEl.focus();
fabricCanvas.renderAll();
updateMarkers();
updateDescriptions();
}, 10);
});
}
// ===================
// DOWNLOAD PHOTO
// ===================
$(document)
.off("click.downloadPhoto", "#downloadPhotoBtnAnnotations")
.on(
"click.downloadPhoto",
"#downloadPhotoBtnAnnotations",
function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #downloadPhotoBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#downloadPhotoBtnAnnotations").length) {
console.error(
"Pulsante #downloadPhotoBtnAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #downloadPhotoBtnAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
const photoSrc = $("#samplePhotoAnnotations").attr("src");
console.log("URL immagine per il download:", photoSrc);
if (!photoSrc) {
console.error("Nessuna foto caricata da scaricare.");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da scaricare.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
// Verifica se l'URL è valido
const img = new Image();
img.src = photoSrc;
img.onload = function () {
const photoName =
photoSrc.split("/").pop() || "downloaded_photo.png";
console.log("Nome file per il download:", photoName);
const link = document.createElement("a");
link.href = photoSrc;
link.download = photoName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log("Download avviato per:", photoName);
};
img.onerror = function () {
console.error(
"Errore: Impossibile caricare l'immagine per il download:",
photoSrc,
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Impossibile caricare l\'immagine per il download.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
};
},
);
// ===================
// TORNA AL MODALE PARTI (modal_partsTable.php)
// ===================
$(document)
.off("click.backToParts", "#backToPartsBtnAnnotations")
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
if (
unsavedChanges &&
!confirm(
"Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?",
)
) {
return;
}
const iddatadb = $("#annotationsModal").data("iddatadb");
const idquotations = $("#annotationsModal").data("idquotations");
const trfHeader = $("#trfHeaderAnnotations").text();
// CHIUDI MODALE ANNOTAZIONI IN MODO SICURO
const annotationsModalElement =
document.getElementById("annotationsModal");
if (annotationsModalElement) {
const modalInstance = bootstrap.Modal.getInstance(
annotationsModalElement,
);
if (modalInstance) {
modalInstance.hide();
} else {
$(annotationsModalElement).modal("hide"); // fallback jQuery
}
}
console.log("Torno a modal_partsTable.php con:", {
iddatadb,
idquotations,
trfHeader,
});
// CARICA E APRI MODALE PARTI
$.get(
"modal_partsTable.php",
{
iddatadb: iddatadb || "",
idquotations: idquotations || "",
trfHeader: trfHeader,
},
function (data) {
// Rimuovi vecchio modale (con ID corretto)
$("#partsTableModal").remove();
$(".modal-backdrop").remove();
// Aggiungi nuovo modale
$("body").append(data);
// Apri con Bootstrap 5
const partsModalElement =
document.getElementById("partsTableModal");
if (partsModalElement) {
const modal = new bootstrap.Modal(partsModalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
modal.show();
} else {
let iddatadb =
$("#annotationsModal").attr("data-iddatadb");
$(
"button.parts-btn[data-iddatadb='" +
iddatadb +
"']",
).trigger("click");
}
},
).fail(function (xhr) {
console.error("Errore caricamento modale parti:", xhr);
alert(
"Errore 404: modal_partsTable.php non trovato o errore server.",
);
});
});
// ===================
// PARTS LIST
// ===================
function updatePartsList() {
console.log(
"updatePartsList chiamato con partsListData:",
partsListData,
);
const showMixParts = $("#showMixPartsAnnotations").is(":checked");
console.log("Stato showMixPartsAnnotations:", showMixParts);
const partsListElement = $("#partsListAnnotations");
if (!partsListElement.length) {
console.error(
"Elemento #partsListAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Elemento #partsListAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
partsListElement.empty();
const predefinedColors = [
"#ff0000", // Rosso
"#0000ff", // Blu
"#00ff00", // Verde
"#ffff00", // Giallo
"#ff00ff", // Magenta
"#00ffff", // Ciano
"#800080", // Viola
"#ffa500", // Arancione
];
partsListData.forEach((part) => {
const partNumber = part.part_number;
const partDescription = part.part_description;
const partColor =
partColors[partNumber] ||
(partDescription.toLowerCase().startsWith("mix")
? "#0000ff"
: "#ff0000");
if (
partNumber &&
partDescription &&
(showMixParts ||
!partDescription.toLowerCase().startsWith("mix"))
) {
const colorOptions = predefinedColors
.map(
(color) =>
`<div class="color-option" style="background-color: ${color}; width: 20px; height: 20px; display: inline-block; margin: 2px; cursor: pointer; pointer-events: auto;" data-color="${color}"></div>`,
)
.join("");
const listItem = `
<li class="list-group-item" data-part-number="${partNumber}">
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
<div class="color-picker-container" style="display:inline-block; position: relative; overflow: visible; z-index: 2000; margin-left:5px;">
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; cursor: pointer; pointer-events: auto;"></div>
<div class="color-picker" style="display: none; position: absolute; right: 0; z-index: 2000; background: #fff; border: 1px solid #ccc; padding: 5px;">${colorOptions}</div>
</div>
<div style="display:inline-flex; align-items:center; gap:4px; margin-left:5px;">
<input type="range" class="marker-size-slider"
min="12" max="48" step="2"
value="${partSizes[partNumber] || globalMarkerSize}"
style="width:70px;">
<span class="marker-size-value" style="font-size:0.8rem;">${partSizes[partNumber] || globalMarkerSize}px</span>
</div>
</li>`;
partsListElement.append(listItem);
}
});
console.log(
"Elementi aggiunti a #partsListAnnotations:",
partsListElement.find("li").length,
);
console.log("HTML di #partsListAnnotations:", partsListElement.html());
// Associa evento di selezione all'intera riga
partsListElement
.off("click.partsList")
.on("click.partsList", ".list-group-item", function (e) {
e.stopPropagation();
e.preventDefault();
// Ignora il click se è sulla paletta dei colori
if ($(e.target).closest(".color-picker-container").length) {
console.log(
"Click sulla paletta dei colori, ignoro selezione parte.",
);
return;
}
const $listItem = $(this);
const partNumber = $listItem.data("part-number");
if (
selectedPartNumber == partNumber &&
$listItem.hasClass("active")
) {
selectedPartNumber = null;
$listItem.removeClass("active");
return;
}
selectedPartNumber = partNumber;
$listItem.addClass("active").siblings().removeClass("active");
console.log(
"Parte selezionata tramite riga:",
selectedPartNumber,
);
});
// Associa eventi alla paletta dei colori
partsListElement
.off("click.selectedColor")
.on("click.selectedColor", ".selected-color", function (e) {
e.stopPropagation();
e.preventDefault();
const $picker = $(this).siblings(".color-picker");
console.log(
"Cliccato .selected-color, mostro/nascondo paletta:",
$picker.is(":visible"),
);
$(".color-picker").not($picker).hide();
$picker.toggle();
});
// === Gestione cambio colore ===
partsListElement
.off("click.colorOption")
.on("click.colorOption", ".color-option", function (e) {
e.stopPropagation();
e.preventDefault();
const $this = $(this);
const color = $this.data("color");
const $listItem = $this.closest("li");
const partNumber = $listItem.data("part-number");
console.log(
"Cliccato .color-option, colore:",
color,
"per parte:",
partNumber,
);
// Salva il nuovo colore
partColors[partNumber] = color;
// Aggiorna il colore visivo nel selettore
$listItem
.find(".selected-color")
.css("background-color", color);
let currentPhoto = $("#samplePhotoAnnotations").attr("src");
let annotations = photoAnnotations[currentPhoto];
if (annotations) {
annotations.markers.forEach(function (m) {
if (m.partNumber == partNumber) {
m.color = color;
let group = markerObjects[m.id];
if (group) {
let circle = group.item(0);
circle.set("fill", color);
circle.set("stroke", color);
}
}
});
fabricCanvas.renderAll();
}
// Chiudi la palette e aggiorna canvas
$this.closest(".color-picker").hide();
updateMarkers();
markUnsaved();
});
// === Slider locale per dimensione marker ===
partsListElement
.off("input.localMarkerSize")
.on("input.localMarkerSize", ".marker-size-slider", function (e) {
e.stopPropagation();
e.preventDefault();
// Riferimenti alla parte e valore scelto
const $slider = $(this);
const partNumber = $slider.closest("li").data("part-number");
const newSize = parseInt($slider.val());
// Aggiorna il valore visivo accanto allo slider
$slider.siblings(".marker-size-value").text(newSize + "px");
// Memorizza la nuova dimensione per quella parte
partSizes[partNumber] = newSize;
// Aggiorna i marker sul canvas
updateMarkers();
// Segnala modifiche non salvate
markUnsaved();
console.log(
`Dimensione marker aggiornata per parte ${partNumber}: ${newSize}px`,
);
});
$(document)
.off("click.colorPicker")
.on("click.colorPicker", function (e) {
if (!$(e.target).closest(".color-picker-container").length) {
console.log("Cliccato fuori, nascondo tutte le palette.");
$(".color-picker").hide();
}
});
}
// Delegazione evento per il checkbox
$(document)
.off("change.showMix", "#showMixPartsAnnotations")
.on("change.showMix", "#showMixPartsAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento change su #showMixPartsAnnotations, ID elemento:",
$(this).attr("id"),
);
const isChecked = $(this).is(":checked");
console.log(
"Checkbox #showMixPartsAnnotations cambiato:",
isChecked,
);
if (!$("#showMixPartsAnnotations").length) {
console.error(
"Checkbox #showMixPartsAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Checkbox #showMixPartsAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
updatePartsList();
updateMarkers();
if (
photoAnnotations[$("#samplePhotoAnnotations").attr("src")]
?.hasDescriptions
) {
updateDescriptions();
}
});
// ===================
// LOAD EXISTING PARTS
// ===================
function loadExistingParts(iddatadb, idquotations) {
console.log("loadExistingParts chiamato con:", {
iddatadb,
idquotations,
});
if (!iddatadb && !idquotations) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante per il caricamento delle parti.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
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) {
console.log("Risposta da load_parts:", response);
partsListData = [];
if (
response.success &&
response.parts &&
response.parts.length > 0
) {
partsListData = response.parts;
response.parts.forEach((part) => {
const defaultColor = part.part_description
.toLowerCase()
.startsWith("mix")
? "#0000ff"
: "#ff0000";
partColors[part.part_number] = defaultColor;
});
updatePartsList();
// Forza aggiornamento iniziale del checkbox
setTimeout(() => {
$("#showMixPartsAnnotations").trigger("change.showMix");
}, 100);
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">' +
(response.message ||
"Nessuna parte trovata per questo elemento.") +
"</div>",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
console.error("Errore AJAX in loadExistingParts:", {
status,
error,
responseText: xhr.responseText,
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle parti: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
}
// ===================
// MARKERS & DESCRIPTIONS
// ===================
function updateMarkers() {
console.log(
"updateMarkers chiamato, markerObjects:",
Object.keys(markerObjects),
);
for (let markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
const currentPhoto = $("#samplePhotoAnnotations").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 = $("#showMixPartsAnnotations").is(":checked");
annotations.markers.forEach((marker) => {
const part = partsListData.find(
(p) => p.part_number == marker.partNumber,
);
const partDescription = part ? part.part_description : "";
if (
!showMixParts &&
partDescription &&
partDescription.toLowerCase().startsWith("mix")
) {
console.log("Ignoro marker per parte Mix:", marker.partNumber);
return;
}
const size = partSizes[marker.partNumber] || globalMarkerSize;
const radius = size / 2;
const fontSize = Math.max(10, Math.round(size * 0.65));
const markerColor =
marker.color || partColors[marker.partNumber] || "#ff0000";
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,
});
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,
});
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.markerId = marker.id;
group.partNumber = marker.partNumber;
group.on("moving", function () {
marker.x = this.left / photoData.scale;
marker.y = this.top / photoData.scale;
console.log("Marker spostato:", {
partNumber: marker.partNumber,
x: marker.x,
y: marker.y,
});
markUnsaved();
});
fabricCanvas.add(group);
markerObjects[marker.id] = group;
});
fabricCanvas.renderAll();
console.log(
"Canvas aggiornato, numero di marker:",
Object.keys(markerObjects).length,
);
}
function updateDescriptions() {
console.log("updateDescriptions chiamato");
const currentPhoto = $("#samplePhotoAnnotations").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 = $("#showMixPartsAnnotations").is(":checked");
if (!annotations.hasDescriptions) {
if (descriptionTextbox) {
fabricCanvas.remove(descriptionTextbox);
descriptionTextbox = null;
fabricCanvas.renderAll();
}
console.log(
"Nessuna descrizione attiva, rimuovo textbox se presente.",
);
return;
}
const partsList = partsListData
.filter(
(part) =>
showMixParts ||
!part.part_description.toLowerCase().startsWith("mix"),
)
.map((part) => `${part.part_number} ${part.part_description}`);
const text = partsList.join("\n");
console.log("Testo descrizione generato:", text);
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: "transparent",
fontFamily: "Arial",
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
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;
console.log(
"Descrizione ridimensionata:",
annotations.descriptionSize,
);
markUnsaved();
});
descriptionTextbox.on("moving", function () {
annotations.descriptionPosition.x = this.left / photoData.scale;
annotations.descriptionPosition.y = this.top / photoData.scale;
console.log(
"Descrizione spostata:",
annotations.descriptionPosition,
);
markUnsaved();
});
fabricCanvas.add(descriptionTextbox);
fitFontToBox(descriptionTextbox);
fabricCanvas.renderAll();
console.log("Descrizione aggiunta al canvas:", text);
}
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) {
console.log(
"clearCanvasMarkers chiamato, clearDescriptions:",
clearDescriptions,
);
const currentPhoto = $("#samplePhotoAnnotations").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 markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
const canvas = document.getElementById("photoCanvasAnnotations");
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 ($("#samplePhotoAnnotations")[0].naturalWidth) {
ctx.drawImage(
$("#samplePhotoAnnotations")[0],
0,
0,
canvas.width,
canvas.height,
);
}
updateMarkers();
if (photoAnnotations[currentPhoto]?.hasDescriptions) {
updateDescriptions();
}
markUnsaved();
}
function undoLastMarker() {
console.log("undoLastMarker chiamato");
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
if (
photoAnnotations[currentPhoto] &&
photoAnnotations[currentPhoto].markers.length > 0
) {
const lastMarker = photoAnnotations[currentPhoto].markers.pop();
if (markerObjects[lastMarker.id]) {
fabricCanvas.remove(markerObjects[lastMarker.id]);
delete markerObjects[lastMarker.id];
fabricCanvas.renderAll();
}
console.log("Ultimo marker rimosso:", lastMarker);
markUnsaved();
}
}
// Delegazione evento per il pulsante "Descrizioni"
$(document)
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
.on(
"click.addDescriptions",
"#addDescriptionsBtnAnnotations",
function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #addDescriptionsBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#addDescriptionsBtnAnnotations").length) {
console.error(
"Pulsante #addDescriptionsBtnAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #addDescriptionsBtnAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
const currentPhoto = $("#samplePhotoAnnotations").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();
},
);
// Delegazione evento per il pulsante "Rimuovi"
$(document)
.off("click.removeAnnotations", "#removeAnnotationsBtnAnnotations")
.on(
"click.removeAnnotations",
"#removeAnnotationsBtnAnnotations",
function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #removeAnnotationsBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#removeAnnotationsBtnAnnotations").length) {
console.error(
"Pulsante #removeAnnotationsBtnAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #removeAnnotationsBtnAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
clearCanvasMarkers(true);
},
);
// Delegazione evento per il pulsante "Undo"
$(document)
.off("click.undoMarker", "#undoMarkerBtnAnnotations")
.on("click.undoMarker", "#undoMarkerBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #undoMarkerBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
undoLastMarker();
});
// ===================
// SAVE PHOTO
// ===================
$(document)
.off("click.savePhoto", "#savePhotoBtnAnnotations")
.on("click.savePhoto", "#savePhotoBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #savePhotoBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#samplePhotoAnnotations").attr("src")) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da salvare.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
if (!fabricCanvas) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Canvas Fabric.js non inizializzato.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
const canvas = document.getElementById("photoCanvasAnnotations");
const ctx = canvas.getContext("2d");
const img = $("#samplePhotoAnnotations")[0];
if (!img.complete || img.naturalWidth === 0) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: l\'immagine non è caricata correttamente.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
canvas.width = photoData.naturalWidth;
canvas.height = photoData.naturalHeight;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
try {
const fabricDataURL = fabricCanvas.toDataURL({
format: "png",
multiplier: 1 / photoData.scale,
});
const tempImg = new Image();
tempImg.src = fabricDataURL;
tempImg.onload = function () {
ctx.drawImage(tempImg, 0, 0, canvas.width, canvas.height);
fetch(fabricDataURL)
.then((res) => res.blob())
.then((blob) => {
canvas.toBlob(function (blob) {
if (!blob) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la creazione dell\'immagine finale.</div>',
);
$("#annotationsModal .modal-body").prepend(
errorMsg,
);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, "-");
const iddatadb =
$("#annotationsModal").data("iddatadb");
const idquotations =
$("#annotationsModal").data("idquotations");
const id = iddatadb || idquotations;
const endpoint = idquotations
? "save_annotated_photo_quotation.php"
: "save_annotated_photo.php";
const finalName = `photo_${id}_${timestamp}.png`;
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) {
if (response.success) {
const successMsg = $(
'<div class="alert alert-success temp-alert" role="alert">Foto salvata con successo: ' +
response.file_path +
"</div>",
);
$(
"#annotationsModal .modal-body",
).prepend(successMsg);
setTimeout(function () {
successMsg.fadeOut(
500,
function () {
$(this).remove();
},
);
}, 5000);
const photoSelector = $(
"#photoSelectorAnnotations",
);
if (photoSelector.length > 0) {
const newPhotoPath =
response.file_path;
const newPhotoName =
newPhotoPath
.split("/")
.pop();
const optionCount =
photoSelector.find(
"option",
).length;
const newOption = $(
"<option></option>",
)
.val(newPhotoPath)
.text(
`Photo ${optionCount + 1} - ${newPhotoName}`,
);
photoSelector.append(newOption);
photoSelector
.val(newPhotoPath)
.trigger("change");
}
unsavedChanges = false;
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della foto: ' +
response.message +
"</div>",
);
$(
"#annotationsModal .modal-body",
).prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(
500,
function () {
$(this).remove();
},
);
}, 5000);
}
},
error: function (xhr, status, error) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della foto: ' +
error +
" (" +
xhr.status +
")</div>",
);
$(
"#annotationsModal .modal-body",
).prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
}, "image/png");
});
};
} catch (e) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la generazione dell\'immagine: ' +
e.message +
"</div>",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
});
function markUnsaved() {
if (!unsavedChanges) {
unsavedChanges = true;
console.log("Modifiche non salvate rilevate.");
}
}
});