1642 lines
64 KiB
JavaScript

$(document).ready(function () {
// ===================
// GLOBAL STATE
// ===================
let photoData = {
naturalWidth: 0,
naturalHeight: 0,
displayWidth: 0,
displayHeight: 0,
scale: 1,
};
let photoAnnotations = {};
let partColors = {};
let selectedPartNumber = null;
let unsavedChanges = false;
let fabricCanvas = null;
let descriptionTextbox = null;
let markerObjects = {};
// ===================
// 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(
'<i class="fas fa-microphone-slash"></i> Stop Voce',
);
recognition.start();
$("#partsTableBody tr:last").find(".part-description").focus();
} else {
$btn.removeClass("btn-danger")
.addClass("btn-secondary")
.html('<i class="fas fa-microphone"></i> 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);
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
const modal = new bootstrap.Modal(
document.getElementById("partsModal"),
);
modal.show();
});
$("#partsModal .close-btn, #partsModal").on("click", function (event) {
if (event.target === this) {
const modal = bootstrap.Modal.getInstance(
document.getElementById("partsModal"),
);
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 () {
// Reset global state to initial values
photoData = {
naturalWidth: 0,
naturalHeight: 0,
displayWidth: 0,
displayHeight: 0,
scale: 1,
};
photoAnnotations = {};
partColors = {};
selectedPartNumber = null;
unsavedChanges = false;
if (fabricCanvas) {
fabricCanvas.off(); // Rimuove tutti gli eventi
fabricCanvas.dispose();
fabricCanvas = null;
}
descriptionTextbox = null;
markerObjects = {};
// Clear UI elements
$("#photoSelectorContainer").empty().hide();
$("#samplePhoto").attr("src", "");
$("#partsTableBody").empty();
// Remove any temporary messages
$(".temp-alert").remove();
});
// ===================
// 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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto trovata per questo elemento.</div>',
);
$("#partsModal .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>",
);
$("#partsModal .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 caricamento della foto: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#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 = $(
'<select id="photoSelector" 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 = $("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da scaricare.</div>',
);
$("#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 = `
<tr data-part-id="">
<td><input type="number" class="form-control form-control-sm part-number" value="${nextPartNumber || 1}" style="width: 80px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" value="${description}" placeholder="Inserisci descrizione" style="width: 100%;"></td>
<td>
<button type="button" class="btn btn-success btn-sm add-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-plus fa-xs"></i></button>
<button type="button" class="btn btn-primary btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
</td>
</tr>`;
$("#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];
if (markerObjects[partNumber]) {
fabricCanvas.remove(markerObjects[partNumber]);
delete markerObjects[partNumber];
fabricCanvas.renderAll();
}
updateRowButtons();
updatePartsList();
markUnsaved();
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nell\'eliminazione: ' +
response.message +
"</div>",
);
$("#partsModal .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 nell\'eliminazione: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
} else {
$row.remove();
delete partColors[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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
response.message +
"</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
},
error: function (xhr, status, error) {
$saveLoading.hide();
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio delle parti: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#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 = `
<tr data-part-id="${part.id}">
<td><input type="number" class="form-control form-control-sm part-number" value="${part.part_number}" style="width: 80px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" value="${part.part_description}" style="width: 100%;"></td>
<td>
<button type="button" class="btn btn-success btn-sm add-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-plus fa-xs"></i></button>
<button type="button" class="btn btn-primary btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash fa-xs"></i></button>
<span class="save-status text-success" style="display: none; margin-left: 5px;"><i class="fas fa-check fa-xs"></i></span>
<span class="save-loading text-warning" style="display: none; margin-left: 5px;"><i class="fas fa-spinner fa-spin fa-xs"></i></span>
</td>
</tr>`;
$("#partsTableBody").append(newRow);
partColors[part.part_number] = defaultColor;
});
} else {
addNewRow(1);
}
updateRowButtons();
updatePartsList();
},
error: function (xhr, status, error) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle parti: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
addNewRow(1);
},
});
}
// ===================
// 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 partColor =
partColors[partNumber] ||
(partDescription.startsWith("Mix") ? "#0000ff" : "#ff0000");
if (
partNumber &&
partDescription &&
(showMixParts || !partDescription.startsWith("Mix"))
) {
const colorOptions = predefinedColors
.map(
(color) =>
`<div class="color-option" style="background-color: ${color};" data-color="${color}"></div>`,
)
.join("");
const listItem = `
<li class="list-group-item" data-part-number="${partNumber}">
${partNumber} - ${partDescription}
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-success btn-sm add-to-mix-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-plus fa-xs"></i></button>
<div class="color-picker-container">
<div class="color-option selected-color" style="background-color: ${partColor}; margin-left: 5px;"></div>
<div class="color-picker">${colorOptions}</div>
</div>
</div>
</li>`;
$("#partsList").append(listItem);
}
});
$(".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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Crea prima una riga Mix usando il pulsante \'M\'.</div>',
);
$("#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();
}
});
$("#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
)
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 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";
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;
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",
}));
$.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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nella rinumerazione delle parti: ' +
response.message +
"</div>",
);
$("#partsModal .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 nella rinumerazione delle parti: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da salvare.</div>',
);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
if (!fabricCanvas) {
console.log("fabricCanvas is undefined");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Canvas Fabric.js non inizializzato.</div>',
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: l\'immagine non è caricata correttamente.</div>',
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la conversione dell\'overlay Fabric.js.</div>',
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante il rendering dell\'overlay Fabric.js.</div>',
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la generazione dell\'immagine annotata.</div>',
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la creazione dell\'immagine finale.</div>',
);
$("#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 = $(
'<div class="alert alert-success temp-alert" role="alert">Foto salvata con successo: ' +
response.file_path +
"</div>",
);
$("#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 = $("<option></option>")
.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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: ' +
(response.message ||
"Errore sconosciuto") +
"</div>",
);
$("#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 = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della foto: ' +
error +
" (" +
xhr.status +
")</div>",
);
$("#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);
});