Compare commits

...

54 Commits

Author SHA1 Message Date
RMubarakzyanov cdd6551e9c remove validation simulate 2026-03-14 19:09:17 +03:00
RMubarakzyanov 1b97bf4362 1,2,3,4,5,6 points of milestone 2026-03-13 00:33:55 +03:00
solocla c516589483 changed notes TRF 2026-03-12 10:28:58 +01:00
solocla 817bbadf22 added flag to photos 2026-03-12 10:20:22 +01:00
solocla 1f27bc48d4 added get utenti 2026-03-11 14:53:38 +01:00
RMubarakzyanov c9fba48d88 fix post data 2026-03-11 16:04:37 +03:00
RMubarakzyanov 70d4c0759e importpayload logging 2026-03-09 17:34:32 +03:00
RMubarakzyanov 5d7880160a send userid to import 2026-03-09 17:12:38 +03:00
RMubarakzyanov bbe74d1529 fetch changes from branch export_to_lims 2026-03-09 17:07:20 +03:00
RMubarakzyanov 93930227a2 fetch changes from branch export_to_lims 2026-03-09 17:04:03 +03:00
solocla 2598a4c91b Step 9 remove comment on export to lims 2026-03-06 09:15:30 +01:00
solocla 5e677a8b9c Merge branch 'feature/export-to-lims' 2026-03-06 08:48:57 +01:00
solocla 540c44d89a fixed column mappings 2026-03-05 14:19:49 +01:00
solocla 2ee9f2ecb1 added proceed on the top 2026-03-04 10:13:33 +01:00
solocla c9122774b1 added additional field in parts template e layout 2026-03-02 14:37:20 +01:00
RMubarakzyanov 1fed113c5c VisualLimsApiClientMock: fake data for all LIMS endpoints; getInstance() branches on SIMULATE_EXPORT_LIMS
get_clienti.php, get_fixed_field_data.php: simulate mode support
CustomField dropdown values mock added (get_customfield_values.php)

exportUnsavedModal: prompt save before export, MutationObserver waits for save, then proceeds to confirm
Removed old jQuery .export-lims-btn handler that bypassed confirm modal
Fix false "Unsaved changes" on page load: data-restoring guard in all programmatic trigger/dispatchEvent calls (populateSelect, populateClientDropdowns, populateDropdowns)

