Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec0c2062e | |||
| 03771e3ca8 | |||
| f6ea17388c | |||
| 1c2b4ab7a6 | |||
| 31cb23b00e | |||
| d29563d20d | |||
| 82af925ac1 | |||
| 5d8360dd87 | |||
| 683073c244 | |||
| 8d6fe92481 | |||
| dbc66723a6 | |||
| 218fc14462 | |||
| 29e4b41874 | |||
| eef9ae8d36 | |||
| 68c867a3f4 |
@@ -12,12 +12,15 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
let photoAnnotations = {};
|
let photoAnnotations = {};
|
||||||
let partColors = {};
|
let partColors = {};
|
||||||
|
let partSizes = {}; // memorizza la dimensione specifica per parte
|
||||||
let selectedPartNumber = null;
|
let selectedPartNumber = null;
|
||||||
let unsavedChanges = false;
|
let unsavedChanges = false;
|
||||||
let fabricCanvas = null;
|
let fabricCanvas = null;
|
||||||
let descriptionTextbox = null;
|
let descriptionTextbox = null;
|
||||||
let markerObjects = {};
|
let markerObjects = {};
|
||||||
let partsListData = [];
|
let partsListData = [];
|
||||||
|
// DIMENSIONE GLOBALE MARKER
|
||||||
|
let globalMarkerSize = 24;
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// MODAL INITIALIZATION
|
// MODAL INITIALIZATION
|
||||||
@@ -29,6 +32,8 @@ $(document).ready(function () {
|
|||||||
trfHeader,
|
trfHeader,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#annotationsModal").attr('data-iddatadb', iddatadb);
|
||||||
|
|
||||||
if (!iddatadb && !idquotations) {
|
if (!iddatadb && !idquotations) {
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante. Impossibile inizializzare il modale delle annotazioni.</div>',
|
'<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();
|
modal.show();
|
||||||
|
// Inizializza slider dimensione marker
|
||||||
|
$("#markerSizeSlider").val(globalMarkerSize);
|
||||||
|
$("#markerSizeValue").text(globalMarkerSize + "px");
|
||||||
|
|
||||||
// Debug: Verifica presenza elementi DOM
|
// Debug: Verifica presenza elementi DOM
|
||||||
console.log(
|
console.log(
|
||||||
@@ -128,6 +136,7 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
descriptionTextbox = null;
|
descriptionTextbox = null;
|
||||||
markerObjects = {};
|
markerObjects = {};
|
||||||
|
globalMarkerSize = 24;
|
||||||
$("#photoSelectorContainerAnnotations").empty().hide();
|
$("#photoSelectorContainerAnnotations").empty().hide();
|
||||||
$("#samplePhotoAnnotations").attr("src", "");
|
$("#samplePhotoAnnotations").attr("src", "");
|
||||||
$("#partsListAnnotations").empty();
|
$("#partsListAnnotations").empty();
|
||||||
@@ -144,6 +153,16 @@ $(document).ready(function () {
|
|||||||
$(":focus").blur();
|
$(":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
|
// PHOTO LOADERS
|
||||||
// ===================
|
// ===================
|
||||||
@@ -465,113 +484,84 @@ $(document).ready(function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// BACK TO PARTS MODAL
|
// TORNA AL MODALE PARTI (modal_partsTable.php)
|
||||||
// ===================
|
// ===================
|
||||||
$(document)
|
$(document)
|
||||||
.off("click.backToParts", "#backToPartsBtnAnnotations")
|
.off("click.backToParts", "#backToPartsBtnAnnotations")
|
||||||
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
|
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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 (
|
if (
|
||||||
unsavedChanges &&
|
unsavedChanges &&
|
||||||
!confirm(
|
!confirm(
|
||||||
"Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?",
|
"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;
|
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 iddatadb = $("#annotationsModal").data("iddatadb");
|
||||||
const idquotations = $("#annotationsModal").data("idquotations");
|
const idquotations = $("#annotationsModal").data("idquotations");
|
||||||
const trfHeader = $("#trfHeaderAnnotations").text();
|
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,
|
iddatadb,
|
||||||
idquotations,
|
idquotations,
|
||||||
trfHeader,
|
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");
|
// Aggiungi nuovo modale
|
||||||
if (!partsModalElement) {
|
$("body").append(data);
|
||||||
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>',
|
|
||||||
);
|
|
||||||
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
||||||
setTimeout(function () {
|
|
||||||
errorMsg.fadeOut(500, function () {
|
|
||||||
$(this).remove();
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let partsModal = bootstrap.Modal.getInstance(partsModalElement);
|
// Apri con Bootstrap 5
|
||||||
if (!partsModal) {
|
const partsModalElement =
|
||||||
partsModal = new bootstrap.Modal(partsModalElement, {
|
document.getElementById("partsTableModal");
|
||||||
|
if (partsModalElement) {
|
||||||
|
const modal = new bootstrap.Modal(partsModalElement, {
|
||||||
backdrop: true,
|
backdrop: true,
|
||||||
keyboard: true,
|
keyboard: true,
|
||||||
focus: true,
|
focus: true,
|
||||||
});
|
});
|
||||||
}
|
modal.show();
|
||||||
|
|
||||||
// Inizializza il modale delle parti
|
|
||||||
if (typeof window.initPartsModal === "function") {
|
|
||||||
window.initPartsModal(iddatadb, idquotations, trfHeader);
|
|
||||||
partsModal.show();
|
|
||||||
console.log("partsModal aperto con successo.");
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Funzione initPartsModal non definita.");
|
let iddatadb = $("#annotationsModal").attr('data-iddatadb');
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
$("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
|
// PARTS LIST
|
||||||
// ===================
|
// ===================
|
||||||
@@ -635,9 +625,16 @@ $(document).ready(function () {
|
|||||||
const listItem = `
|
const listItem = `
|
||||||
<li class="list-group-item" data-part-number="${partNumber}">
|
<li class="list-group-item" data-part-number="${partNumber}">
|
||||||
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
|
<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-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; margin-left: 5px; cursor: pointer; pointer-events: auto;"></div>
|
<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 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>
|
</div>
|
||||||
</li>`;
|
</li>`;
|
||||||
partsListElement.append(listItem);
|
partsListElement.append(listItem);
|
||||||
@@ -687,30 +684,77 @@ $(document).ready(function () {
|
|||||||
$picker.toggle();
|
$picker.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// === Gestione cambio colore ===
|
||||||
partsListElement
|
partsListElement
|
||||||
.off("click.colorOption")
|
.off("click.colorOption")
|
||||||
.on("click.colorOption", ".color-option", function (e) {
|
.on("click.colorOption", ".color-option", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const color = $this.data("color");
|
const color = $this.data("color");
|
||||||
const $listItem = $this.closest("li");
|
const $listItem = $this.closest("li");
|
||||||
const partNumber = $listItem.data("part-number");
|
const partNumber = $listItem.data("part-number");
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Cliccato .color-option, colore:",
|
"Cliccato .color-option, colore:",
|
||||||
color,
|
color,
|
||||||
"per parte:",
|
"per parte:",
|
||||||
partNumber,
|
partNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Salva il nuovo colore
|
||||||
partColors[partNumber] = color;
|
partColors[partNumber] = color;
|
||||||
|
|
||||||
|
// Aggiorna il colore visivo nel selettore
|
||||||
$listItem
|
$listItem
|
||||||
.find(".selected-color")
|
.find(".selected-color")
|
||||||
.css("background-color", 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();
|
$this.closest(".color-picker").hide();
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
markUnsaved();
|
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)
|
$(document)
|
||||||
.off("click.colorPicker")
|
.off("click.colorPicker")
|
||||||
.on("click.colorPicker", function (e) {
|
.on("click.colorPicker", function (e) {
|
||||||
@@ -892,8 +936,10 @@ $(document).ready(function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const radius = 12;
|
const size = partSizes[marker.partNumber] || globalMarkerSize;
|
||||||
const fontSize = 16;
|
const radius = size / 2;
|
||||||
|
const fontSize = Math.max(10, Math.round(size * 0.65));
|
||||||
|
|
||||||
const markerColor =
|
const markerColor =
|
||||||
marker.color || partColors[marker.partNumber] || "#ff0000";
|
marker.color || partColors[marker.partNumber] || "#ff0000";
|
||||||
|
|
||||||
@@ -1013,7 +1059,7 @@ $(document).ready(function () {
|
|||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
fontFamily: "Arial",
|
fontFamily: "Arial",
|
||||||
fontSize: 24,
|
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
||||||
fill: "#000000",
|
fill: "#000000",
|
||||||
padding: 10,
|
padding: 10,
|
||||||
editable: false,
|
editable: false,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class VisualLimsApiClient
|
|||||||
return self::$instance;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function authenticate()
|
private function authenticate($retryCount = 0, $maxRetries = 3)
|
||||||
{
|
{
|
||||||
$ch = curl_init("{$this->baseUrl}/api/authentication/authenticate");
|
$ch = curl_init("{$this->baseUrl}/api/authentication/authenticate");
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
@@ -45,16 +45,22 @@ class VisualLimsApiClient
|
|||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||||||
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'w') ?: fopen('php://stderr', 'w');
|
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'a') ?: fopen('php://stderr', 'w');
|
||||||
curl_setopt($ch, CURLOPT_STDERR, $log);
|
curl_setopt($ch, CURLOPT_STDERR, $log);
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$curl_error = curl_error($ch);
|
$curl_error = curl_error($ch);
|
||||||
|
$log_message = date('Y-m-d H:i:s') . " - Auth attempt {$retryCount}: HTTP {$http_code}, Error: {$curl_error}, Response: " . substr($response, 0, 1000) . "\n";
|
||||||
|
fwrite($log, $log_message);
|
||||||
fclose($log);
|
fclose($log);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($response === false || $http_code != 200) {
|
if ($response === false || $http_code != 200) {
|
||||||
|
if ($http_code === 400 && strpos($response, 'Cannot persist the object') !== false && $retryCount < $maxRetries) {
|
||||||
|
usleep(500000); // Ritardo di 500ms
|
||||||
|
return $this->authenticate($retryCount + 1, $maxRetries); // Riprova
|
||||||
|
}
|
||||||
throw new Exception("Autenticazione fallita: HTTP {$http_code}, Errore cURL: {$curl_error}, Risposta: " . substr($response, 0, 1000));
|
throw new Exception("Autenticazione fallita: HTTP {$http_code}, Errore cURL: {$curl_error}, Risposta: " . substr($response, 0, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,5 +197,4 @@ class VisualLimsApiClient
|
|||||||
{
|
{
|
||||||
return $this->baseUrl;
|
return $this->baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -33,9 +33,9 @@ try {
|
|||||||
throw new Exception("Missing iddatadb");
|
throw new Exception("Missing iddatadb");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 STEP 1+2: Fetch Cliente ID + Schema ID
|
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT et.idclient AS clienteId, et.idschema AS schemaId
|
SELECT d.idclient AS clienteId, et.idschema AS schemaId
|
||||||
FROM datadb as d
|
FROM datadb as d
|
||||||
INNER JOIN excel_templates as et ON d.templateid = et.id
|
INNER JOIN excel_templates as et ON d.templateid = et.id
|
||||||
WHERE d.iddatadb = :iddatadb
|
WHERE d.iddatadb = :iddatadb
|
||||||
|
|||||||
@@ -23,16 +23,52 @@ try {
|
|||||||
// Componi endpoint finale
|
// Componi endpoint finale
|
||||||
$endpoint = "Cliente?$queryString";
|
$endpoint = "Cliente?$queryString";
|
||||||
|
|
||||||
// Richiama API
|
// Funzione per eseguire la chiamata con retry
|
||||||
|
function makeApiRequest($api, $endpoint, $maxRetries = 3)
|
||||||
|
{
|
||||||
|
for ($retry = 0; $retry < $maxRetries; $retry++) {
|
||||||
|
try {
|
||||||
|
// Tenta la chiamata API
|
||||||
$data = $api->get($endpoint);
|
$data = $api->get($endpoint);
|
||||||
|
|
||||||
// Salva risposta per debug
|
// Salva risposta per debug
|
||||||
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
|
||||||
|
return $data;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
|
||||||
|
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
|
||||||
|
// Forza il refresh del token
|
||||||
|
try {
|
||||||
|
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
|
||||||
|
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
|
||||||
|
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
|
||||||
|
} catch (Exception $refreshEx) {
|
||||||
|
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
|
||||||
|
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
|
||||||
|
}
|
||||||
|
// Ritarda leggermente prima del retry
|
||||||
|
usleep(500000); // 500ms
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Altri errori non gestiti dal retry
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esegui la chiamata con retry
|
||||||
|
$data = makeApiRequest($api, $endpoint);
|
||||||
|
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode([
|
$errorResponse = [
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage(),
|
||||||
]);
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
];
|
||||||
|
error_log("Errore in get_clienti.php: " . json_encode($errorResponse));
|
||||||
|
echo json_encode($errorResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ foreach ($allMappings as $mapping) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recupera l'idclient di default dal template (se presente)
|
||||||
|
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||||
|
$template_stmt->execute([$template_id]);
|
||||||
|
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$default_idclient = $template['idclient'] ?? null;
|
||||||
|
|
||||||
$insertedIds = $_POST['inserted_ids'] ?? $_SESSION['inserted_ids'];
|
$insertedIds = $_POST['inserted_ids'] ?? $_SESSION['inserted_ids'];
|
||||||
|
|
||||||
// Recupera i dati appena inseriti con i nomi degli utenti
|
// Recupera i dati appena inseriti con i nomi degli utenti
|
||||||
@@ -410,6 +416,46 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
.flatpickr-input {
|
.flatpickr-input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.client-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #80bdff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-dropdown-smaller {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
height: 31px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 31px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
</head>
|
</head>
|
||||||
@@ -471,6 +517,13 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<div class="grid-cell" style="flex: 0 0 300px;">
|
||||||
|
<select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect" name="idclient">
|
||||||
|
<option value="">Select a client...</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="propagate-btn" data-column="idclient"><i class="fas fa-arrow-down"></i></button>
|
||||||
|
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span>
|
||||||
|
</div>
|
||||||
<div class="grid-cell" style="flex: 0 0 150px;"></div>
|
<div class="grid-cell" style="flex: 0 0 150px;"></div>
|
||||||
<?php
|
<?php
|
||||||
$autoIndex = 0;
|
$autoIndex = 0;
|
||||||
@@ -535,10 +588,12 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
<div class="resizer"></div>
|
<div class="resizer"></div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="grid-header" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 150px; position: relative;">Status<div class="resizer"></div>
|
<div class="grid-header" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 300px; position: relative;">Client<div class="resizer"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-header" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 150px; position: relative;">Status<div class="resizer"></div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
$headerIndex = $mainFieldMapping ? 3 : 2;
|
$headerIndex = $mainFieldMapping ? 4 : 3;
|
||||||
foreach ($allMappings as $mapping) {
|
foreach ($allMappings as $mapping) {
|
||||||
if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
|
if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
|
||||||
echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 150px; position: relative;'>" . htmlspecialchars($mapping['field_label']) . "<div class='resizer'></div></div>";
|
echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 150px; position: relative;'>" . htmlspecialchars($mapping['field_label']) . "<div class='resizer'></div></div>";
|
||||||
@@ -604,14 +659,19 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="grid-cell editable-cell" data-col="status" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 150px;">
|
<div class="grid-cell editable-cell" data-col="idclient" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 2 : 1 ?>" style="flex: 0 0 300px;">
|
||||||
|
<select name="rows[<?= $index ?>][idclient]" class="cell-input dropdown-select client-select searchable-client" data-current-value="<?= htmlspecialchars($row['idclient'] ?? $default_idclient) ?>">
|
||||||
|
<option value="">Select a client...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="grid-cell editable-cell" data-col="status" data-row="<?= $index ?>" data-index="<?= $mainFieldMapping ? 3 : 2 ?>" style="flex: 0 0 150px;">
|
||||||
<span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
<span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
||||||
<?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
|
<?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
|
||||||
</span>
|
</span>
|
||||||
<input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
<input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
$cellIndex = $mainFieldMapping ? 3 : 2;
|
$cellIndex = $mainFieldMapping ? 4 : 3;
|
||||||
$rowDetails = array_filter($manualDetails, fn($d) => $d['datadb_id'] == $row['iddatadb']);
|
$rowDetails = array_filter($manualDetails, fn($d) => $d['datadb_id'] == $row['iddatadb']);
|
||||||
$autoIndex = 0;
|
$autoIndex = 0;
|
||||||
foreach ($allMappings as $mapping) {
|
foreach ($allMappings as $mapping) {
|
||||||
@@ -840,11 +900,16 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
if (matches) {
|
if (matches) {
|
||||||
const mappingId = matches[1];
|
const mappingId = matches[1];
|
||||||
formData.append(`details${mappingId}field_value`, input.value);
|
formData.append(`details${mappingId}field_value`, input.value);
|
||||||
if (input.tagName === 'SELECT' && input.classList.contains('dropdown-select')) {
|
if (input.tagName === 'SELECT') {
|
||||||
input.setAttribute('data-selected-value', input.value);
|
input.setAttribute('data-selected-value', input.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const idclientSelect = row.querySelector(`select[name="rows[${rowIndex}][idclient]"]`);
|
||||||
|
if (idclientSelect) {
|
||||||
|
formData.append('idclient', idclientSelect.value);
|
||||||
|
}
|
||||||
formData.append('iddatadb', iddatadb);
|
formData.append('iddatadb', iddatadb);
|
||||||
|
|
||||||
fetch('save_edited_row.php', {
|
fetch('save_edited_row.php', {
|
||||||
@@ -912,6 +977,11 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const idclientSelect = row.querySelector(`select[name="rows[${rowIndex}][idclient]"]`);
|
||||||
|
if (idclientSelect) {
|
||||||
|
formData.append('idclient', idclientSelect.value);
|
||||||
|
}
|
||||||
formData.append('iddatadb', iddatadb);
|
formData.append('iddatadb', iddatadb);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -984,6 +1054,90 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const inputs = document.querySelectorAll('.cell-input');
|
const inputs = document.querySelectorAll('.cell-input');
|
||||||
|
let clientData = []; // Dichiarazione di clientData qui
|
||||||
|
|
||||||
|
// Funzione per caricare i client
|
||||||
|
async function loadClients(retryCount = 0, maxRetries = 3) {
|
||||||
|
const clientLoadingStatus = document.getElementById("clientLoadingStatus");
|
||||||
|
try {
|
||||||
|
clientLoadingStatus.style.display = 'inline';
|
||||||
|
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
|
||||||
|
const response = await fetch("get_clienti.php", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 500 && data.error.includes('Cannot persist the object') && retryCount < maxRetries) {
|
||||||
|
console.log(`Tentativo ${retryCount + 1}/${maxRetries}: Riprovo a caricare i clienti...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return loadClients(retryCount + 1, maxRetries);
|
||||||
|
}
|
||||||
|
throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData = data.value || [];
|
||||||
|
const select = document.getElementById("clientSelect");
|
||||||
|
select.innerHTML = '<option value="">Select a client...</option>';
|
||||||
|
clientData.forEach(client => {
|
||||||
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
|
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
|
||||||
|
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
select.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
populateClientDropdowns();
|
||||||
|
clientLoadingStatus.textContent = "Clienti caricati.";
|
||||||
|
} catch (error) {
|
||||||
|
clientLoadingStatus.textContent = "Errore nel caricamento.";
|
||||||
|
console.error("Errore nel caricamento dei client:", error);
|
||||||
|
Swal.fire({
|
||||||
|
title: "Errore!",
|
||||||
|
text: "Impossibile caricare i clienti: " + error.message,
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funzione per popolare i dropdown dei client
|
||||||
|
function populateClientDropdowns() {
|
||||||
|
const clientDropdowns = document.querySelectorAll('select[name^="rows"][name$="[idclient]"]');
|
||||||
|
clientDropdowns.forEach(dropdown => {
|
||||||
|
const currentValue = dropdown.getAttribute('data-current-value') || '';
|
||||||
|
dropdown.innerHTML = '<option value="">Select a client...</option>';
|
||||||
|
clientData.forEach(client => {
|
||||||
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
|
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
|
||||||
|
if (String(id) === String(currentValue)) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
dropdown.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ripristina il valore corrente
|
||||||
|
if (currentValue) {
|
||||||
|
dropdown.value = currentValue;
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
dropdown.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carica i client all'avvio
|
||||||
|
loadClients();
|
||||||
|
|
||||||
|
// Gestione degli input
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
input.addEventListener('focus', function() {
|
input.addEventListener('focus', function() {
|
||||||
this.closest('.grid-cell').classList.add('expanded');
|
this.closest('.grid-cell').classList.add('expanded');
|
||||||
@@ -993,42 +1147,60 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestione della propagazione
|
||||||
const propagateButtons = document.querySelectorAll('.propagate-btn');
|
const propagateButtons = document.querySelectorAll('.propagate-btn');
|
||||||
propagateButtons.forEach(button => {
|
propagateButtons.forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', async function() {
|
||||||
const column = this.getAttribute('data-column');
|
const column = this.getAttribute('data-column');
|
||||||
const input = this.previousElementSibling;
|
const input = this.previousElementSibling;
|
||||||
const value = input.value;
|
const value = input.tagName === 'SELECT' ? input.value : input.value;
|
||||||
|
|
||||||
|
console.log('Propagate clicked for column:', column, 'with value:', value); // Debug
|
||||||
|
|
||||||
|
// Assicurati che i dropdown dei client siano popolati
|
||||||
|
if (column === 'idclient' && clientData.length === 0) {
|
||||||
|
await loadClients(); // Carica i client se non ancora caricati
|
||||||
|
}
|
||||||
|
|
||||||
const gridTopCells = document.querySelector('.grid-top').querySelectorAll('.grid-cell');
|
const gridTopCells = document.querySelector('.grid-top').querySelectorAll('.grid-cell');
|
||||||
const targetTopIndex = Array.from(gridTopCells).findIndex(cell =>
|
const targetTopIndex = Array.from(gridTopCells).findIndex(cell =>
|
||||||
cell.querySelector('.propagate-btn[data-column="' + column + '"]')
|
cell.querySelector('.propagate-btn[data-column="' + column + '"]')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('Target index found:', targetTopIndex); // Debug
|
||||||
|
|
||||||
if (targetTopIndex !== -1) {
|
if (targetTopIndex !== -1) {
|
||||||
const rows = document.querySelectorAll('.grid-row');
|
const rows = document.querySelectorAll('.grid-row');
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const cells = row.querySelectorAll('.grid-cell');
|
const cells = row.querySelectorAll('.grid-cell');
|
||||||
if (cells.length > targetTopIndex) {
|
if (cells.length > targetTopIndex) {
|
||||||
const targetInput = cells[targetTopIndex].querySelector('input, select');
|
const targetInput = cells[targetTopIndex].querySelector('select, input');
|
||||||
if (targetInput) {
|
if (targetInput) {
|
||||||
targetInput.value = value;
|
console.log('Setting value on target input:', targetInput, 'with value:', value); // Debug
|
||||||
if (targetInput.tagName === 'SELECT') {
|
if (targetInput.tagName === 'SELECT') {
|
||||||
targetInput.setAttribute('data-selected-value', value);
|
targetInput.value = value;
|
||||||
const event = new Event('change');
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
targetInput.dispatchEvent(event);
|
targetInput.dispatchEvent(event);
|
||||||
} else if (targetInput.classList.contains('date-picker')) {
|
} else if (targetInput.classList.contains('date-picker')) {
|
||||||
// Update Flatpickr instance
|
|
||||||
const flatpickrInstance = targetInput._flatpickr;
|
const flatpickrInstance = targetInput._flatpickr;
|
||||||
if (flatpickrInstance && value) {
|
if (flatpickrInstance && value) {
|
||||||
flatpickrInstance.setDate(value, true);
|
flatpickrInstance.setDate(value, true);
|
||||||
}
|
}
|
||||||
const event = new Event('change');
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
targetInput.dispatchEvent(event);
|
targetInput.dispatchEvent(event);
|
||||||
} else {
|
} else {
|
||||||
const event = new Event('change');
|
targetInput.value = value;
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
targetInput.dispatchEvent(event);
|
targetInput.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('No target input found in cell'); // Debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1036,6 +1208,7 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestione del ridimensionamento delle colonne
|
||||||
const resizers = document.querySelectorAll('.resizer');
|
const resizers = document.querySelectorAll('.resizer');
|
||||||
let currentResizer = null;
|
let currentResizer = null;
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
@@ -1078,7 +1251,7 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
const dropdownData = {};
|
const dropdownData = {};
|
||||||
|
|
||||||
async function populateDropdowns() {
|
async function populateDropdowns() {
|
||||||
const dropdowns = document.querySelectorAll('.dropdown-select');
|
const dropdowns = document.querySelectorAll('.dropdown-select:not(.client-select)');
|
||||||
if (dropdowns.length === 0) {
|
if (dropdowns.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1102,13 +1275,15 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
dropdownData[fieldId] = data[fieldId] || [];
|
dropdownData[fieldId] = data[fieldId] || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.error('Errore nel caricamento dei valori per dropdown:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dropdowns.forEach(dropdown => {
|
dropdowns.forEach(dropdown => {
|
||||||
const fieldId = dropdown.getAttribute('data-field-id');
|
const fieldId = dropdown.getAttribute('data-field-id');
|
||||||
const selectedValue = dropdown.getAttribute('data-selected-value') || '';
|
const selectedValue = dropdown.getAttribute('data-selected-value') || '';
|
||||||
const currentValue = dropdown.value;
|
const currentValue = dropdown.value || '';
|
||||||
|
|
||||||
if (!fieldId || !dropdownData[fieldId]) {
|
if (!fieldId || !dropdownData[fieldId]) {
|
||||||
dropdown.innerHTML = '<option value="">Errore nel caricamento</option>';
|
dropdown.innerHTML = '<option value="">Errore nel caricamento</option>';
|
||||||
@@ -1128,63 +1303,182 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|||||||
dropdown.appendChild(option);
|
dropdown.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((currentValue || selectedValue) && dropdown.value !== (currentValue || selectedValue)) {
|
// Ripristina il valore corrente
|
||||||
dropdown.value = '';
|
if (currentValue || selectedValue) {
|
||||||
|
dropdown.value = currentValue || selectedValue;
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
dropdown.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
populateDropdowns();
|
populateDropdowns();
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
$(document).on('click', '.add-part-btn', function() {
|
|
||||||
const rowIndex = $(this).data('row');
|
|
||||||
const row = $(this).closest('.grid-row');
|
|
||||||
const iddatadb = row.data('id');
|
|
||||||
const input = row.find(`input[name="rows[${rowIndex}][tested_component]"]`);
|
|
||||||
const description = input.val().trim();
|
|
||||||
|
|
||||||
if (!description) {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
alert('Inserisci un valore per Tested Component');
|
let clientData = [];
|
||||||
return;
|
|
||||||
|
async function loadClients(retryCount = 0, maxRetries = 3) {
|
||||||
|
const clientLoadingStatus = document.getElementById("clientLoadingStatus");
|
||||||
|
try {
|
||||||
|
clientLoadingStatus.style.display = 'inline';
|
||||||
|
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
|
||||||
|
const response = await fetch("get_clienti.php", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 500 && data.error.includes('Cannot persist the object') && retryCount < maxRetries) {
|
||||||
|
console.log(`Tentativo ${retryCount + 1}/${maxRetries}: Riprovo a caricare i clienti...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return loadClients(retryCount + 1, maxRetries);
|
||||||
|
}
|
||||||
|
throw new Error(data.error || `Errore HTTP: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
clientData = data.value || [];
|
||||||
iddatadb: iddatadb,
|
const select = document.getElementById("clientSelect");
|
||||||
parts: [{
|
select.innerHTML = '<option value="">Select a client...</option>';
|
||||||
part_number: '1', // Imposta part_number a '1'
|
clientData.forEach(client => {
|
||||||
part_description: description,
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
mix: 'N'
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
}]
|
const codice = (client.CodiceNazioneFatturazione || '').trim();
|
||||||
};
|
const option = new Option(`${nome.trim()} - ${codice} (ID: ${id})`, id);
|
||||||
|
|
||||||
$.ajax({
|
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
|
||||||
url: 'save_parts.php',
|
option.selected = true;
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
alert('Parte aggiunta con successo!');
|
|
||||||
input.val(''); // Pulisci l'input dopo l'aggiunta
|
|
||||||
// Opzionale: aggiorna la tabella delle parti se il modal è aperto
|
|
||||||
const partsModal = $('#partsModal');
|
|
||||||
if (partsModal.hasClass('show')) {
|
|
||||||
loadParts(iddatadb);
|
|
||||||
}
|
}
|
||||||
} else {
|
select.add(option);
|
||||||
alert('Errore: ' + response.message);
|
});
|
||||||
|
|
||||||
|
populateClientDropdowns();
|
||||||
|
clientLoadingStatus.textContent = "Clienti caricati.";
|
||||||
|
} catch (error) {
|
||||||
|
clientLoadingStatus.textContent = "Errore nel caricamento.";
|
||||||
|
Swal.fire({
|
||||||
|
title: "Errore!",
|
||||||
|
text: "Impossibile caricare i clienti: " + error.message,
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
error: function() {
|
|
||||||
alert('Errore durante la richiesta AJAX');
|
function populateClientDropdowns() {
|
||||||
|
const clientDropdowns = document.querySelectorAll('select[name^="rows"][name$="[idclient]"]');
|
||||||
|
clientDropdowns.forEach(dropdown => {
|
||||||
|
const currentValue = dropdown.getAttribute('data-current-value') || '';
|
||||||
|
dropdown.innerHTML = '<option value="">Select a client...</option>';
|
||||||
|
clientData.forEach(client => {
|
||||||
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
|
const codice = (client.CodiceNazioneFatturazione || '').trim();
|
||||||
|
const option = new Option(`${nome.trim()} - ${codice} (ID: ${id})`, id);
|
||||||
|
|
||||||
|
if (String(id) === String(currentValue)) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
dropdown.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ripristina il valore corrente
|
||||||
|
if (currentValue) {
|
||||||
|
dropdown.value = currentValue;
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
dropdown.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadClients();
|
||||||
|
|
||||||
|
document.getElementById('clientSelect').addEventListener('change', function() {
|
||||||
|
const gridCell = this.closest('.grid-cell');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
gridCell.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target.matches('select[name^="rows"][name$="[idclient]"]')) {
|
||||||
|
const gridCell = e.target.closest('.grid-cell');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
gridCell.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Gestione del cambio valore per il dropdown principale dei client
|
||||||
|
document.getElementById('clientSelect').addEventListener('change', function() {
|
||||||
|
const gridCell = this.closest('.grid-cell');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
gridCell.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestione del cambio valore per i dropdown dei client nelle righe
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target.matches('select[name^="rows"][name$="[idclient]"]')) {
|
||||||
|
const gridCell = e.target.closest('.grid-cell');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
gridCell.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Initialize Select2 for searchable client dropdowns
|
||||||
|
$('.searchable-client').select2({
|
||||||
|
placeholder: "Select a client...",
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%',
|
||||||
|
dropdownCssClass: 'select2-dropdown-smaller',
|
||||||
|
minimumInputLength: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure Select2 dropdowns trigger change events for unsaved changes tracking
|
||||||
|
$('.searchable-client').on('select2:select select2:clear', function(e) {
|
||||||
|
const gridCell = this.closest('.grid-cell');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
gridCell.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update propagate functionality for client dropdown
|
||||||
|
$('.propagate-btn[data-column="idclient"]').on('click', async function() {
|
||||||
|
const value = $('#clientSelect').val();
|
||||||
|
const rows = document.querySelectorAll('.grid-row');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const clientSelect = row.querySelector('select[name$="[idclient]"]');
|
||||||
|
if (clientSelect) {
|
||||||
|
$(clientSelect).val(value).trigger('change.select2');
|
||||||
|
const event = new Event('change', {
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
clientSelect.closest('.grid-cell').dispatchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.parts-btn', function() {
|
$(document).on('click', '.parts-btn', function() {
|
||||||
const iddatadb = $(this).data('iddatadb') || null;
|
const iddatadb = $(this).data('iddatadb') || null;
|
||||||
const idquotations = $(this).data('idquotations') || null;
|
const idquotations = $(this).data('idquotations') || null;
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recupera l'idclient di default dal template
|
||||||
|
$template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
|
||||||
|
$template_stmt->execute([$template_id]);
|
||||||
|
$template = $template_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$default_idclient = $template['idclient'] ?? null;
|
||||||
|
|
||||||
$values = [
|
$values = [
|
||||||
$template_id,
|
$template_id,
|
||||||
$importReferenceCode,
|
$importReferenceCode,
|
||||||
@@ -83,9 +89,10 @@ foreach ($selected_rows as $rowIndex) {
|
|||||||
$user_id,
|
$user_id,
|
||||||
null,
|
null,
|
||||||
date('Y-m-d'),
|
date('Y-m-d'),
|
||||||
$excelrow // Aggiunto excelrow per la colonna excelrow
|
$excelrow,
|
||||||
|
$default_idclient // Aggiungi idclient
|
||||||
];
|
];
|
||||||
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
$sql = "INSERT INTO datadb (templateid, importreferencecode, filename_import, status, user_id, limscode, importdate, excelrow, idclient) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute($values);
|
$stmt->execute($values);
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ if (session_status() == PHP_SESSION_NONE) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Imposta variabili di sessione di default per evitare errori
|
// Imposta variabili di sessione di default per evitare errori
|
||||||
$_SESSION['iduserlogin'] = null; // Nessun utente loggato
|
$_SESSION['iduserlogin'] = '1'; // Nessun utente loggato
|
||||||
$_SESSION['nameuser'] = 'Ospite';
|
$_SESSION['nameuser'] = 'Ospite';
|
||||||
$_SESSION['surnameuser'] = '';
|
$_SESSION['surnameuser'] = '';
|
||||||
$_SESSION['emailuser'] = '';
|
$_SESSION['emailuser'] = '';
|
||||||
$_SESSION['photouser'] = '';
|
$_SESSION['photouser'] = '';
|
||||||
$photouser = $_SESSION['photouser'];
|
$photouser = $_SESSION['photouser'];
|
||||||
$photousername = '';
|
$photousername = '';
|
||||||
|
$iduserlogin = $_SESSION['iduserlogin'];
|
||||||
// Include file di lingua, se necessario
|
// Include file di lingua, se necessario
|
||||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
// Recupera l'ID dell'utente loggato
|
||||||
|
$user_id = $iduserlogin ?? 1;
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => "ID dell'utente autenticato mancante"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT DISTINCT q.*
|
||||||
|
FROM quotations q
|
||||||
|
INNER JOIN identification_parts ip
|
||||||
|
ON ip.idquotations = q.id
|
||||||
|
AND ip.iddatadb IS NULL
|
||||||
|
WHERE q.iduser = :iduser"
|
||||||
|
);
|
||||||
|
$stmt->execute([':iduser' => $user_id]);
|
||||||
|
$quotations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'quotations' => $quotations]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento delle quotations: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -1,13 +1,22 @@
|
|||||||
<!-- Modal per la gestione delle annotazioni -->
|
<!-- Modal per la gestione delle annotazioni -->
|
||||||
<div class="modal fade" id="annotationsModal" tabindex="-1" aria-labelledby="annotationsModalLabel" aria-hidden="true">
|
<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-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="annotationsModalLabel">Annotazioni per TRF: <span id="trfHeaderAnnotations"></span></h5>
|
<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>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<!-- COLONNA SINISTRA RIDOTTA -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<h6 style="margin: 0;">Elenco Parti</h6>
|
<h6 style="margin: 0;">Elenco Parti</h6>
|
||||||
@@ -16,15 +25,17 @@
|
|||||||
<label for="showMixPartsAnnotations" style="font-size: 0.9rem;">Mix</label>
|
<label for="showMixPartsAnnotations" style="font-size: 0.9rem;">Mix</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul id="partsListAnnotations" class="list-group"></ul>
|
<ul id="partsListAnnotations" class="list-group" style="max-height: 500px; overflow-y: auto;"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNA DESTRA PIÙ GRANDE -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6>Foto del Campione</h6>
|
<h6>Foto del Campione</h6>
|
||||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
<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>
|
<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 id="photoSelectorContainerAnnotations" style="display: none;"></div>
|
||||||
</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;">
|
<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="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>
|
<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%);
|
transform: translateX(-50%);
|
||||||
z-index: 3000;
|
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>
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row parts-row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
|
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
<div style="display: flex; align-items: center;">
|
<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-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-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-info btn-sm ms-2 d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Add Quotation</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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
||||||
@@ -141,174 +146,351 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Finestra modale "Aggiungi preventivo" -->
|
||||||
|
<div class="modal fade" id="addQuotationModal" tabindex="-1" aria-labelledby="addQuotationModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-md">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addQuotationModalLabel">Choose a quotation</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<select id="addQuotationSelect" class="form-control form-control-sm" style="width: 100% !important; min-width: 100% !important"></select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="addQuotationBtn">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* --- Base --- */
|
||||||
#partsModal {
|
#partsModal {
|
||||||
z-index: 1060 !important;
|
z-index: 1060 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsModal .modal-backdrop {
|
#partsModal .modal-backdrop {
|
||||||
z-index: 1055 !important;
|
z-index: 1055 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsModal .modal-content {
|
#partsModal .modal-content {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addQuotationModal {
|
||||||
|
z-index: 1060 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
#addQuotationModal .modal-backdrop {
|
||||||
|
z-index: 1055 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabelle */
|
||||||
#partsTable tr {
|
#partsTable tr {
|
||||||
display: table-row !important;
|
display: table-row !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable tr:hover {
|
#partsTable tr:hover {
|
||||||
background-color: #f5f5f5;
|
background: #f5f5f5
|
||||||
display: table-row !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable td,
|
#partsTable td,
|
||||||
#partsTable th {
|
#partsTable th {
|
||||||
padding: 0.2rem;
|
padding: .2rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable input,
|
#partsTable input,
|
||||||
#partsTable select {
|
#partsTable select {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: .1rem .3rem
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable button {
|
#partsTable button {
|
||||||
padding: 0.1rem 0.3rem;
|
padding: .1rem .3rem;
|
||||||
margin: 0 2px;
|
margin: 0 2px
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable i {
|
#partsTable i {
|
||||||
font-size: 0.6rem !important;
|
font-size: .6rem !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#global-matrice,
|
/* --- Larghezze fisse header --- */
|
||||||
.part-matrice,
|
/* MacroMatrici = 250px */
|
||||||
#macro-matrice-filter {
|
#macro-matrice-filter {
|
||||||
width: 100% !important;
|
width: 250px !important;
|
||||||
min-width: 100% !important;
|
min-width: 250px !important;
|
||||||
|
max-width: 250px !important;
|
||||||
|
flex: 0 0 250px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default #global-matrice {
|
#macro-matrice-filter.select2-hidden-accessible+.select2 {
|
||||||
width: 350px !important;
|
width: 250px !important;
|
||||||
|
min-width: 250px !important;
|
||||||
|
max-width: 250px !important;
|
||||||
|
flex: 0 0 250px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#macro-matrice-filter.select2-hidden-accessible+.select2 .select2-selection__rendered {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matrice globale = 450px */
|
||||||
|
#global-matrice {
|
||||||
|
width: 450px !important;
|
||||||
|
min-width: 450px !important;
|
||||||
|
max-width: 450px !important;
|
||||||
|
flex: 0 0 450px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#global-matrice.select2-hidden-accessible+.select2 {
|
||||||
|
width: 450px !important;
|
||||||
|
min-width: 450px !important;
|
||||||
|
max-width: 450px !important;
|
||||||
|
flex: 0 0 450px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#global-matrice.select2-hidden-accessible+.select2 .select2-selection__rendered {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select delle righe (colonna Matrice) = 150px */
|
||||||
|
.part-matrice {
|
||||||
|
width: 300px !important;
|
||||||
|
min-width: 300px !important;
|
||||||
|
max-width: 300px !important;
|
||||||
|
flex: 0 0 300px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-matrice.select2-hidden-accessible+.select2 {
|
||||||
|
width: 300px !important;
|
||||||
|
min-width: 300px !important;
|
||||||
|
max-width: 300px !important;
|
||||||
|
flex: 0 0 300px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colonna Descrizione (2ª colonna) = 420px */
|
||||||
|
#partsTable th:nth-child(2),
|
||||||
|
#partsTable td:nth-child(2) {
|
||||||
|
width: 350 !important;
|
||||||
min-width: 350px !important;
|
min-width: 350px !important;
|
||||||
|
max-width: 350px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default #macro-matrice-filter {
|
#partsTable td:nth-child(2) .part-description {
|
||||||
width: 200px !important;
|
width: 100% !important;
|
||||||
min-width: 200px !important;
|
max-width: 100% !important;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
.select2-container--default .part-matrice {
|
white-space: nowrap;
|
||||||
width: 150px !important;
|
|
||||||
min-width: 150px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Aspetto Select2 */
|
||||||
.select2-container--default .select2-selection--single {
|
.select2-container--default .select2-selection--single {
|
||||||
height: 24px !important;
|
height: 24px !important;
|
||||||
padding: 0.1rem 0.3rem !important;
|
padding: .1rem .3rem !important;
|
||||||
font-size: 0.8rem !important;
|
font-size: .8rem !important;
|
||||||
border: 1px solid #ced4da !important;
|
border: 1px solid #ced4da !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default .select2-selection__rendered {
|
|
||||||
line-height: 22px !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
text-overflow: ellipsis !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--default .select2-selection__arrow {
|
.select2-container--default .select2-selection__arrow {
|
||||||
height: 24px !important;
|
height: 24px !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--open .select2-dropdown {
|
.select2-container--open .select2-dropdown {
|
||||||
z-index: 1061 !important;
|
z-index: 1061 !important;
|
||||||
border: 1px solid #aaa !important;
|
border: 1px solid #aaa !important;
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
background: white !important;
|
background: #fff !important;
|
||||||
overflow-y: auto !important;
|
|
||||||
max-height: 200px !important;
|
max-height: 200px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Evita stretching del flex nella riga dei filtri */
|
||||||
|
#partsModal .modal-body>.row .col-md-9>div[style*="display: flex"]>* {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Altri modali e pulsanti */
|
||||||
.propagate-matrice-btn,
|
.propagate-matrice-btn,
|
||||||
.propagate-all-btn {
|
.propagate-all-btn {
|
||||||
padding: 0.1rem 0.3rem !important;
|
padding: .1rem .3rem !important;
|
||||||
font-size: 0.8rem !important;
|
font-size: .8rem !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-status,
|
.save-status,
|
||||||
.save-loading {
|
.save-loading {
|
||||||
margin-left: 5px;
|
margin-left: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmDeleteModal {
|
#confirmDeleteModal {
|
||||||
z-index: 1070 !important;
|
z-index: 1070 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirmDeleteModal .modal-backdrop {
|
#confirmDeleteModal .modal-backdrop {
|
||||||
z-index: 1065 !important;
|
z-index: 1065 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-btn {
|
.note-btn {
|
||||||
padding: 0.2rem 0.4rem !important;
|
padding: .2rem .4rem !important;
|
||||||
/* Aumentato leggermente il padding per un pulsante più grande */
|
font-size: .9rem !important
|
||||||
font-size: 0.9rem !important;
|
|
||||||
/* Aumentato il font-size per un'icona più grande */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-btn.has-note {
|
.note-btn.has-note {
|
||||||
color: #dc3545 !important;
|
color: #dc3545 !important
|
||||||
/* Rosso quando la nota è presente */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#noteModal {
|
#noteModal {
|
||||||
z-index: 1090 !important;
|
z-index: 1090 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#noteModal .modal-backdrop {
|
#noteModal .modal-backdrop {
|
||||||
z-index: 1085 !important;
|
z-index: 1085 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#noteModal .modal-dialog {
|
#noteModal .modal-dialog {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1090 !important;
|
z-index: 1090 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#noteModal textarea {
|
#noteModal textarea,
|
||||||
resize: vertical;
|
#commonNoteModal textarea {
|
||||||
}
|
resize: vertical
|
||||||
|
|
||||||
.propagate-date-btn {
|
|
||||||
padding: 0.2rem 0.4rem !important;
|
|
||||||
font-size: 0.9rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.propagate-note-btn {
|
|
||||||
padding: 0.2rem 0.4rem !important;
|
|
||||||
font-size: 0.9rem !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#commonNoteModal {
|
#commonNoteModal {
|
||||||
z-index: 1095 !important;
|
z-index: 1095 !important
|
||||||
/* Sopra #noteModal (1090) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#commonNoteModal .modal-backdrop {
|
#commonNoteModal .modal-backdrop {
|
||||||
z-index: 1090 !important;
|
z-index: 1090 !important
|
||||||
/* Sopra il backdrop di #noteModal (1085) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#commonNoteModal .modal-dialog {
|
#commonNoteModal .modal-dialog {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1095 !important;
|
z-index: 1095 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#commonNoteModal textarea {
|
/* Evidenza salvataggio riga nel parts table */
|
||||||
resize: vertical;
|
/* 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>
|
</style>
|
||||||
+392
-126
@@ -6,6 +6,36 @@ $(document).ready(function () {
|
|||||||
let unsavedChanges = false;
|
let unsavedChanges = false;
|
||||||
let matrici = [];
|
let matrici = [];
|
||||||
let macroMatrici = [];
|
let macroMatrici = [];
|
||||||
|
let quotations = [];
|
||||||
|
|
||||||
|
// --- 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
|
// VOICE RECOGNITION SETUP
|
||||||
@@ -112,7 +142,7 @@ $(document).ready(function () {
|
|||||||
// ===================
|
// ===================
|
||||||
// MODAL HANDLING
|
// MODAL HANDLING
|
||||||
// ===================
|
// ===================
|
||||||
function loadParts(iddatadb, idquotations) {
|
function loadParts(iddatadb, idquotations, callback = null) {
|
||||||
if (iddatadb) {
|
if (iddatadb) {
|
||||||
if (matrici.length === 0) {
|
if (matrici.length === 0) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -124,14 +154,14 @@ $(document).ready(function () {
|
|||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPhoto(iddatadb, idquotations);
|
||||||
loadExistingParts(iddatadb, idquotations);
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
matrici = [];
|
matrici = [];
|
||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPhoto(iddatadb, idquotations);
|
||||||
loadExistingParts(iddatadb, idquotations);
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
|
||||||
error +
|
error +
|
||||||
@@ -151,11 +181,11 @@ $(document).ready(function () {
|
|||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPhoto(iddatadb, idquotations);
|
||||||
loadExistingParts(iddatadb, idquotations);
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPhoto(iddatadb, idquotations);
|
||||||
loadExistingParts(iddatadb, idquotations);
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +469,7 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
$(document).on("click", ".add-mix-global", function (e) {
|
$(document).on("click", ".add-mix-global", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const maxPartNumber = Math.max(
|
const maxPartNumber = Math.max(
|
||||||
...$("#partsTableBody tr")
|
...$("#partsTableBody tr")
|
||||||
.map(function () {
|
.map(function () {
|
||||||
@@ -446,36 +477,179 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
.get(),
|
.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);
|
||||||
|
|
||||||
|
if (!$("#quotationeBtn").hasClass('d-none')) $("#quotationeBtn").addClass("d-none");
|
||||||
|
} 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) {
|
$(document).on("click", ".add-mix-row", function (e) {
|
||||||
e.preventDefault();
|
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) {
|
if (!partDescription) {
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
'<div class="alert alert-danger temp-alert" role="alert">Inserisci una descrizione valida prima di aggiungerla al Mix.</div>',
|
'<div class="alert alert-danger temp-alert" role="alert">Inserisci una descrizione valida prima di aggiungerla al Mix.</div>',
|
||||||
);
|
);
|
||||||
$("#partsModal .modal-body").prepend(errorMsg);
|
$("#partsModal .modal-body").prepend(errorMsg);
|
||||||
setTimeout(function () {
|
setTimeout(
|
||||||
|
() =>
|
||||||
errorMsg.fadeOut(500, function () {
|
errorMsg.fadeOut(500, function () {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
});
|
}),
|
||||||
}, 5000);
|
5000,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxPartNumber = Math.max(
|
let $mixRow = $("#partsTableBody tr")
|
||||||
...$("#partsTableBody tr")
|
|
||||||
.map(function () {
|
|
||||||
return parseInt($(this).find(".part-number").val()) || 0;
|
|
||||||
})
|
|
||||||
.get(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mixDescription = `Mix ${partDescription}`;
|
|
||||||
const $mixRow = $("#partsTableBody tr")
|
|
||||||
.filter(function () {
|
.filter(function () {
|
||||||
return $(this)
|
return $(this)
|
||||||
.find(".part-description")
|
.find(".part-description")
|
||||||
@@ -485,31 +659,59 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
.last();
|
.last();
|
||||||
|
|
||||||
if ($mixRow.length > 0) {
|
// Se non esiste una riga Mix, ne creo una e la INSERISCO SUBITO (come fa il bottone in header)
|
||||||
let currentMix = $mixRow.find(".part-description").val().trim();
|
if ($mixRow.length === 0) {
|
||||||
if (currentMix === "Mix") {
|
const maxPartNumber = Math.max(
|
||||||
mixDescription = currentMix + " " + partDescription;
|
...$("#partsTableBody tr")
|
||||||
} else if (!currentMix.includes(partDescription)) {
|
.map(function () {
|
||||||
mixDescription = currentMix + " + " + partDescription;
|
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 {
|
} else {
|
||||||
addNewRow(maxPartNumber + 1, true); // Crea nuova riga Mix
|
// libero: salva subito
|
||||||
const $newMixRow = $("#partsTableBody tr:last");
|
saveRow($mixRow);
|
||||||
$newMixRow
|
|
||||||
.find(".part-description")
|
|
||||||
.val(mixDescription)
|
|
||||||
.trigger("blur");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function addNewRow(nextPartNumber, isMix = false) {
|
function addNewRow(nextPartNumber, isMix = false) {
|
||||||
const description = isMix ? "Mix" : "";
|
const description = isMix ? "Mix" : "";
|
||||||
const newRow = `
|
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="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><input type="text" class="form-control form-control-sm part-description" value="${description}" placeholder="Inserisci descrizione"></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -540,7 +742,7 @@ $(document).ready(function () {
|
|||||||
// ===================
|
// ===================
|
||||||
$(document).on("click", ".note-btn", function () {
|
$(document).on("click", ".note-btn", function () {
|
||||||
const $row = $(this).closest("tr");
|
const $row = $(this).closest("tr");
|
||||||
const partId = $row.data("part-id");
|
const partId = getPartId($row);
|
||||||
const note = $row.data("note") || "";
|
const note = $row.data("note") || "";
|
||||||
const $noteModal = $("#noteModal");
|
const $noteModal = $("#noteModal");
|
||||||
$noteModal.find(".part-note").val(note);
|
$noteModal.find(".part-note").val(note);
|
||||||
@@ -655,7 +857,7 @@ $(document).ready(function () {
|
|||||||
$(document).on("change", ".part-dateexpiry", function () {
|
$(document).on("change", ".part-dateexpiry", function () {
|
||||||
const $input = $(this);
|
const $input = $(this);
|
||||||
const $row = $input.closest("tr");
|
const $row = $input.closest("tr");
|
||||||
const partId = $row.data("part-id");
|
const partId = getPartId($row);
|
||||||
const dateexpiry = $input.val();
|
const dateexpiry = $input.val();
|
||||||
const iddatadb = $("#partsModal").data("iddatadb");
|
const iddatadb = $("#partsModal").data("iddatadb");
|
||||||
const idquotations = $("#partsModal").data("idquotations");
|
const idquotations = $("#partsModal").data("idquotations");
|
||||||
@@ -953,91 +1155,10 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("blur", ".part-description, .part-number", function () {
|
$(document).on("blur", ".part-description, .part-number", function () {
|
||||||
const $input = $(this);
|
saveRow($(this).closest("tr"));
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadExistingParts(iddatadb, idquotations) {
|
function loadExistingParts(iddatadb, idquotations, callback = null) {
|
||||||
const endpoint = idquotations
|
const endpoint = idquotations
|
||||||
? "load_parts_quotation.php"
|
? "load_parts_quotation.php"
|
||||||
: "load_parts.php";
|
: "load_parts.php";
|
||||||
@@ -1115,8 +1236,11 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addNewRow(1, false); // Riga iniziale normale
|
addNewRow(1, false); // Riga iniziale normale
|
||||||
|
$("#quotationeBtn").removeClass("d-none");
|
||||||
}
|
}
|
||||||
updateRowButtons();
|
updateRowButtons();
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
@@ -1221,6 +1345,7 @@ $(document).ready(function () {
|
|||||||
partId,
|
partId,
|
||||||
currentValue,
|
currentValue,
|
||||||
selectedMacro,
|
selectedMacro,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1297,6 +1422,7 @@ $(document).ready(function () {
|
|||||||
partId,
|
partId,
|
||||||
idmatrice,
|
idmatrice,
|
||||||
selectedMacro = null,
|
selectedMacro = null,
|
||||||
|
fromFilter = false
|
||||||
) {
|
) {
|
||||||
if (typeof $.fn.select2 === "undefined") {
|
if (typeof $.fn.select2 === "undefined") {
|
||||||
$select.replaceWith(
|
$select.replaceWith(
|
||||||
@@ -1366,16 +1492,24 @@ $(document).ready(function () {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
$select.append(option).trigger("change");
|
|
||||||
|
if (!fromFilter) $select.append(option).trigger("change");
|
||||||
|
else $select.append(option);
|
||||||
|
|
||||||
partMatrice[partNumber] = matrice.IdMatrice;
|
partMatrice[partNumber] = matrice.IdMatrice;
|
||||||
} else {
|
} else {
|
||||||
// Aggiusta valore non valido
|
// Aggiusta valore non valido
|
||||||
$select.val(null).trigger("change");
|
if (!fromFilter) $select.val(null).trigger("change");
|
||||||
|
|
||||||
partMatrice[partNumber] = null;
|
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 idmatrice = $(this).val();
|
||||||
const $row = $(this).closest("tr");
|
const $row = $(this).closest("tr");
|
||||||
const partId = $row.data("part-id");
|
const partId = $row.data("part-id");
|
||||||
@@ -1659,6 +1793,122 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
// Esporta la funzione loadParts per essere usata da import_Edit2.php
|
// Esporta la funzione loadParts per essere usata da import_Edit2.php
|
||||||
window.loadParts = loadParts;
|
window.loadParts = loadParts;
|
||||||
|
|
||||||
|
$(document).on("click", "#quotationeBtn", function () {
|
||||||
|
$("#addQuotationModal").modal("show");
|
||||||
|
|
||||||
|
if (quotations.length > 0) {
|
||||||
|
reloadQuotations();
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
url: "load_quotations.php",
|
||||||
|
method: "GET",
|
||||||
|
success: function (response) {
|
||||||
|
quotations = response?.quotations || [];
|
||||||
|
|
||||||
|
reloadQuotations();
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error("Errore AJAX caricamento quotations:", error, xhr.responseText);
|
||||||
|
let message = `
|
||||||
|
<div class="alert alert-danger temp-alert" role="alert">
|
||||||
|
Errore nel caricamento delle quotations: ${error} (${xhr.status})
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$("#partsModal .modal-body").prepend(errorMsg);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
message.fadeOut(500, function () {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", "#addQuotationBtn", function () {
|
||||||
|
const quotationId = $("#addQuotationSelect").val();
|
||||||
|
|
||||||
|
if (quotationId && confirm("Confermo di collegare la quotazione al campione?")) {
|
||||||
|
$("#addQuotationModal").modal("hide");
|
||||||
|
|
||||||
|
loadParts(null, quotationId, () => {
|
||||||
|
$("#quotationeBtn").addClass("d-none");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#partsTableBody tr").each(function () {
|
||||||
|
$(this).find(".save-loading").show();
|
||||||
|
});
|
||||||
|
|
||||||
|
let photoList = $('#photoSelector option').map(function () {
|
||||||
|
return this.value?.split('/')?.pop();
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
if (!photoList.length) {
|
||||||
|
let path = $('#samplePhoto').attr("src")?.split('/')?.pop();
|
||||||
|
|
||||||
|
if (path) photoList = [path];
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "save_parts_photo_iddatadb.php",
|
||||||
|
method: "POST",
|
||||||
|
data: JSON.stringify({
|
||||||
|
iddatadb: $("#partsModal").data("iddatadb"),
|
||||||
|
photoList,
|
||||||
|
partIds: $('#partsTableBody tr').map(function () {
|
||||||
|
return $(this).data("part-id");
|
||||||
|
}).get(),
|
||||||
|
}),
|
||||||
|
success: function () {
|
||||||
|
$("#partsTableBody tr").each(function () {
|
||||||
|
let $row = $(this);
|
||||||
|
let $saveStatus = $row.find(".save-status");
|
||||||
|
let $saveLoading = $row.find(".save-loading");
|
||||||
|
|
||||||
|
$saveLoading.hide();
|
||||||
|
$saveStatus.show();
|
||||||
|
|
||||||
|
setTimeout(() => $saveStatus.hide(), 2000);
|
||||||
|
});
|
||||||
|
}, error: function (xhr, status, error) {
|
||||||
|
let message = `
|
||||||
|
<div class="alert alert-danger temp-alert" role="alert">
|
||||||
|
Errore di salvataggio: ${error} (${xhr.status})
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$("#partsModal .modal-body").prepend(message);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
message.fadeOut(500, function () {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function reloadQuotations() {
|
||||||
|
if (quotations.length > 0) {
|
||||||
|
$("#addQuotationSelect").empty();
|
||||||
|
|
||||||
|
$("#addQuotationSelect").append("<option value=''>Seleziona Quotation</option>");
|
||||||
|
|
||||||
|
quotations.forEach(quotation => {
|
||||||
|
if (quotation?.description) {
|
||||||
|
$("#addQuotationSelect").append(`<option value='${quotation?.id}'>${quotation?.description}</option>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#addQuotationSelect").select2();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("change", ".propagate-date-input", function () {
|
$(document).on("change", ".propagate-date-input", function () {
|
||||||
@@ -1882,3 +2132,19 @@ $(document).on("click", ".save-common-note-btn", function () {
|
|||||||
markUnsaved();
|
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>");
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -108,7 +108,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recupera routine dal template
|
// Recupera routine dal template
|
||||||
$stmt = $pdo->prepare("SELECT idroutine FROM excel_templates WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?");
|
||||||
$stmt->execute([$template_id]);
|
$stmt->execute([$template_id]);
|
||||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -133,6 +133,9 @@ try {
|
|||||||
error_log("Nessuna routine associata al template {$template_id}");
|
error_log("Nessuna routine associata al template {$template_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggiungi idclient alla risposta
|
||||||
|
$response['idclient'] = $template['idclient'] ?? null;
|
||||||
|
|
||||||
// Salva i dati in sessione
|
// Salva i dati in sessione
|
||||||
$_SESSION['excel_data'] = $excelData;
|
$_SESSION['excel_data'] = $excelData;
|
||||||
$_SESSION['template_id'] = $template_id;
|
$_SESSION['template_id'] = $template_id;
|
||||||
|
|||||||
@@ -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>
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
||||||
<?php include('include/footer.php'); ?>
|
<?php include('include/footer.php'); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php include('modal_parts.php'); ?>
|
<div id="partsModalContainer"></div>
|
||||||
<?php include('photos_functions.php'); ?>
|
<div id="annotationsModalContainer"></div>
|
||||||
|
<?php include 'photos_functions.php'; ?>
|
||||||
|
|
||||||
<?php include('jsinclude.php'); ?>
|
<?php include('jsinclude.php'); ?>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<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.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="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
||||||
<script src="photos.js"></script>
|
<script src="photos.js"></script>
|
||||||
<script src="parts.js"></script>
|
<script src="annotationsModal.js"></script>
|
||||||
|
<script src="partsTable.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
// Mostra messaggi di stato se presenti
|
// Mostra messaggi di stato se presenti
|
||||||
@@ -423,6 +427,45 @@ if (isset($_GET['edit_id'])) {
|
|||||||
}, 5000);
|
}, 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
|
// Inizializza DataTables se non siamo in modalità modifica
|
||||||
if (!document.querySelector('#editForm')) {
|
if (!document.querySelector('#editForm')) {
|
||||||
$('#quotationsTable').DataTable({
|
$('#quotationsTable').DataTable({
|
||||||
@@ -524,12 +567,7 @@ if (isset($_GET['edit_id'])) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Modale per le foto in quotations.php -->
|
<!-- 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>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$iddatadb = intval($_POST['iddatadb']);
|
$iddatadb = intval($_POST['iddatadb']);
|
||||||
|
$idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null;
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$data = $_POST;
|
$data = $_POST;
|
||||||
$details = [];
|
$details = [];
|
||||||
|
|
||||||
// 1. POST-დან ამოვიღოთ მხოლოდ details
|
// 1. Estrarre i dettagli da POST
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
if (preg_match('/^details(\d+)field_value$/', $key, $matches)) {
|
if (preg_match('/^details(\d+)field_value$/', $key, $matches)) {
|
||||||
$id = $matches[1];
|
$id = $matches[1];
|
||||||
@@ -25,16 +26,15 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. DB-დან წამოვიღოთ არსებული მნიშვნელობები
|
// 2. Recupera i valori esistenti da import_data_details
|
||||||
$stmt = $pdo->prepare("SELECT mapping_id, field_value FROM import_data_details WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT mapping_id, field_value FROM import_data_details WHERE id = ?");
|
||||||
$stmt->execute([$iddatadb]);
|
$stmt->execute([$iddatadb]);
|
||||||
|
|
||||||
$currentValues = [];
|
$currentValues = [];
|
||||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$currentValues[$row['mapping_id']] = $row['field_value'];
|
$currentValues[$row['mapping_id']] = $row['field_value'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. შევადაროთ POST-ს და DB-ს
|
// 3. Confronta i valori nuovi con quelli esistenti
|
||||||
$changed = [];
|
$changed = [];
|
||||||
foreach ($details as $id => $newValue) {
|
foreach ($details as $id => $newValue) {
|
||||||
$oldValue = $currentValues[$id] ?? null;
|
$oldValue = $currentValues[$id] ?? null;
|
||||||
@@ -46,7 +46,7 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. თუ არის ცვლილებები → UPDATE
|
// 4. Aggiorna i dettagli se ci sono modifiche
|
||||||
if (!empty($changed)) {
|
if (!empty($changed)) {
|
||||||
$updateStmt = $pdo->prepare("
|
$updateStmt = $pdo->prepare("
|
||||||
UPDATE import_data_details
|
UPDATE import_data_details
|
||||||
@@ -61,14 +61,26 @@ try {
|
|||||||
':mappingId' => $mappingId
|
':mappingId' => $mappingId
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Aggiorna idclient in datadb
|
||||||
|
if (isset($idclient)) {
|
||||||
|
$updateStmt = $pdo->prepare("
|
||||||
|
UPDATE datadb
|
||||||
|
SET idclient = :idclient
|
||||||
|
WHERE iddatadb = :iddatadb
|
||||||
|
");
|
||||||
|
$updateStmt->execute([
|
||||||
|
':idclient' => $idclient,
|
||||||
|
':iddatadb' => $iddatadb
|
||||||
|
]);
|
||||||
|
$response['message'] = !empty($changed) ? "Updated details and idclient successfully" : "Updated idclient successfully";
|
||||||
|
} else {
|
||||||
|
$response['message'] = !empty($changed) ? "Updated details successfully" : "No changes found";
|
||||||
|
}
|
||||||
|
|
||||||
$response['success'] = true;
|
$response['success'] = true;
|
||||||
$response['message'] = "Updated successfully";
|
|
||||||
$response['changed'] = $changed; // Debug / optional
|
$response['changed'] = $changed; // Debug / optional
|
||||||
} else {
|
|
||||||
$response['success'] = true;
|
|
||||||
$response['message'] = "No changes found";
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$response['success'] = false;
|
$response['success'] = false;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$iddatadb = $data['iddatadb'] ?? null;
|
||||||
|
$partIds = $data['partIds'] ?? [];
|
||||||
|
$photoList = $data['photoList'] ?? null;
|
||||||
|
|
||||||
|
if ($iddatadb) {
|
||||||
|
if (count($partIds) != 0) {
|
||||||
|
$idList = array_values(array_map('intval', $partIds));
|
||||||
|
$placeholders = [];
|
||||||
|
$parameters = [':iddatadb' => $iddatadb];
|
||||||
|
|
||||||
|
foreach ($idList as $index => $id) {
|
||||||
|
$paramName = ":id{$index}";
|
||||||
|
$placeholders[] = $paramName;
|
||||||
|
$parameters[$paramName] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'UPDATE identification_parts SET iddatadb = :iddatadb WHERE id IN (' . implode(',', $placeholders) . ')';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($photoList) != 0) {
|
||||||
|
$placeholders = [];
|
||||||
|
$parameters = [':iddatadb' => $iddatadb];
|
||||||
|
|
||||||
|
foreach ($photoList as $index => $photo) {
|
||||||
|
$paramName = ":photo{$index}";
|
||||||
|
$placeholders[] = $paramName;
|
||||||
|
$parameters[$paramName] = $photo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('UPDATE datadb_photos SET iddatadb = :iddatadb WHERE file_path IN (' . implode(',', $placeholders) . ')');
|
||||||
|
$stmt->execute($parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => '']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Dati mancanti']);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// upload_photos_mobile.php
|
// upload_photos_mobile.php
|
||||||
include('include/headscript.php');
|
include('include/headscriptnologin.php');
|
||||||
|
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|||||||
Reference in New Issue
Block a user