Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6ea17388c | |||
| 1c2b4ab7a6 | |||
| 31cb23b00e | |||
| d29563d20d | |||
| 82af925ac1 | |||
| 5d8360dd87 | |||
| 683073c244 | |||
| 8d6fe92481 | |||
| dbc66723a6 |
@@ -12,12 +12,15 @@ $(document).ready(function () {
|
||||
|
||||
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 partsListData = [];
|
||||
// DIMENSIONE GLOBALE MARKER
|
||||
let globalMarkerSize = 24;
|
||||
|
||||
// ===================
|
||||
// MODAL INITIALIZATION
|
||||
@@ -29,6 +32,8 @@ $(document).ready(function () {
|
||||
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>',
|
||||
@@ -67,6 +72,9 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
modal.show();
|
||||
// Inizializza slider dimensione marker
|
||||
$("#markerSizeSlider").val(globalMarkerSize);
|
||||
$("#markerSizeValue").text(globalMarkerSize + "px");
|
||||
|
||||
// Debug: Verifica presenza elementi DOM
|
||||
console.log(
|
||||
@@ -128,6 +136,7 @@ $(document).ready(function () {
|
||||
}
|
||||
descriptionTextbox = null;
|
||||
markerObjects = {};
|
||||
globalMarkerSize = 24;
|
||||
$("#photoSelectorContainerAnnotations").empty().hide();
|
||||
$("#samplePhotoAnnotations").attr("src", "");
|
||||
$("#partsListAnnotations").empty();
|
||||
@@ -144,6 +153,16 @@ $(document).ready(function () {
|
||||
$(":focus").blur();
|
||||
});
|
||||
|
||||
// SLIDER DIMENSIONE MARKER
|
||||
$(document)
|
||||
.off("input", "#markerSizeSlider")
|
||||
.on("input", "#markerSizeSlider", function () {
|
||||
globalMarkerSize = parseInt($(this).val());
|
||||
$("#markerSizeValue").text(globalMarkerSize + "px");
|
||||
updateMarkers();
|
||||
markUnsaved();
|
||||
});
|
||||
|
||||
// ===================
|
||||
// PHOTO LOADERS
|
||||
// ===================
|
||||
@@ -465,113 +484,84 @@ $(document).ready(function () {
|
||||
);
|
||||
|
||||
// ===================
|
||||
// BACK TO PARTS MODAL
|
||||
// TORNA AL MODALE PARTI (modal_partsTable.php)
|
||||
// ===================
|
||||
$(document)
|
||||
.off("click.backToParts", "#backToPartsBtnAnnotations")
|
||||
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log(
|
||||
"Evento click su #backToPartsBtnAnnotations, ID elemento:",
|
||||
$(this).attr("id"),
|
||||
);
|
||||
if (!$("#backToPartsBtnAnnotations").length) {
|
||||
console.error(
|
||||
"Pulsante #backToPartsBtnAnnotations non trovato nel DOM.",
|
||||
);
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #backToPartsBtnAnnotations non trovato nel DOM.</div>',
|
||||
);
|
||||
$("#annotationsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Controlla modifiche non salvate
|
||||
if (
|
||||
unsavedChanges &&
|
||||
!confirm(
|
||||
"Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?",
|
||||
)
|
||||
) {
|
||||
console.log(
|
||||
"Tornare al modale delle parti annullato a causa di modifiche non salvate.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiudi annotationsModal
|
||||
const annotationsModalElement =
|
||||
document.getElementById("annotationsModal");
|
||||
const annotationsModal = bootstrap.Modal.getInstance(
|
||||
annotationsModalElement,
|
||||
);
|
||||
if (annotationsModal) {
|
||||
annotationsModal.hide();
|
||||
} else {
|
||||
console.error(
|
||||
"Impossibile trovare l'istanza del modale #annotationsModal.",
|
||||
);
|
||||
}
|
||||
|
||||
// Apri partsModal
|
||||
const iddatadb = $("#annotationsModal").data("iddatadb");
|
||||
const idquotations = $("#annotationsModal").data("idquotations");
|
||||
const trfHeader = $("#trfHeaderAnnotations").text();
|
||||
console.log("Apertura partsModal con:", {
|
||||
|
||||
// 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();
|
||||
|
||||
const partsModalElement = document.getElementById("partsModal");
|
||||
if (!partsModalElement) {
|
||||
console.error("Elemento #partsModal non trovato nel DOM.");
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Errore: Il modale delle parti non è presente nel DOM.</div>',
|
||||
// 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.",
|
||||
);
|
||||
$("#annotationsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
let partsModal = bootstrap.Modal.getInstance(partsModalElement);
|
||||
if (!partsModal) {
|
||||
partsModal = new bootstrap.Modal(partsModalElement, {
|
||||
backdrop: true,
|
||||
keyboard: true,
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Inizializza il modale delle parti
|
||||
if (typeof window.initPartsModal === "function") {
|
||||
window.initPartsModal(iddatadb, idquotations, trfHeader);
|
||||
partsModal.show();
|
||||
console.log("partsModal aperto con successo.");
|
||||
} else {
|
||||
console.error("Funzione initPartsModal non definita.");
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Errore: Funzione initPartsModal non trovata.</div>',
|
||||
);
|
||||
$("#annotationsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===================
|
||||
// PARTS LIST
|
||||
// ===================
|
||||
@@ -633,12 +623,19 @@ $(document).ready(function () {
|
||||
)
|
||||
.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;">
|
||||
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; margin-left: 5px; 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>
|
||||
<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);
|
||||
}
|
||||
@@ -687,30 +684,77 @@ $(document).ready(function () {
|
||||
$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);
|
||||
|
||||
// Se il marker è già presente, aggiorna anche sul canvas
|
||||
if (markerObjects[partNumber]) {
|
||||
const group = markerObjects[partNumber];
|
||||
const circle = group.item(0); // il cerchio
|
||||
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) {
|
||||
@@ -892,8 +936,10 @@ $(document).ready(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
const radius = 12;
|
||||
const fontSize = 16;
|
||||
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";
|
||||
|
||||
@@ -1013,7 +1059,7 @@ $(document).ready(function () {
|
||||
scaleY: 1,
|
||||
backgroundColor: "transparent",
|
||||
fontFamily: "Arial",
|
||||
fontSize: 24,
|
||||
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
||||
fill: "#000000",
|
||||
padding: 10,
|
||||
editable: false,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,22 @@
|
||||
<!-- Modal per la gestione delle annotazioni -->
|
||||
<div class="modal fade" id="annotationsModal" tabindex="-1" aria-labelledby="annotationsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl" style="max-width: 80% !important; width: 80% !important;">
|
||||
<div class="modal-dialog modal-xl" style="max-width: 90% !important; width: 90% !important;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="annotationsModalLabel">Annotazioni per TRF: <span id="trfHeaderAnnotations"></span></h5>
|
||||
|
||||
<!-- SLIDER PER DIMENSIONE MARKER -->
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-left: 20px;">
|
||||
<label for="markerSizeSlider" style="margin: 0; font-size: 0.9rem; white-space: nowrap;">Dimensione marker:</label>
|
||||
<input type="range" id="markerSizeSlider" min="16" max="48" value="24" step="2" style="width: 120px;">
|
||||
<span id="markerSizeValue" style="font-weight: bold; min-width: 30px;">24px</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<!-- COLONNA SINISTRA RIDOTTA -->
|
||||
<div class="col-md-6">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h6 style="margin: 0;">Elenco Parti</h6>
|
||||
@@ -16,15 +25,17 @@
|
||||
<label for="showMixPartsAnnotations" style="font-size: 0.9rem;">Mix</label>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="partsListAnnotations" class="list-group"></ul>
|
||||
<ul id="partsListAnnotations" class="list-group" style="max-height: 500px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
|
||||
<!-- COLONNA DESTRA PIÙ GRANDE -->
|
||||
<div class="col-md-6">
|
||||
<h6>Foto del Campione</h6>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="downloadPhotoBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem; margin-right: 10px;"><i class="fas fa-download"></i></button>
|
||||
<div id="photoSelectorContainerAnnotations" style="display: none;"></div>
|
||||
</div>
|
||||
<div style="position: relative; width: 100%; min-height: 400px;">
|
||||
<div style="position: relative; width: 100%; min-height: 500px; border: 1px solid #ddd; border-radius: 4px; overflow: hidden;">
|
||||
<img id="samplePhotoAnnotations" src="" alt="Foto del campione" style="max-width: 100%; max-height: 100%; object-fit: contain; position: absolute; top: 0; left: 0;">
|
||||
<canvas id="photoCanvasAnnotations" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
|
||||
<canvas id="overlayCanvasAnnotations" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000;"></canvas>
|
||||
@@ -216,4 +227,32 @@
|
||||
transform: translateX(-50%);
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
/* Stile per lo slider */
|
||||
#markerSizeSlider {
|
||||
-webkit-appearance: none;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #ddd;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#markerSizeSlider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#markerSizeSlider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="row parts-row">
|
||||
<div class="col-md-9">
|
||||
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
@@ -14,6 +14,10 @@
|
||||
<div style="display: flex; align-items: center;">
|
||||
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button>
|
||||
<button type="button" class="btn btn-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
|
||||
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
|
||||
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
||||
@@ -354,4 +358,112 @@
|
||||
position: relative;
|
||||
z-index: 1095 !important
|
||||
}
|
||||
|
||||
/* Evidenza salvataggio riga nel parts table */
|
||||
/* Aumenta la specificità per le classi di flash */
|
||||
table#partsTable tr.row-saving {
|
||||
background-color: #f0ad4e !important;
|
||||
/* Arancione per salvataggio in corso */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
table#partsTable tr.row-success {
|
||||
background-color: #5cb85c !important;
|
||||
/* Verde per successo */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
table#partsTable tr.row-error {
|
||||
background-color: #d9534f !important;
|
||||
/* Rosso per errore */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Stato base: nascosti (verrà sovrascritto dallo style inline di jQuery) */
|
||||
#partsModal .save-loading,
|
||||
#partsModal .save-status {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* Quando NON sono nascosti via style inline (jQuery .show()), forzali a inline-flex */
|
||||
#partsModal .save-loading:not([style*="display: none"]),
|
||||
#partsModal .save-status:not([style*="display: none"]) {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Loading (giallo) */
|
||||
/* Loading (giallo) */
|
||||
#partsModal .save-loading {
|
||||
background: #ffd753ff;
|
||||
border: 1px solid #ffd042ff;
|
||||
color: #111;
|
||||
/* testo nero */
|
||||
}
|
||||
|
||||
#partsModal .save-loading i {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* icona nera */
|
||||
|
||||
#partsModal .save-loading::after {
|
||||
content: " Salvataggio…";
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* Salvato (verde) */
|
||||
#partsModal .save-status {
|
||||
background: #5dff83ff;
|
||||
border: 1px solid #4effafff;
|
||||
color: #111;
|
||||
/* testo nero */
|
||||
}
|
||||
|
||||
#partsModal .save-status i {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* icona nera */
|
||||
#partsModal .save-status::after {
|
||||
content: " Salvato";
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* Animazioni */
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: .9
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(.85);
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* rosso */
|
||||
</style>
|
||||
+267
-123
@@ -7,6 +7,35 @@ $(document).ready(function () {
|
||||
let matrici = [];
|
||||
let macroMatrici = [];
|
||||
|
||||
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
|
||||
function getPartId($row) {
|
||||
// Legge in ordine: cache jQuery, nostra cache, attributo
|
||||
const d = $row.data("part-id");
|
||||
const ours = $row.data("__pid");
|
||||
const attr = $row.attr("data-part-id");
|
||||
|
||||
// Scegli il primo definito
|
||||
const id =
|
||||
d !== undefined
|
||||
? d
|
||||
: ours !== undefined
|
||||
? ours
|
||||
: attr !== undefined
|
||||
? attr
|
||||
: null;
|
||||
|
||||
// Normalizza: "new" o "" NON sono ID validi
|
||||
return id === "new" || id === "" || id === null ? null : id;
|
||||
}
|
||||
|
||||
function setPartId($row, id) {
|
||||
if (!id) return;
|
||||
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
|
||||
$row.attr("data-part-id", id);
|
||||
$row.data("part-id", id); // <<< fondamentale per invalidare la cache di jQuery
|
||||
$row.data("__pid", id);
|
||||
}
|
||||
|
||||
// ===================
|
||||
// VOICE RECOGNITION SETUP
|
||||
// ===================
|
||||
@@ -439,6 +468,7 @@ $(document).ready(function () {
|
||||
|
||||
$(document).on("click", ".add-mix-global", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const maxPartNumber = Math.max(
|
||||
...$("#partsTableBody tr")
|
||||
.map(function () {
|
||||
@@ -446,36 +476,177 @@ $(document).ready(function () {
|
||||
})
|
||||
.get(),
|
||||
);
|
||||
addNewRow(maxPartNumber + 1, true); // Riga Mix
|
||||
|
||||
// Crea la riga Mix
|
||||
addNewRow(maxPartNumber + 1, true);
|
||||
const $mixRow = $("#partsTableBody tr:last");
|
||||
|
||||
// Consenti SOLO ora la creazione (INSERT) della riga Mix
|
||||
$mixRow.data("allowCreateMix", true);
|
||||
|
||||
// esegue SUBITO l'INSERT così ottieni part-id
|
||||
saveRow($mixRow);
|
||||
});
|
||||
|
||||
function extractPartId(response) {
|
||||
// prova i campi più comuni
|
||||
if (response == null) return null;
|
||||
|
||||
if (response.part_id) return response.part_id;
|
||||
if (response.id) return response.id;
|
||||
if (response.insert_id) return response.insert_id;
|
||||
if (response.lastId) return response.lastId;
|
||||
|
||||
// array di id
|
||||
if (Array.isArray(response.part_ids) && response.part_ids[0]) {
|
||||
return response.part_ids[0];
|
||||
}
|
||||
|
||||
// oggetti annidati
|
||||
if (response.parts && response.parts[0]) {
|
||||
if (response.parts[0].id) return response.parts[0].id;
|
||||
if (response.parts[0].part_id) return response.parts[0].part_id;
|
||||
if (response.parts[0].insert_id) return response.parts[0].insert_id;
|
||||
}
|
||||
|
||||
// altri possibili nomi dal backend
|
||||
if (response.new_id) return response.new_id;
|
||||
if (response.partId) return response.partId;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveRow($row) {
|
||||
const partNumber = $row.find(".part-number").val();
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
const dateexpiry = $row.find(".part-dateexpiry").val();
|
||||
const note = $row.data("note") || null;
|
||||
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";
|
||||
let partId = getPartId($row);
|
||||
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
|
||||
const endpoint = idquotations
|
||||
? "save_parts_quotation.php"
|
||||
: "save_parts.php";
|
||||
const data = idquotations ? { idquotations } : { iddatadb };
|
||||
|
||||
// Evita salvataggi concorrenti
|
||||
if ($row.data("saving") === true) return;
|
||||
|
||||
// Blocca INSERT del Mix se non esplicitamente richiesta
|
||||
if (isMix === "Y" && !partId && $row.data("allowCreateMix") !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row.data("saving", true);
|
||||
|
||||
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,
|
||||
dateexpiry: dateexpiry || null,
|
||||
note: note,
|
||||
},
|
||||
],
|
||||
}),
|
||||
contentType: "application/json",
|
||||
|
||||
success: function (response) {
|
||||
// assegna ID appena arriva (robusto)
|
||||
if (response.success) {
|
||||
const newId = extractPartId(response);
|
||||
if (newId) {
|
||||
setPartId($row, newId);
|
||||
} else {
|
||||
console.warn(
|
||||
"Parte salvata ma ID non presente nella risposta. Ricarico parti per sincronizzare gli ID.",
|
||||
);
|
||||
loadExistingParts(iddatadb, idquotations); // <<< ORA ANCHE PER RIGHE NORMALI
|
||||
}
|
||||
}
|
||||
|
||||
$row.data("saving", false);
|
||||
$row.removeData("allowCreateMix");
|
||||
|
||||
// ora che l'ID c'è di sicuro, sblocco gli update pendenti (es. M+)
|
||||
$row.trigger("row:saved");
|
||||
|
||||
$saveLoading.hide();
|
||||
if (response.success) {
|
||||
$saveStatus.show();
|
||||
setTimeout(() => $saveStatus.hide(), 2000);
|
||||
} else {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
|
||||
response.message +
|
||||
"</div>",
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
error: function (xhr, status, error) {
|
||||
$row.data("saving", false);
|
||||
$row.removeData("allowCreateMix");
|
||||
$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(() => {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on("click", ".add-mix-row", function (e) {
|
||||
e.preventDefault();
|
||||
const $row = $(this).closest("tr");
|
||||
const partDescription = $row.find(".part-description").val().trim();
|
||||
|
||||
const $srcRow = $(this).closest("tr");
|
||||
const partDescription = $srcRow.find(".part-description").val().trim();
|
||||
if (!partDescription) {
|
||||
const errorMsg = $(
|
||||
'<div class="alert alert-danger temp-alert" role="alert">Inserisci una descrizione valida prima di aggiungerla al Mix.</div>',
|
||||
);
|
||||
$("#partsModal .modal-body").prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
setTimeout(
|
||||
() =>
|
||||
errorMsg.fadeOut(500, function () {
|
||||
$(this).remove();
|
||||
}),
|
||||
5000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPartNumber = Math.max(
|
||||
...$("#partsTableBody tr")
|
||||
.map(function () {
|
||||
return parseInt($(this).find(".part-number").val()) || 0;
|
||||
})
|
||||
.get(),
|
||||
);
|
||||
|
||||
let mixDescription = `Mix ${partDescription}`;
|
||||
const $mixRow = $("#partsTableBody tr")
|
||||
let $mixRow = $("#partsTableBody tr")
|
||||
.filter(function () {
|
||||
return $(this)
|
||||
.find(".part-description")
|
||||
@@ -485,31 +656,59 @@ $(document).ready(function () {
|
||||
})
|
||||
.last();
|
||||
|
||||
if ($mixRow.length > 0) {
|
||||
let currentMix = $mixRow.find(".part-description").val().trim();
|
||||
if (currentMix === "Mix") {
|
||||
mixDescription = currentMix + " " + partDescription;
|
||||
} else if (!currentMix.includes(partDescription)) {
|
||||
mixDescription = currentMix + " + " + partDescription;
|
||||
// Se non esiste una riga Mix, ne creo una e la INSERISCO SUBITO (come fa il bottone in header)
|
||||
if ($mixRow.length === 0) {
|
||||
const maxPartNumber = Math.max(
|
||||
...$("#partsTableBody tr")
|
||||
.map(function () {
|
||||
return (
|
||||
parseInt($(this).find(".part-number").val()) || 0
|
||||
);
|
||||
})
|
||||
.get(),
|
||||
);
|
||||
|
||||
addNewRow(maxPartNumber + 1, true);
|
||||
$mixRow = $("#partsTableBody tr:last");
|
||||
$mixRow.find(".part-description").val(`Mix ${partDescription}`);
|
||||
|
||||
// Consenti la creazione (INSERT) della riga Mix e salvala subito
|
||||
$mixRow.data("allowCreateMix", true);
|
||||
|
||||
saveRow($mixRow); // -> INSERT
|
||||
return; // la descrizione include già l'elemento appena aggiunto
|
||||
}
|
||||
|
||||
// Aggiorna la descrizione del Mix esistente
|
||||
const currentMix = $mixRow.find(".part-description").val().trim();
|
||||
let newDesc = currentMix;
|
||||
if (currentMix === "Mix") newDesc = currentMix + " " + partDescription;
|
||||
else if (!currentMix.includes(partDescription))
|
||||
newDesc = currentMix + " + " + partDescription;
|
||||
|
||||
$mixRow.find(".part-description").val(newDesc);
|
||||
|
||||
// Se il Mix è già in salvataggio (INSERT o UPDATE in corso), accodiamo un solo UPDATE
|
||||
if ($mixRow.data("saving") === true) {
|
||||
// evita più code accumulate
|
||||
if (!$mixRow.data("pendingUpdate")) {
|
||||
$mixRow.data("pendingUpdate", true);
|
||||
$mixRow.one("row:saved", function () {
|
||||
$mixRow.removeData("pendingUpdate");
|
||||
// ora che saving è false, salviamo l'ultima descrizione impostata
|
||||
saveRow($mixRow);
|
||||
});
|
||||
}
|
||||
$mixRow
|
||||
.find(".part-description")
|
||||
.val(mixDescription)
|
||||
.trigger("blur");
|
||||
} else {
|
||||
addNewRow(maxPartNumber + 1, true); // Crea nuova riga Mix
|
||||
const $newMixRow = $("#partsTableBody tr:last");
|
||||
$newMixRow
|
||||
.find(".part-description")
|
||||
.val(mixDescription)
|
||||
.trigger("blur");
|
||||
// libero: salva subito
|
||||
saveRow($mixRow);
|
||||
}
|
||||
});
|
||||
|
||||
function addNewRow(nextPartNumber, isMix = false) {
|
||||
const description = isMix ? "Mix" : "";
|
||||
const newRow = `
|
||||
<tr data-part-id="">
|
||||
<tr data-part-id="new">
|
||||
<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"></td>
|
||||
<td>
|
||||
@@ -540,7 +739,7 @@ $(document).ready(function () {
|
||||
// ===================
|
||||
$(document).on("click", ".note-btn", function () {
|
||||
const $row = $(this).closest("tr");
|
||||
const partId = $row.data("part-id");
|
||||
const partId = getPartId($row);
|
||||
const note = $row.data("note") || "";
|
||||
const $noteModal = $("#noteModal");
|
||||
$noteModal.find(".part-note").val(note);
|
||||
@@ -655,7 +854,7 @@ $(document).ready(function () {
|
||||
$(document).on("change", ".part-dateexpiry", function () {
|
||||
const $input = $(this);
|
||||
const $row = $input.closest("tr");
|
||||
const partId = $row.data("part-id");
|
||||
const partId = getPartId($row);
|
||||
const dateexpiry = $input.val();
|
||||
const iddatadb = $("#partsModal").data("iddatadb");
|
||||
const idquotations = $("#partsModal").data("idquotations");
|
||||
@@ -953,88 +1152,7 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
$(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 dateexpiry = $row.find(".part-dateexpiry").val();
|
||||
const note = $row.data("note") || null;
|
||||
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,
|
||||
dateexpiry: dateexpiry || null,
|
||||
note: note,
|
||||
},
|
||||
],
|
||||
}),
|
||||
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);
|
||||
} 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
saveRow($(this).closest("tr"));
|
||||
});
|
||||
|
||||
function loadExistingParts(iddatadb, idquotations) {
|
||||
@@ -1221,6 +1339,7 @@ $(document).ready(function () {
|
||||
partId,
|
||||
currentValue,
|
||||
selectedMacro,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1297,6 +1416,7 @@ $(document).ready(function () {
|
||||
partId,
|
||||
idmatrice,
|
||||
selectedMacro = null,
|
||||
fromFilter = false
|
||||
) {
|
||||
if (typeof $.fn.select2 === "undefined") {
|
||||
$select.replaceWith(
|
||||
@@ -1366,16 +1486,24 @@ $(document).ready(function () {
|
||||
true,
|
||||
true,
|
||||
);
|
||||
$select.append(option).trigger("change");
|
||||
|
||||
if (!fromFilter) $select.append(option).trigger("change");
|
||||
else $select.append(option);
|
||||
|
||||
partMatrice[partNumber] = matrice.IdMatrice;
|
||||
} else {
|
||||
// Aggiusta valore non valido
|
||||
$select.val(null).trigger("change");
|
||||
if (!fromFilter) $select.val(null).trigger("change");
|
||||
|
||||
partMatrice[partNumber] = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$select.val(null).trigger("change", [{ skipHandler: true }]);
|
||||
}
|
||||
|
||||
$select.on("change", function () {
|
||||
$select.on("change", function (event, data) {
|
||||
if (data && data?.skipHandler) return;
|
||||
|
||||
const idmatrice = $(this).val();
|
||||
const $row = $(this).closest("tr");
|
||||
const partId = $row.data("part-id");
|
||||
@@ -1882,3 +2010,19 @@ $(document).on("click", ".save-common-note-btn", function () {
|
||||
markUnsaved();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("click", "#showHideImageBtn", function () {
|
||||
let mainRow = $(this).closest(".parts-row");
|
||||
let photoContainer = mainRow.find(".col-md-3");
|
||||
let tableContainer = mainRow.find("#partsTable").closest("div[class*='col-md']");
|
||||
|
||||
if (photoContainer.hasClass("d-none")) {
|
||||
photoContainer.removeClass("d-none");
|
||||
tableContainer.removeClass("col-md-12").addClass("col-md-9");
|
||||
$(this).html("<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
|
||||
} else {
|
||||
photoContainer.addClass("d-none");
|
||||
tableContainer.removeClass("col-md-9").addClass("col-md-12");
|
||||
$(this).html("<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
|
||||
}
|
||||
});
|
||||
@@ -399,14 +399,18 @@ if (isset($_GET['edit_id'])) {
|
||||
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
<?php include('modal_parts.php'); ?>
|
||||
<?php include('photos_functions.php'); ?>
|
||||
<div id="partsModalContainer"></div>
|
||||
<div id="annotationsModalContainer"></div>
|
||||
<?php include 'photos_functions.php'; ?>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
||||
<script src="photos.js"></script>
|
||||
<script src="parts.js"></script>
|
||||
<script src="annotationsModal.js"></script>
|
||||
<script src="partsTable.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Mostra messaggi di stato se presenti
|
||||
@@ -423,6 +427,45 @@ if (isset($_GET['edit_id'])) {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
$(document).on('click', '.parts-btn', function() {
|
||||
const idquotations = $(this).data('idquotations');
|
||||
$.ajax({
|
||||
url: 'modal_partsTable.php',
|
||||
method: 'GET',
|
||||
data: {
|
||||
idquotations: idquotations
|
||||
},
|
||||
success: function(response) {
|
||||
$('#partsModalContainer').html(response);
|
||||
const modalElement = document.getElementById('partsModal');
|
||||
if (!modalElement) return;
|
||||
$("#trfHeader").text(`Quotation #${idquotations}`);
|
||||
$("#partsModal").data("idquotations", idquotations);
|
||||
let modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement, {
|
||||
backdrop: true
|
||||
});
|
||||
modal.show();
|
||||
if (typeof window.loadParts === 'function') window.loadParts(null, idquotations);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
alert('Errore nel caricamento del modale: ' + error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('hidden.bs.modal', '#partsModal', function() {
|
||||
$('#partsModalContainer').empty();
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open').css('padding-right', '');
|
||||
});
|
||||
|
||||
$(document).on('hidden.bs.modal', '#annotationsModal', function() {
|
||||
$('#annotationsModalContainer').empty();
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open').css('padding-right', '');
|
||||
});
|
||||
|
||||
|
||||
// Inizializza DataTables se non siamo in modalità modifica
|
||||
if (!document.querySelector('#editForm')) {
|
||||
$('#quotationsTable').DataTable({
|
||||
@@ -524,12 +567,7 @@ if (isset($_GET['edit_id'])) {
|
||||
</script>
|
||||
|
||||
<!-- Modale per le foto in quotations.php -->
|
||||
<div class="modal" id="photosModal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<div class="popup-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user