Fix ConsegnaRichiesta not shown on refresh: add to PHP $fixedAliasMap
Add step5_2_photos, step9_1_importa
2026-02-28 00:44:21 +03:00
RMubarakzyanov e3994d6f9f fix notices running on php-8.2 2026-02-26 22:59:41 +03:00
solocla 9af0df3cca env example update 2026-02-26 17:14:16 +01:00
solocla 48d2a3ff42 time and date setting 2026-02-25 22:07:44 +01:00
solocla cf44e67922 added auto user accettatore id 2026-02-25 15:24:41 +01:00
solocla 35021e9d9b dropdown in alphabetical order 2026-02-25 10:02:45 +01:00
solocla 497ebda65a update gitignore 2026-02-24 09:09:32 +01:00
solocla b0024edb70 mapping fixed 2026-02-24 09:06:45 +01:00
solocla b9852ba226 saved rows 2026-02-19 12:07:32 +01:00
solocla 407d6884a1 fixed note save and import special character 2026-02-19 11:55:24 +01:00
solocla 1a4beadbb2 fixed propagate dropdown 2026-02-18 11:52:26 +01:00
solocla 4bb0445cff added select 2 on all fields (with limit of 12) 2026-02-18 10:48:04 +01:00
solocla 73dd8f4ce8 fixed column size final 2026-02-18 10:04:27 +01:00
solocla bdc4e0e60c fixed size column 2026-02-18 09:58:42 +01:00
solocla ef8f2d8000 fixed labels 2026-02-18 09:54:21 +01:00
solocla 74c9a1d02a added time field 2026-02-18 09:34:26 +01:00
solocla cd3bccd183 added multiple tested components 2026-02-17 09:04:30 +01:00
solocla 78154e43a9 fixed tested components 2026-02-05 10:32:28 +01:00
solocla 9fe9243e60 fixed mapping template 2026-02-05 09:05:51 +01:00
solocla a8330d4aba fixed layout 2026-02-04 15:31:53 +01:00
MGrigoryan 82d6a2ee18 feat(import_edit2): expand entire column including header and fix sticky columns 2026-02-04 17:00:01 +04:00
MGrigoryan d8eddb3aa5 feat(import_edit2): enhance grid UI with sticky columns and textarea conversion
- Fix grid-top alignment and datepicker positioning
- Extend column resizer to grid-top cells
- Add sticky columns for Actions and Sample Description
- Implement input-to-textarea conversion on focus
- Increase Select2 dropdown height
- Improve grid-container CSS for sticky support
2026-02-04 11:59:59 +04:00
solocla f60dc64b2d fixed importedit 2026-02-02 08:46:47 +01:00
solocla 4e4cae1df8 added fixed fields 2026-01-30 12:07:43 +01:00
solocla 8838edf3a1 fixed fields 2026-01-28 11:49:11 +01:00
solocla e75be99e43 added resort manually 2025-11-08 17:55:10 +01:00
solocla a482d975da Merge branch 'feature/parts-photo-load-from-quotation' 2025-10-31 10:38:28 +01:00
solocla 598a2cc84c change marker dimension 2025-10-31 10:16:57 +01:00
MGrigoryan 6ec0c2062e fix: Include single sample photo in save operation when no photos selected 2025-10-31 11:45:04 +04:00
MGrigoryan 5eb5bd1613 fix: deselect part when clicking selected row 2025-10-31 11:30:18 +04:00
MGrigoryan 03771e3ca8 feat: Add quotation modal and iddatadb update for quotation parts/images
- Add quotation modal for selecting quotation
- Load quotation parts and photos
- Update iddatadb with quotation parts and photo references
2025-10-31 11:26:11 +04:00
MGrigoryan 03642fdfab feat(annotations): support multiple pins per part 2025-10-30 17:08:57 +04:00
MGrigoryan f6ea17388c fix(partsTable, annotationsModal): correct table height and restore "Torna alle Parti"
- partsTable: reduce excessive height to prevent oversized rendering
- annotationsModal: fix "Torna alle Parti" button behavior to return to parts view
2025-10-30 10:26:46 +04:00
solocla 1c2b4ab7a6 Merge branch 'bugfix/part-creation-matrice-dropdown-fix' 2025-10-29 18:25:26 +01:00
MGrigoryan 31cb23b00e fix(partsTable): skip change handler when setting Select2 value programmatically
Pass skipHandler flag in change event data to prevent handler execution
during programmatic value updates.
2025-10-29 19:18:33 +04:00
MGrigoryan d29563d20d feat(partsTable): add image icon to show/hide photo button
- Add image icon alongside eye icon in show/hide photo button
- Improve visual indication of photo toggle functionality
2025-10-29 18:27:33 +04:00
solocla 82af925ac1 added size to line markers 2025-10-29 13:45:38 +01:00
solocla 5d8360dd87 marker with size 2025-10-29 12:32:09 +01:00
MGrigoryan 683073c244 fix(partsTable): initialize new item matrice as empty and refresh on global filter change
- Default new items' matrice to empty to prevent stale values leaking from prior state
- Force matrice update when the global filter changes to keep items in sync
2025-10-29 14:17:00 +04:00
59 changed files with 10513 additions and 828 deletions
+6 -17
View File
@@ -7,9 +7,9 @@ LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST="localhost"
DB_DATABASE="trfcertest"
DB_USERNAME="solocla"
DB_PASSWORD="xxxxxxx"
DB_DATABASE="xxxx"
DB_USERNAME="xxxx"
DB_PASSWORD="xxxxx"
DB_PREFIX="auth_"
BROADCAST_DRIVER=log
@@ -41,19 +41,8 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Credenziali API VisualLims
SIMULATE_EXPORT_LIMS=true
API_BASE_URL=https://93.43.5.102/limsapi
API_USERNAME=xxxx
API_PASSWORD=XXXX
API_BASE_URL=https://bvcpsitaly-elims.com/limsapi
API_USERNAME=WebApiUser
API_PASSWORD=webapiuser01
BASE_URL=http://localhost:8000/userarea/
# Credenziali ENTRAID
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
+31 -42
View File
@@ -1,66 +1,55 @@
.DS_Store
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
/.idea
/.fleet
/.vscode
/.vagrant
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.phpunit.result.cache
.php_cs.cache
/documentation
/.phpunit.cache
/public/hot
/public/storage
/public/build
/storage/*.key
.env
.env.backup
.env.production
auth.json
# File di debug e temporanei JSON e log
.phpunit.result.cache
.php_cs.cache
/.phpunit.cache
npm-debug.log
yarn-error.log
/documentation
# --- Runtime / Debug (userarea) ---
/public/userarea/*.json
/public/userarea/*.log
/public/userarea/*.txt
# File di log nella sottocartella class
/public/userarea/*_response.json
/public/userarea/error_log.txt
/public/userarea/import_debug.log
/public/userarea/last_url.txt
/public/userarea/logaspi/
/public/userarea/logsapi/
/public/userarea/photostrf/
/public/userarea/class/*.log
/public/userarea/class/curl_auth_debug.log
/public/userarea/class/curl_request_debug.log
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
/public/userarea/xlstemplates/*.xlsx
# Ignora cartelle di foto generate
# Cartelle foto generate
/public/photostrf/
/public/photostrf/qrcodes/
public/userarea/import_debug.log
public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log
public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log
# Ignora tutti i log
# Ignora tutti i log ovunque
*.log
# Ignora cartella photostrf in public/userarea
/public/userarea/photostrf/
public/userarea/customfield_values_response.json
/public/userarea/logaspi/
public/userarea/logsapi/campione_762_1.json
public/userarea/logsapi/campione_763_1.json
public/userarea/logsapi/campione_762_2.json
public/userarea/logsapi/campione_763_2.json
public/userarea/logsapi/commessaweb_create_762.json
public/userarea/logsapi/commessaweb_create_763.json
public/userarea/logsapi/commessaweb_customfields_762.json
public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt
+91
View File
@@ -0,0 +1,91 @@
<?php
header('Content-Type: application/json');
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once(__DIR__ . '/include/headscript.php'); // Auth + $iduserlogin + DBHandlerSelect
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$iddatadb = isset($_POST['iddatadb']) ? (int)$_POST['iddatadb'] : 0;
if ($iddatadb <= 0) {
throw new Exception('Missing iddatadb');
}
// ✅ Supporta sia singola stringa (con |) che array part_descriptions[]
$parts = [];
if (isset($_POST['part_descriptions']) && is_array($_POST['part_descriptions'])) {
$parts = $_POST['part_descriptions'];
} else {
$raw = isset($_POST['part_description']) ? (string)$_POST['part_description'] : '';
// split su "|" per multi-part
$parts = preg_split('/\s*\|\s*/', $raw);
}
// normalizza: trim + rimuovi vuoti + dedup
$cleanParts = [];
foreach ($parts as $p) {
$p = trim((string)$p);
if ($p === '') continue;
// limita lunghezza come da DB varchar(255)
if (mb_strlen($p) > 255) {
$p = mb_substr($p, 0, 255);
}
$cleanParts[] = $p;
}
$cleanParts = array_values(array_unique($cleanParts));
if (count($cleanParts) === 0) {
throw new Exception('Part description is empty');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->beginTransaction();
try {
// prende il prossimo part_number per iddatadb
$stmtMax = $pdo->prepare("SELECT COALESCE(MAX(part_number), 0) AS maxnum FROM identification_parts WHERE iddatadb = ?");
$stmtMax->execute([$iddatadb]);
$nextNumber = (int)$stmtMax->fetchColumn() + 1;
$stmtIns = $pdo->prepare("
INSERT INTO identification_parts (iddatadb, part_number, part_description)
VALUES (?, ?, ?)
");
$insertedIds = [];
foreach ($cleanParts as $p) {
$ok = $stmtIns->execute([$iddatadb, $nextNumber, $p]);
if (!$ok) {
throw new Exception('Insert failed');
}
$insertedIds[] = $pdo->lastInsertId();
$nextNumber++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'Parts added',
'count' => count($insertedIds),
'ids' => $insertedIds
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
throw $e;
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
+231 -106
View File
@@ -12,12 +12,16 @@ $(document).ready(function () {
let photoAnnotations = {};
let partColors = {};
let partSizes = {}; // memorizza la dimensione specifica per parte
let selectedPartNumber = null;
let unsavedChanges = false;
let fabricCanvas = null;
let descriptionTextbox = null;
let markerObjects = {};
let nextMarkerId = 1;
let partsListData = [];
// DIMENSIONE GLOBALE MARKER
let globalMarkerSize = 16;
// ===================
// MODAL INITIALIZATION
@@ -29,6 +33,8 @@ $(document).ready(function () {
trfHeader,
});
$("#annotationsModal").attr("data-iddatadb", iddatadb);
if (!iddatadb && !idquotations) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante. Impossibile inizializzare il modale delle annotazioni.</div>',
@@ -67,6 +73,9 @@ $(document).ready(function () {
});
}
modal.show();
// Inizializza slider dimensione marker
$("#markerSizeSlider").val(globalMarkerSize);
$("#markerSizeValue").text(globalMarkerSize + "px");
// Debug: Verifica presenza elementi DOM
console.log(
@@ -128,6 +137,7 @@ $(document).ready(function () {
}
descriptionTextbox = null;
markerObjects = {};
globalMarkerSize = 16;
$("#photoSelectorContainerAnnotations").empty().hide();
$("#samplePhotoAnnotations").attr("src", "");
$("#partsListAnnotations").empty();
@@ -144,6 +154,30 @@ $(document).ready(function () {
$(":focus").blur();
});
// SLIDER DIMENSIONE MARKER
$(document)
.off("input", "#markerSizeSlider")
.on("input", "#markerSizeSlider", function () {
globalMarkerSize = parseInt($(this).val());
$("#markerSizeValue").text(globalMarkerSize + "px");
partsListData.forEach(function (part) {
if (part && part.part_number != null) {
partSizes[part.part_number] = globalMarkerSize;
}
});
$("#partsListAnnotations .marker-size-slider").val(
globalMarkerSize,
);
$("#partsListAnnotations .marker-size-value").text(
globalMarkerSize + "px",
);
updateMarkers();
markUnsaved();
});
// ===================
// PHOTO LOADERS
// ===================
@@ -347,22 +381,13 @@ $(document).ready(function () {
const partColor =
partColors[selectedPartNumber] || "#ff0000";
const existingMarker = photoAnnotations[
currentPhoto
].markers.find((m) => m.partNumber == selectedPartNumber);
if (existingMarker) {
existingMarker.x = x;
existingMarker.y = y;
existingMarker.color = partColor;
} else {
photoAnnotations[currentPhoto].markers.push({
id: nextMarkerId++,
partNumber: selectedPartNumber,
x,
y,
color: partColor,
});
}
console.log("Marker aggiunto/spostato:", {
partNumber: selectedPartNumber,
@@ -372,8 +397,6 @@ $(document).ready(function () {
});
updateMarkers();
markUnsaved();
selectedPartNumber = null;
$("#partsListAnnotations li").removeClass("active");
});
fabricCanvas.upperCanvasEl.focus();
@@ -465,113 +488,89 @@ $(document).ready(function () {
);
// ===================
// BACK TO PARTS MODAL
// TORNA AL MODALE PARTI (modal_partsTable.php)
// ===================
$(document)
.off("click.backToParts", "#backToPartsBtnAnnotations")
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #backToPartsBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#backToPartsBtnAnnotations").length) {
console.error(
"Pulsante #backToPartsBtnAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #backToPartsBtnAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
// Controlla modifiche non salvate
if (
unsavedChanges &&
!confirm(
"Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?",
)
) {
console.log(
"Tornare al modale delle parti annullato a causa di modifiche non salvate.",
);
return;
}
// Chiudi annotationsModal
const annotationsModalElement =
document.getElementById("annotationsModal");
const annotationsModal = bootstrap.Modal.getInstance(
annotationsModalElement,
);
if (annotationsModal) {
annotationsModal.hide();
} else {
console.error(
"Impossibile trovare l'istanza del modale #annotationsModal.",
);
}
// Apri partsModal
const iddatadb = $("#annotationsModal").data("iddatadb");
const idquotations = $("#annotationsModal").data("idquotations");
const trfHeader = $("#trfHeaderAnnotations").text();
console.log("Apertura partsModal con:", {
// CHIUDI MODALE ANNOTAZIONI IN MODO SICURO
const annotationsModalElement =
document.getElementById("annotationsModal");
if (annotationsModalElement) {
const modalInstance = bootstrap.Modal.getInstance(
annotationsModalElement,
);
if (modalInstance) {
modalInstance.hide();
} else {
$(annotationsModalElement).modal("hide"); // fallback jQuery
}
}
console.log("Torno a modal_partsTable.php con:", {
iddatadb,
idquotations,
trfHeader,
});
// CARICA E APRI MODALE PARTI
$.get(
"modal_partsTable.php",
{
iddatadb: iddatadb || "",
idquotations: idquotations || "",
trfHeader: trfHeader,
},
function (data) {
// Rimuovi vecchio modale (con ID corretto)
$("#partsTableModal").remove();
$(".modal-backdrop").remove();
const partsModalElement = document.getElementById("partsModal");
if (!partsModalElement) {
console.error("Elemento #partsModal non trovato nel DOM.");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Il modale delle parti non è presente nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
// Aggiungi nuovo modale
$("body").append(data);
let partsModal = bootstrap.Modal.getInstance(partsModalElement);
if (!partsModal) {
partsModal = new bootstrap.Modal(partsModalElement, {
// Apri con Bootstrap 5
const partsModalElement =
document.getElementById("partsTableModal");
if (partsModalElement) {
const modal = new bootstrap.Modal(partsModalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
}
// Inizializza il modale delle parti
if (typeof window.initPartsModal === "function") {
window.initPartsModal(iddatadb, idquotations, trfHeader);
partsModal.show();
console.log("partsModal aperto con successo.");
modal.show();
} else {
console.error("Funzione initPartsModal non definita.");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Funzione initPartsModal non trovata.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
});
let iddatadb =
$("#annotationsModal").attr("data-iddatadb");
$(
"button.parts-btn[data-iddatadb='" +
iddatadb +
"']",
).trigger("click");
}
},
).fail(function (xhr) {
console.error("Errore caricamento modale parti:", xhr);
alert(
"Errore 404: modal_partsTable.php non trovato o errore server.",
);
});
});
// ===================
// PARTS LIST
// ===================
@@ -634,11 +633,18 @@ $(document).ready(function () {
.join("");
const listItem = `
<li class="list-group-item" data-part-number="${partNumber}">
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
<div class="color-picker-container" style="display: inline-block; position: relative; overflow: visible; z-index: 2000;">
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; margin-left: 5px; cursor: pointer; pointer-events: auto;"></div>
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
<div class="color-picker-container" style="display:inline-block; position: relative; overflow: visible; z-index: 2000; margin-left:5px;">
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; cursor: pointer; pointer-events: auto;"></div>
<div class="color-picker" style="display: none; position: absolute; right: 0; z-index: 2000; background: #fff; border: 1px solid #ccc; padding: 5px;">${colorOptions}</div>
</div>
</div>
<div style="display:inline-flex; align-items:center; gap:4px; margin-left:5px;">
<input type="range" class="marker-size-slider"
min="12" max="48" step="2"
value="${partSizes[partNumber] || globalMarkerSize}"
style="width:70px;">
<span class="marker-size-value" style="font-size:0.8rem;">${partSizes[partNumber] || globalMarkerSize}px</span>
</div>
</li>`;
partsListElement.append(listItem);
}
@@ -663,8 +669,21 @@ $(document).ready(function () {
);
return;
}
const $listItem = $(this);
selectedPartNumber = $listItem.data("part-number");
const partNumber = $listItem.data("part-number");
if (
selectedPartNumber == partNumber &&
$listItem.hasClass("active")
) {
selectedPartNumber = null;
$listItem.removeClass("active");
return;
}
selectedPartNumber = partNumber;
$listItem.addClass("active").siblings().removeClass("active");
console.log(
"Parte selezionata tramite riga:",
@@ -687,30 +706,88 @@ $(document).ready(function () {
$picker.toggle();
});
// === Gestione cambio colore ===
partsListElement
.off("click.colorOption")
.on("click.colorOption", ".color-option", function (e) {
e.stopPropagation();
e.preventDefault();
const $this = $(this);
const color = $this.data("color");
const $listItem = $this.closest("li");
const partNumber = $listItem.data("part-number");
console.log(
"Cliccato .color-option, colore:",
color,
"per parte:",
partNumber,
);
// Salva il nuovo colore
partColors[partNumber] = color;
// Aggiorna il colore visivo nel selettore
$listItem
.find(".selected-color")
.css("background-color", color);
let currentPhoto = $("#samplePhotoAnnotations").attr("src");
let annotations = photoAnnotations[currentPhoto];
if (annotations) {
annotations.markers.forEach(function (m) {
if (m.partNumber == partNumber) {
m.color = color;
let group = markerObjects[m.id];
if (group) {
let circle = group.item(0);
circle.set("fill", color);
circle.set("stroke", color);
}
}
});
fabricCanvas.renderAll();
}
// Chiudi la palette e aggiorna canvas
$this.closest(".color-picker").hide();
updateMarkers();
markUnsaved();
});
// === Slider locale per dimensione marker ===
partsListElement
.off("input.localMarkerSize")
.on("input.localMarkerSize", ".marker-size-slider", function (e) {
e.stopPropagation();
e.preventDefault();
// Riferimenti alla parte e valore scelto
const $slider = $(this);
const partNumber = $slider.closest("li").data("part-number");
const newSize = parseInt($slider.val());
// Aggiorna il valore visivo accanto allo slider
$slider.siblings(".marker-size-value").text(newSize + "px");
// Memorizza la nuova dimensione per quella parte
partSizes[partNumber] = newSize;
// Aggiorna i marker sul canvas
updateMarkers();
// Segnala modifiche non salvate
markUnsaved();
console.log(
`Dimensione marker aggiornata per parte ${partNumber}: ${newSize}px`,
);
});
$(document)
.off("click.colorPicker")
.on("click.colorPicker", function (e) {
@@ -721,6 +798,48 @@ $(document).ready(function () {
});
}
function enableDragDropPartsList() {
const list = $("#partsListAnnotations");
if (!list.length || typeof $.ui === "undefined" || !$.ui.sortable) {
console.warn("jQuery UI o .sortable non disponibile. Ritento...");
setTimeout(enableDragDropPartsList, 100);
return;
}
if (list.hasClass("ui-sortable")) {
list.sortable("destroy"); // evita duplicati
}
list.sortable({
items: "li.list-group-item",
placeholder: "list-group-item placeholder",
axis: "y",
containment: "parent",
tolerance: "pointer",
start: function (e, ui) {
ui.item.addClass("dragging");
},
stop: function (e, ui) {
ui.item.removeClass("dragging");
const newOrder = [];
list.find("li").each(function () {
const partNumber = $(this).data("part-number");
const part = partsListData.find(
(p) => p.part_number == partNumber,
);
if (part) newOrder.push(part);
});
partsListData = newOrder;
console.log(
"Ordine parti aggiornato:",
partsListData.map((p) => p.part_number),
);
markUnsaved();
updateMarkers();
},
});
}
// Delegazione evento per il checkbox
$(document)
.off("change.showMix", "#showMixPartsAnnotations")
@@ -753,6 +872,7 @@ $(document).ready(function () {
}
updatePartsList();
updateMarkers();
setTimeout(enableDragDropPartsList, 50);
if (
photoAnnotations[$("#samplePhotoAnnotations").attr("src")]
?.hasDescriptions
@@ -860,9 +980,9 @@ $(document).ready(function () {
"updateMarkers chiamato, markerObjects:",
Object.keys(markerObjects),
);
for (let partNumber in markerObjects) {
fabricCanvas.remove(markerObjects[partNumber]);
delete markerObjects[partNumber];
for (let markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
@@ -892,8 +1012,10 @@ $(document).ready(function () {
return;
}
const radius = 12;
const fontSize = 16;
const size = partSizes[marker.partNumber] || globalMarkerSize;
const radius = size / 2;
const fontSize = Math.max(10, Math.round(size * 0.65));
const markerColor =
marker.color || partColors[marker.partNumber] || "#ff0000";
@@ -941,6 +1063,9 @@ $(document).ready(function () {
lockRotation: true,
});
group.markerId = marker.id;
group.partNumber = marker.partNumber;
group.on("moving", function () {
marker.x = this.left / photoData.scale;
marker.y = this.top / photoData.scale;
@@ -953,7 +1078,7 @@ $(document).ready(function () {
});
fabricCanvas.add(group);
markerObjects[marker.partNumber] = group;
markerObjects[marker.id] = group;
});
fabricCanvas.renderAll();
@@ -1013,7 +1138,7 @@ $(document).ready(function () {
scaleY: 1,
backgroundColor: "transparent",
fontFamily: "Arial",
fontSize: 24,
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
fill: "#000000",
padding: 10,
editable: false,
@@ -1107,9 +1232,9 @@ $(document).ready(function () {
}
}
for (let partNumber in markerObjects) {
fabricCanvas.remove(markerObjects[partNumber]);
delete markerObjects[partNumber];
for (let markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
@@ -1144,9 +1269,9 @@ $(document).ready(function () {
photoAnnotations[currentPhoto].markers.length > 0
) {
const lastMarker = photoAnnotations[currentPhoto].markers.pop();
if (markerObjects[lastMarker.partNumber]) {
fabricCanvas.remove(markerObjects[lastMarker.partNumber]);
delete markerObjects[lastMarker.partNumber];
if (markerObjects[lastMarker.id]) {
fabricCanvas.remove(markerObjects[lastMarker.id]);
delete markerObjects[lastMarker.id];
fabricCanvas.renderAll();
}
console.log("Ultimo marker rimosso:", lastMarker);
@@ -24,8 +24,18 @@ class VisualLimsApiClient
public static function getInstance()
{
if (self::$instance === null) {
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 3));
$dotenv->load();
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
if ($simulate) {
require_once __DIR__ . '/VisualLimsApiClientMock.class.php';
self::$instance = new VisualLimsApiClientMock();
} else {
self::$instance = new VisualLimsApiClient();
}
}
return self::$instance;
}
@@ -193,6 +203,54 @@ class VisualLimsApiClient
return json_decode($response, true);
}
/**
* POST a file as multipart/form-data (used for photo/attachment uploads).
*
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
* @param string $filePath Absolute path to the file on disk
* @param string $fileName Original file name to send
* @param array $extraFields Additional form fields to include
* @return array|null Decoded JSON response
*/
public function postMultipart($endpoint, $filePath, $fileName, array $extraFields = [])
{
$token = $this->getToken();
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
$payload = array_merge($extraFields, [
'file' => $cfile,
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/json",
// Content-Type is set automatically to multipart/form-data by cURL
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore nella richiesta POST multipart: {$curl_error}");
}
if ($http_code < 200 || $http_code >= 300) {
throw new Exception("POST multipart fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
}
return json_decode($response, true);
}
public function getBaseUrl()
{
return $this->baseUrl;
@@ -0,0 +1,135 @@
<?php
/**
* Mock implementation of VisualLimsApiClient.
* Activated when SIMULATE_EXPORT_LIMS=true in .env.
* All HTTP calls are skipped; fake but structurally valid data is returned.
* Every simulated call is logged via error_log() with a [SIMULATE] prefix.
*/
class VisualLimsApiClientMock
{
private int $fakeCommessaId;
public function __construct()
{
// Stable fake ID for the lifetime of a single request
$this->fakeCommessaId = mt_rand(90001, 99999);
error_log("[SIMULATE] VisualLimsApiClientMock initialised (fakeCommessaId={$this->fakeCommessaId})");
}
public function get(string $endpoint): array
{
error_log("[SIMULATE] GET {$endpoint}");
// --- Fixed-field dropdown lists ---
if (str_starts_with($endpoint, 'MoltiplicatorePrezzi')) {
return ['value' => [
['IdMoltiplicatorePrezzo' => 1, 'Codice' => 'MP-01', 'Descrizione' => 'Standard (1x)'],
['IdMoltiplicatorePrezzo' => 2, 'Codice' => 'MP-02', 'Descrizione' => 'Urgente (1.5x)'],
['IdMoltiplicatorePrezzo' => 3, 'Codice' => 'MP-03', 'Descrizione' => 'Extra Urgente (2x)'],
]];
}
if (str_starts_with($endpoint, 'AnagraficaCertestObject')) {
return ['value' => [
['IdAnagrafica' => 1, 'Codice' => 'OBJ-01', 'NomeAnagrafica' => 'Articolo Tessile'],
['IdAnagrafica' => 2, 'Codice' => 'OBJ-02', 'NomeAnagrafica' => 'Componente Meccanico'],
['IdAnagrafica' => 3, 'Codice' => 'OBJ-03', 'NomeAnagrafica' => 'Materiale Plastico'],
]];
}
if (str_starts_with($endpoint, 'AnagraficaCertestService')) {
return ['value' => [
['IdAnagrafica' => 1, 'Codice' => 'SRV-01', 'NomeAnagrafica' => 'Analisi Chimica'],
['IdAnagrafica' => 2, 'Codice' => 'SRV-02', 'NomeAnagrafica' => 'Test Meccanico'],
['IdAnagrafica' => 3, 'Codice' => 'SRV-03', 'NomeAnagrafica' => 'Prova Ambientale'],
]];
}
// Cliente? list — get_clienti.php exits early in simulate mode, but guard here too
if (str_starts_with($endpoint, 'Cliente?')) {
return ['value' => []];
}
// Cliente(N)?$expand=Responsabili
if (str_starts_with($endpoint, 'Cliente(')) {
preg_match('/Cliente\((\d+)\)/', $endpoint, $m);
$clienteId = isset($m[1]) ? (int) $m[1] : 0;
return [
'IdCliente' => $clienteId,
'Responsabili' => [
['IdClienteResponsabile' => 1, 'Nominativo' => 'Marco Bianchi'],
['IdClienteResponsabile' => 2, 'Nominativo' => 'Giulia Ferrari'],
['IdClienteResponsabile' => 3, 'Nominativo' => 'Andrea Russo'],
],
];
}
// --- CustomField dropdown values (get_customfield_values.php) ---
if (str_starts_with($endpoint, 'CustomField(')) {
preg_match('/CustomField\((\d+)\)/', $endpoint, $m);
$fieldId = isset($m[1]) ? (int) $m[1] : 0;
return [
'CustomFieldsValues' => [
['IdCustomFieldsValue' => $fieldId * 10 + 1, 'Valore' => 'Opzione A'],
['IdCustomFieldsValue' => $fieldId * 10 + 2, 'Valore' => 'Opzione B'],
['IdCustomFieldsValue' => $fieldId * 10 + 3, 'Valore' => 'Opzione C'],
],
];
}
// --- CommessaWeb OData calls (STEP 7 GET + STEP 10 verification) ---
preg_match('/\((\d+)\)/', $endpoint, $m);
$id = isset($m[1]) ? (int) $m[1] : $this->fakeCommessaId;
return [
'IdCommessa' => $id,
'CodiceCommessa' => "SIM-{$id}",
'CommesseCustomFields' => [], // Empty → PATCH step is skipped correctly
];
}
public function post(string $endpoint, array $payload): array
{
error_log("[SIMULATE] POST {$endpoint} payload=" . json_encode($payload));
// CommessaWeb creation
if ($endpoint === 'CommessaWeb') {
return [
'IdCommessa' => $this->fakeCommessaId,
'CodiceCommessa' => "SIM-{$this->fakeCommessaId}",
'Richiedente' => $payload['Richiedente'] ?? '',
'Descrizione' => $payload['Descrizione'] ?? '',
];
}
// Campione creation
if ($endpoint === 'Campione') {
return [
'IdCampione' => mt_rand(10001, 19999),
'Commessa' => $payload['Commessa'] ?? null,
'Matrice' => $payload['Matrice'] ?? null,
];
}
// InviaCommessa / ImportaCommessa (currently commented out upstream)
return ['simulated' => true, 'endpoint' => $endpoint];
}
public function patch(string $endpoint, array $payload): array
{
error_log("[SIMULATE] PATCH {$endpoint} payload=" . json_encode($payload));
return [];
}
public function postMultipart(string $endpoint, string $filePath, string $fileName, array $extraFields = []): array
{
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
return ['simulated' => true, 'file' => $fileName];
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$templateId = (int)($input['template_id'] ?? 0);
if ($templateId <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid template_id']);
exit;
}
// If already exists, do nothing
$stmt = $pdo->prepare("SELECT COUNT(*) FROM template_fixed_mapping WHERE template_id = ?");
$stmt->execute([$templateId]);
if ((int)$stmt->fetchColumn() > 0) {
echo json_encode(['success' => true, 'created' => 0, 'message' => 'Fixed fields already exist']);
exit;
}
/**
* FIXED FIELDS STANDARD (no UI selection)
* is_manual always 1
* is_visible_import default 1
*/
$fixedFields = [
// fixed_field_key, data_type
['ClienteResponsabile', 'INT'],
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
['AnagraficaCertestService', 'INT'],
['ConsegnaRichiesta', 'DATE'],
];
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO template_fixed_mapping
(template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import)
VALUES
(:template_id, :fixed_field_key, 1, :data_type, 1, NULL, 1)
");
foreach ($fixedFields as $f) {
$ins->execute([
':template_id' => $templateId,
':fixed_field_key' => $f[0],
':data_type' => $f[1],
]);
}
$pdo->commit();
// Return rows (for client render)
$stmt = $pdo->prepare("
SELECT id, template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
$stmt->execute([$templateId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'created' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
File diff suppressed because one or more lines are too long
+302
View File
@@ -12,3 +12,305 @@
2025-08-26 16:49:24 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:50:23 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-09-08 08:39:17 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2026-01-27 15:33:53 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:06 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:10 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:35:13 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:38 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:39 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-29 14:34:04 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:37:29 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:41:55 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:03 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:52 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:43:00 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-30 10:50:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 10:50:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 11:09:22 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:44 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-23 10:44:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:53 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:52 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:59 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 15:54:16 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:22:45 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:32:17 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
+715 -181
View File
@@ -1,29 +1,362 @@
document.addEventListener("DOMContentLoaded", () => {
console.log("export_to_lims.js loaded");
// Debug: verifica che i pulsanti siano trovati
const exportButtons = document.querySelectorAll(".export-lims-btn");
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
if (exportButtons.length === 0) {
console.warn("No .export-lims-btn buttons found in the DOM");
// Tracks the active confirm handler so it can be replaced on re-open
let pendingConfirmHandler = null;
let batchRunning = false;
// Expose for Save All to check
Object.defineProperty(window, "batchRunning", {
get: () => batchRunning,
});
// ── Helpers ──────────────────────────────────────────────────────────────
function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon");
if (overlay) overlay.style.display = "none";
}
// ── Validation ───────────────────────────────────────────────────────────
/**
* Call the validation endpoint for an array of { iddatadb, index } objects.
* Returns the parsed JSON response.
*/
async function validateRows(rowsToValidate) {
const response = await fetch("validate_export.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok)
throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
/**
* Clear all validation-error highlights from the grid.
*/
function clearValidationErrors() {
document.querySelectorAll(".grid-cell.validation-error").forEach((cell) => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach((el) => {
el.classList.remove("input-validation-error");
});
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document.querySelectorAll(".grid-row.validation-row-error").forEach((row) => {
row.classList.remove("validation-row-error");
});
// Also clear batch-row-error that came from validation
clearAllRowErrors();
}
/**
* Highlight specific cells on a row based on validation errors.
* Each error has { field, message }.
* field can be: a data-col value, "parts", "field_label:SomeLabel", or null (row-level).
*/
function showValidationErrors(row, iddatadb, errors) {
row.classList.add("validation-row-error");
const messages = [];
errors.forEach((err) => {
messages.push(err.message);
if (!err.field) return; // row-level error, no specific cell
let cell = null;
if (err.field === "parts") {
// No specific cell to highlight, but we highlight the parts button area
// just add to messages
return;
} else if (err.field.startsWith("field_label:")) {
// Match by field_label text — find the header with this label, get its index
const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header");
let targetIndex = null;
headers.forEach((h) => {
if (h.textContent.trim() === label) {
targetIndex = h.getAttribute("data-index");
}
});
if (targetIndex) {
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-row="${rowIndex}"][data-index="${targetIndex}"]`);
}
}
} else {
// Direct data-col match
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`);
}
}
if (cell) {
cell.classList.add("validation-error");
// Mark the input/select inside the cell
cell.querySelectorAll("input, select").forEach((el) => {
el.classList.add("input-validation-error");
});
// Add tooltip with error message
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "validation-tooltip";
cell.appendChild(tooltip);
}
tooltip.textContent = err.message;
}
});
// Show aggregated error on the row using existing mechanism
showRowError(row, iddatadb, messages.join("\n"));
}
/**
* Send a single export request and update the row UI on success.
* Returns the parsed JSON response.
*/
async function sendExport(iddatadb, gridRow, batchUuid = null) {
const formData = new FormData();
formData.append("iddatadb", iddatadb);
if (batchUuid) {
formData.append("batch_uuid", batchUuid);
}
const response = await fetch("export_to_lims.php", {
method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success && gridRow) {
// Update status badge
const statusBadge = gridRow.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
// Insert/update CommessaWeb code span
const statusCell = gridRow.querySelector(
'.grid-cell[data-col="status"]',
);
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText =
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
cwSpan.title = "CommessaWeb";
const hiddenInput = statusCell.querySelector(
'input[type="hidden"]',
);
hiddenInput
? statusCell.insertBefore(cwSpan, hiddenInput)
: statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
// Disable export button for this row
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.style.background = "#ccc";
exportBtn.style.cursor = "not-allowed";
exportBtn.style.opacity = "0.5";
exportBtn.title = "Già esportato";
}
}
return data;
}
/**
* Show the result of a single export in the response modal.
*/
function showExportResult(data) {
const responseModalElement =
document.getElementById("exportResponseModal");
if (!responseModalElement) {
alert("Errore: Modale di risposta non trovato");
return;
}
exportButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const rowIndex = btn.dataset.row;
const iddatadb = btn.dataset.iddatadb;
console.log(
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
const responseModal = new bootstrap.Modal(responseModalElement, {
keyboard: false,
});
const responseMessage = document.getElementById(
"exportResponseMessage",
);
// Mostra il modale di conferma
if (data.success) {
responseMessage.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0
? `<br>Foto trovate: ${data.totalPhotos}`
: "");
document.getElementById("exportResponseModalLabel").textContent =
"Esportazione Completata";
} else {
responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`;
document.getElementById("exportResponseModalLabel").textContent =
"Errore Esportazione";
}
responseModal.show();
responseModalElement.addEventListener(
"hidden.bs.modal",
cleanupBackdrop,
{ once: true },
);
}
// ── Row button helpers (disable/enable during batch) ────────────────────
function setRowExporting(row, active) {
const btnCell = row.querySelector(".button-cell");
if (active) {
row.classList.remove("batch-disabled");
row.classList.add("batch-exporting");
if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.dataset.prevDisplay = b.style.display;
b.style.display = "none";
});
const spinner = document.createElement("span");
spinner.className = "batch-row-spinner";
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner);
}
} else {
row.classList.remove("batch-exporting");
row.classList.add("batch-disabled");
if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.style.display = b.dataset.prevDisplay || "";
delete b.dataset.prevDisplay;
});
}
}
}
function showRowError(row, iddatadb, message) {
row.classList.add("batch-row-error");
const btnCell = row.querySelector(".button-cell");
if (btnCell) {
// Remove existing error msg
const old = btnCell.querySelector(".batch-error-msg");
if (old) old.remove();
const errorEl = document.createElement("div");
errorEl.className = "batch-error-msg";
errorEl.textContent = "⚠ Errore — clicca per dettagli";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
document.getElementById("exportResponseModalLabel").textContent = "Errore Validazione (id: " + iddatadb + ")";
new bootstrap.Modal(document.getElementById("exportResponseModal"), { keyboard: false }).show();
});
btnCell.appendChild(errorEl);
}
}
function clearAllRowErrors() {
document.querySelectorAll(".grid-row.batch-row-error").forEach((row) => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
}
function disableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach((row) => {
row.classList.add("batch-disabled");
});
// Disable Actions dropdown
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = true;
toggle.style.opacity = "0.5";
toggle.style.pointerEvents = "none";
}
}
function enableAllRowButtons() {
document.querySelectorAll(".grid-row[data-id]").forEach((row) => {
row.classList.remove("batch-disabled");
});
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = false;
toggle.style.opacity = "";
toggle.style.pointerEvents = "";
}
}
// ── Single row export: validate, confirm modal, then send ───────────────
function startExportConfirmFlow(iddatadb, btn) {
const gridRow = btn.closest(".grid-row");
const rowIndex = btn.dataset.row;
// Validate first
clearValidationErrors();
validateRows([{ iddatadb: parseInt(iddatadb), index: parseInt(rowIndex) }])
.then((validationData) => {
if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Errore di validazione" });
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
// Show validation errors on the row
showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
// Validation passed — show confirm modal
showConfirmAndExport(iddatadb, btn);
})
.catch((error) => {
console.error("Validation error:", error);
showExportResult({ success: false, message: "Errore di validazione: " + error.message });
});
}
function showConfirmAndExport(iddatadb, btn) {
const confirmModalElement =
document.getElementById("exportConfirmModal");
if (!confirmModalElement) {
console.error("exportConfirmModal not found in the DOM");
alert("Errore: Modale di conferma non trovato");
return;
}
@@ -34,206 +367,407 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show();
// Gestisci il click su "Conferma"
const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) {
console.error("exportConfirmBtn not found in the DOM");
confirmModal.hide();
alert("Errore: Pulsante di conferma non trovato");
return;
}
const confirmHandler = async () => {
pendingConfirmHandler = null;
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
confirmModal.hide();
const formData = new FormData();
formData.append("iddatadb", iddatadb);
try {
const response = await fetch("export_to_lims.php", {
method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(
`HTTP error! status: ${response.status}`,
);
const data = await response.json();
const gridRow = btn.closest(".grid-row");
const data = await sendExport(iddatadb, gridRow);
console.log("Export response:", data);
// Mostra il modale di risposta
const responseModalElement = document.getElementById(
"exportResponseModal",
);
if (!responseModalElement) {
console.error(
"exportResponseModal not found in the DOM",
);
alert("Errore: Modale di risposta non trovato");
return;
}
const responseModal = new bootstrap.Modal(
responseModalElement,
{
keyboard: false,
},
);
const responseMessage = document.getElementById(
"exportResponseMessage",
);
if (data.success) {
responseMessage.innerHTML = `${data.message.replace(/\n/g, "<br>")}<br>ID CommessaWeb: ${data.idcommessaweb}`;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Esportazione Completata";
responseModal.show();
// Aggiorna la UI per riflettere lo stato 'To LIMS'
const statusCell = btn
.closest(".grid-row")
.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusCell) {
statusCell.classList.remove("status-i", "status-P");
statusCell.classList.add("status-l");
statusCell.textContent = "To LIMS";
}
// Gestisci la chiusura del modale di risposta
responseModalElement.addEventListener(
"hidden.bs.modal",
() => {
console.log(
"exportResponseModal closed, cleaning up",
);
// Rimuovi tutti i backdrop residui
document
.querySelectorAll(".modal-backdrop")
.forEach((backdrop) => {
console.log(
"Removing backdrop:",
backdrop,
);
backdrop.remove();
});
// Ripristina il body
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
// Nascondi l'overlay
const overlay = document.querySelector(
".overlay.toggle-icon",
);
if (overlay) {
overlay.style.display = "none";
}
},
{ once: true },
);
} else {
responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Errore Esportazione";
responseModal.show();
// Gestisci la chiusura del modale di risposta anche in caso di errore
responseModalElement.addEventListener(
"hidden.bs.modal",
() => {
console.log(
"exportResponseModal closed, cleaning up",
);
// Rimuovi tutti i backdrop residui
document
.querySelectorAll(".modal-backdrop")
.forEach((backdrop) => {
console.log(
"Removing backdrop:",
backdrop,
);
backdrop.remove();
});
// Ripristina il body
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
// Nascondi l'overlay
const overlay = document.querySelector(
".overlay.toggle-icon",
);
if (overlay) {
overlay.style.display = "none";
}
},
{ once: true },
);
}
showExportResult(data);
} catch (error) {
console.error("Export error:", error);
const responseModalElement = document.getElementById(
"exportResponseModal",
showExportResult({
success: false,
message: error.message,
});
}
};
if (pendingConfirmHandler) {
confirmBtn.removeEventListener("click", pendingConfirmHandler);
}
pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click handler ────────────────────────────────────────────
exportButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (batchRunning) return;
const rowIndex = btn.dataset.row;
const iddatadb = btn.dataset.iddatadb;
console.log(
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
);
if (!responseModalElement) {
console.error(
"exportResponseModal not found in the DOM",
const gridRow = btn.closest(".grid-row");
if (gridRow && gridRow.querySelector(".cell-changed")) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportUnsavedModal"),
{ keyboard: false },
);
alert("Errore: Modale di risposta non trovato");
unsavedModal.show();
document.getElementById("saveAndExportBtn").addEventListener(
"click",
() => {
unsavedModal.hide();
const saveBtn = gridRow.querySelector(".save-btn");
if (!saveBtn) return;
const observer = new MutationObserver(() => {
if (!gridRow.querySelector(".cell-changed")) {
observer.disconnect();
startExportConfirmFlow(iddatadb, btn);
}
});
observer.observe(gridRow, {
subtree: true,
attributes: true,
attributeFilter: ["class"],
});
saveBtn.click();
},
{ once: true },
);
return;
}
const responseModal = new bootstrap.Modal(
responseModalElement,
{
keyboard: false,
},
// No unsaved changes — go straight to validate + export confirm
startExportConfirmFlow(iddatadb, btn);
});
});
// ── Batch export (Export All) ───────────────────────────────────────────
const exportAllBtn = document.querySelector(".export-all-lims-btn");
if (!exportAllBtn) return;
let batchCancelled = false;
let pendingBatchConfirmHandler = null;
function collectEligibleRows() {
const allRows = document.querySelectorAll(".grid-row[data-id]");
const eligible = [];
allRows.forEach((row) => {
const statusBadge = row.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge && !statusBadge.classList.contains("status-l")) {
const iddatadb = row.dataset.id;
if (iddatadb) {
eligible.push({ iddatadb, row });
}
}
});
return eligible;
}
/**
* Get the data-row index for a grid row element.
*/
function getRowIndex(row) {
const cell = row.querySelector(".grid-cell[data-row]");
return cell ? parseInt(cell.getAttribute("data-row")) : null;
}
function hasUnsavedChanges() {
return !!document.querySelector(".grid-row[data-id] .cell-changed");
}
/**
* Validate all eligible rows, show errors, and return only the valid ones.
*/
async function validateAndFilter(eligibleRows) {
const rowsToValidate = eligibleRows.map(({ iddatadb, row }) => ({
iddatadb: parseInt(iddatadb),
index: getRowIndex(row),
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success) {
throw new Error(validationData.message || "Errore di validazione");
}
const validRows = [];
let invalidCount = 0;
for (const { iddatadb, row } of eligibleRows) {
const rowIdx = getRowIndex(row);
const result = validationData.results[rowIdx];
if (result && !result.valid) {
showValidationErrors(row, iddatadb, result.errors);
invalidCount++;
} else {
validRows.push({ iddatadb, row });
}
}
return { validRows, invalidCount };
}
function showValidationSpinner(show) {
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (show) {
bar.style.display = "";
statusEl.textContent = "Validazione in corso...";
cancelBtn.style.display = "none";
} else {
bar.style.display = "none";
cancelBtn.style.display = "";
}
}
function showBatchConfirm(eligibleRows) {
clearValidationErrors();
showValidationSpinner(true);
// Validate before showing confirm
validateAndFilter(eligibleRows)
.then(({ validRows, invalidCount }) => {
showValidationSpinner(false);
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`Nessuna riga valida per l'esportazione.<br>` +
`<strong>${invalidCount}</strong> righe con errori di validazione.`;
document.getElementById("exportResponseModalLabel").textContent =
"Validazione Fallita";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
return;
}
const confirmModal = new bootstrap.Modal(
document.getElementById("exportBatchConfirmModal"),
{ keyboard: false },
);
let countText = String(validRows.length);
if (invalidCount > 0) {
countText += ` (${invalidCount} escluse per errori di validazione)`;
}
document.getElementById("exportBatchCount").textContent = countText;
confirmModal.show();
const confirmBtn = document.getElementById("exportBatchConfirmBtn");
if (pendingBatchConfirmHandler) {
confirmBtn.removeEventListener("click", pendingBatchConfirmHandler);
}
pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null;
confirmModal.hide();
startBatchExport(validRows);
};
confirmBtn.addEventListener("click", pendingBatchConfirmHandler, { once: true });
})
.catch((error) => {
showValidationSpinner(false);
console.error("Batch validation error:", error);
document.getElementById("exportResponseMessage").textContent =
"Errore di validazione: " + error.message;
document.getElementById("exportResponseModalLabel").textContent =
"Errore Validazione";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
});
}
exportAllBtn.addEventListener("click", (e) => {
e.preventDefault();
if (batchRunning) return;
// Check unsaved changes first
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportBatchUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show();
document
.getElementById("batchSaveAndExportBtn")
.addEventListener(
"click",
() => {
unsavedModal.hide();
// Trigger Save All, then proceed
const saveAllEl =
document.querySelector(".save-all-btn");
if (!saveAllEl) return;
// Watch for all .cell-changed to disappear
const observer = new MutationObserver(() => {
if (!hasUnsavedChanges()) {
observer.disconnect();
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById(
"exportResponseMessage",
).textContent =
`Errore durante la generazione dei payload: ${error.message}`;
"Tutte le righe sono già state esportate al LIMS.";
document.getElementById(
"exportResponseModalLabel",
).textContent = "Errore Esportazione";
responseModal.show();
// Gestisci la chiusura del modale di risposta in caso di errore
responseModalElement.addEventListener(
"hidden.bs.modal",
() => {
console.log(
"exportResponseModal closed, cleaning up",
);
// Rimuovi tutti i backdrop residui
document
.querySelectorAll(".modal-backdrop")
.forEach((backdrop) => {
console.log("Removing backdrop:", backdrop);
backdrop.remove();
});
// Ripristina il body
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
// Nascondi l'overlay
const overlay = document.querySelector(
".overlay.toggle-icon",
);
if (overlay) {
overlay.style.display = "none";
).textContent = "Export All";
new bootstrap.Modal(
document.getElementById(
"exportResponseModal",
),
{ keyboard: false },
).show();
return;
}
showBatchConfirm(eligibleRows);
}
});
observer.observe(
document.querySelector(".grid-container"),
{
subtree: true,
attributes: true,
attributeFilter: ["class"],
},
);
saveAllEl.click();
},
{ once: true },
);
return;
}
// Rimuovi il listener dopo l'esecuzione
confirmBtn.removeEventListener("click", confirmHandler);
};
const eligibleRows = collectEligibleRows();
// Rimuovi eventuali listener precedenti
confirmBtn.removeEventListener("click", confirmHandler);
confirmBtn.addEventListener("click", confirmHandler);
});
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent =
"Tutte le righe sono già state esportate al LIMS.";
document.getElementById("exportResponseModalLabel").textContent =
"Export All";
const modal = new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
);
modal.show();
return;
}
showBatchConfirm(eligibleRows);
});
function startBatchExport(eligibleRows) {
batchCancelled = false;
batchRunning = true;
// Don't clear validation errors — they should stay visible for invalid rows
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0;
let succeeded = 0;
let failed = 0;
// Disable all row buttons
disableAllRowButtons();
// Show inline status bar
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
bar.style.display = "";
cancelBtn.disabled = false;
statusEl.textContent = `Esportazione 0 / ${total}...`;
// Cancel handler
cancelBtn.addEventListener(
"click",
() => {
batchCancelled = true;
statusEl.textContent =
"Annullamento... (attendi riga corrente)";
cancelBtn.disabled = true;
},
{ once: true },
);
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
statusEl.textContent = `Esportazione ${processed + 1} / ${total} (id: ${iddatadb})...`;
// Highlight current row
setRowExporting(row, true);
try {
const data = await sendExport(iddatadb, row, batchUuid);
processed++;
if (data.success) {
succeeded++;
} else {
failed++;
showRowError(row, iddatadb, data.message || "Errore sconosciuto");
}
} catch (error) {
processed++;
failed++;
showRowError(row, iddatadb, error.message);
}
// Remove highlight from current row
setRowExporting(row, false);
}
// Finished
batchRunning = false;
enableAllRowButtons();
bar.style.display = "none";
// Show result modal
const msgEl = document.getElementById("exportResponseMessage");
const labelEl = document.getElementById("exportResponseModalLabel");
if (batchCancelled) {
labelEl.textContent = "Export All — Annullato";
msgEl.innerHTML =
`Esportate: <strong>${succeeded}</strong><br>` +
`Errori: <strong>${failed}</strong><br>` +
`Non processate: <strong>${total - processed}</strong>`;
} else if (failed === 0) {
labelEl.textContent = "Export All — Completato";
msgEl.innerHTML = `Tutte le <strong>${succeeded}</strong> righe esportate con successo.`;
} else {
labelEl.textContent = "Export All — Completato con errori";
msgEl.innerHTML =
`Esportate: <strong>${succeeded}</strong><br>` +
`Errori: <strong>${failed}</strong>`;
}
const modalEl = document.getElementById("exportResponseModal");
const modal = new bootstrap.Modal(modalEl, { keyboard: false });
modal.show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
})();
}
});
+168 -15
View File
@@ -13,9 +13,31 @@ if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
// 🔹 Base URL API
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
// 🔹 Batch UUID — if present, all logs go to a single file
$batchUuid = $_POST['batch_uuid'] ?? null;
$writeLog = (function () use ($batchUuid, $logDir) {
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
if ($batchLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
} else {
file_put_contents($individualPath, $content);
}
};
})();
// 🔹 Funzione per validare e convertire date
function validateDate($value)
{
@@ -33,9 +55,21 @@ try {
throw new Exception("Missing iddatadb");
}
// TEMP: simulate error on every other row for testing
if (env('SIMULATE_EXPORT_LIMS') && $iddatadb % 2 === 0) {
throw new Exception("Simulated error for iddatadb $iddatadb");
}
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
// Also fetch fixed fields stored in datadb
$stmt = $pdo->prepare("
SELECT d.idclient AS clienteId, et.idschema AS schemaId
SELECT d.idclient AS clienteId, et.idschema AS schemaId,
d.cliente_responsabile_id,
d.moltiplicatore_prezzo_id,
d.anagrafica_certest_object_id,
d.anagrafica_certest_service_id,
d.cliente_fornitore_id,
d.consegna_richiesta
FROM datadb as d
INNER JOIN excel_templates as et ON d.templateid = et.id
WHERE d.iddatadb = :iddatadb
@@ -51,6 +85,14 @@ try {
$clienteId = (int) $result['clienteId'];
$schemaId = (int) $result['schemaId'];
// Extract fixed fields (nullable INTs)
$clienteResponsabile = !empty($result['cliente_responsabile_id']) ? (int) $result['cliente_responsabile_id'] : null;
$moltiplicatorePrezzo = !empty($result['moltiplicatore_prezzo_id']) ? (int) $result['moltiplicatore_prezzo_id'] : null;
$anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null;
$anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null;
$clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null;
$consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null;
// 🔹 STEP 3: Fetch Parts (including idmatrice)
$stmt = $pdo->prepare("
SELECT part_number, part_description, material, color, mix, idmatrice
@@ -60,6 +102,23 @@ try {
$stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
$stmt = $pdo->prepare("
UPDATE import_data_details idd
JOIN template_mapping m ON idd.mapping_id = m.id
SET idd.field_value = CASE m.auto_value
WHEN 'export_date' THEN :export_date
WHEN 'export_time' THEN :export_time
END
WHERE idd.id = :iddatadb
AND m.auto_value IN ('export_date', 'export_time')
");
$stmt->execute([
'iddatadb' => $iddatadb,
'export_date' => date('Y-m-d'),
'export_time' => date('H:i'),
]);
// 🔹 STEP 4: Fetch Field Values with Labels
$stmt = $pdo->prepare("
SELECT
@@ -93,12 +152,20 @@ try {
// 🔹 Initialize API client
$api = VisualLimsApiClient::getInstance();
// 🔹 STEP 5: Create CommessaWeb (NOT WebOrder)
// 🔹 STEP 5: Create CommessaWeb
// Fixed fields are sent as direct properties — they are navigation properties on Commessa
// accepted by the XAF API even though not explicitly listed in OData $metadata
$commessaWebPayload = [
"Cliente" => $clienteId,
"SchemaCustomField" => $schemaId,
"Richiedente" => "Test Web Import",
"Descrizione" => "TEST CommessaWeb",
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
"Descrizione" => "From TRFSmart Application", // TODO: replace with real value
"ClienteResponsabile" => $clienteResponsabile,
"MoltiplicatorePrezzo" => $moltiplicatorePrezzo,
"AnagraficaCertestObject" => $anagraficaObject,
"AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
// DeliveryRequest goes to Campione, not CommessaWeb
];
// Costruisci log curl-like per STEP 5
@@ -114,7 +181,7 @@ try {
// Salva log
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
file_put_contents($logFileStep5, $logContentStep5);
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
@@ -135,7 +202,9 @@ try {
"Matrice" => $matriceId,
"SottoMatrice" => null,
"SchemaCustomField" => $schemaId,
"NoteWeb" => $part["part_description"] ?? ""
"Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $part["part_description"] ?? "",
"ConsegnaRichiesta" => $consegnaRichiesta,
];
// Costruisci curl-like per questo campione
@@ -160,20 +229,80 @@ try {
// Salva log per STEP 6
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
file_put_contents($logFileStep6, $logContentStep6);
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
$stmtPhotos = $pdo->prepare("
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
FROM datadb_photos
WHERE iddatadb = :iddatadb
ORDER BY id ASC
");
$stmtPhotos->execute(['iddatadb' => $iddatadb]);
$photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 6.2: Upload photos to the first (main) Campione only
$photosUploaded = 0;
$logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n";
$logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n";
if (!empty($campioni) && !empty($photos)) {
$mainCampione = $campioni[0];
$campioneId = (int)($mainCampione['IdCampione'] ?? 0);
if ($campioneId > 0) {
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
foreach ($photos as $photo) {
$photoPath = $uploadDir . '/' . ltrim($photo['file_path'], './');
$fullPath = realpath($photoPath);
if (!$fullPath || !file_exists($fullPath)) {
$logContentPhotos .= "SKIP (file not found): {$photoPath}\n";
continue;
}
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 'true' : 'false';
$primaPagina = !empty($photo['PrimaPagina']) ? 'true' : 'false';
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--form 'file=@{$fullPath}' \\\n" .
"--form 'StampaNelRapporto={$stampaNelRapporto}' \\\n" .
"--form 'PrimaPagina={$primaPagina}'\n\n";
$extraFields = [
'StampaNelRapporto' => $stampaNelRapporto,
'PrimaPagina' => $primaPagina,
];
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name'], $extraFields);
$logContentPhotos .= "RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n---\n";
$photosUploaded++;
}
} else {
$logContentPhotos .= "SKIP: main campione has invalid IdCampione\n";
}
} elseif (empty($campioni)) {
$logContentPhotos .= "SKIP: no campioni created, cannot upload photos\n";
}
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) {
// GET con espansione per CustomField
$expand = "CommesseCustomFields(\$expand=CustomField)";
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
$commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}");
// Logga il GET
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
file_put_contents($logFileGet, $logContentGet);
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
// Prepara payload PATCH
$commessaCustomFields = [];
@@ -195,6 +324,7 @@ try {
];
}
$logFileStep7 = null;
if (!empty($commessaCustomFields)) {
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
@@ -209,7 +339,7 @@ try {
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
file_put_contents($logFileStep7, $logContentStep7);
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
}
}
@@ -226,7 +356,7 @@ try {
]);
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
/*
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
// Logga il POST
@@ -236,8 +366,24 @@ try {
"--data '{}'\n\n" .
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
file_put_contents($logFileStep9, $logContentStep9);
*/
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importPayload = ["IdUtente" => 285]; // user-id
$importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
// Logga il POST
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" .
"RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})");
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -248,7 +394,7 @@ try {
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
file_put_contents($logFileStep10, $logContentStep10);
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
@@ -263,15 +409,20 @@ try {
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos),
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6,
"step7_patch" => $logFileStep7,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10
]
]);
@@ -283,8 +434,10 @@ try {
"message" => "Export failed: " . $e->getMessage(),
"logFiles" => [
"step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null
]
]);
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint: GET api/odata/AnagraficaCertestObject
$endpoint = 'AnagraficaCertestObject';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = []; // es: ['$top' => 100]
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/anagrafica_certest_object_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint: GET api/odata/AnagraficaCertestService
$endpoint = 'AnagraficaCertestService';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = []; // es: ['$top' => 100]
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/anagrafica_certest_service_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
if ($idCliente <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_cliente']);
exit;
}
$api = VisualLimsApiClient::getInstance();
// Build endpoint with OData $expand WITHOUT encoding '$' as %24
$endpoint = "Cliente($idCliente)?\$expand=Responsabili";
// Debug URL (real final URL)
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Call API: options must be empty because expand is already in endpoint
$data = $api->get($endpoint, []);
file_put_contents(__DIR__ . '/cliente_responsabili_response.json', json_encode($data, JSON_PRETTY_PRINT));
// Return only the list (standard shape used by the frontend)
$responsabili = $data['Responsabili'] ?? [];
echo json_encode(['value' => $responsabili]);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+19 -2
View File
@@ -9,11 +9,28 @@ ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceNazioneFatturazione',
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$orderby' => 'Nominativo asc'
];
+93
View File
@@ -0,0 +1,93 @@
<?php
// get_fixed_field_data.php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
$field = $_GET['field'] ?? ''; // es: MoltiplicatorePrezzo, AnagraficaCertestObject, ...
if (!$field) {
http_response_code(400);
echo json_encode(['error' => 'Parametro "field" mancante']);
exit;
}
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$data = null;
$endpoint = null;
$cache_file = null;
$options = []; // qui puoi aggiungere filtri/ordering per campo se serve
try {
switch ($field) {
case 'MoltiplicatorePrezzo':
$endpoint = 'MoltiplicatorePrezzi';
$cache_file = __DIR__ . '/cache/moltiplicatori_prezzo.json';
break;
case 'AnagraficaCertestObject':
$endpoint = 'AnagraficaCertestObject';
$cache_file = __DIR__ . '/cache/anagrafica_certest_object.json';
break;
case 'AnagraficaCertestService':
$endpoint = 'AnagraficaCertestService';
$cache_file = __DIR__ . '/cache/anagrafica_certest_service.json';
break;
case 'ClienteResponsabile':
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
if ($id_cliente <= 0) {
if (!$simulate) {
http_response_code(400);
echo json_encode(['error' => 'Manca o invalido id_cliente']);
exit;
}
$id_cliente = 1; // dummy — mock ignores the actual ID
}
$endpoint = "Cliente($id_cliente)?\$expand=Responsabili";
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
break;
// case 'FuturoCampo5':
// $endpoint = 'QualcosaElse';
// break;
default:
http_response_code(400);
echo json_encode(['error' => "Campo non supportato: $field"]);
exit;
}
// Caching skipped in simulate mode to avoid polluting real-data cache files
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
} else {
$data = $api->get($endpoint, $options);
if (!$simulate && $cache_file) {
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
}
}
// Log ultimo URL (opzionale, per debug)
$full_url = $base_url . $endpoint . ($options ? '?' . http_build_query($options) : '');
file_put_contents(__DIR__ . '/last_urls.log', date('c') . " - $field - $full_url\n", FILE_APPEND);
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . " [$field] " . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,55 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint per recuperare i Moltiplicatori Prezzo
// (dal documento: GET api/odata/MoltiplicatorePrezzi)
$endpoint = 'MoltiplicatorePrezzi';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = [
'$orderby' => 'Descrizione asc'
];
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$queryParts = [];
foreach ($options as $k => $v) {
// mantieni il $ nella chiave, encoda solo il valore
$queryParts[] = $k . '=' . rawurlencode($v);
}
$query = implode('&', $queryParts);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// ✅ Force sort locally by "Descrizione" (A→Z)
if (isset($data['value']) && is_array($data['value'])) {
usort($data['value'], function ($a, $b) {
$da = isset($a['Descrizione']) ? trim((string)$a['Descrizione']) : '';
$db = isset($b['Descrizione']) ? trim((string)$b['Descrizione']) : '';
return strcasecmp($da, $db); // case-insensitive alphabetical
});
}
// Salva il JSON in locale
file_put_contents(__DIR__ . '/moltiplicatori_prezzo_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/include/headscript.php'; // o il tuo bootstrap standard
header('Content-Type: application/json');
try {
if (!isset($_GET['iddatadb']) || !is_numeric($_GET['iddatadb'])) {
echo json_encode(['success' => true, 'field' => null]);
exit;
}
$iddatadb = (int)$_GET['iddatadb'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// 1) prendo templateid da datadb
// 2) cerco max 1 record in template_mapping con is_visible_parts=1
$sql = "
SELECT tm.field_id, tm.field_label, tm.data_type, tm.has_list
FROM datadb d
JOIN template_mapping tm ON tm.template_id = d.templateid
WHERE d.iddatadb = ?
AND tm.is_visible_parts = 1
ORDER BY tm.id ASC
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'field' => $row ? $row : null
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake users
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
$fakeUsers = [
[
'IdUtente' => 1001,
'Nominativo' => 'Utente Simulato 1001'
],
[
'IdUtente' => 1002,
'Nominativo' => 'Utente Simulato 1002'
]
];
echo json_encode(['value' => $fakeUsers]);
exit;
}
// OData parameters
$params = [];
// Build query string
$queryString = http_build_query($params);
// Final endpoint
$endpoint = "Utente?$queryString";
// Function to execute request with retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$data = $api->get($endpoint);
// Save response for debug
file_put_contents(__DIR__ . '/utenti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Retry only on token/auth-related issue
if (
strpos($errorMessage, 'HTTP 400') !== false &&
strpos($errorMessage, 'Cannot persist the object') !== false
) {
try {
$api->refreshToken(); // must exist in VisualLimsApiClient
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());
}
usleep(500000); // 500 ms
continue;
}
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Execute request
$data = makeApiRequest($api, $endpoint);
echo json_encode($data);
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_utenti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
+2 -1
View File
@@ -59,7 +59,8 @@ foreach ($allMappings as $mapping) {
}
}
if (!$mainFieldMapping) {
$mainFieldMapping = reset(array_filter($allMappings, fn($m) => !$m['is_manual']));
$filtered = array_filter($allMappings, fn($m) => !$m['is_manual']);
$mainFieldMapping = reset($filtered);
}
// Retrieve data from datadb
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -20,9 +20,10 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !i
$template_id = intval($_POST['template_id']);
$selected_rows = array_map('intval', $_POST['selected_rows']);
$columns = json_decode($_POST['columns'], true);
$rows = json_decode($_POST['rows'], true);
$excelrows = json_decode($_POST['excelrows'], true);
$columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = htmlspecialchars($_POST['filename']);
$_SESSION['template_id'] = $template_id;
+71 -16
View File
@@ -121,6 +121,16 @@ error_log("Loaded template: " . print_r($template, true));
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
@@ -303,15 +313,18 @@ error_log("Loaded template: " . print_r($template, true));
function showTable(data) {
console.log('Mostro tabella con dati:', data);
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value='${JSON.stringify(data.columns)}'>
<input type="hidden" name="rows" value='${JSON.stringify(data.rows)}'>
<input type="hidden" name="excelrows" value='${JSON.stringify(data.excel_data.map(row => row.excelrow))}'>
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" value="${encodeURIComponent(JSON.stringify(data.rows))}">
<input type="hidden" name="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
<input type="hidden" name="filename" value="${data.filename}">
<div class="search-container">
<input type="text" id="searchInput" class="form-control" placeholder="Cerca nelle righe...">
<!-- TOP BUTTON -->
<div class="d-flex justify-content-end mb-3">
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
<div class="table-container">
<table class="table table-striped table-bordered">
<thead>
@@ -319,6 +332,17 @@ error_log("Loaded template: " . print_r($template, true));
<th><input type="checkbox" id="selectAll"> Seleziona</th>
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
@@ -330,17 +354,23 @@ error_log("Loaded template: " . print_r($template, true));
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButton" disabled>Prosegui</button>
</form>
`;
<!-- BOTTOM BUTTON -->
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
const proceedButton = document.getElementById('proceedButton');
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
proceedButton.disabled = !Array.from(checkboxes).some(cb => cb.checked);
const enabled = Array.from(checkboxes).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
@@ -393,14 +423,39 @@ error_log("Loaded template: " . print_r($template, true));
}
});
const searchInput = document.getElementById('searchInput');
const rows = document.querySelectorAll('.table tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
// Stato filtri: key = colIndex, value = testo
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
const text = Array.from(row.cells).slice(1).map(cell => cell.textContent.toLowerCase()).join(' ');
row.style.display = text.includes(searchTerm) ? '' : 'none';
// Le celle di data partono da index 1 (perché index 0 è checkbox)
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10); // 0..N-1 sulle colonne dati
const cell = row.cells[colIndex + 1]; // +1 per saltare la colonna checkbox
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
}
filterInputs.forEach(input => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex; // string
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
+1 -8
View File
@@ -35,8 +35,6 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1";
//$nameuser="Claudio";
//$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
@@ -49,13 +47,8 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"];
$photousername = basename($avatar);
?>
<?php //include files
//include files
require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php");
?>
+48 -2
View File
@@ -14,11 +14,57 @@ if (!$iddatadb) {
}
try {
$stmt = $pdo->prepare("SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC");
// 1) prendo templateid da datadb
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = :iddatadb LIMIT 1");
$stmtTpl->execute([':iddatadb' => $iddatadb]);
$templateid = $stmtTpl->fetchColumn();
// 2) prendo (max 1) field_id visibile in parts
$extraFieldId = null;
if ($templateid) {
$stmtEF = $pdo->prepare("SELECT field_id FROM template_mapping WHERE template_id = :templateid AND is_visible_parts = 1 ORDER BY id ASC LIMIT 1");
$stmtEF->execute([':templateid' => $templateid]);
$extraFieldId = $stmtEF->fetchColumn();
if ($extraFieldId !== false) $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
}
// 3) carico parts + join su tabella figlia
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
WHERE p.iddatadb = :iddatadb
ORDER BY p.part_number ASC
");
$stmt->execute([
':iddatadb' => $iddatadb,
':extraFieldId' => $extraFieldId
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
");
$stmt->execute([':iddatadb' => $iddatadb]);
}
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'parts' => $parts]);
echo json_encode([
'success' => true,
'parts' => $parts,
'extra_field_id' => $extraFieldId,
'debug_sql' => ($extraFieldId ? 'WITH_CF_JOIN' : 'NO_CF')
]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
}
+32
View File
@@ -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()]);
}
@@ -0,0 +1,60 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte AAA",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734582,
"CodiceCampione": "10978",
"CodiceCampioneWeb": "10978",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:02.1595894+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "PARE BBB",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734583,
"CodiceCampione": "10979",
"CodiceCampioneWeb": "10979",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:03.9972635+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "PARE BBB",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.76+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,44 @@
Photos for CommessaWeb 563528 (iddatadb=1259):
Total photos found: 2, campioni: 2
=== Campione 734582 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779561,
"FileName": "1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:06.7051549+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779562,
"FileName": "1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:08.5442313+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.7602299+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23254249,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254250,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254251,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254252,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254253,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254254,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254260,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254261,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254262,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254264,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254265,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254266,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254267,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254268,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254228,
"Valore": "13526"
},
{
"IdCommesseCustomFields": 23254229,
"Valore": "20262"
},
{
"IdCommesseCustomFields": 23254230,
"Valore": "ART. PEACH"
},
{
"IdCommesseCustomFields": 23254269,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254270,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254271,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254272,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254273,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254274,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254275,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254276,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254277,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254278,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254279,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254280,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254281,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254231,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23254232,
"Valore": "BLACK"
},
{
"IdCommesseCustomFields": 23254233,
"Valore": "PE007J"
},
{
"IdCommesseCustomFields": 23254234,
"Valore": "262"
},
{
"IdCommesseCustomFields": 23254235,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254236,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254237,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254238,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254239,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254240,
"Valore": "Oggi"
},
{
"IdCommesseCustomFields": 23254241,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254242,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254243,
"Valore": "236"
},
{
"IdCommesseCustomFields": 23254244,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254245,
"Valore": "BBB"
},
{
"IdCommesseCustomFields": 23254246,
"Valore": "solocla"
},
{
"IdCommesseCustomFields": 23254247,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254248,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254255,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254256,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254257,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254258,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254259,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254263,
"Valore": ""
}
]
}'
RESPONSE:
null
@@ -0,0 +1,40 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 8413,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "aaa"
}'
RESPONSE:
{
"IdCampione": 14445,
"Commessa": 95833,
"Matrice": 8413
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "bbb"
}'
RESPONSE:
{
"IdCampione": 15750,
"Commessa": 95833,
"Matrice": 3879
}
---
@@ -0,0 +1,9 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}
@@ -0,0 +1,9 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}
@@ -0,0 +1,10 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"simulated": true,
"endpoint": "CommessaWeb(95833)\/ImportaCommessa"
}
@@ -0,0 +1,15 @@
Photos for CommessaWeb 95833 (iddatadb=1237):
Total photos found: 1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/AllegatoCommessaWeb' \
--header 'Authorization: Bearer ••••••' \
--form 'IdCommessa=95833' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png'
RESPONSE:
{
"simulated": true,
"file": "1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png"
}
---
@@ -0,0 +1,22 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 3,
"MoltiplicatorePrezzo": 1,
"AnagraficaCertestObject": 2,
"AnagraficaCertestService": 1,
"ClienteFornitore": null
}'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,28 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 7586,
"MoltiplicatorePrezzo": 3,
"AnagraficaCertestObject": 8963,
"AnagraficaCertestService": 9007,
"ClienteFornitore": null
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 562479,
"CodiceCommessa": "26C0013",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-06T09:09:16.9338074+01:00",
"CodiceCommessaWeb": "26C0013",
"DataInviatoWeb": null,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,28 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 10653,
"MoltiplicatorePrezzo": 1,
"AnagraficaCertestObject": 8960,
"AnagraficaCertestService": 9010,
"ClienteFornitore": null
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-11T14:10:00.8960358+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": null,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
+117 -1
View File
@@ -97,6 +97,31 @@ if (!$template) {
<ul id="associationsList" class="list-group border p-3"></ul>
</div>
<!-- Default Values (Fixed fields) -->
<div class="mt-4">
<h5>Default Values (Fixed fields)</h5>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">MoltiplicatorePrezzo (default)</label>
<select id="defaultMoltiplicatorePrezzo" class="form-select">
<option value="">Loading...</option>
</select>
<small class="text-muted">Loaded from API: get_moltiplicatoreprezzo.php</small>
</div>
<div class="col-md-6">
<label class="form-label">ConsegnaRichiesta (default)</label>
<input type="date" id="defaultConsegnaRichiesta" class="form-control">
</div>
</div>
<div class="mt-3">
<button class="btn btn-success" id="saveDefaultsBtn">💾 Save defaults</button>
<span id="defaultsStatus" class="ms-2 text-muted"></span>
</div>
</div>
<!-- Save Button -->
<div class="mt-4 text-end">
<a href="templates_dashboard.php" class="btn btn-primary"> Back to Template Dashboard</a>
@@ -273,7 +298,7 @@ if (!$template) {
}
})
.catch(error => console.error('Error loading table columns:', error));
*/
*/
/** =======================
* CARICAMENTO MAPPATURE GIÀ ESISTENTI
* ======================= */
@@ -441,6 +466,97 @@ if (!$template) {
})
.catch(error => console.error("❌ Fetch error:", error));
}
/** =======================
* DEFAULTS (Fixed fields)
* - stored in localStorage for now
* ======================= */
const defaultsKey = "tpl_defaults_" + templateId;
function loadDefaultsFromLocal() {
try {
const raw = localStorage.getItem(defaultsKey);
if (!raw) return;
const saved = JSON.parse(raw);
if (saved && typeof saved === "object") {
if (saved.moltiplicatore_prezzo_id !== undefined) {
document.getElementById('defaultMoltiplicatorePrezzo').value = saved.moltiplicatore_prezzo_id || "";
}
if (saved.consegna_richiesta !== undefined) {
document.getElementById('defaultConsegnaRichiesta').value = saved.consegna_richiesta || "";
}
}
} catch (e) {
console.warn("⚠️ Cannot parse saved defaults:", e);
}
}
function saveDefaultsToLocal() {
const moltiplicatoreId = document.getElementById('defaultMoltiplicatorePrezzo').value || "";
const consegna = document.getElementById('defaultConsegnaRichiesta').value || "";
const payload = {
moltiplicatore_prezzo_id: moltiplicatoreId,
consegna_richiesta: consegna
};
localStorage.setItem(defaultsKey, JSON.stringify(payload));
const status = document.getElementById('defaultsStatus');
status.textContent = "✅ Defaults saved for this template (local)";
setTimeout(() => status.textContent = "", 2500);
}
function loadMoltiplicatoriPrezzo() {
const select = document.getElementById('defaultMoltiplicatorePrezzo');
select.innerHTML = `<option value="">-- Select --</option>`;
fetch('get_moltiplicatoreprezzo.php')
.then(r => r.json())
.then(data => {
// OData tipico: { value: [...] }
const rows = Array.isArray(data?.value) ? data.value : (Array.isArray(data) ? data : []);
if (!rows.length) {
select.innerHTML = `<option value="">(No data)</option>`;
return;
}
rows.forEach(item => {
// ⚠️ Qui i nomi campi dipendono dal JSON reale:
// - id: item.Id / item.ID / item.idMoltiplicatorePrezzo
// - label: item.Descrizione / item.Nome / item.Codice
const id = item.IdMoltiplicatorePrezzo;
const label = `${item.Descrizione} (x${item.Fattore})`;
if (id === undefined || id === null) return;
const opt = document.createElement('option');
opt.value = String(id);
opt.textContent = String(label);
select.appendChild(opt);
});
// dopo aver popolato, prova a rimettere il valore salvato
loadDefaultsFromLocal();
})
.catch(err => {
console.error("❌ Error loading MoltiplicatorePrezzo:", err);
select.innerHTML = `<option value="">(Load error)</option>`;
});
}
// Events
document.getElementById('saveDefaultsBtn').addEventListener('click', function() {
saveDefaultsToLocal();
});
// Init
loadMoltiplicatoriPrezzo();
loadDefaultsFromLocal();
});
</script>
</body>
File diff suppressed because it is too large Load Diff
+58 -3
View File
@@ -1,13 +1,22 @@
<!-- Modal per la gestione delle annotazioni -->
<div class="modal fade" id="annotationsModal" tabindex="-1" aria-labelledby="annotationsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 80% !important; width: 80% !important;">
<div class="modal-dialog modal-xl" style="max-width: 90% !important; width: 90% !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="annotationsModalLabel">Annotazioni per TRF: <span id="trfHeaderAnnotations"></span></h5>
<!-- SLIDER PER DIMENSIONE MARKER -->
<div style="display: flex; align-items: center; gap: 10px; margin-left: 20px;">
<label for="markerSizeSlider" style="margin: 0; font-size: 0.9rem; white-space: nowrap;">Dimensione marker:</label>
<input type="range" id="markerSizeSlider" min="16" max="48" value="16" step="2" style="width: 120px;">
<span id="markerSizeValue" style="font-weight: bold; min-width: 30px;">24px</span>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<!-- COLONNA SINISTRA RIDOTTA -->
<div class="col-md-6">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h6 style="margin: 0;">Elenco Parti</h6>
@@ -16,15 +25,17 @@
<label for="showMixPartsAnnotations" style="font-size: 0.9rem;">Mix</label>
</div>
</div>
<ul id="partsListAnnotations" class="list-group"></ul>
<ul id="partsListAnnotations" class="list-group" style="max-height: 500px; overflow-y: auto;"></ul>
</div>
<!-- COLONNA DESTRA PIÙ GRANDE -->
<div class="col-md-6">
<h6>Foto del Campione</h6>
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<button type="button" class="btn btn-primary btn-sm" id="downloadPhotoBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem; margin-right: 10px;"><i class="fas fa-download"></i></button>
<div id="photoSelectorContainerAnnotations" style="display: none;"></div>
</div>
<div style="position: relative; width: 100%; min-height: 400px;">
<div style="position: relative; width: 100%; min-height: 500px; border: 1px solid #ddd; border-radius: 4px; overflow: hidden;">
<img id="samplePhotoAnnotations" src="" alt="Foto del campione" style="max-width: 100%; max-height: 100%; object-fit: contain; position: absolute; top: 0; left: 0;">
<canvas id="photoCanvasAnnotations" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
<canvas id="overlayCanvasAnnotations" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000;"></canvas>
@@ -216,4 +227,48 @@
transform: translateX(-50%);
z-index: 3000;
}
/* Stile per lo slider */
#markerSizeSlider {
-webkit-appearance: none;
height: 6px;
border-radius: 3px;
background: #ddd;
outline: none;
}
#markerSizeSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
}
#markerSizeSlider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
border: none;
}
#partsListAnnotations .list-group-item.dragging {
opacity: 0.6;
background-color: #d1e7dd !important;
transform: rotate(3deg);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#partsListAnnotations .placeholder {
background-color: #f8d7da !important;
border: 2px dashed #dc3545;
height: 40px;
margin: 2px 0;
border-radius: 4px;
}
</style>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
+42 -7
View File
@@ -1,12 +1,12 @@
<div class="modal fade" id="partsModal" tabindex="-1" aria-labelledby="partsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 80% !important;">
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="row parts-row">
<div class="col-md-9">
<!-- Prima riga: Elenco Parti, Rinumera, Voce -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
@@ -14,6 +14,11 @@
<div style="display: flex; align-items: center;">
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button>
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button>
<button type="button" class="btn btn-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>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
@@ -29,7 +34,7 @@
<table class="table table-striped table-sm" id="partsTable">
<thead>
<tr>
<th style="width: 80px;">Numero</th>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
@@ -46,7 +51,7 @@
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 80px;"></td>
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
@@ -141,6 +146,24 @@
</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>
/* --- Base --- */
@@ -157,6 +180,14 @@
max-width: 100% !important
}
#addQuotationModal {
z-index: 1060 !important
}
#addQuotationModal .modal-backdrop {
z-index: 1055 !important
}
/* Tabelle */
#partsTable tr {
display: table-row !important
@@ -256,9 +287,13 @@
/* Colonna Descrizione (2ª colonna) = 420px */
#partsTable th:nth-child(2),
#partsTable td:nth-child(2) {
width: 350 !important;
min-width: 350px !important;
max-width: 350px !important;
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
}
#partsTable .part-number {
text-align: center;
}
#partsTable td:nth-child(2) .part-description {
+375 -11
View File
@@ -6,6 +6,9 @@ $(document).ready(function () {
let unsavedChanges = false;
let matrici = [];
let macroMatrici = [];
let quotations = [];
let partsExtraField = null; // {field_id, field_label} oppure null
let extraFieldOptions = []; // [{id,label}]
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
function getPartId($row) {
@@ -28,6 +31,148 @@ $(document).ready(function () {
return id === "new" || id === "" || id === null ? null : id;
}
function loadPartsExtraField(iddatadb, done) {
partsExtraField = null;
if (!iddatadb) return done();
$.ajax({
url: "get_parts_extra_field.php",
method: "GET",
dataType: "json",
data: { iddatadb },
success: function (res) {
partsExtraField =
res && res.success && res.field ? res.field : null;
if (
partsExtraField &&
(partsExtraField.data_type || "").toLowerCase() ===
"sceltamultipla"
) {
loadExtraFieldOptions(
String(partsExtraField.field_id),
function () {
applyExtraFieldColumn();
done();
},
);
} else {
applyExtraFieldColumn();
done();
}
},
error: function () {
partsExtraField = null;
applyExtraFieldColumn();
done();
},
});
}
function loadExtraFieldOptions(fieldId, done) {
extraFieldOptions = [];
if (!fieldId) return done();
$.ajax({
url: "get_customfield_values.php",
method: "GET",
dataType: "json",
data: { field_ids: fieldId },
success: function (res) {
const arr = res && res[fieldId] ? res[fieldId] : [];
extraFieldOptions = (arr || []).map((x) => ({
id: x.IdCustomFieldsValue,
label: x.Valore,
}));
done();
},
error: function () {
extraFieldOptions = [];
done();
},
});
}
function buildExtraFieldCellHtml($row = null) {
if (!partsExtraField) return "";
const selectedValueId = $row ? $row.data("extra-value-id") || "" : "";
// SceltaMultipla -> select + hidden value id
if (
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
) {
const opts = [`<option value="">Select…</option>`]
.concat(
extraFieldOptions.map(
(o) => `<option value="${o.id}">${o.label}</option>`,
),
)
.join("");
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="hidden" class="part-extra-value-id" value="">
<select class="form-control form-control-sm part-extra-select" data-selected="${selectedValueId || ""}">${opts}</select>
</td>`;
}
// Testo -> input + hidden field_id
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="text" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}">
</td>`;
}
$(document).on("change", ".part-extra-select", function () {
const valId = $(this).val() || "";
const $row = $(this).closest("tr");
// salva nello stato della riga (così sopravvive ai re-render)
$row.data("extra-value-id", valId);
$(this).closest("td").find(".part-extra-value-id").val(valId);
saveRow($row);
});
function applyExtraFieldColumn() {
const $theadRow = $("#partsTable thead tr");
// 1) rimuovi header extra
$theadRow.find("th.extra-field-th").remove();
// 2) rimuovi celle extra esistenti
$("#partsTableBody tr").each(function () {
$(this).find("td.extra-field-td").remove();
});
// 3) se non c'è campo extra -> fine
if (!partsExtraField) return;
// 4) inserisci header prima di "Azioni" (ultima colonna)
$theadRow
.find("th:last")
.before(
`<th class="extra-field-th" style="width:200px;">${partsExtraField.field_label}</th>`,
);
// 5) aggiungi cella a ogni riga già presente (una sola volta)
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.find("td:last").before(buildExtraFieldCellHtml($row));
// ripristina selezione se già presente nello stato riga
const selected = ($row.data("extra-value-id") || "").toString();
if (selected) {
$row.find(".part-extra-value-id").val(selected);
$row.find(".part-extra-select").val(selected);
}
});
}
function setPartId($row, id) {
if (!id) return;
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
@@ -141,7 +286,7 @@ $(document).ready(function () {
// ===================
// MODAL HANDLING
// ===================
function loadParts(iddatadb, idquotations) {
function loadParts(iddatadb, idquotations, callback = null) {
if (iddatadb) {
if (matrici.length === 0) {
$.ajax({
@@ -152,15 +297,19 @@ $(document).ready(function () {
matrici = data.value || [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
},
error: function (xhr, status, error) {
matrici = [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
error +
@@ -179,12 +328,16 @@ $(document).ready(function () {
} else {
loadMacroMatrici();
initializeGlobalSelect2();
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
} else {
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
}
@@ -513,6 +666,11 @@ $(document).ready(function () {
if (response.new_id) return response.new_id;
if (response.partId) return response.partId;
// ✅ RISPOSTA DI save_parts.php: { success:true, results:[{part_id:...}] }
if (Array.isArray(response.results) && response.results[0]) {
if (response.results[0].part_id) return response.results[0].part_id;
if (response.results[0].id) return response.results[0].id;
}
return null;
}
@@ -526,6 +684,13 @@ $(document).ready(function () {
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const isMix = partDescription.startsWith("Mix") ? "Y" : "N";
// EXTRA FIELD (0/1)
const extra_field_id = $row.find(".part-extra-field-id").val() || null;
const extra_value_id = $row.find(".part-extra-value-id").val() || null;
const extra_value_text = $row.find(".part-extra-field").length
? ($row.find(".part-extra-field").val() || "").trim()
: null;
let partId = getPartId($row);
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
const endpoint = idquotations
@@ -558,8 +723,14 @@ $(document).ready(function () {
part_number: partNumber,
part_description: partDescription,
mix: isMix,
idmatrice: $row.find(".part-matrice").val() || null,
dateexpiry: dateexpiry || null,
note: note,
// extra custom field
extra_field_id: extra_field_id,
extra_value_id: extra_value_id,
extra_value_text: extra_value_text,
},
],
}),
@@ -589,6 +760,9 @@ $(document).ready(function () {
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: ' +
@@ -718,6 +892,7 @@ $(document).ready(function () {
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml(null) : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
@@ -1151,11 +1326,15 @@ $(document).ready(function () {
}
});
$(document).on("blur", ".part-description, .part-number", function () {
$(document).on(
"blur",
".part-description, .part-number, .part-extra-field",
function () {
saveRow($(this).closest("tr"));
});
},
);
function loadExistingParts(iddatadb, idquotations) {
function loadExistingParts(iddatadb, idquotations, callback = null) {
const endpoint = idquotations
? "load_parts_quotation.php"
: "load_parts.php";
@@ -1206,6 +1385,7 @@ $(document).ready(function () {
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml() : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn ${part.note ? "has-note" : ""}" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
@@ -1215,6 +1395,20 @@ $(document).ready(function () {
</td>
</tr>`;
$("#partsTableBody").append(newRow);
const $row = $(
`#partsTableBody tr[data-part-id="${part.id}"]`,
);
if (
part.extra_value_id !== undefined &&
part.extra_value_id !== null
) {
const vid = String(part.extra_value_id);
$row.data("extra-value-id", vid);
$row.find(".part-extra-value-id").val(vid);
$row.find(".part-extra-select").val(vid);
}
const $select = $("#partsTableBody").find(
`tr[data-part-id="${part.id}"] .part-matrice`,
);
@@ -1233,8 +1427,11 @@ $(document).ready(function () {
});
} else {
addNewRow(1, false); // Riga iniziale normale
$("#quotationeBtn").removeClass("d-none");
}
updateRowButtons();
if (callback) callback();
},
error: function (xhr, status, error) {
const errorMsg = $(
@@ -1339,6 +1536,7 @@ $(document).ready(function () {
partId,
currentValue,
selectedMacro,
true,
);
});
});
@@ -1415,6 +1613,7 @@ $(document).ready(function () {
partId,
idmatrice,
selectedMacro = null,
fromFilter = false,
) {
if (typeof $.fn.select2 === "undefined") {
$select.replaceWith(
@@ -1484,16 +1683,24 @@ $(document).ready(function () {
true,
true,
);
$select.append(option).trigger("change");
if (!fromFilter) $select.append(option).trigger("change");
else $select.append(option);
partMatrice[partNumber] = matrice.IdMatrice;
} else {
// Aggiusta valore non valido
$select.val(null).trigger("change");
if (!fromFilter) $select.val(null).trigger("change");
partMatrice[partNumber] = null;
}
} else {
$select.val(null).trigger("change", [{ skipHandler: true }]);
}
$select.on("change", function () {
$select.on("change", function (event, data) {
if (data && data?.skipHandler) return;
const idmatrice = $(this).val();
const $row = $(this).closest("tr");
const partId = $row.data("part-id");
@@ -1777,6 +1984,141 @@ $(document).ready(function () {
// Esporta la funzione loadParts per essere usata da import_Edit2.php
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 () {
@@ -2000,3 +2342,25 @@ $(document).on("click", ".save-common-note-btn", function () {
markUnsaved();
}
});
$(document).on("click", "#showHideImageBtn", function () {
let mainRow = $(this).closest(".parts-row");
let photoContainer = mainRow.find(".col-md-3");
let tableContainer = mainRow
.find("#partsTable")
.closest("div[class*='col-md']");
if (photoContainer.hasClass("d-none")) {
photoContainer.removeClass("d-none");
tableContainer.removeClass("col-md-12").addClass("col-md-9");
$(this).html(
"<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
} else {
photoContainer.addClass("d-none");
tableContainer.removeClass("col-md-9").addClass("col-md-12");
$(this).html(
"<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
}
});
+63
View File
@@ -349,6 +349,69 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
document
.querySelectorAll(".photo-flag-checkbox")
.forEach((checkbox) => {
checkbox.addEventListener("change", async function () {
const photoId = this.getAttribute("data-photo-id");
const field = this.getAttribute("data-field");
const value = this.checked ? 1 : 0;
const currentCheckbox = this;
// Only one PrimaPagina visually in the current popup
if (field === "PrimaPagina" && value === 1) {
document
.querySelectorAll(
".photo-flag-checkbox[data-field='PrimaPagina']",
)
.forEach((cb) => {
if (cb !== currentCheckbox) {
cb.checked = false;
}
});
}
try {
const formData = new URLSearchParams();
formData.append("photo_id", photoId);
formData.append("field", field);
formData.append("value", value);
const response = await fetch("update_photo_flags.php", {
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
},
body: formData.toString(),
});
const result = await response.json();
if (!result.success) {
throw new Error(
result.message || "Errore salvataggio flag",
);
}
// Safety refresh for PrimaPagina to ensure UI matches DB
if (field === "PrimaPagina") {
loadPopupContent(iddatadb, idquotations);
}
} catch (error) {
alert(
"Errore durante il salvataggio del flag: " +
error.message,
);
// rollback current checkbox
currentCheckbox.checked = !currentCheckbox.checked;
// reload popup to restore coherent state
loadPopupContent(iddatadb, idquotations);
}
});
});
document.querySelectorAll(".thumbnail").forEach((img) => {
img.addEventListener("click", function () {
const enlargedImage = document.getElementById("enlargedImage");
+129 -13
View File
@@ -117,12 +117,24 @@ $code = $row[$field] ?? 'Non disponibile';
// Recupera le foto associate alla riga
try {
$stmt = $pdo->prepare("SELECT id, file_path, file_name, description, uploaded_at FROM {$photoTable} WHERE {$photoParamName} = ? ORDER BY uploaded_at DESC");
$stmt = $pdo->prepare("
SELECT
id,
file_path,
file_name,
uploaded_at,
description,
StampaNelRapporto,
PrimaPagina
FROM {$photoTable}
WHERE {$photoParamName} = ?
ORDER BY uploaded_at DESC
");
$stmt->execute([$paramValue]);
$photos = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Errore query foto: " . $e->getMessage());
$photos = []; // Imposta array vuoto in caso di errore
$photos = [];
}
// Definisci il percorso base per le foto
@@ -134,7 +146,7 @@ $uploadUrl = $iddatadb
? $baseUrl . "/upload_photos_mobile.php?iddatadb=" . $iddatadb
: $baseUrl . "/upload_photos_mobile.php?idquotations=" . $idquotations;
// Genera il QR code con endroid/qr-code 6.0.6
// Genera il QR code con endroid/qr-code
$qrCodeDir = '../photostrf/qrcodes/';
if (!is_dir($qrCodeDir)) {
mkdir($qrCodeDir, 0755, true);
@@ -164,6 +176,7 @@ $result->saveToFile($qrCodeFile);
<p>Caricamento in corso...</p>
</div>
</div>
<h3>Manage Photos</h3>
<p><strong>ID:</strong> <?= htmlspecialchars($id) ?></p>
<p><strong>Code:</strong> <?= htmlspecialchars($code) ?></p>
@@ -182,6 +195,7 @@ $result->saveToFile($qrCodeFile);
<p>Drag the photo here or click to select</p>
<input type="file" id="photoInput" multiple accept="image/*" style="display: none;">
</div>
<!-- Area per la webcam -->
<div id="webcamArea" style="display: none; text-align: center; margin-bottom: 20px;">
<p>Webcam Preview</p>
@@ -194,7 +208,9 @@ $result->saveToFile($qrCodeFile);
<button id="closeWebcamBtn" style="padding: 10px 20px; background: #dc3545; color: white; border: none; cursor: pointer;">Close Webcam</button>
</div>
</div>
<button id="openWebcamBtn" style="padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; margin-bottom: 20px;">Take Photo with Webcam</button>
<!-- Elenco delle foto -->
<div id="photosList">
<?php if (empty($photos)): ?>
@@ -204,25 +220,57 @@ $result->saveToFile($qrCodeFile);
<?php
$filePath = $photoBasePath . $photo['file_path'];
$fileExists = file_exists($filePath);
$stampaNelRapporto = !empty($photo['StampaNelRapporto']) ? 1 : 0;
$primaPagina = !empty($photo['PrimaPagina']) ? 1 : 0;
?>
<div class="photo-item" data-photo-id="<?= $photo['id'] ?>" style="display: flex; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<div style="flex: 1;">
<div class="photo-item" data-photo-id="<?= (int)$photo['id'] ?>">
<div class="photo-thumb-area">
<?php if ($fileExists): ?>
<img src="../photostrf/<?= htmlspecialchars($photo['file_path']) ?>" alt="<?= htmlspecialchars($photo['file_name']) ?>" class="thumbnail" style="max-width: 100px; max-height: 100px; margin-right: 10px; cursor: pointer;">
<img src="../photostrf/<?= htmlspecialchars($photo['file_path']) ?>"
alt="<?= htmlspecialchars($photo['file_name']) ?>"
class="thumbnail"
style="max-width: 100px; max-height: 100px; cursor: pointer;">
<?php else: ?>
<p style="color: red;">[File non trovato]</p>
<div class="missing-file-box">[File non trovato]</div>
<?php endif; ?>
<p style="margin: 0;">
<strong>Nome:</strong> <?= htmlspecialchars($photo['file_name']) ?><br>
<strong>Caricata il:</strong> <?= htmlspecialchars($photo['uploaded_at']) ?><br>
<strong>Descrizione:</strong> <?= htmlspecialchars($photo['description'] ?? 'Nessuna descrizione') ?>
</p>
</div>
<button class="delete-photo-btn" data-photo-id="<?= $photo['id'] ?>" style="background: none; border: none; color: #dc3545; cursor: pointer;">
<div class="photo-info-area">
<p class="photo-name">
<strong>Name:</strong> <?= htmlspecialchars($photo['file_name']) ?>
</p>
<div class="photo-flags">
<label class="flag-label">
<input
type="checkbox"
class="photo-flag-checkbox"
data-photo-id="<?= (int)$photo['id'] ?>"
data-field="StampaNelRapporto"
<?= $stampaNelRapporto ? 'checked' : '' ?>>
Print in Report
</label>
<label class="flag-label">
<input
type="checkbox"
class="photo-flag-checkbox prima-pagina-checkbox"
data-photo-id="<?= (int)$photo['id'] ?>"
data-field="PrimaPagina"
<?= $primaPagina ? 'checked' : '' ?>>
First Page
</label>
</div>
</div>
<div class="photo-actions-area">
<button class="delete-photo-btn" data-photo-id="<?= (int)$photo['id'] ?>" type="button">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<?php endforeach; ?>
<!-- Bottone Crea Collage -->
<button id="createCollageBtn" style="padding: 10px 20px; background: #ffc107; color: white; border: none; cursor: pointer; margin-top: 20px;">Crea Collage</button>
<?php endif; ?>
@@ -283,6 +331,12 @@ $result->saveToFile($qrCodeFile);
<style>
.photo-item {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
padding: 12px 0;
transition: background-color 0.3s;
}
@@ -290,6 +344,68 @@ $result->saveToFile($qrCodeFile);
background-color: #f8f9fa;
}
.photo-thumb-area {
flex: 0 0 110px;
text-align: center;
}
.photo-info-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.photo-actions-area {
flex: 0 0 40px;
display: flex;
justify-content: center;
align-items: center;
}
.photo-name {
margin: 0;
word-break: break-word;
}
.photo-flags {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin-top: 4px;
}
.flag-label {
display: flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 14px;
cursor: pointer;
}
.flag-label input[type="checkbox"] {
transform: scale(1.1);
cursor: pointer;
}
.delete-photo-btn {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
font-size: 18px;
}
.delete-photo-btn:hover {
color: #b02a37;
}
.missing-file-box {
color: red;
font-size: 13px;
}
#dropArea.highlight {
border-color: #28a745;
background-color: #e9ecef;
+47 -9
View File
@@ -399,14 +399,18 @@ if (isset($_GET['edit_id'])) {
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<?php include('modal_parts.php'); ?>
<?php include('photos_functions.php'); ?>
<div id="partsModalContainer"></div>
<div id="annotationsModalContainer"></div>
<?php include 'photos_functions.php'; ?>
<?php include('jsinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script src="photos.js"></script>
<script src="parts.js"></script>
<script src="annotationsModal.js"></script>
<script src="partsTable.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Mostra messaggi di stato se presenti
@@ -423,6 +427,45 @@ if (isset($_GET['edit_id'])) {
}, 5000);
}
$(document).on('click', '.parts-btn', function() {
const idquotations = $(this).data('idquotations');
$.ajax({
url: 'modal_partsTable.php',
method: 'GET',
data: {
idquotations: idquotations
},
success: function(response) {
$('#partsModalContainer').html(response);
const modalElement = document.getElementById('partsModal');
if (!modalElement) return;
$("#trfHeader").text(`Quotation #${idquotations}`);
$("#partsModal").data("idquotations", idquotations);
let modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement, {
backdrop: true
});
modal.show();
if (typeof window.loadParts === 'function') window.loadParts(null, idquotations);
},
error: function(xhr, status, error) {
alert('Errore nel caricamento del modale: ' + error);
}
});
});
$(document).on('hidden.bs.modal', '#partsModal', function() {
$('#partsModalContainer').empty();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('padding-right', '');
});
$(document).on('hidden.bs.modal', '#annotationsModal', function() {
$('#annotationsModalContainer').empty();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open').css('padding-right', '');
});
// Inizializza DataTables se non siamo in modalità modifica
if (!document.querySelector('#editForm')) {
$('#quotationsTable').DataTable({
@@ -524,12 +567,7 @@ if (isset($_GET['edit_id'])) {
</script>
<!-- Modale per le foto in quotations.php -->
<div class="modal" id="photosModal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<div class="popup-content"></div>
</div>
</div>
</body>
+131 -12
View File
@@ -12,9 +12,73 @@ try {
$iddatadb = intval($_POST['iddatadb']);
$idclient = isset($_POST['idclient']) ? (is_numeric($_POST['idclient']) ? intval($_POST['idclient']) : null) : null;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// ---------------- FIXED FIELDS (template_fixed_mapping) ----------------
// ALIAS: fixed_field_key "logico" -> colonna reale su datadb
// (NON tocchiamo MySQL, gestiamo solo qui la traduzione)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
// 1) Recupera templateid dalla riga datadb (serve per sapere quali fixed_field_key sono permessi)
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = ?");
$stmtTpl->execute([$iddatadb]);
$tplRow = $stmtTpl->fetch(PDO::FETCH_ASSOC);
$templateId = isset($tplRow['templateid']) ? (int)$tplRow['templateid'] : 0;
if ($templateId <= 0) {
throw new Exception("Template non trovato per iddatadb=$iddatadb");
}
// 2) Recupera elenco fixed fields visibili per quel template
$fxStmt = $pdo->prepare("
SELECT fixed_field_key, data_type, is_required, default_value
FROM template_fixed_mapping
WHERE template_id = ? AND is_visible_import = 1
");
$fxStmt->execute([$templateId]);
$fixedList = $fxStmt->fetchAll(PDO::FETCH_ASSOC);
// 3) Crea whitelist LOGICA: fixed_field_key => metadata
$fixedWhitelist = [];
foreach ($fixedList as $fx) {
$k = (string)$fx['fixed_field_key'];
// sicurezza: key ammessa solo se "safe"
if (!preg_match('/^[a-zA-Z0-9_]+$/', $k)) {
continue;
}
$fixedWhitelist[$k] = [
'data_type' => (string)$fx['data_type'], // INT | DATE
'is_required' => (int)$fx['is_required'],
'default_value' => $fx['default_value'] ?? null
];
}
// 3b) Crea whitelist REALE: colonna datadb reale => metadata
$realWhitelist = [];
foreach ($fixedWhitelist as $logicalKey => $meta) {
$realCol = $fixedAliasMap[$logicalKey] ?? $logicalKey;
// sicurezza: anche la colonna reale deve essere "safe"
if (!preg_match('/^[a-zA-Z0-9_]+$/', $realCol)) {
continue;
}
$realWhitelist[$realCol] = $meta;
}
// ---------------- DETAILS (import_data_details) ----------------
$data = $_POST;
$details = [];
@@ -63,20 +127,75 @@ try {
}
}
// 5. Aggiorna idclient in datadb
// ---------------- UPDATE datadb: idclient + FIXED FIELDS ----------------
$setParts = [];
$params = [];
// 5a) idclient (se presente)
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";
$setParts[] = "idclient = ?";
$params[] = $idclient;
}
// 5b) fixed fields dal POST
// QUI è il punto chiave: accettiamo SOLO colonne reali (realWhitelist),
// ma se dal JS arrivassero ancora key logiche, funzionerebbe uguale
// perché sotto controlliamo anche l'alias.
foreach ($realWhitelist as $realCol => $meta) {
$postKeyToRead = null;
// Caso 1: POST contiene già la colonna reale
if (array_key_exists($realCol, $_POST)) {
$postKeyToRead = $realCol;
} else {
$response['message'] = !empty($changed) ? "Updated details successfully" : "No changes found";
// Caso 2: POST contiene la key logica -> troviamo quale logica mappa su questa colonna reale
// (fallback, utile se non hai ancora aggiornato il JS)
foreach ($fixedAliasMap as $logical => $mappedReal) {
if ($mappedReal === $realCol && array_key_exists($logical, $_POST)) {
$postKeyToRead = $logical;
break;
}
}
}
if ($postKeyToRead === null) {
continue; // non inviato
}
$val = $_POST[$postKeyToRead];
// Normalizzazione per tipo
if ($meta['data_type'] === 'DATE') {
$val = trim((string)$val);
$val = ($val === '') ? null : $val; // atteso formato Y-m-d
} else { // INT
$val = trim((string)$val);
$val = ($val === '') ? null : (int)$val;
}
$setParts[] = "`$realCol` = ?";
$params[] = $val;
}
// esegui update solo se c'è qualcosa da aggiornare
if (!empty($setParts)) {
$params[] = $iddatadb;
$sqlUpd = "UPDATE datadb SET " . implode(", ", $setParts) . " WHERE iddatadb = ?";
$updStmt = $pdo->prepare($sqlUpd);
$updStmt->execute($params);
}
// Messaggio risposta (mantengo la tua logica ma includo fixed)
if (!empty($setParts) && !empty($changed)) {
$response['message'] = "Updated details and datadb fields successfully";
} elseif (!empty($setParts)) {
$response['message'] = "Updated datadb fields successfully";
} elseif (!empty($changed)) {
$response['message'] = "Updated details successfully";
} else {
$response['message'] = "No changes found";
}
$response['success'] = true;
+67 -4
View File
@@ -20,19 +20,82 @@ $mappingId = $data['id'];
$mappingType = $data['mapping_type'] ?? '';
$excelColumn = $data['excel_column'] ?? null;
$manualDefault = $data['manual_default'] ?? null;
$autoValue = $data['auto_value'] ?? 'none';
$tablename = $data['tablename'] ?? '';
try {
$stmt = $pdo->prepare("UPDATE template_mapping SET is_manual = ?, excel_column = ?, manual_default = ? WHERE id = ?");
$isManual = ($mappingType === 'manual') ? 1 : 0;
$result = $stmt->execute([$isManual, $excelColumn, $manualDefault, $mappingId]);
// Normalize mapping type
$allowedTypes = ['', 'xls', 'manual', 'auto'];
if (!in_array($mappingType, $allowedTypes, true)) {
echo json_encode(["success" => false, "message" => "Invalid mapping_type"]);
exit;
}
// Normalize auto_value
$allowedAuto = ['none', 'import_date', 'import_time', 'export_date', 'export_time'];
if (!in_array($autoValue, $allowedAuto, true)) {
$autoValue = 'none';
}
// Decide what to persist based on mapping_type
$isManual = 0;
$excelToSave = null;
$manualToSave = null;
$autoToSave = 'none';
if ($mappingType === 'xls') {
$isManual = 0;
$excelToSave = $excelColumn ?: null;
$manualToSave = null;
$autoToSave = 'none';
} elseif ($mappingType === 'manual') {
$isManual = 1;
$excelToSave = null;
$manualToSave = $manualDefault;
$autoToSave = 'none';
} elseif ($mappingType === 'auto') {
$isManual = 0;
$excelToSave = null;
$manualToSave = null;
$autoToSave = $autoValue ?: 'none';
} else {
// reset
$isManual = 0;
$excelToSave = null;
$manualToSave = null;
$autoToSave = 'none';
}
$stmt = $pdo->prepare("
UPDATE template_mapping
SET
is_manual = ?,
excel_column = ?,
manual_default = ?,
auto_value = ?
WHERE id = ?
");
$result = $stmt->execute([$isManual, $excelToSave, $manualToSave, $autoToSave, $mappingId]);
if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]);
exit;
}
echo json_encode(["success" => true, "message" => "Mapping updated successfully", "data" => $data]); // Aggiunto debug
echo json_encode([
"success" => true,
"message" => "Mapping updated successfully",
"saved" => [
"id" => (int)$mappingId,
"mapping_type" => $mappingType,
"is_manual" => $isManual,
"excel_column" => $excelToSave,
"manual_default" => $manualToSave,
"auto_value" => $autoToSave
]
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]);
}
+96 -7
View File
@@ -4,6 +4,7 @@ include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
@@ -19,6 +20,21 @@ try {
$pdo->beginTransaction();
$results = [];
// Custom fields statements (child table)
$stmtUpsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields (part_id, field_id, value_id, value_text)
VALUES (:part_id, :field_id, :value_id, :value_text)
ON DUPLICATE KEY UPDATE
value_id = VALUES(value_id),
value_text = VALUES(value_text),
updated_at = NOW()
");
$stmtDeleteCF = $pdo->prepare("
DELETE FROM identification_parts_customfields
WHERE part_id = :part_id AND field_id = :field_id
");
foreach ($parts as $part) {
$partId = $part['id'] ?? null;
$partNumber = $part['part_number'] ?? null;
@@ -28,9 +44,23 @@ try {
$note = $part['note'] ?? null;
$dateexpiry = $part['dateexpiry'] ?? null;
if ($partDescription || $note || $dateexpiry) {
// Extra field (0/1)
$extraFieldId = $part['extra_field_id'] ?? null;
$extraValueId = $part['extra_value_id'] ?? null;
$extraValueText = $part['extra_value_text'] ?? null;
// Normalizza vuoti
if ($extraFieldId !== null && $extraFieldId !== '') $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
if ($extraValueId !== null && $extraValueId !== '') $extraValueId = (int)$extraValueId;
else $extraValueId = null;
if ($extraValueText !== null) {
$extraValueText = trim((string)$extraValueText);
if ($extraValueText === '') $extraValueText = null;
}
if ($partId) {
// UPDATE se la parte esiste
// UPDATE se la parte esiste (sempre)
$stmt = $pdo->prepare("UPDATE identification_parts
SET part_number = :part_number,
part_description = :part_description,
@@ -49,9 +79,39 @@ try {
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$results[] = ['part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo'];
// Save extra custom field (if provided)
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
]);
} else {
// INSERT per nuova parte
$stmtUpsertCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
}
}
$cf_row = null;
if ($extraFieldId !== null) {
$chk = $pdo->prepare("SELECT id, value_id, value_text FROM identification_parts_customfields WHERE part_id = ? AND field_id = ?");
$chk->execute([$partId, $extraFieldId]);
$cf_row = $chk->fetch(PDO::FETCH_ASSOC);
}
$results[] = [
'part_id' => $partId,
'part_number' => $partNumber,
'message' => 'Parte aggiornata con successo',
'cf_row' => $cf_row
];
} else if ($partDescription || $note || $dateexpiry) {
// INSERT per nuova parte (solo se ha contenuto)
$stmt = $pdo->prepare("INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES (:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
@@ -64,14 +124,43 @@ try {
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$newId = $pdo->lastInsertId();
$results[] = ['part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo'];
$newId = (int)$pdo->lastInsertId();
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
]);
} else {
$stmtUpsertCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
}
}
$results[] = [
'part_id' => $newId,
'part_number' => $partNumber,
'message' => 'Parte salvata con successo'
];
}
}
$pdo->commit();
echo json_encode(['success' => true, 'results' => $results]);
echo json_encode([
'success' => true,
'results' => $results,
'debug_last' => [
'extra_field_id' => $extraFieldId ?? null,
'extra_value_id' => $extraValueId ?? null,
'extra_value_text' => $extraValueText ?? null,
'part_id' => $partId ?? ($newId ?? null),
]
]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
@@ -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']);
}
File diff suppressed because one or more lines are too long
+822 -1
View File
@@ -1,60 +1,881 @@
{
"@odata.context": "https:\/\/93.43.5.102\/limsapi\/api\/odata\/$metadata#SchemaCustomField",
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#SchemaCustomField",
"value": [
{
"IdSchemaCustomFields": 41,
"ConteggioClienti": 0,
"Nome": "Labostudio",
"Descrizione": null
},
{
"IdSchemaCustomFields": 42,
"ConteggioClienti": 0,
"Nome": "Schema 1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 43,
"ConteggioClienti": 0,
"Nome": "Schema1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 44,
"ConteggioClienti": 0,
"Nome": "Cuoio\/Pelle Standard \/ Leather",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 45,
"ConteggioClienti": 0,
"Nome": "Borse",
"Descrizione": null
},
{
"IdSchemaCustomFields": 46,
"ConteggioClienti": 0,
"Nome": "Borse Burberry",
"Descrizione": null
},
{
"IdSchemaCustomFields": 47,
"ConteggioClienti": 0,
"Nome": "pelle calzatura",
"Descrizione": null
},
{
"IdSchemaCustomFields": 48,
"ConteggioClienti": 0,
"Nome": "Standard Generico \/ Generic Standard",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 49,
"ConteggioClienti": 0,
"Nome": "Accessori Metallici \/ Metallic Accessories",
"Descrizione": "Schema per tutti gli Accessori Metallici\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 50,
"ConteggioClienti": 0,
"Nome": "Tessile \/ Textiles",
"Descrizione": "Schema per tutti i Tessuti\r\n"
},
{
"IdSchemaCustomFields": 52,
"ConteggioClienti": 0,
"Nome": "Brasport",
"Descrizione": null
},
{
"IdSchemaCustomFields": 54,
"ConteggioClienti": 0,
"Nome": "Chanel Calzatura",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 55,
"ConteggioClienti": 0,
"Nome": "Burberry",
"Descrizione": null
},
{
"IdSchemaCustomFields": 57,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Tessile",
"Descrizione": "Schema per tutti i campioni in tessuto per Ralph Lauren.\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 58,
"ConteggioClienti": 0,
"Nome": "LIU JO",
"Descrizione": null
},
{
"IdSchemaCustomFields": 59,
"ConteggioClienti": 0,
"Nome": "Migrazione tra Componenti",
"Descrizione": null
},
{
"IdSchemaCustomFields": 60,
"ConteggioClienti": 0,
"Nome": "Prova Outsourcing",
"Descrizione": null
},
{
"IdSchemaCustomFields": 61,
"ConteggioClienti": 0,
"Nome": "Marc Jacobs",
"Descrizione": null
},
{
"IdSchemaCustomFields": 62,
"ConteggioClienti": 0,
"Nome": "A. TESTONI",
"Descrizione": null
},
{
"IdSchemaCustomFields": 63,
"ConteggioClienti": 0,
"Nome": "Chanel CHINA PROJECT",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 66,
"ConteggioClienti": 0,
"Nome": "GUESS",
"Descrizione": null
},
{
"IdSchemaCustomFields": 68,
"ConteggioClienti": 0,
"Nome": "Valentino",
"Descrizione": null
},
{
"IdSchemaCustomFields": 69,
"ConteggioClienti": 0,
"Nome": "DESIGUAL",
"Descrizione": null
},
{
"IdSchemaCustomFields": 70,
"ConteggioClienti": 0,
"Nome": "FENDI PELLETTERIA",
"Descrizione": "Solo per FENDI Pelletteria_INGLESE\r\n"
},
{
"IdSchemaCustomFields": 71,
"ConteggioClienti": 0,
"Nome": "Z_Cuoio\/Pelle Standard ACCREDIA",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 72,
"ConteggioClienti": 0,
"Nome": "Z_BALLY",
"Descrizione": "sia per calzatura che pelletteria. Aggiunto campo TEST DESCRIPTION, compreso nella fatturazione per le divisioni pelletteria.\r\n\r\n"
},
{
"IdSchemaCustomFields": 73,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Leather",
"Descrizione": null
},
{
"IdSchemaCustomFields": 74,
"ConteggioClienti": 0,
"Nome": "Z_Ralph Lauren - Fabric",
"Descrizione": null
},
{
"IdSchemaCustomFields": 76,
"ConteggioClienti": 0,
"Nome": "NORDSTROM",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 77,
"ConteggioClienti": 0,
"Nome": "ONWARD LUXURY GROUP",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 78,
"ConteggioClienti": 0,
"Nome": "P&G SRL_Progetto Durability",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 79,
"ConteggioClienti": 0,
"Nome": "Bally qualsiasi divisione",
"Descrizione": null
},
{
"IdSchemaCustomFields": 80,
"ConteggioClienti": 0,
"Nome": "FENDI CALZATURA",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 81,
"ConteggioClienti": 0,
"Nome": "LouBoutin",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 82,
"ConteggioClienti": 0,
"Nome": "MONCLER Brand",
"Descrizione": "Da utilizzare solo per il Brand Diretto\r\nGR 19\/03\/2024"
},
{
"IdSchemaCustomFields": 83,
"ConteggioClienti": 0,
"Nome": "JIMMY CHOO",
"Descrizione": null
},
{
"IdSchemaCustomFields": 84,
"ConteggioClienti": 0,
"Nome": "Yves Saint Laurent",
"Descrizione": null
},
{
"IdSchemaCustomFields": 85,
"ConteggioClienti": 0,
"Nome": "BENETTON",
"Descrizione": null
},
{
"IdSchemaCustomFields": 86,
"ConteggioClienti": 0,
"Nome": "UPLOAD REPORT",
"Descrizione": null
},
{
"IdSchemaCustomFields": 87,
"ConteggioClienti": 0,
"Nome": "Hermes Calzatura",
"Descrizione": "\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 88,
"ConteggioClienti": 0,
"Nome": "C&A",
"Descrizione": "Schema per fornitori e C&A diretto\r\n"
},
{
"IdSchemaCustomFields": 89,
"ConteggioClienti": 0,
"Nome": "AMERICAN EAGLE",
"Descrizione": "Schema per American Eagle\/Todd snyder\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 91,
"ConteggioClienti": 0,
"Nome": "Ferragamo",
"Descrizione": "\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 93,
"ConteggioClienti": 0,
"Nome": "Z_Alexander McQueen",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 95,
"ConteggioClienti": 0,
"Nome": "Balenciaga Pelletteria",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 96,
"ConteggioClienti": 0,
"Nome": "DUNHILL",
"Descrizione": "SCHEMA DUNHILL tutti i tipi di campioni\r\n"
},
{
"IdSchemaCustomFields": 97,
"ConteggioClienti": 0,
"Nome": "PRELIEVI ACQUE",
"Descrizione": null
},
{
"IdSchemaCustomFields": 98,
"ConteggioClienti": 0,
"Nome": "LouBoutin_Tossicologico",
"Descrizione": null
},
{
"IdSchemaCustomFields": 99,
"ConteggioClienti": 0,
"Nome": "ROBAN'S ",
"Descrizione": "tutte le divisioni\r\n\r\n"
},
{
"IdSchemaCustomFields": 100,
"ConteggioClienti": 0,
"Nome": "PROGETTO LUGGAGE",
"Descrizione": "Progetto Luggage Hermes\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 102,
"ConteggioClienti": 0,
"Nome": "Balmain",
"Descrizione": "Schema per tutti i campioni Balmain"
},
{
"IdSchemaCustomFields": 103,
"ConteggioClienti": 0,
"Nome": "ZDHC",
"Descrizione": "Schema di Riferimento ZDHC Wastewaters"
},
{
"IdSchemaCustomFields": 104,
"ConteggioClienti": 0,
"Nome": "GUCCI CDC 4172",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 105,
"ConteggioClienti": 0,
"Nome": "Z_ASOS",
"Descrizione": null
},
{
"IdSchemaCustomFields": 106,
"ConteggioClienti": 0,
"Nome": "Z_BOUX AVENUE",
"Descrizione": "28\/09 no indicazioni su esigenze di fatturazione (detto da Frosini)\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 107,
"ConteggioClienti": 0,
"Nome": "BONPOINT",
"Descrizione": "05\/10 creazione\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 108,
"ConteggioClienti": 0,
"Nome": "Canada Goose",
"Descrizione": null
},
{
"IdSchemaCustomFields": 109,
"ConteggioClienti": 0,
"Nome": "DIESEL",
"Descrizione": null
},
{
"IdSchemaCustomFields": 110,
"ConteggioClienti": 0,
"Nome": "AQC_Leather",
"Descrizione": null
},
{
"IdSchemaCustomFields": 111,
"ConteggioClienti": 0,
"Nome": "BEAUMANOIR (CC-Mor-Bon-Breal)",
"Descrizione": "04\/02\/21_ AGGIORNATO 05\/08\r\n"
},
{
"IdSchemaCustomFields": 112,
"ConteggioClienti": 0,
"Nome": "BooHoo",
"Descrizione": null
},
{
"IdSchemaCustomFields": 114,
"ConteggioClienti": 0,
"Nome": "Golden Goose",
"Descrizione": "Golden goose\r\n"
},
{
"IdSchemaCustomFields": 115,
"ConteggioClienti": 0,
"Nome": "Z_ZLABEL",
"Descrizione": "Schema per ZLABEL"
},
{
"IdSchemaCustomFields": 116,
"ConteggioClienti": 0,
"Nome": "Zadig & Voltaire",
"Descrizione": "Schema per Zadig & Voltaire"
},
{
"IdSchemaCustomFields": 117,
"ConteggioClienti": 0,
"Nome": "Gemo",
"Descrizione": "Schema solo per Gemo\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 118,
"ConteggioClienti": 0,
"Nome": "PROMOD",
"Descrizione": "CHF 06\/08\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 119,
"ConteggioClienti": 0,
"Nome": "LA HALLE (BEAUMANOIR)",
"Descrizione": "13\/08\/2021 CHF\r\n\r\n"
},
{
"IdSchemaCustomFields": 120,
"ConteggioClienti": 0,
"Nome": "LA REDOUTE",
"Descrizione": "14\/09\/2021 AD\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 121,
"ConteggioClienti": 0,
"Nome": "TOD'S",
"Descrizione": "Schema per materiale gb18401 e GB20400\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 122,
"ConteggioClienti": 0,
"Nome": "ASOS rev 1",
"Descrizione": null
},
{
"IdSchemaCustomFields": 123,
"ConteggioClienti": 0,
"Nome": "Monoprix",
"Descrizione": "Schema per Monoprix\r\n\r\n"
},
{
"IdSchemaCustomFields": 124,
"ConteggioClienti": 0,
"Nome": "DIOR Ita",
"Descrizione": "CHF 21\/03\r\n"
},
{
"IdSchemaCustomFields": 125,
"ConteggioClienti": 0,
"Nome": "GEOX - Raw Material",
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER LE SUOLE E MATERIALI DENOMINATI RPU \/ RAW MATERIAL"
},
{
"IdSchemaCustomFields": 126,
"ConteggioClienti": 0,
"Nome": "CAMAIEU",
"Descrizione": "SCHEMA DA USARE SOLO PER CAMAIEU\r\n"
},
{
"IdSchemaCustomFields": 127,
"ConteggioClienti": 0,
"Nome": "Z_Golden Goose FTW",
"Descrizione": "Golden goose\r\n"
},
{
"IdSchemaCustomFields": 128,
"ConteggioClienti": 0,
"Nome": "STEVE MADDEN - FOOTWEAR",
"Descrizione": "DA UTILIZZARE SOLO PER DESTINAZIONE D'USO CALZATURA\r\n"
},
{
"IdSchemaCustomFields": 129,
"ConteggioClienti": 0,
"Nome": "Z_Carhartt",
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER RICHIESTE UFFICIALI CARHARTT\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 131,
"ConteggioClienti": 0,
"Nome": "DAMART",
"Descrizione": "\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 132,
"ConteggioClienti": 0,
"Nome": "Gruppo OTB",
"Descrizione": "Schema per Staff International, Diesel e Brave kid\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 133,
"ConteggioClienti": 0,
"Nome": "FERRARI",
"Descrizione": "Schema da usare per Ferrari e fornitori\r\n\r\n"
},
{
"IdSchemaCustomFields": 134,
"ConteggioClienti": 0,
"Nome": "GAP - ABBIGLIAMENTO",
"Descrizione": "Schema GAP da utilizzare solo per destinazione abbigliamento\r\n"
},
{
"IdSchemaCustomFields": 135,
"ConteggioClienti": 0,
"Nome": "GAP - CALZATURA",
"Descrizione": "Schema GAP da utilizzare solo per destinazione calzatura\r\n\r\n"
},
{
"IdSchemaCustomFields": 136,
"ConteggioClienti": 0,
"Nome": "SALLING GROUP",
"Descrizione": "Schema per Salling Group\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 137,
"ConteggioClienti": 0,
"Nome": "SOFT SURROUNDINGS",
"Descrizione": "Schema da utilizzare per Soft Surroundings\r\n\r\n"
},
{
"IdSchemaCustomFields": 138,
"ConteggioClienti": 0,
"Nome": "E-LIMS - Cuoio\/Pelle Standard \/ Leather",
"Descrizione": "Schema da utilizzare solo per accettazioni online\r\n"
},
{
"IdSchemaCustomFields": 139,
"ConteggioClienti": 0,
"Nome": "Z_ZALANDO",
"Descrizione": "Schema per ZALANDO ------ (NON per ZLABEL che ha il suo)\r\n"
},
{
"IdSchemaCustomFields": 140,
"ConteggioClienti": 0,
"Nome": "LVMH",
"Descrizione": "gennaio 2023\r\n"
},
{
"IdSchemaCustomFields": 141,
"ConteggioClienti": 0,
"Nome": "Valentino - eLims",
"Descrizione": "SCHEMA DA UTILIZZARE SOLO PER L'ACCETTAZIONE ONLINE DI VALENTINO"
},
{
"IdSchemaCustomFields": 142,
"ConteggioClienti": 0,
"Nome": "FENDI PELLETTERIA COMPLIANCE",
"Descrizione": "Solo per FENDI Pelletteria_INGLESE 04\/05\/2023\r\n"
},
{
"IdSchemaCustomFields": 143,
"ConteggioClienti": 0,
"Nome": "TAPESTRY",
"Descrizione": "04\/05\/2023, agg secondo procedura 08\/11\/23\r\n"
},
{
"IdSchemaCustomFields": 144,
"ConteggioClienti": 0,
"Nome": "ALEXANDER MC QUEEN",
"Descrizione": "ALEXANDER MC QUEEN 15\/05\/2023\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 145,
"ConteggioClienti": 0,
"Nome": "Hermes_Chimici_Parigi",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 146,
"ConteggioClienti": 0,
"Nome": "CELINE ABBIGLIAMENTO_Progetto Speciale",
"Descrizione": "IO 202 CELINE ABBIGLIAMENTO_06\/06\/203_CHF\r\n\r\n"
},
{
"IdSchemaCustomFields": 147,
"ConteggioClienti": 0,
"Nome": "GEOX - Prodotto Finito",
"Descrizione": "SCHEMA DA USARE SOLO PER GEOX - SOLO PER PRODOTTO FINITO"
},
{
"IdSchemaCustomFields": 149,
"ConteggioClienti": 0,
"Nome": "Hermes\/IDO",
"Descrizione": "creato il 05\/07\/2023 CHF\r\n"
},
{
"IdSchemaCustomFields": 150,
"ConteggioClienti": 0,
"Nome": "Tessile \/ Textiles PER CLIENTI CON OBLO",
"Descrizione": "Schema per tutti i Tessuti QUANDO IL CLIENTE NECESSITA DI NR OBLO\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 151,
"ConteggioClienti": 0,
"Nome": "J JIll",
"Descrizione": "Schema per cliente J JIll, necessario riportare la recommended fiber content in schema verde\r\n\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 152,
"ConteggioClienti": 0,
"Nome": "AUCHAN - TAPE A L'OEIL",
"Descrizione": "Schema AUCHAN da utilizzare solo per la divisione TAPE A L'OEIL SA, successivamente se uguale da usare anche per le altre divisioni ed uniformarlo come generico"
},
{
"IdSchemaCustomFields": 153,
"ConteggioClienti": 0,
"Nome": "ALL SAINTS - BUSCEMI",
"Descrizione": "SCHE DA USARE SOLO PER ALL SAINTS E BUSCEMI\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 154,
"ConteggioClienti": 0,
"Nome": "CELINE - LVMH",
"Descrizione": "Schema con aggiunta campi LVMH per estrazioni\r\nAD-29\/03\/2024"
},
{
"IdSchemaCustomFields": 155,
"ConteggioClienti": 0,
"Nome": "SeyMeChamLou",
"Descrizione": "DA USARE SOLO PER SEYMECHAMLOU (LOUBOUTIN)\r\n"
},
{
"IdSchemaCustomFields": 156,
"ConteggioClienti": 0,
"Nome": "PRADA",
"Descrizione": "PER ADESSO DA USARE PER ABBIGLIAMENTO E CALZATURA\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 157,
"ConteggioClienti": 0,
"Nome": "Hermes_HCP",
"Descrizione": "Schema per unicamente HCP"
},
{
"IdSchemaCustomFields": 158,
"ConteggioClienti": 0,
"Nome": "STONE ISLAND",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 159,
"ConteggioClienti": 0,
"Nome": "Chanel_Pierre Damien",
"Descrizione": "Schema da usare per Campioni ricevuti da Pierre-Damien Verine e suo staff CHANEL.\r\n"
},
{
"IdSchemaCustomFields": 160,
"ConteggioClienti": 0,
"Nome": "Z_ACNE",
"Descrizione": "\r\n.\r\n"
},
{
"IdSchemaCustomFields": 161,
"ConteggioClienti": 0,
"Nome": "OBERALP GROUP",
"Descrizione": "Schema da usare solo per Oberalp Group\r\n"
},
{
"IdSchemaCustomFields": 162,
"ConteggioClienti": 0,
"Nome": "Club Monaco",
"Descrizione": "da usare solo per Club Monaco GR 20\/02\/2024"
},
{
"IdSchemaCustomFields": 163,
"ConteggioClienti": 0,
"Nome": "Devred",
"Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024\r\n\r\n"
},
{
"IdSchemaCustomFields": 164,
"ConteggioClienti": 0,
"Nome": "MONCLER Supplier",
"Descrizione": "Da utilizzare per fornitori Moncler \r\nGR 19\/03\/2024"
},
{
"IdSchemaCustomFields": 165,
"ConteggioClienti": 0,
"Nome": "Kering Sunglasses",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 166,
"ConteggioClienti": 0,
"Nome": "Giorgio Armani",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 167,
"ConteggioClienti": 0,
"Nome": "Chanel USA",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 168,
"ConteggioClienti": 0,
"Nome": "Staff Calzatura",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 169,
"ConteggioClienti": 0,
"Nome": "P&G SRL",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 170,
"ConteggioClienti": 0,
"Nome": "INCOM",
"Descrizione": "Schema da utilizzare per INCOM"
},
{
"IdSchemaCustomFields": 171,
"ConteggioClienti": 0,
"Nome": "RIVER ISLAND",
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - NO CALZATURA\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 172,
"ConteggioClienti": 0,
"Nome": "Longchamp",
"Descrizione": "Schema per tutti i campioni con RIchiesta Longchamp ufficiale\r\n"
},
{
"IdSchemaCustomFields": 173,
"ConteggioClienti": 0,
"Nome": "Bottega Veneta",
"Descrizione": "Schema per tutti i campioni di Bottega Veneta di Stella"
},
{
"IdSchemaCustomFields": 174,
"ConteggioClienti": 0,
"Nome": "Mint Velvet",
"Descrizione": "Schema per tutti i campioni di Mint Velvel con richiesta ufficiale.\r\n\r\n"
},
{
"IdSchemaCustomFields": 176,
"ConteggioClienti": 0,
"Nome": "RIVER ISLAND FOOTWEAR",
"Descrizione": "SCHEDA DA USARE SOLO PER RIVER ISLAND - USARE SOLO PER\r\n CALZATURA\r\n"
},
{
"IdSchemaCustomFields": 177,
"ConteggioClienti": 0,
"Nome": "Phoebe philo ACC",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
},
{
"IdSchemaCustomFields": 178,
"ConteggioClienti": 0,
"Nome": "New Guards Group",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 179,
"ConteggioClienti": 0,
"Nome": "Giorgio Armani Operation",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 180,
"ConteggioClienti": 0,
"Nome": "Chloe'",
"Descrizione": "\r\n"
},
{
"IdSchemaCustomFields": 181,
"ConteggioClienti": 0,
"Nome": "LVMH non diretti",
"Descrizione": "utilizzato ogni qualvolta siano caricati pacchetti \/ prove singole che non rientrano nel programma LVMH \r\n"
},
{
"IdSchemaCustomFields": 182,
"ConteggioClienti": 0,
"Nome": "Ralph Lauren - All testing V.6",
"Descrizione": null
},
{
"IdSchemaCustomFields": 183,
"ConteggioClienti": 0,
"Nome": "TWINSET",
"Descrizione": "Schema da usare per Twinset - come da mail di EP del 20\/05\/2025\r\n\r\n"
},
{
"IdSchemaCustomFields": 184,
"ConteggioClienti": 0,
"Nome": "ACNE STUDIOS rev 01",
"Descrizione": null
},
{
"IdSchemaCustomFields": 185,
"ConteggioClienti": 0,
"Nome": "Brunello cucinelli",
"Descrizione": null
},
{
"IdSchemaCustomFields": 186,
"ConteggioClienti": 0,
"Nome": "TWINSET",
"Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n"
},
{
"IdSchemaCustomFields": 187,
"ConteggioClienti": 0,
"Nome": "LORO PIANA",
"Descrizione": "Schema per tutti i campioni Loro Piana\r\n"
},
{
"IdSchemaCustomFields": 188,
"ConteggioClienti": 0,
"Nome": "LEMAIRE",
"Descrizione": "Schema per tutti i campioni in cuoio\/pelle per i quali non \u00e8 specificato un destinatario.\r\n"
},
{
"IdSchemaCustomFields": 189,
"ConteggioClienti": 0,
"Nome": "TEST_IT_SCHEMA",
"Descrizione": "Test per IT"
},
{
"IdSchemaCustomFields": 190,
"ConteggioClienti": 0,
"Nome": "LBS Interscambio",
"Descrizione": "Schema per tutti i campioni interscambio LBS\r\n"
},
{
"IdSchemaCustomFields": 191,
"ConteggioClienti": 0,
"Nome": "KIABI",
"Descrizione": "Schema per il cliente Kiabi\r\n"
},
{
"IdSchemaCustomFields": 192,
"ConteggioClienti": 0,
"Nome": "INDITEX",
"Descrizione": "Schema da usare solo per richieste INDITEX"
},
{
"IdSchemaCustomFields": 193,
"ConteggioClienti": 0,
"Nome": "AQC_Other materials",
"Descrizione": null
},
{
"IdSchemaCustomFields": 194,
"ConteggioClienti": 0,
"Nome": "Hermes ADM & Silk",
"Descrizione": "SCHEMA DA USARE SOLO PER LA DIVISIONE ADM & SILK\r\n\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 195,
"ConteggioClienti": 0,
"Nome": "Chloe'_APPAREL",
"Descrizione": "Schema aggiornato il 18\/12\/25 secondo modulo excel x codifica Elims\r\nGR"
},
{
"IdSchemaCustomFields": 196,
"ConteggioClienti": 0,
"Nome": "DUNHILL_DRAFT NON USARE",
"Descrizione": "SCHEMA DUNHILL NON USARE STUDIO NUOVA PROCEDURA NOVEMBRE 2025\r\n\r\n"
},
{
"IdSchemaCustomFields": 197,
"ConteggioClienti": 0,
"Nome": "BERLUTI - LVMH",
"Descrizione": "Creato per accettazione Elims\r\n"
},
{
"IdSchemaCustomFields": 198,
"ConteggioClienti": 0,
"Nome": "PIAGET (gruppo Cartier)",
"Descrizione": "Schema per tutti i campioni PIAGET (gruppo Cartier)\r\n\r\n"
},
{
"IdSchemaCustomFields": 199,
"ConteggioClienti": 0,
"Nome": "HERMES PAP FEMME",
"Descrizione": "Schema per tutti i campioni di HERMES PAP FEMME \r\n15\/01\/2025\r\n\r\n\r\n"
},
{
"IdSchemaCustomFields": 200,
"ConteggioClienti": 0,
"Nome": "CARHARTT ( provvisiorio in revisione)",
"Descrizione": "Schema per tutti i campioni CARHARTT provvissorio\r\n\r\n"
},
{
"IdSchemaCustomFields": 201,
"ConteggioClienti": 0,
"Nome": "ROSSIMODA",
"Descrizione": "Per tutti i campioni di ROSSIMODA\r\n\r\n"
}
]
}
+35
View File
@@ -0,0 +1,35 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$allowed = ['default_value', 'is_visible_import', 'is_required'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([':val' => $value, ':id' => $id]);
echo json_encode(['success' => (bool)$ok]);
+91
View File
@@ -0,0 +1,91 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Metodo non valido');
}
$photoId = isset($_POST['photo_id']) ? (int)$_POST['photo_id'] : 0;
$field = isset($_POST['field']) ? trim($_POST['field']) : '';
$value = isset($_POST['value']) ? (int)$_POST['value'] : 0;
if ($photoId <= 0) {
throw new Exception('photo_id mancante o non valido');
}
$allowedFields = ['StampaNelRapporto', 'PrimaPagina'];
if (!in_array($field, $allowedFields, true)) {
throw new Exception('Campo non consentito');
}
$value = $value === 1 ? 1 : 0;
// Recupera la foto per sapere a quale record padre appartiene
$stmt = $pdo->prepare("
SELECT id, iddatadb, idquotations
FROM datadb_photos
WHERE id = ?
LIMIT 1
");
$stmt->execute([$photoId]);
$photo = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$photo) {
throw new Exception('Foto non trovata');
}
$iddatadb = !empty($photo['iddatadb']) ? (int)$photo['iddatadb'] : null;
$idquotations = !empty($photo['idquotations']) ? (int)$photo['idquotations'] : null;
$pdo->beginTransaction();
if ($field === 'PrimaPagina' && $value === 1) {
// Solo una foto può essere PrimaPagina per lo stesso record padre
if ($iddatadb) {
$stmtReset = $pdo->prepare("
UPDATE datadb_photos
SET PrimaPagina = 0
WHERE iddatadb = ?
");
$stmtReset->execute([$iddatadb]);
} elseif ($idquotations) {
$stmtReset = $pdo->prepare("
UPDATE datadb_photos
SET PrimaPagina = 0
WHERE idquotations = ?
");
$stmtReset->execute([$idquotations]);
}
}
$stmtUpdate = $pdo->prepare("
UPDATE datadb_photos
SET {$field} = ?
WHERE id = ?
");
$stmtUpdate->execute([$value, $photoId]);
$pdo->commit();
echo json_encode([
'success' => true,
'photo_id' => $photoId,
'field' => $field,
'value' => $value
]);
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+57
View File
@@ -0,0 +1,57 @@
<?php
require_once 'include/headscript.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$template_id = isset($data['template_id']) ? (int)$data['template_id'] : 0;
$mapping_id = isset($data['mapping_id']) ? (int)$data['mapping_id'] : 0;
$value = isset($data['value']) ? (int)$data['value'] : null;
// Validate input
if ($template_id <= 0 || $mapping_id <= 0 || $value === null) {
echo json_encode(['success' => false, 'message' => 'Invalid input']);
exit;
}
// Normalize to 0/1
$value = ($value === 1) ? 1 : 0;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$pdo->beginTransaction();
if ($value === 1) {
// 1) Force ONLY ONE row visible_parts per template:
// set all to 0 for this template
$stmtReset = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = 0
WHERE template_id = ?
");
$stmtReset->execute([$template_id]);
}
// 2) Set requested mapping to 1 or 0
$stmtSet = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = ?
WHERE id = ? AND template_id = ?
");
$stmtSet->execute([$value, $mapping_id, $template_id]);
$pdo->commit();
echo json_encode([
'success' => true,
'template_id' => $template_id,
'mapping_id' => $mapping_id,
'value' => $value
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+120
View File
@@ -0,0 +1,120 @@
<?php
/**
* Validate rows before export to LIMS.
*
* Expects POST JSON: { rows: [ { iddatadb: int, index: int }, ... ] }
* Returns JSON:
* {
* success: true,
* results: {
* "<index>": {
* valid: bool,
* iddatadb: int,
* errors: [ { field: "<data-col>", message: "..." }, ... ]
* }
* }
* }
*
* Validators are closures registered in $validators[].
* Each receives ($iddatadb, $ctx) and returns an array of errors (or empty []).
* $ctx holds all prefetched data (record, parts, field values, mappings, etc.).
* To add a new rule just append another closure to $validators.
*/
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
header("Content-Type: application/json");
// ── Validators ──────────────────────────────────────────────────────────────
// Each validator is a closure: fn(int $iddatadb, array $ctx): array<{field,message}>
// $ctx keys: record, parts, fieldValues, requiredFixed, requiredMappings, fixedAliasMap
$validators = [];
// 1. Every part must have a valid Matrice
// Without Matrice the LIMS API rejects the Campione creation.
$validators[] = function (int $iddatadb, array $ctx): array {
$parts = $ctx['parts'];
if (empty($parts)) {
return [];
}
$missing = [];
foreach ($parts as $p) {
$matrice = (int)($p['idmatrice'] ?? 0);
if ($matrice <= 0) {
$label = $p['part_number'] ?: ($p['part_description'] ?: '(senza nome)');
$missing[] = $label;
}
}
if (!empty($missing)) {
return [[
'field' => 'parts',
'message' => 'Matrice mancante — il LIMS rifiuterà l\'esportazione. '
. 'Part senza matrice: ' . implode(', ', $missing)
. '. Aprire la sezione Parts e assegnare la matrice.',
]];
}
return [];
};
// ── Main ────────────────────────────────────────────────────────────────────
try {
$input = json_decode(file_get_contents('php://input'), true);
$rows = $input['rows'] ?? [];
if (empty($rows)) {
echo json_encode(['success' => false, 'message' => 'No rows provided']);
exit;
}
// ── Prefetch all data in bulk ───────────────────────────────────────────
$iddatadbList = array_column($rows, 'iddatadb');
$placeholders = implode(',', array_fill(0, count($iddatadbList), '?'));
// Parts (full rows, so validators can report which parts have issues)
$stmt = $pdo->prepare("
SELECT iddatadb, part_number, part_description, idmatrice
FROM identification_parts
WHERE iddatadb IN ($placeholders)
");
$stmt->execute($iddatadbList);
$partsInfo = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$partsInfo[(int)$r['iddatadb']][] = $r;
}
// ── Run validators per row ──────────────────────────────────────────────
$results = [];
foreach ($rows as $rowInfo) {
$iddatadb = (int)$rowInfo['iddatadb'];
$index = $rowInfo['index'];
// Build context for validators
$ctx = [
'parts' => $partsInfo[$iddatadb] ?? [],
];
$errors = [];
foreach ($validators as $validator) {
$errors = array_merge($errors, $validator($iddatadb, $ctx));
}
$results[$index] = [
'valid' => empty($errors),
'iddatadb' => $iddatadb,
'errors' => $errors,
];
}
echo json_encode(['success' => true, 'results' => $results]);
} catch (Exception $e) {
error_log("Validation error: " . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}