Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd6551e9c | |||
| 1b97bf4362 | |||
| c516589483 | |||
| 817bbadf22 | |||
| 1f27bc48d4 | |||
| c9fba48d88 | |||
| 70d4c0759e | |||
| 5d7880160a | |||
| bbe74d1529 | |||
| 93930227a2 | |||
| 2598a4c91b | |||
| 5e677a8b9c | |||
| 540c44d89a | |||
| 2ee9f2ecb1 | |||
| c9122774b1 | |||
| 1fed113c5c |
@@ -24,7 +24,17 @@ class VisualLimsApiClient
|
|||||||
public static function getInstance()
|
public static function getInstance()
|
||||||
{
|
{
|
||||||
if (self::$instance === null) {
|
if (self::$instance === null) {
|
||||||
self::$instance = new VisualLimsApiClient();
|
$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;
|
return self::$instance;
|
||||||
}
|
}
|
||||||
@@ -193,6 +203,54 @@ class VisualLimsApiClient
|
|||||||
return json_decode($response, true);
|
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()
|
public function getBaseUrl()
|
||||||
{
|
{
|
||||||
return $this->baseUrl;
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -312,3 +312,5 @@
|
|||||||
|
|
||||||
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-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-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"}
|
||||||
|
|||||||
+747
-213
@@ -1,239 +1,773 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
console.log("export_to_lims.js loaded");
|
console.log("export_to_lims.js loaded");
|
||||||
|
|
||||||
// Debug: verifica che i pulsanti siano trovati
|
|
||||||
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
const exportButtons = document.querySelectorAll(".export-lims-btn");
|
||||||
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
|
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
|
||||||
|
|
||||||
if (exportButtons.length === 0) {
|
// Tracks the active confirm handler so it can be replaced on re-open
|
||||||
console.warn("No .export-lims-btn buttons found in the DOM");
|
let pendingConfirmHandler = null;
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}` +
|
||||||
|
`<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) {
|
||||||
|
alert("Errore: Modale di conferma non trovato");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmModal = new bootstrap.Modal(confirmModalElement, {
|
||||||
|
keyboard: false,
|
||||||
|
});
|
||||||
|
document.getElementById("exportIddatadb").textContent = iddatadb;
|
||||||
|
confirmModal.show();
|
||||||
|
|
||||||
|
const confirmBtn = document.getElementById("exportConfirmBtn");
|
||||||
|
if (!confirmBtn) {
|
||||||
|
confirmModal.hide();
|
||||||
|
alert("Errore: Pulsante di conferma non trovato");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmHandler = async () => {
|
||||||
|
pendingConfirmHandler = null;
|
||||||
|
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
||||||
|
confirmModal.hide();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gridRow = btn.closest(".grid-row");
|
||||||
|
const data = await sendExport(iddatadb, gridRow);
|
||||||
|
console.log("Export response:", data);
|
||||||
|
showExportResult(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Export error:", error);
|
||||||
|
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) => {
|
exportButtons.forEach((btn) => {
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (batchRunning) return;
|
||||||
|
|
||||||
const rowIndex = btn.dataset.row;
|
const rowIndex = btn.dataset.row;
|
||||||
const iddatadb = btn.dataset.iddatadb;
|
const iddatadb = btn.dataset.iddatadb;
|
||||||
console.log(
|
console.log(
|
||||||
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
|
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mostra il modale di conferma
|
const gridRow = btn.closest(".grid-row");
|
||||||
const confirmModalElement =
|
|
||||||
document.getElementById("exportConfirmModal");
|
|
||||||
if (!confirmModalElement) {
|
|
||||||
console.error("exportConfirmModal not found in the DOM");
|
|
||||||
alert("Errore: Modale di conferma non trovato");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmModal = new bootstrap.Modal(confirmModalElement, {
|
if (gridRow && gridRow.querySelector(".cell-changed")) {
|
||||||
keyboard: false,
|
const unsavedModal = new bootstrap.Modal(
|
||||||
});
|
document.getElementById("exportUnsavedModal"),
|
||||||
document.getElementById("exportIddatadb").textContent = iddatadb;
|
{ keyboard: false },
|
||||||
confirmModal.show();
|
);
|
||||||
|
unsavedModal.show();
|
||||||
|
|
||||||
// Gestisci il click su "Conferma"
|
document.getElementById("saveAndExportBtn").addEventListener(
|
||||||
const confirmBtn = document.getElementById("exportConfirmBtn");
|
"click",
|
||||||
if (!confirmBtn) {
|
() => {
|
||||||
console.error("exportConfirmBtn not found in the DOM");
|
unsavedModal.hide();
|
||||||
confirmModal.hide();
|
const saveBtn = gridRow.querySelector(".save-btn");
|
||||||
alert("Errore: Pulsante di conferma non trovato");
|
if (!saveBtn) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmHandler = async () => {
|
const observer = new MutationObserver(() => {
|
||||||
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
|
if (!gridRow.querySelector(".cell-changed")) {
|
||||||
confirmModal.hide();
|
observer.disconnect();
|
||||||
|
startExportConfirmFlow(iddatadb, btn);
|
||||||
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();
|
|
||||||
|
|
||||||
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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Export error:", error);
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
document.getElementById(
|
|
||||||
"exportResponseMessage",
|
|
||||||
).textContent =
|
|
||||||
`Errore durante la generazione dei payload: ${error.message}`;
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
{ once: true },
|
observer.observe(gridRow, {
|
||||||
);
|
subtree: true,
|
||||||
}
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
|
||||||
// Rimuovi il listener dopo l'esecuzione
|
saveBtn.click();
|
||||||
confirmBtn.removeEventListener("click", confirmHandler);
|
},
|
||||||
};
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
// Rimuovi eventuali listener precedenti
|
return;
|
||||||
confirmBtn.removeEventListener("click", confirmHandler);
|
}
|
||||||
confirmBtn.addEventListener("click", confirmHandler);
|
|
||||||
|
// 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 =
|
||||||
|
"Tutte le righe sono già state esportate al LIMS.";
|
||||||
|
document.getElementById(
|
||||||
|
"exportResponseModalLabel",
|
||||||
|
).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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eligibleRows = collectEligibleRows();
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,9 +13,31 @@ if (!is_dir($logDir)) {
|
|||||||
mkdir($logDir, 0755, true);
|
mkdir($logDir, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
|
||||||
|
|
||||||
// 🔹 Base URL API
|
// 🔹 Base URL API
|
||||||
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
|
$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
|
// 🔹 Funzione per validare e convertire date
|
||||||
function validateDate($value)
|
function validateDate($value)
|
||||||
{
|
{
|
||||||
@@ -33,9 +55,21 @@ try {
|
|||||||
throw new Exception("Missing iddatadb");
|
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
|
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
|
||||||
|
// Also fetch fixed fields stored in datadb
|
||||||
$stmt = $pdo->prepare("
|
$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
|
FROM datadb as d
|
||||||
INNER JOIN excel_templates as et ON d.templateid = et.id
|
INNER JOIN excel_templates as et ON d.templateid = et.id
|
||||||
WHERE d.iddatadb = :iddatadb
|
WHERE d.iddatadb = :iddatadb
|
||||||
@@ -51,6 +85,14 @@ try {
|
|||||||
$clienteId = (int) $result['clienteId'];
|
$clienteId = (int) $result['clienteId'];
|
||||||
$schemaId = (int) $result['schemaId'];
|
$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)
|
// 🔹 STEP 3: Fetch Parts (including idmatrice)
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT part_number, part_description, material, color, mix, idmatrice
|
SELECT part_number, part_description, material, color, mix, idmatrice
|
||||||
@@ -60,6 +102,23 @@ try {
|
|||||||
$stmt->execute(['iddatadb' => $iddatadb]);
|
$stmt->execute(['iddatadb' => $iddatadb]);
|
||||||
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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
|
// 🔹 STEP 4: Fetch Field Values with Labels
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
@@ -93,12 +152,20 @@ try {
|
|||||||
// 🔹 Initialize API client
|
// 🔹 Initialize API client
|
||||||
$api = VisualLimsApiClient::getInstance();
|
$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 = [
|
$commessaWebPayload = [
|
||||||
"Cliente" => $clienteId,
|
"Cliente" => $clienteId,
|
||||||
"SchemaCustomField" => $schemaId,
|
"SchemaCustomField" => $schemaId,
|
||||||
"Richiedente" => "Test Web Import",
|
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
|
||||||
"Descrizione" => "TEST CommessaWeb",
|
"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
|
// Costruisci log curl-like per STEP 5
|
||||||
@@ -114,7 +181,7 @@ try {
|
|||||||
|
|
||||||
// Salva log
|
// Salva log
|
||||||
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
|
$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"];
|
$commessaId = $commessaWeb["IdCommessa"];
|
||||||
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
|
||||||
@@ -131,11 +198,13 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$campionePayload = [
|
$campionePayload = [
|
||||||
"Commessa" => $commessaId,
|
"Commessa" => $commessaId,
|
||||||
"Matrice" => $matriceId,
|
"Matrice" => $matriceId,
|
||||||
"SottoMatrice" => null,
|
"SottoMatrice" => null,
|
||||||
"SchemaCustomField" => $schemaId,
|
"SchemaCustomField" => $schemaId,
|
||||||
"NoteWeb" => $part["part_description"] ?? ""
|
"Riferimento" => $part["part_description"] ?? "",
|
||||||
|
"NoteWeb" => $part["part_description"] ?? "",
|
||||||
|
"ConsegnaRichiesta" => $consegnaRichiesta,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Costruisci curl-like per questo campione
|
// Costruisci curl-like per questo campione
|
||||||
@@ -160,20 +229,80 @@ try {
|
|||||||
|
|
||||||
// Salva log per STEP 6
|
// Salva log per STEP 6
|
||||||
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
|
$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
|
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
|
||||||
if (!empty($fieldValues)) {
|
if (!empty($fieldValues)) {
|
||||||
// GET con espansione per CustomField
|
// GET con espansione per CustomField
|
||||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||||
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
|
$commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}");
|
||||||
|
|
||||||
// Logga il GET
|
// Logga il GET
|
||||||
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
|
||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
|
||||||
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
|
$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
|
// Prepara payload PATCH
|
||||||
$commessaCustomFields = [];
|
$commessaCustomFields = [];
|
||||||
@@ -195,6 +324,7 @@ try {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$logFileStep7 = null;
|
||||||
if (!empty($commessaCustomFields)) {
|
if (!empty($commessaCustomFields)) {
|
||||||
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
|
||||||
|
|
||||||
@@ -209,7 +339,7 @@ try {
|
|||||||
|
|
||||||
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
|
||||||
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
|
||||||
file_put_contents($logFileStep7, $logContentStep7);
|
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,18 +356,34 @@ try {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
|
||||||
/*
|
|
||||||
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
|
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
|
||||||
|
|
||||||
// Logga il POST
|
// Logga il POST
|
||||||
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
|
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
|
||||||
"--header 'Content-Type: application/json' \\\n" .
|
"--header 'Content-Type: application/json' \\\n" .
|
||||||
"--header 'Authorization: Bearer ••••••' \\\n" .
|
"--header 'Authorization: Bearer ••••••' \\\n" .
|
||||||
"--data '{}'\n\n" .
|
"--data '{}'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
|
||||||
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
|
$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
|
// 🔹 STEP 10: GET di controllo post-PATCH
|
||||||
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
$expand = "CommesseCustomFields(\$expand=CustomField)";
|
||||||
@@ -248,7 +394,7 @@ try {
|
|||||||
"--header 'Authorization: Bearer ••••••'\n\n" .
|
"--header 'Authorization: Bearer ••••••'\n\n" .
|
||||||
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
|
||||||
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
|
$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
|
// 🔹 STEP 11: Prepare final response
|
||||||
$finalCommessa = [
|
$finalCommessa = [
|
||||||
@@ -262,17 +408,22 @@ try {
|
|||||||
];
|
];
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"commessaWeb" => $finalCommessa,
|
"idcommessaweb" => $commessaId,
|
||||||
|
"commessaweb" => $commessaWebCode,
|
||||||
|
"commessaWeb" => $finalCommessa,
|
||||||
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
|
||||||
"totalCampioni" => count($campioni),
|
"totalCampioni" => count($campioni),
|
||||||
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
|
||||||
"message" => "Export successful",
|
"totalPhotos" => count($photos),
|
||||||
"logFiles" => [
|
"message" => "Export successful",
|
||||||
"step5_create" => $logFileStep5,
|
"logFiles" => [
|
||||||
"step6_campioni" => $logFileStep6,
|
"step5_create" => $logFileStep5,
|
||||||
"step7_patch" => $logFileStep7,
|
"step5_2_photos" => $logFilePhotos,
|
||||||
"step10_get" => $logFileStep10
|
"step6_campioni" => $logFileStep6,
|
||||||
|
"step7_patch" => $logFileStep7 ?? null,
|
||||||
|
"step9_1_importa" => $logFileStep91,
|
||||||
|
"step10_get" => $logFileStep10
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@@ -282,10 +433,12 @@ try {
|
|||||||
"success" => false,
|
"success" => false,
|
||||||
"message" => "Export failed: " . $e->getMessage(),
|
"message" => "Export failed: " . $e->getMessage(),
|
||||||
"logFiles" => [
|
"logFiles" => [
|
||||||
"step5_create" => $logFileStep5 ?? null,
|
"step5_create" => $logFileStep5 ?? null,
|
||||||
"step6_campioni" => $logFileStep6 ?? null,
|
"step5_2_photos" => $logFilePhotos ?? null,
|
||||||
"step7_patch" => $logFileStep7 ?? null,
|
"step6_campioni" => $logFileStep6 ?? null,
|
||||||
"step10_get" => $logFileStep10 ?? null
|
"step7_patch" => $logFileStep7 ?? null,
|
||||||
|
"step9_1_importa" => $logFileStep91 ?? null,
|
||||||
|
"step10_get" => $logFileStep10 ?? null
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,24 @@ ini_set('display_errors', '0');
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
try {
|
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
|
// Parametri OData
|
||||||
$params = [
|
$params = [
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ if (!$field) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$api = VisualLimsApiClient::getInstance();
|
$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/';
|
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
|
||||||
|
|
||||||
$data = null;
|
$data = null;
|
||||||
@@ -44,9 +45,12 @@ try {
|
|||||||
case 'ClienteResponsabile':
|
case 'ClienteResponsabile':
|
||||||
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
|
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
|
||||||
if ($id_cliente <= 0) {
|
if ($id_cliente <= 0) {
|
||||||
http_response_code(400);
|
if (!$simulate) {
|
||||||
echo json_encode(['error' => 'Manca o invalido id_cliente']);
|
http_response_code(400);
|
||||||
exit;
|
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";
|
$endpoint = "Cliente($id_cliente)?\$expand=Responsabili";
|
||||||
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
|
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
|
||||||
@@ -62,13 +66,13 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opzionale: caching semplice (molto utile se i dati cambiano poco)
|
// Caching skipped in simulate mode to avoid polluting real-data cache files
|
||||||
if ($cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
|
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
|
||||||
$data = json_decode(file_get_contents($cache_file), true);
|
$data = json_decode(file_get_contents($cache_file), true);
|
||||||
} else {
|
} else {
|
||||||
$data = $api->get($endpoint, $options);
|
$data = $api->get($endpoint, $options);
|
||||||
|
|
||||||
if ($cache_file) {
|
if (!$simulate && $cache_file) {
|
||||||
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
|
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()]);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -192,6 +192,16 @@ foreach ($tempMap as $f) {
|
|||||||
$fixedFields[] = $f;
|
$fixedFields[] = $f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maps logical fixed_field_key → real datadb column name (mirrors JS fixedAliasMap)
|
||||||
|
$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',
|
||||||
|
];
|
||||||
|
|
||||||
// helper default (DATE can use 'today' if you already use it elsewhere)
|
// helper default (DATE can use 'today' if you already use it elsewhere)
|
||||||
function fixedDefaultValue(array $f): string
|
function fixedDefaultValue(array $f): string
|
||||||
{
|
{
|
||||||
@@ -337,6 +347,118 @@ function fixedDefaultValue(array $f): string
|
|||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-exporting {
|
||||||
|
background: linear-gradient(90deg, #fff0f0 0%, #ffe0e0 50%, #fff0f0 100%) !important;
|
||||||
|
background-size: 200% 100% !important;
|
||||||
|
animation: batch-pulse 1.5s ease-in-out infinite;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-exporting .button-cell {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes batch-pulse {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-row-spinner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #eb0b0b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-row-spinner i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-disabled .action-btn {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-row-error {
|
||||||
|
background: #fff3f3 !important;
|
||||||
|
border-left: 3px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.batch-row-error .button-cell {
|
||||||
|
background: #fff3f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-error-msg {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
padding: 2px 0;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-error-msg:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row.validation-row-error {
|
||||||
|
background: #fff3f3 !important;
|
||||||
|
border-left: 3px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cell.validation-error {
|
||||||
|
background-color: #f8d7da !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-validation-error,
|
||||||
|
.input-validation-error:focus {
|
||||||
|
border: 2px solid #dc3545 !important;
|
||||||
|
background-color: #fff5f5 !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25) !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-tooltip {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1050;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-tooltip::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-top-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cell.validation-error:hover .validation-tooltip {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-header,
|
.grid-header,
|
||||||
.grid-cell {
|
.grid-cell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -462,7 +584,10 @@ function fixedDefaultValue(array $f): string
|
|||||||
|
|
||||||
.grid-top .save-all-cell {
|
.grid-top .save-all-cell {
|
||||||
flex: 0 0 210px;
|
flex: 0 0 210px;
|
||||||
align-items: flex-start;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-top .grid-cell input,
|
.grid-top .grid-cell input,
|
||||||
@@ -623,27 +748,51 @@ function fixedDefaultValue(array $f): string
|
|||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-all-btn {
|
.actions-dropdown .dropdown-toggle {
|
||||||
background-color: #28a745;
|
background-color: #6c757d;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 8px 16px;
|
padding: 6px 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-all-btn:hover {
|
.actions-dropdown .dropdown-toggle:hover,
|
||||||
background-color: #218838;
|
.actions-dropdown .dropdown-toggle:focus {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-dropdown .dropdown-menu {
|
||||||
|
min-width: 160px;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.actions-dropdown .dropdown-item i {
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#exportConfirmModal,
|
#exportConfirmModal,
|
||||||
#exportResponseModal {
|
#exportResponseModal,
|
||||||
|
#exportUnsavedModal,
|
||||||
|
#exportBatchConfirmModal,
|
||||||
|
#exportBatchUnsavedModal,
|
||||||
|
#saveAllConfirmModal,
|
||||||
|
#saveAllResultModal {
|
||||||
z-index: 1300 !important;
|
z-index: 1300 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#exportConfirmModal .modal-backdrop,
|
#exportConfirmModal .modal-backdrop,
|
||||||
#exportResponseModal .modal-backdrop {
|
#exportResponseModal .modal-backdrop,
|
||||||
|
#exportUnsavedModal .modal-backdrop,
|
||||||
|
#exportBatchConfirmModal .modal-backdrop,
|
||||||
|
#exportBatchUnsavedModal .modal-backdrop,
|
||||||
|
#saveAllConfirmModal .modal-backdrop,
|
||||||
|
#saveAllResultModal .modal-backdrop {
|
||||||
z-index: 1299 !important;
|
z-index: 1299 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,21 +882,36 @@ function fixedDefaultValue(array $f): string
|
|||||||
</div>
|
</div>
|
||||||
<div class="card radius-10">
|
<div class="card radius-10">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
|
||||||
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; margin:10px 0;">
|
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
|
||||||
⚠️ Unsaved changes detected! Please save before leaving this page.<br>
|
<button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-cogs"></i> Actions
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
||||||
|
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
|
||||||
|
⚠️ Unsaved changes detected!
|
||||||
<span id="changedRows" style="font-weight:normal; color:darkred;"></span>
|
<span id="changedRows" style="font-weight:normal; color:darkred;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="batchExportBar" style="display:none; flex: 1; min-width: 0;">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-spinner fa-spin" style="color: #eb0b0b;"></i>
|
||||||
|
<span id="batchExportStatus" style="font-size: 13px; font-weight: 500;">Esportazione...</span>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" id="exportBatchCancelBtn" style="padding: 2px 10px; font-size: 12px;">Annulla</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="editForm">
|
<form id="editForm">
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-top">
|
<div class="grid-top">
|
||||||
<div class="grid-cell save-all-cell">
|
<div class="grid-cell save-all-cell"></div>
|
||||||
<button type="button" class="save-all-btn" title="Save All Rows"><i class="fas fa-save"></i> Save All</button>
|
|
||||||
</div>
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
$topColIndex = 1;
|
$topColIndex = 1;
|
||||||
@@ -1013,7 +1177,8 @@ function fixedDefaultValue(array $f): string
|
|||||||
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
|
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
|
||||||
<!-- commented only for admin roles -->
|
<!-- commented only for admin roles -->
|
||||||
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
|
||||||
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Export to LIMS" style="background: #eb0b0bff; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-upload"></i></button>
|
<?php $isExported = (($row['status'] ?? '') === 'l'); ?>
|
||||||
|
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="<?= $isExported ? 'Già esportato' : 'Export to LIMS' ?>" style="background: <?= $isExported ? '#ccc' : '#eb0b0b' ?>; color: white; border: none; border-radius: 5px; cursor: <?= $isExported ? 'not-allowed' : 'pointer' ?>; <?= $isExported ? 'opacity: 0.5;' : '' ?>"<?= $isExported ? ' disabled' : '' ?>><i class="fas fa-upload"></i></button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<button type="button" class="save-btn action-btn" data-row="<?= $index ?>" title="Save" style="background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-save"></i></button>
|
<button type="button" class="save-btn action-btn" data-row="<?= $index ?>" title="Save" style="background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-save"></i></button>
|
||||||
@@ -1055,6 +1220,9 @@ function fixedDefaultValue(array $f): string
|
|||||||
<span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
<span class="status-badge status-<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
||||||
<?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
|
<?= htmlspecialchars($row['status'] === 'i' ? 'Imported' : ($row['status'] === 'P' ? 'In Progress' : 'To LIMS')) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php if (!empty($row['commessaweb'])): ?>
|
||||||
|
<span class="commessaweb-code" style="display:block; font-size:0.75em; color:#555; margin-top:2px;" title="CommessaWeb"><?= htmlspecialchars($row['commessaweb']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
<input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
<input type="hidden" name="rows[<?= $index ?>][status]" value="<?= htmlspecialchars($row['status'] ?? 'i') ?>">
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
@@ -1159,7 +1327,8 @@ function fixedDefaultValue(array $f): string
|
|||||||
if (!empty($fixedFields)) {
|
if (!empty($fixedFields)) {
|
||||||
foreach ($fixedFields as $f) {
|
foreach ($fixedFields as $f) {
|
||||||
$key = $f['fixed_field_key'];
|
$key = $f['fixed_field_key'];
|
||||||
$val = $row[$key] ?? '';
|
$dbCol = $fixedAliasMap[$key] ?? $key;
|
||||||
|
$val = $row[$dbCol] ?? '';
|
||||||
if ($val === '' || $val === null) {
|
if ($val === '' || $val === null) {
|
||||||
$val = fixedDefaultValue($f);
|
$val = fixedDefaultValue($f);
|
||||||
}
|
}
|
||||||
@@ -1290,32 +1459,6 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("click", ".export-lims-btn", function() {
|
|
||||||
let rowId = $(this).data("row");
|
|
||||||
let idDataDb = $(this).data("iddatadb");
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "export_to_lims.php",
|
|
||||||
method: "POST",
|
|
||||||
data: {
|
|
||||||
iddatadb: idDataDb
|
|
||||||
},
|
|
||||||
dataType: "json",
|
|
||||||
beforeSend: function() {
|
|
||||||
alert("Export started in background for row " + rowId);
|
|
||||||
},
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
alert(response.message);
|
|
||||||
} else {
|
|
||||||
alert("❌ Error: " + response.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
alert("❌ AJAX error: " + error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const inputs = document.querySelectorAll(".cell-input, .dropdown-select, .carrier-select, .awb-input, .date-picker");
|
const inputs = document.querySelectorAll(".cell-input, .dropdown-select, .carrier-select, .awb-input, .date-picker");
|
||||||
@@ -1345,6 +1488,7 @@ function fixedDefaultValue(array $f): string
|
|||||||
|
|
||||||
inputs.forEach(el => {
|
inputs.forEach(el => {
|
||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
|
if (el.hasAttribute('data-restoring')) return;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
|
|
||||||
const gridCell = el.closest(".grid-cell");
|
const gridCell = el.closest(".grid-cell");
|
||||||
@@ -1358,6 +1502,19 @@ function fixedDefaultValue(array $f): string
|
|||||||
// (se vuoi mantenere highlight cella gialla, lascia questa riga)
|
// (se vuoi mantenere highlight cella gialla, lascia questa riga)
|
||||||
if (gridCell) gridCell.classList.add("cell-changed");
|
if (gridCell) gridCell.classList.add("cell-changed");
|
||||||
|
|
||||||
|
// Clear validation error on this field when user edits it
|
||||||
|
el.classList.remove("input-validation-error");
|
||||||
|
if (gridCell) {
|
||||||
|
gridCell.classList.remove("validation-error");
|
||||||
|
const tooltip = gridCell.querySelector(".validation-tooltip");
|
||||||
|
if (tooltip) tooltip.remove();
|
||||||
|
// If no more validation errors on the row, remove row highlight
|
||||||
|
const gridRow = gridCell.closest(".grid-row");
|
||||||
|
if (gridRow && !gridRow.querySelector(".validation-error")) {
|
||||||
|
gridRow.classList.remove("validation-row-error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderChangedRows();
|
renderChangedRows();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1443,7 +1600,8 @@ function fixedDefaultValue(array $f): string
|
|||||||
hasChanges = changedRows.size > 0;
|
hasChanges = changedRows.size > 0;
|
||||||
|
|
||||||
|
|
||||||
alert('Salvataggio riga avvenuto con successo!');
|
const toastEl = document.getElementById('saveSuccessToast');
|
||||||
|
if (toastEl) bootstrap.Toast.getOrCreateInstance(toastEl).show();
|
||||||
} else {
|
} else {
|
||||||
alert('Errore durante il salvataggio: ' + data.message);
|
alert('Errore durante il salvataggio: ' + data.message);
|
||||||
}
|
}
|
||||||
@@ -1454,21 +1612,65 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('.save-all-btn').addEventListener('click', async () => {
|
let saveAllRunning = false;
|
||||||
const rows = document.querySelectorAll('.grid-row');
|
document.querySelector('.save-all-btn').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (saveAllRunning || window.batchRunning) return;
|
||||||
|
const confirmModal = new bootstrap.Modal(document.getElementById('saveAllConfirmModal'), { keyboard: false });
|
||||||
|
confirmModal.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('saveAllConfirmBtn').addEventListener('click', async () => {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal')).hide();
|
||||||
|
saveAllRunning = true;
|
||||||
|
|
||||||
|
// Clear previous row errors
|
||||||
|
document.querySelectorAll('.grid-row.batch-row-error').forEach(r => {
|
||||||
|
r.classList.remove('batch-row-error');
|
||||||
|
const msg = r.querySelector('.batch-error-msg');
|
||||||
|
if (msg) msg.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable all row buttons
|
||||||
|
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.add('batch-disabled'));
|
||||||
|
const toggle = document.querySelector('.actions-dropdown .dropdown-toggle');
|
||||||
|
if (toggle) { toggle.disabled = true; toggle.style.opacity = '0.5'; toggle.style.pointerEvents = 'none'; }
|
||||||
|
|
||||||
|
// Show status bar
|
||||||
|
const bar = document.getElementById('batchExportBar');
|
||||||
|
const statusEl = document.getElementById('batchExportStatus');
|
||||||
|
const cancelBtn = document.getElementById('exportBatchCancelBtn');
|
||||||
|
bar.style.display = '';
|
||||||
|
cancelBtn.style.display = 'none';
|
||||||
|
|
||||||
|
const rows = document.querySelectorAll('.grid-row[data-id]');
|
||||||
|
const total = rows.length;
|
||||||
|
let processed = 0;
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorMessages = [];
|
let errorMessages = [];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const saveBtn = row.querySelector('.save-btn');
|
const saveBtn = row.querySelector('.save-btn');
|
||||||
if (!saveBtn) {
|
if (!saveBtn) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const rowIndex = saveBtn.dataset.row;
|
const rowIndex = saveBtn.dataset.row;
|
||||||
const iddatadb = row.getAttribute('data-id');
|
const iddatadb = row.getAttribute('data-id');
|
||||||
if (!rowIndex || !iddatadb) {
|
if (!rowIndex || !iddatadb) { continue; }
|
||||||
continue;
|
|
||||||
|
processed++;
|
||||||
|
statusEl.textContent = `Salvataggio ${processed} / ${total} (id: ${iddatadb})...`;
|
||||||
|
|
||||||
|
// Highlight current row
|
||||||
|
row.classList.remove('batch-disabled');
|
||||||
|
row.classList.add('batch-exporting');
|
||||||
|
const btnCell = row.querySelector('.button-cell');
|
||||||
|
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> Saving...';
|
||||||
|
btnCell.appendChild(spinner);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
|
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
|
||||||
@@ -1487,12 +1689,11 @@ function fixedDefaultValue(array $f): string
|
|||||||
if (idclientSelect) {
|
if (idclientSelect) {
|
||||||
formData.append('idclient', idclientSelect.value);
|
formData.append('idclient', idclientSelect.value);
|
||||||
}
|
}
|
||||||
// ---- FIXED FIELDS (NEW) ----
|
// ---- FIXED FIELDS ----
|
||||||
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
|
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
|
||||||
fixedInputs.forEach(inp => {
|
fixedInputs.forEach(inp => {
|
||||||
if (!inp.classList.contains('fixed-input')) return;
|
if (!inp.classList.contains('fixed-input')) return;
|
||||||
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
|
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
|
||||||
// Map: fixed key (logical) -> datadb real column
|
|
||||||
const fixedAliasMap = {
|
const fixedAliasMap = {
|
||||||
ClienteResponsabile: 'cliente_responsabile_id',
|
ClienteResponsabile: 'cliente_responsabile_id',
|
||||||
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
|
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
|
||||||
@@ -1500,13 +1701,10 @@ function fixedDefaultValue(array $f): string
|
|||||||
AnagraficaCertestService: 'anagrafica_certest_service_id',
|
AnagraficaCertestService: 'anagrafica_certest_service_id',
|
||||||
ConsegnaRichiesta: 'consegna_richiesta'
|
ConsegnaRichiesta: 'consegna_richiesta'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (m && m[1]) {
|
if (m && m[1]) {
|
||||||
const logicalKey = m[1];
|
const realKey = fixedAliasMap[m[1]] || m[1];
|
||||||
const realKey = fixedAliasMap[logicalKey] || logicalKey;
|
|
||||||
formData.append(realKey, inp.value);
|
formData.append(realKey, inp.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
formData.append('iddatadb', iddatadb);
|
formData.append('iddatadb', iddatadb);
|
||||||
@@ -1532,24 +1730,59 @@ function fixedDefaultValue(array $f): string
|
|||||||
changedRows.delete(rowIndex);
|
changedRows.delete(rowIndex);
|
||||||
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
|
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
|
||||||
.forEach(cell => cell.classList.remove("cell-changed"));
|
.forEach(cell => cell.classList.remove("cell-changed"));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${data.message}`);
|
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${data.message}`);
|
||||||
|
// Show error on the row
|
||||||
|
row.classList.add('batch-row-error');
|
||||||
|
if (btnCell) {
|
||||||
|
const errEl = document.createElement('div');
|
||||||
|
errEl.className = 'batch-error-msg';
|
||||||
|
errEl.textContent = '⚠ ' + (data.message || 'Errore');
|
||||||
|
btnCell.appendChild(errEl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${error.message}`);
|
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${error.message}`);
|
||||||
|
row.classList.add('batch-row-error');
|
||||||
|
if (btnCell) {
|
||||||
|
const errEl = document.createElement('div');
|
||||||
|
errEl.className = 'batch-error-msg';
|
||||||
|
errEl.textContent = '⚠ ' + error.message;
|
||||||
|
btnCell.appendChild(errEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove highlight from current row
|
||||||
|
row.classList.remove('batch-exporting');
|
||||||
|
row.classList.add('batch-disabled');
|
||||||
|
if (btnCell) {
|
||||||
|
const sp = btnCell.querySelector('.batch-row-spinner');
|
||||||
|
if (sp) sp.remove();
|
||||||
|
btnCell.querySelectorAll('.action-btn').forEach(b => { b.style.display = b.dataset.prevDisplay || ''; delete b.dataset.prevDisplay; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finished
|
||||||
renderChangedRows();
|
renderChangedRows();
|
||||||
hasChanges = changedRows.size > 0;
|
hasChanges = changedRows.size > 0;
|
||||||
|
saveAllRunning = false;
|
||||||
|
bar.style.display = 'none';
|
||||||
|
cancelBtn.style.display = '';
|
||||||
|
|
||||||
|
// Re-enable all row buttons
|
||||||
|
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.remove('batch-disabled'));
|
||||||
|
if (toggle) { toggle.disabled = false; toggle.style.opacity = ''; toggle.style.pointerEvents = ''; }
|
||||||
|
|
||||||
|
const resultEl = document.getElementById('saveAllResultMessage');
|
||||||
|
const resultLabel = document.getElementById('saveAllResultModalLabel');
|
||||||
if (errorMessages.length === 0) {
|
if (errorMessages.length === 0) {
|
||||||
alert(`Tutte le ${successCount} righe salvate con successo!`);
|
resultLabel.textContent = 'Salvataggio Completato';
|
||||||
|
resultEl.innerHTML = `<i class="fas fa-check-circle" style="color:#28a745;"></i> Tutte le <strong>${successCount}</strong> righe salvate con successo.`;
|
||||||
} else {
|
} else {
|
||||||
alert(`Salvate ${successCount} righe con successo.\nErrori:\n${errorMessages.join('\n')}`);
|
resultLabel.textContent = 'Salvataggio con Errori';
|
||||||
|
resultEl.innerHTML = `<i class="fas fa-exclamation-triangle" style="color:#ffc107;"></i> Salvate <strong>${successCount}</strong> righe con successo.<br><br><strong>Errori:</strong><br>` + errorMessages.join('<br>');
|
||||||
}
|
}
|
||||||
|
new bootstrap.Modal(document.getElementById('saveAllResultModal'), { keyboard: false }).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("beforeunload", function(e) {
|
window.addEventListener("beforeunload", function(e) {
|
||||||
@@ -1561,7 +1794,7 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Gestisci la chiusura dei modali per rimuovere i backdrop
|
// Gestisci la chiusura dei modali per rimuovere i backdrop
|
||||||
document.querySelectorAll('#exportConfirmModal, #exportResponseModal').forEach(modal => {
|
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal, #exportBatchConfirmModal, #exportBatchUnsavedModal, #saveAllConfirmModal, #saveAllResultModal').forEach(modal => {
|
||||||
modal.addEventListener('hidden.bs.modal', () => {
|
modal.addEventListener('hidden.bs.modal', () => {
|
||||||
// Rimuovi tutti i backdrop residui
|
// Rimuovi tutti i backdrop residui
|
||||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||||
@@ -1611,7 +1844,11 @@ function fixedDefaultValue(array $f): string
|
|||||||
clientData.forEach(client => {
|
clientData.forEach(client => {
|
||||||
const nome = client.Nominativo || "Nome non disponibile";
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
const id = client.IdCliente || "ID non disponibile";
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
|
const codiceCliente = (client.CodiceCliente || '').toString().trim();
|
||||||
|
const suffix = (codiceCliente.split('_')[1] || '').trim();
|
||||||
|
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
|
||||||
|
|
||||||
|
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
|
||||||
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
|
if (parseInt(id) === parseInt(<?php echo json_encode($default_idclient ?? 0); ?>)) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
}
|
}
|
||||||
@@ -1647,7 +1884,11 @@ function fixedDefaultValue(array $f): string
|
|||||||
clientData.forEach(client => {
|
clientData.forEach(client => {
|
||||||
const nome = client.Nominativo || "Nome non disponibile";
|
const nome = client.Nominativo || "Nome non disponibile";
|
||||||
const id = client.IdCliente || "ID non disponibile";
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
const option = new Option(`${nome.trim()} - ${client.CodiceNazioneFatturazione} (ID: ${id})`, id);
|
const codiceCliente = (client.CodiceCliente || '').toString().trim();
|
||||||
|
const suffix = (codiceCliente.split('_')[1] || '').trim();
|
||||||
|
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : '--');
|
||||||
|
|
||||||
|
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
|
||||||
if (String(id) === String(currentValue)) {
|
if (String(id) === String(currentValue)) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
}
|
}
|
||||||
@@ -1657,10 +1898,12 @@ function fixedDefaultValue(array $f): string
|
|||||||
// Ripristina il valore corrente
|
// Ripristina il valore corrente
|
||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
dropdown.value = currentValue;
|
dropdown.value = currentValue;
|
||||||
|
dropdown.setAttribute('data-restoring', '');
|
||||||
const event = new Event('change', {
|
const event = new Event('change', {
|
||||||
bubbles: true
|
bubbles: true
|
||||||
});
|
});
|
||||||
dropdown.dispatchEvent(event);
|
dropdown.dispatchEvent(event);
|
||||||
|
dropdown.removeAttribute('data-restoring');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1969,7 +2212,9 @@ function fixedDefaultValue(array $f): string
|
|||||||
s2container.after(dropdown);
|
s2container.after(dropdown);
|
||||||
}
|
}
|
||||||
if (valToSelect) {
|
if (valToSelect) {
|
||||||
|
dropdown.setAttribute('data-restoring', '');
|
||||||
$(dropdown).val(valToSelect).trigger('change');
|
$(dropdown).val(valToSelect).trigger('change');
|
||||||
|
dropdown.removeAttribute('data-restoring');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Select nativa
|
// Select nativa
|
||||||
@@ -1988,9 +2233,11 @@ function fixedDefaultValue(array $f): string
|
|||||||
});
|
});
|
||||||
if (valToSelect) {
|
if (valToSelect) {
|
||||||
dropdown.value = valToSelect;
|
dropdown.value = valToSelect;
|
||||||
|
dropdown.setAttribute('data-restoring', '');
|
||||||
dropdown.dispatchEvent(new Event('change', {
|
dropdown.dispatchEvent(new Event('change', {
|
||||||
bubbles: true
|
bubbles: true
|
||||||
}));
|
}));
|
||||||
|
dropdown.removeAttribute('data-restoring');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2031,7 +2278,7 @@ function fixedDefaultValue(array $f): string
|
|||||||
const id = client.IdCliente || "ID non disponibile";
|
const id = client.IdCliente || "ID non disponibile";
|
||||||
|
|
||||||
// CodiceCliente es: "Bl01858_E" -> vogliamo "E"
|
// CodiceCliente es: "Bl01858_E" -> vogliamo "E"
|
||||||
const codiceCliente = (client.CodiceCliente || '').toString().trim();
|
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? '').toString().trim();
|
||||||
const suffix = (codiceCliente.split('_')[1] || '').trim(); // parte dopo "_"
|
const suffix = (codiceCliente.split('_')[1] || '').trim(); // parte dopo "_"
|
||||||
const shortCode = suffix || '--';
|
const shortCode = suffix || '--';
|
||||||
|
|
||||||
@@ -2083,10 +2330,12 @@ function fixedDefaultValue(array $f): string
|
|||||||
// Ripristina il valore corrente
|
// Ripristina il valore corrente
|
||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
dropdown.value = currentValue;
|
dropdown.value = currentValue;
|
||||||
|
dropdown.setAttribute('data-restoring', '');
|
||||||
const event = new Event('change', {
|
const event = new Event('change', {
|
||||||
bubbles: true
|
bubbles: true
|
||||||
});
|
});
|
||||||
dropdown.dispatchEvent(event);
|
dropdown.dispatchEvent(event);
|
||||||
|
dropdown.removeAttribute('data-restoring');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2528,7 +2777,9 @@ function fixedDefaultValue(array $f): string
|
|||||||
if (currentVal) {
|
if (currentVal) {
|
||||||
const found = results.find(r => String(r.id) === String(currentVal));
|
const found = results.find(r => String(r.id) === String(currentVal));
|
||||||
if (found) {
|
if (found) {
|
||||||
|
$select[0].setAttribute('data-restoring', '');
|
||||||
$select.val(currentVal).trigger('change');
|
$select.val(currentVal).trigger('change');
|
||||||
|
$select[0].removeAttribute('data-restoring');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2715,6 +2966,111 @@ function fixedDefaultValue(array $f): string
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: unsaved changes before export -->
|
||||||
|
<div class="modal fade" id="exportUnsavedModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Modifiche non salvate</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
La riga contiene modifiche non salvate. Salvare prima di procedere con l'esportazione?
|
||||||
|
</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="saveAndExportBtn">Salva ed Esporta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: batch export confirm -->
|
||||||
|
<div class="modal fade" id="exportBatchConfirmModal" tabindex="-1" aria-labelledby="exportBatchConfirmModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exportBatchConfirmModalLabel">Conferma Export All</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Verranno esportate <strong><span id="exportBatchCount"></span></strong> righe al LIMS, una alla volta dall'alto verso il basso.<br>
|
||||||
|
Potrai annullare il batch in qualsiasi momento.
|
||||||
|
</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-danger btn-sm" id="exportBatchConfirmBtn"><i class="fas fa-upload"></i> Avvia Export</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: batch export unsaved changes -->
|
||||||
|
<div class="modal fade" id="exportBatchUnsavedModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Modifiche non salvate</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Ci sono modifiche non salvate. Salvare tutte le righe prima di procedere con l'esportazione?
|
||||||
|
</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="batchSaveAndExportBtn"><i class="fas fa-save"></i> Salva ed Esporta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Save All confirm -->
|
||||||
|
<div class="modal fade" id="saveAllConfirmModal" tabindex="-1" aria-labelledby="saveAllConfirmModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="saveAllConfirmModalLabel">Conferma Salvataggio</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Confermi il salvataggio di tutte le righe?
|
||||||
|
</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-success btn-sm" id="saveAllConfirmBtn"><i class="fas fa-save"></i> Conferma</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal: Save All result -->
|
||||||
|
<div class="modal fade" id="saveAllResultModal" tabindex="-1" aria-labelledby="saveAllResultModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="saveAllResultModalLabel">Risultato Salvataggio</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="saveAllResultMessage"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">Chiudi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast: row saved successfully -->
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
||||||
|
<div id="saveSuccessToast" class="toast align-items-center text-bg-success border-0" role="alert" data-bs-delay="2500">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body"><i class="fas fa-check-circle me-2"></i>Riga salvata con successo.</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -313,54 +313,64 @@ error_log("Loaded template: " . print_r($template, true));
|
|||||||
function showTable(data) {
|
function showTable(data) {
|
||||||
console.log('Mostro tabella con dati:', data);
|
console.log('Mostro tabella con dati:', data);
|
||||||
let html = `
|
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="template_id" value="${data.template_id}">
|
||||||
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
|
<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="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="excelrows" value="${encodeURIComponent(JSON.stringify(data.excel_data.map(r => r.excelrow)))}">
|
||||||
|
<input type="hidden" name="filename" value="${data.filename}">
|
||||||
|
|
||||||
<input type="hidden" name="filename" value="${data.filename}">
|
<!-- 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">
|
<div class="table-container">
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
<th><input type="checkbox" id="selectAll"> Seleziona</th>
|
||||||
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
|
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="column-filters">
|
<tr class="column-filters">
|
||||||
<th></th>
|
<th></th>
|
||||||
${data.columns.map((col, i) => `
|
${data.columns.map((col, i) => `
|
||||||
<th>
|
<th>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control form-control-sm column-filter"
|
class="form-control form-control-sm column-filter"
|
||||||
data-col-index="${i}"
|
data-col-index="${i}"
|
||||||
placeholder="Filter...">
|
placeholder="Filter...">
|
||||||
</th>
|
</th>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${data.excel_data.map((row, index) => `
|
${data.excel_data.map((row, index) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
|
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
|
||||||
${row.data.map(cell => `<td>${cell}</td>`).join('')}
|
${row.data.map(cell => `<td>${cell}</td>`).join('')}
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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;
|
tableContainer.innerHTML = html;
|
||||||
|
|
||||||
const proceedButton = document.getElementById('proceedButton');
|
const proceedButtonTop = document.getElementById('proceedButtonTop');
|
||||||
|
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
|
||||||
const selectAllCheckbox = document.getElementById('selectAll');
|
const selectAllCheckbox = document.getElementById('selectAll');
|
||||||
const checkboxes = document.querySelectorAll('.row-checkbox');
|
const checkboxes = document.querySelectorAll('.row-checkbox');
|
||||||
|
|
||||||
function updateProceedButton() {
|
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() {
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
|
|||||||
@@ -14,11 +14,57 @@ if (!$iddatadb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
||||||
$stmt->execute([':iddatadb' => $iddatadb]);
|
$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);
|
$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) {
|
} catch (PDOException $e) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
|
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $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"
|
||||||
|
}
|
||||||
@@ -96,12 +96,66 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
|||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#schemaFieldsTable th:first-child,
|
/* Force predictable column sizing */
|
||||||
#schemaFieldsTable td:first-child,
|
/* Stable sizing */
|
||||||
|
#schemaFieldsTable {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox columns = minimal */
|
||||||
|
#schemaFieldsTable th:nth-child(1),
|
||||||
|
#schemaFieldsTable td:nth-child(1),
|
||||||
#schemaFieldsTable th:nth-child(2),
|
#schemaFieldsTable th:nth-child(2),
|
||||||
#schemaFieldsTable td:nth-child(2) {
|
#schemaFieldsTable td:nth-child(2),
|
||||||
width: 100px;
|
#schemaFieldsTable th:nth-child(3),
|
||||||
|
#schemaFieldsTable td:nth-child(3) {
|
||||||
|
width: 48px;
|
||||||
|
/* 🔥 più stretto */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
/* 🔥 meno padding */
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make checkbox itself not oversized */
|
||||||
|
#schemaFieldsTable td:nth-child(1) input[type="checkbox"],
|
||||||
|
#schemaFieldsTable td:nth-child(2) input[type="checkbox"],
|
||||||
|
#schemaFieldsTable td:nth-child(3) input[type="checkbox"] {
|
||||||
|
transform: scale(0.95);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title */
|
||||||
|
#schemaFieldsTable th:nth-child(4),
|
||||||
|
#schemaFieldsTable td:nth-child(4) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type */
|
||||||
|
#schemaFieldsTable th:nth-child(5),
|
||||||
|
#schemaFieldsTable td:nth-child(5) {
|
||||||
|
width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mapping = wide but NOT insane */
|
||||||
|
#schemaFieldsTable th:nth-child(6),
|
||||||
|
#schemaFieldsTable td:nth-child(6) {
|
||||||
|
width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default Value = wider */
|
||||||
|
#schemaFieldsTable th:nth-child(7),
|
||||||
|
#schemaFieldsTable td:nth-child(7) {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* selects fill the cell */
|
||||||
|
#schemaFieldsTable td:nth-child(6) .form-select,
|
||||||
|
#schemaFieldsTable td:nth-child(7) .form-control,
|
||||||
|
#schemaFieldsTable td:nth-child(7) .form-select {
|
||||||
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fix bootstrap/select2 width */
|
/* fix bootstrap/select2 width */
|
||||||
@@ -165,11 +219,11 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
|||||||
<table id="schemaFieldsTable" class="table table-striped">
|
<table id="schemaFieldsTable" class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:90px; text-align:center;">Main</th>
|
<th style="width:45px; text-align:center;">Main</th>
|
||||||
<th style="width:110px; text-align:center;">Import</th>
|
<th style="width:45px; text-align:center;">Import</th>
|
||||||
<th style="width:110px; text-align:center;">Parts</th>
|
<th style="width:45px; text-align:center;">Parts</th>
|
||||||
<th style="width:320px;">Title</th>
|
<th style="width:320px;">Title</th>
|
||||||
<th style="width:140px;">Type</th>
|
<th style="width:120px;">Type</th>
|
||||||
<th>Mapping</th>
|
<th>Mapping</th>
|
||||||
<th>Default Value</th>
|
<th>Default Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -913,12 +967,22 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
|||||||
cb.checked = cb.dataset.originalChecked === 'true';
|
cb.checked = cb.dataset.originalChecked === 'true';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (event.target.classList.contains('visible-import-checkbox')) {
|
} else if (event.target.classList.contains('visible-parts-checkbox')) {
|
||||||
const checkbox = event.target;
|
const checkbox = event.target;
|
||||||
const mappingId = checkbox.dataset.mappingId;
|
const mappingId = checkbox.dataset.mappingId;
|
||||||
const value = checkbox.checked ? 1 : 0;
|
const value = checkbox.checked ? 1 : 0;
|
||||||
|
|
||||||
fetch('update_visible_import.php', {
|
// salva stato per rollback
|
||||||
|
const prevChecked = checkbox.checked;
|
||||||
|
|
||||||
|
// ✅ UI: se sto mettendo a 1, tolgo la spunta a tutti gli altri SUBITO
|
||||||
|
if (value === 1) {
|
||||||
|
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
|
||||||
|
if (cb !== checkbox) cb.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('update_visible_parts.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -929,16 +993,25 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
|
|||||||
value: value
|
value: value
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
console.error("❌ Error updating is_visible_import:", data.message);
|
console.error("❌ Error updating is_visible_parts:", data.message);
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
|
// rollback UI
|
||||||
|
checkbox.checked = !prevChecked;
|
||||||
|
|
||||||
|
// se avevo tolto le spunte agli altri, ricarico per riallineare la UI al DB
|
||||||
|
// (semplice e safe)
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("❌ Fetch error:", error);
|
console.error("❌ Fetch error:", error);
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
|
// rollback UI
|
||||||
|
checkbox.checked = !prevChecked;
|
||||||
|
location.reload();
|
||||||
});
|
});
|
||||||
} else if (event.target.classList.contains('visible-parts-checkbox')) {
|
} else if (event.target.classList.contains('visible-parts-checkbox')) {
|
||||||
const checkbox = event.target;
|
const checkbox = event.target;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="modal fade" id="partsModal" tabindex="-1" aria-labelledby="partsModalLabel" aria-hidden="true">
|
<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-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
|
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button>
|
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button>
|
||||||
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button>
|
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button>
|
||||||
<button type="button" class="btn btn-info btn-sm ms-2 d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Add Quotation</button>
|
<button type="button" class="btn btn-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;">
|
<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-eye-slash" style="font-size: 0.8rem;"></i>
|
||||||
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
|
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<table class="table table-striped table-sm" id="partsTable">
|
<table class="table table-striped table-sm" id="partsTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 80px;">Numero</th>
|
<th style="width: 55px;">Num</th>
|
||||||
<th>Descrizione</th>
|
<th>Descrizione</th>
|
||||||
<th style="width: 200px;">Matrice</th>
|
<th style="width: 200px;">Matrice</th>
|
||||||
<th style="width: 150px;">
|
<th style="width: 150px;">
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="partsTableBody">
|
<tbody id="partsTableBody">
|
||||||
<tr data-part-id="new">
|
<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><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
@@ -151,11 +151,11 @@
|
|||||||
<div class="modal-dialog modal-md">
|
<div class="modal-dialog modal-md">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="addQuotationModalLabel">Choose a quotation</h5>
|
<h5 class="modal-title" id="addQuotationModalLabel">Choose a quotation</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<select id="addQuotationSelect" class="form-control form-control-sm" style="width: 100% !important; min-width: 100% !important"></select>
|
<select id="addQuotationSelect" class="form-control form-control-sm" style="width: 100% !important; min-width: 100% !important"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
|
||||||
@@ -180,13 +180,13 @@
|
|||||||
max-width: 100% !important
|
max-width: 100% !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#addQuotationModal {
|
#addQuotationModal {
|
||||||
z-index: 1060 !important
|
z-index: 1060 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
#addQuotationModal .modal-backdrop {
|
#addQuotationModal .modal-backdrop {
|
||||||
z-index: 1055 !important
|
z-index: 1055 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabelle */
|
/* Tabelle */
|
||||||
#partsTable tr {
|
#partsTable tr {
|
||||||
@@ -287,9 +287,13 @@
|
|||||||
/* Colonna Descrizione (2ª colonna) = 420px */
|
/* Colonna Descrizione (2ª colonna) = 420px */
|
||||||
#partsTable th:nth-child(2),
|
#partsTable th:nth-child(2),
|
||||||
#partsTable td:nth-child(2) {
|
#partsTable td:nth-child(2) {
|
||||||
width: 350 !important;
|
width: 300px !important;
|
||||||
min-width: 350px !important;
|
min-width: 300px !important;
|
||||||
max-width: 350px !important;
|
max-width: 300px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#partsTable .part-number {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#partsTable td:nth-child(2) .part-description {
|
#partsTable td:nth-child(2) .part-description {
|
||||||
|
|||||||
+333
-117
@@ -6,7 +6,9 @@ $(document).ready(function () {
|
|||||||
let unsavedChanges = false;
|
let unsavedChanges = false;
|
||||||
let matrici = [];
|
let matrici = [];
|
||||||
let macroMatrici = [];
|
let macroMatrici = [];
|
||||||
let quotations = [];
|
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() ---
|
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
|
||||||
function getPartId($row) {
|
function getPartId($row) {
|
||||||
@@ -29,6 +31,148 @@ $(document).ready(function () {
|
|||||||
return id === "new" || id === "" || id === null ? null : id;
|
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) {
|
function setPartId($row, id) {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
|
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
|
||||||
@@ -153,15 +297,19 @@ $(document).ready(function () {
|
|||||||
matrici = data.value || [];
|
matrici = data.value || [];
|
||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPartsExtraField(iddatadb, function () {
|
||||||
loadExistingParts(iddatadb, idquotations, callback);
|
loadPhoto(iddatadb, idquotations);
|
||||||
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
matrici = [];
|
matrici = [];
|
||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPartsExtraField(iddatadb, function () {
|
||||||
loadExistingParts(iddatadb, idquotations, callback);
|
loadPhoto(iddatadb, idquotations);
|
||||||
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
|
});
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
|
||||||
error +
|
error +
|
||||||
@@ -180,12 +328,16 @@ $(document).ready(function () {
|
|||||||
} else {
|
} else {
|
||||||
loadMacroMatrici();
|
loadMacroMatrici();
|
||||||
initializeGlobalSelect2();
|
initializeGlobalSelect2();
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPartsExtraField(iddatadb, function () {
|
||||||
loadExistingParts(iddatadb, idquotations, callback);
|
loadPhoto(iddatadb, idquotations);
|
||||||
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loadPhoto(iddatadb, idquotations);
|
loadPartsExtraField(iddatadb, function () {
|
||||||
loadExistingParts(iddatadb, idquotations, callback);
|
loadPhoto(iddatadb, idquotations);
|
||||||
|
loadExistingParts(iddatadb, idquotations, callback);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,6 +666,11 @@ $(document).ready(function () {
|
|||||||
if (response.new_id) return response.new_id;
|
if (response.new_id) return response.new_id;
|
||||||
if (response.partId) return response.partId;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +684,13 @@ $(document).ready(function () {
|
|||||||
const iddatadb = $("#partsModal").data("iddatadb");
|
const iddatadb = $("#partsModal").data("iddatadb");
|
||||||
const idquotations = $("#partsModal").data("idquotations");
|
const idquotations = $("#partsModal").data("idquotations");
|
||||||
const isMix = partDescription.startsWith("Mix") ? "Y" : "N";
|
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);
|
let partId = getPartId($row);
|
||||||
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
|
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
|
||||||
const endpoint = idquotations
|
const endpoint = idquotations
|
||||||
@@ -559,8 +723,14 @@ $(document).ready(function () {
|
|||||||
part_number: partNumber,
|
part_number: partNumber,
|
||||||
part_description: partDescription,
|
part_description: partDescription,
|
||||||
mix: isMix,
|
mix: isMix,
|
||||||
|
idmatrice: $row.find(".part-matrice").val() || null,
|
||||||
dateexpiry: dateexpiry || null,
|
dateexpiry: dateexpiry || null,
|
||||||
note: note,
|
note: note,
|
||||||
|
|
||||||
|
// extra custom field
|
||||||
|
extra_field_id: extra_field_id,
|
||||||
|
extra_value_id: extra_value_id,
|
||||||
|
extra_value_text: extra_value_text,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -591,7 +761,8 @@ $(document).ready(function () {
|
|||||||
$saveStatus.show();
|
$saveStatus.show();
|
||||||
setTimeout(() => $saveStatus.hide(), 2000);
|
setTimeout(() => $saveStatus.hide(), 2000);
|
||||||
|
|
||||||
if (!$("#quotationeBtn").hasClass('d-none')) $("#quotationeBtn").addClass("d-none");
|
if (!$("#quotationeBtn").hasClass("d-none"))
|
||||||
|
$("#quotationeBtn").addClass("d-none");
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
|
||||||
@@ -720,8 +891,9 @@ $(document).ready(function () {
|
|||||||
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
|
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
|
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
|
||||||
<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-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>
|
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
|
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
|
||||||
@@ -1154,9 +1326,13 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("blur", ".part-description, .part-number", function () {
|
$(document).on(
|
||||||
saveRow($(this).closest("tr"));
|
"blur",
|
||||||
});
|
".part-description, .part-number, .part-extra-field",
|
||||||
|
function () {
|
||||||
|
saveRow($(this).closest("tr"));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function loadExistingParts(iddatadb, idquotations, callback = null) {
|
function loadExistingParts(iddatadb, idquotations, callback = null) {
|
||||||
const endpoint = idquotations
|
const endpoint = idquotations
|
||||||
@@ -1208,8 +1384,9 @@ $(document).ready(function () {
|
|||||||
<select class="part-matrice form-control form-control-sm" style="width: 150px;" ${idquotations && !part.idmatrice ? "disabled" : ""}></select>
|
<select class="part-matrice form-control form-control-sm" style="width: 150px;" ${idquotations && !part.idmatrice ? "disabled" : ""}></select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
|
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
|
||||||
<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-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>
|
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash fa-xs"></i></button>
|
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash fa-xs"></i></button>
|
||||||
@@ -1218,6 +1395,20 @@ $(document).ready(function () {
|
|||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
$("#partsTableBody").append(newRow);
|
$("#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(
|
const $select = $("#partsTableBody").find(
|
||||||
`tr[data-part-id="${part.id}"] .part-matrice`,
|
`tr[data-part-id="${part.id}"] .part-matrice`,
|
||||||
);
|
);
|
||||||
@@ -1236,11 +1427,11 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addNewRow(1, false); // Riga iniziale normale
|
addNewRow(1, false); // Riga iniziale normale
|
||||||
$("#quotationeBtn").removeClass("d-none");
|
$("#quotationeBtn").removeClass("d-none");
|
||||||
}
|
}
|
||||||
updateRowButtons();
|
updateRowButtons();
|
||||||
|
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
const errorMsg = $(
|
const errorMsg = $(
|
||||||
@@ -1345,7 +1536,7 @@ $(document).ready(function () {
|
|||||||
partId,
|
partId,
|
||||||
currentValue,
|
currentValue,
|
||||||
selectedMacro,
|
selectedMacro,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1422,7 +1613,7 @@ $(document).ready(function () {
|
|||||||
partId,
|
partId,
|
||||||
idmatrice,
|
idmatrice,
|
||||||
selectedMacro = null,
|
selectedMacro = null,
|
||||||
fromFilter = false
|
fromFilter = false,
|
||||||
) {
|
) {
|
||||||
if (typeof $.fn.select2 === "undefined") {
|
if (typeof $.fn.select2 === "undefined") {
|
||||||
$select.replaceWith(
|
$select.replaceWith(
|
||||||
@@ -1493,22 +1684,22 @@ $(document).ready(function () {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fromFilter) $select.append(option).trigger("change");
|
if (!fromFilter) $select.append(option).trigger("change");
|
||||||
else $select.append(option);
|
else $select.append(option);
|
||||||
|
|
||||||
partMatrice[partNumber] = matrice.IdMatrice;
|
partMatrice[partNumber] = matrice.IdMatrice;
|
||||||
} else {
|
} else {
|
||||||
// Aggiusta valore non valido
|
// Aggiusta valore non valido
|
||||||
if (!fromFilter) $select.val(null).trigger("change");
|
if (!fromFilter) $select.val(null).trigger("change");
|
||||||
|
|
||||||
partMatrice[partNumber] = null;
|
partMatrice[partNumber] = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$select.val(null).trigger("change", [{ skipHandler: true }]);
|
$select.val(null).trigger("change", [{ skipHandler: true }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$select.on("change", function (event, data) {
|
$select.on("change", function (event, data) {
|
||||||
if (data && data?.skipHandler) return;
|
if (data && data?.skipHandler) return;
|
||||||
|
|
||||||
const idmatrice = $(this).val();
|
const idmatrice = $(this).val();
|
||||||
const $row = $(this).closest("tr");
|
const $row = $(this).closest("tr");
|
||||||
@@ -1794,121 +1985,140 @@ $(document).ready(function () {
|
|||||||
// Esporta la funzione loadParts per essere usata da import_Edit2.php
|
// Esporta la funzione loadParts per essere usata da import_Edit2.php
|
||||||
window.loadParts = loadParts;
|
window.loadParts = loadParts;
|
||||||
|
|
||||||
$(document).on("click", "#quotationeBtn", function () {
|
$(document).on("click", "#quotationeBtn", function () {
|
||||||
$("#addQuotationModal").modal("show");
|
$("#addQuotationModal").modal("show");
|
||||||
|
|
||||||
if (quotations.length > 0) {
|
if (quotations.length > 0) {
|
||||||
reloadQuotations();
|
reloadQuotations();
|
||||||
} else {
|
} else {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "load_quotations.php",
|
url: "load_quotations.php",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
success: function (response) {
|
success: function (response) {
|
||||||
quotations = response?.quotations || [];
|
quotations = response?.quotations || [];
|
||||||
|
|
||||||
reloadQuotations();
|
reloadQuotations();
|
||||||
},
|
},
|
||||||
error: function (xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error("Errore AJAX caricamento quotations:", error, xhr.responseText);
|
console.error(
|
||||||
let message = `
|
"Errore AJAX caricamento quotations:",
|
||||||
|
error,
|
||||||
|
xhr.responseText,
|
||||||
|
);
|
||||||
|
let message = `
|
||||||
<div class="alert alert-danger temp-alert" role="alert">
|
<div class="alert alert-danger temp-alert" role="alert">
|
||||||
Errore nel caricamento delle quotations: ${error} (${xhr.status})
|
Errore nel caricamento delle quotations: ${error} (${xhr.status})
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$("#partsModal .modal-body").prepend(errorMsg);
|
$("#partsModal .modal-body").prepend(errorMsg);
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
message.fadeOut(500, function () {
|
message.fadeOut(500, function () {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("click", "#addQuotationBtn", function () {
|
$(document).on("click", "#addQuotationBtn", function () {
|
||||||
const quotationId = $("#addQuotationSelect").val();
|
const quotationId = $("#addQuotationSelect").val();
|
||||||
|
|
||||||
if (quotationId && confirm("Confermo di collegare la quotazione al campione?")) {
|
if (
|
||||||
$("#addQuotationModal").modal("hide");
|
quotationId &&
|
||||||
|
confirm("Confermo di collegare la quotazione al campione?")
|
||||||
|
) {
|
||||||
|
$("#addQuotationModal").modal("hide");
|
||||||
|
|
||||||
loadParts(null, quotationId, () => {
|
loadParts(null, quotationId, () => {
|
||||||
$("#quotationeBtn").addClass("d-none");
|
$("#quotationeBtn").addClass("d-none");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$("#partsTableBody tr").each(function () {
|
$("#partsTableBody tr").each(function () {
|
||||||
$(this).find(".save-loading").show();
|
$(this).find(".save-loading").show();
|
||||||
});
|
});
|
||||||
|
|
||||||
let photoList = $('#photoSelector option').map(function () {
|
let photoList = $("#photoSelector option")
|
||||||
return this.value?.split('/')?.pop();
|
.map(function () {
|
||||||
}).get();
|
return this.value?.split("/")?.pop();
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
if (!photoList.length) {
|
if (!photoList.length) {
|
||||||
let path = $('#samplePhoto').attr("src")?.split('/')?.pop();
|
let path = $("#samplePhoto")
|
||||||
|
.attr("src")
|
||||||
|
?.split("/")
|
||||||
|
?.pop();
|
||||||
|
|
||||||
if (path) photoList = [path];
|
if (path) photoList = [path];
|
||||||
}
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "save_parts_photo_iddatadb.php",
|
url: "save_parts_photo_iddatadb.php",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
iddatadb: $("#partsModal").data("iddatadb"),
|
iddatadb: $("#partsModal").data("iddatadb"),
|
||||||
photoList,
|
photoList,
|
||||||
partIds: $('#partsTableBody tr').map(function () {
|
partIds: $("#partsTableBody tr")
|
||||||
return $(this).data("part-id");
|
.map(function () {
|
||||||
}).get(),
|
return $(this).data("part-id");
|
||||||
}),
|
})
|
||||||
success: function () {
|
.get(),
|
||||||
$("#partsTableBody tr").each(function () {
|
}),
|
||||||
let $row = $(this);
|
success: function () {
|
||||||
let $saveStatus = $row.find(".save-status");
|
$("#partsTableBody tr").each(function () {
|
||||||
let $saveLoading = $row.find(".save-loading");
|
let $row = $(this);
|
||||||
|
let $saveStatus = $row.find(".save-status");
|
||||||
|
let $saveLoading = $row.find(".save-loading");
|
||||||
|
|
||||||
$saveLoading.hide();
|
$saveLoading.hide();
|
||||||
$saveStatus.show();
|
$saveStatus.show();
|
||||||
|
|
||||||
setTimeout(() => $saveStatus.hide(), 2000);
|
setTimeout(() => $saveStatus.hide(), 2000);
|
||||||
});
|
});
|
||||||
}, error: function (xhr, status, error) {
|
},
|
||||||
let message = `
|
error: function (xhr, status, error) {
|
||||||
|
let message = `
|
||||||
<div class="alert alert-danger temp-alert" role="alert">
|
<div class="alert alert-danger temp-alert" role="alert">
|
||||||
Errore di salvataggio: ${error} (${xhr.status})
|
Errore di salvataggio: ${error} (${xhr.status})
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$("#partsModal .modal-body").prepend(message);
|
$("#partsModal .modal-body").prepend(message);
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
message.fadeOut(500, function () {
|
message.fadeOut(500, function () {
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function reloadQuotations() {
|
function reloadQuotations() {
|
||||||
if (quotations.length > 0) {
|
if (quotations.length > 0) {
|
||||||
$("#addQuotationSelect").empty();
|
$("#addQuotationSelect").empty();
|
||||||
|
|
||||||
$("#addQuotationSelect").append("<option value=''>Seleziona Quotation</option>");
|
$("#addQuotationSelect").append(
|
||||||
|
"<option value=''>Seleziona Quotation</option>",
|
||||||
|
);
|
||||||
|
|
||||||
quotations.forEach(quotation => {
|
quotations.forEach((quotation) => {
|
||||||
if (quotation?.description) {
|
if (quotation?.description) {
|
||||||
$("#addQuotationSelect").append(`<option value='${quotation?.id}'>${quotation?.description}</option>`);
|
$("#addQuotationSelect").append(
|
||||||
}
|
`<option value='${quotation?.id}'>${quotation?.description}</option>`,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$("#addQuotationSelect").select2();
|
$("#addQuotationSelect").select2();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("change", ".propagate-date-input", function () {
|
$(document).on("change", ".propagate-date-input", function () {
|
||||||
@@ -2136,15 +2346,21 @@ $(document).on("click", ".save-common-note-btn", function () {
|
|||||||
$(document).on("click", "#showHideImageBtn", function () {
|
$(document).on("click", "#showHideImageBtn", function () {
|
||||||
let mainRow = $(this).closest(".parts-row");
|
let mainRow = $(this).closest(".parts-row");
|
||||||
let photoContainer = mainRow.find(".col-md-3");
|
let photoContainer = mainRow.find(".col-md-3");
|
||||||
let tableContainer = mainRow.find("#partsTable").closest("div[class*='col-md']");
|
let tableContainer = mainRow
|
||||||
|
.find("#partsTable")
|
||||||
|
.closest("div[class*='col-md']");
|
||||||
|
|
||||||
if (photoContainer.hasClass("d-none")) {
|
if (photoContainer.hasClass("d-none")) {
|
||||||
photoContainer.removeClass("d-none");
|
photoContainer.removeClass("d-none");
|
||||||
tableContainer.removeClass("col-md-12").addClass("col-md-9");
|
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>");
|
$(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 {
|
} else {
|
||||||
photoContainer.addClass("d-none");
|
photoContainer.addClass("d-none");
|
||||||
tableContainer.removeClass("col-md-9").addClass("col-md-12");
|
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>");
|
$(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>",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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) => {
|
document.querySelectorAll(".thumbnail").forEach((img) => {
|
||||||
img.addEventListener("click", function () {
|
img.addEventListener("click", function () {
|
||||||
const enlargedImage = document.getElementById("enlargedImage");
|
const enlargedImage = document.getElementById("enlargedImage");
|
||||||
|
|||||||
@@ -117,12 +117,24 @@ $code = $row[$field] ?? 'Non disponibile';
|
|||||||
|
|
||||||
// Recupera le foto associate alla riga
|
// Recupera le foto associate alla riga
|
||||||
try {
|
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]);
|
$stmt->execute([$paramValue]);
|
||||||
$photos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$photos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("Errore query foto: " . $e->getMessage());
|
error_log("Errore query foto: " . $e->getMessage());
|
||||||
$photos = []; // Imposta array vuoto in caso di errore
|
$photos = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Definisci il percorso base per le foto
|
// 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?iddatadb=" . $iddatadb
|
||||||
: $baseUrl . "/upload_photos_mobile.php?idquotations=" . $idquotations;
|
: $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/';
|
$qrCodeDir = '../photostrf/qrcodes/';
|
||||||
if (!is_dir($qrCodeDir)) {
|
if (!is_dir($qrCodeDir)) {
|
||||||
mkdir($qrCodeDir, 0755, true);
|
mkdir($qrCodeDir, 0755, true);
|
||||||
@@ -164,6 +176,7 @@ $result->saveToFile($qrCodeFile);
|
|||||||
<p>Caricamento in corso...</p>
|
<p>Caricamento in corso...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Manage Photos</h3>
|
<h3>Manage Photos</h3>
|
||||||
<p><strong>ID:</strong> <?= htmlspecialchars($id) ?></p>
|
<p><strong>ID:</strong> <?= htmlspecialchars($id) ?></p>
|
||||||
<p><strong>Code:</strong> <?= htmlspecialchars($code) ?></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>
|
<p>Drag the photo here or click to select</p>
|
||||||
<input type="file" id="photoInput" multiple accept="image/*" style="display: none;">
|
<input type="file" id="photoInput" multiple accept="image/*" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Area per la webcam -->
|
<!-- Area per la webcam -->
|
||||||
<div id="webcamArea" style="display: none; text-align: center; margin-bottom: 20px;">
|
<div id="webcamArea" style="display: none; text-align: center; margin-bottom: 20px;">
|
||||||
<p>Webcam Preview</p>
|
<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>
|
<button id="closeWebcamBtn" style="padding: 10px 20px; background: #dc3545; color: white; border: none; cursor: pointer;">Close Webcam</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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 -->
|
<!-- Elenco delle foto -->
|
||||||
<div id="photosList">
|
<div id="photosList">
|
||||||
<?php if (empty($photos)): ?>
|
<?php if (empty($photos)): ?>
|
||||||
@@ -204,25 +220,57 @@ $result->saveToFile($qrCodeFile);
|
|||||||
<?php
|
<?php
|
||||||
$filePath = $photoBasePath . $photo['file_path'];
|
$filePath = $photoBasePath . $photo['file_path'];
|
||||||
$fileExists = file_exists($filePath);
|
$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 class="photo-item" data-photo-id="<?= (int)$photo['id'] ?>">
|
||||||
<div style="flex: 1;">
|
<div class="photo-thumb-area">
|
||||||
<?php if ($fileExists): ?>
|
<?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: ?>
|
<?php else: ?>
|
||||||
<p style="color: red;">[File non trovato]</p>
|
<div class="missing-file-box">[File non trovato]</div>
|
||||||
<?php endif; ?>
|
<?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>
|
</div>
|
||||||
<button class="delete-photo-btn" data-photo-id="<?= $photo['id'] ?>" style="background: none; border: none; color: #dc3545; cursor: pointer;">
|
|
||||||
<i class="fas fa-trash"></i>
|
<div class="photo-info-area">
|
||||||
</button>
|
<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>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<!-- Bottone Crea Collage -->
|
<!-- Bottone Crea Collage -->
|
||||||
<button id="createCollageBtn" style="padding: 10px 20px; background: #ffc107; color: white; border: none; cursor: pointer; margin-top: 20px;">Crea Collage</button>
|
<button id="createCollageBtn" style="padding: 10px 20px; background: #ffc107; color: white; border: none; cursor: pointer; margin-top: 20px;">Crea Collage</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -283,6 +331,12 @@ $result->saveToFile($qrCodeFile);
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.photo-item {
|
.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;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,6 +344,68 @@ $result->saveToFile($qrCodeFile);
|
|||||||
background-color: #f8f9fa;
|
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 {
|
#dropArea.highlight {
|
||||||
border-color: #28a745;
|
border-color: #28a745;
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
|
|||||||
+128
-39
@@ -4,6 +4,7 @@ include('include/headscript.php');
|
|||||||
|
|
||||||
$dbHandler = DBHandlerSelect::getInstance();
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
$pdo = $dbHandler->getConnection();
|
$pdo = $dbHandler->getConnection();
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
@@ -19,6 +20,21 @@ try {
|
|||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
$results = [];
|
$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) {
|
foreach ($parts as $part) {
|
||||||
$partId = $part['id'] ?? null;
|
$partId = $part['id'] ?? null;
|
||||||
$partNumber = $part['part_number'] ?? null;
|
$partNumber = $part['part_number'] ?? null;
|
||||||
@@ -28,50 +44,123 @@ try {
|
|||||||
$note = $part['note'] ?? null;
|
$note = $part['note'] ?? null;
|
||||||
$dateexpiry = $part['dateexpiry'] ?? null;
|
$dateexpiry = $part['dateexpiry'] ?? null;
|
||||||
|
|
||||||
if ($partDescription || $note || $dateexpiry) {
|
// Extra field (0/1)
|
||||||
if ($partId) {
|
$extraFieldId = $part['extra_field_id'] ?? null;
|
||||||
// UPDATE se la parte esiste
|
$extraValueId = $part['extra_value_id'] ?? null;
|
||||||
$stmt = $pdo->prepare("UPDATE identification_parts
|
$extraValueText = $part['extra_value_text'] ?? null;
|
||||||
SET part_number = :part_number,
|
|
||||||
part_description = :part_description,
|
// Normalizza vuoti
|
||||||
mix = :mix,
|
if ($extraFieldId !== null && $extraFieldId !== '') $extraFieldId = (int)$extraFieldId;
|
||||||
idmatrice = :idmatrice,
|
else $extraFieldId = null;
|
||||||
note = :note,
|
if ($extraValueId !== null && $extraValueId !== '') $extraValueId = (int)$extraValueId;
|
||||||
dateexpiry = :dateexpiry,
|
else $extraValueId = null;
|
||||||
updated_at = NOW()
|
if ($extraValueText !== null) {
|
||||||
WHERE id = :id");
|
$extraValueText = trim((string)$extraValueText);
|
||||||
$stmt->execute([
|
if ($extraValueText === '') $extraValueText = null;
|
||||||
':id' => $partId,
|
}
|
||||||
':part_number' => $partNumber,
|
|
||||||
':part_description' => $partDescription,
|
if ($partId) {
|
||||||
':mix' => $mix,
|
// UPDATE se la parte esiste (sempre)
|
||||||
':idmatrice' => $idmatrice,
|
$stmt = $pdo->prepare("UPDATE identification_parts
|
||||||
':note' => $note,
|
SET part_number = :part_number,
|
||||||
':dateexpiry' => $dateexpiry,
|
part_description = :part_description,
|
||||||
]);
|
mix = :mix,
|
||||||
$results[] = ['part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo'];
|
idmatrice = :idmatrice,
|
||||||
} else {
|
note = :note,
|
||||||
// INSERT per nuova parte
|
dateexpiry = :dateexpiry,
|
||||||
$stmt = $pdo->prepare("INSERT INTO identification_parts
|
updated_at = NOW()
|
||||||
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
|
WHERE id = :id");
|
||||||
VALUES (:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
|
$stmt->execute([
|
||||||
$stmt->execute([
|
':id' => $partId,
|
||||||
':iddatadb' => $iddatadb,
|
':part_number' => $partNumber,
|
||||||
':part_number' => $partNumber,
|
':part_description' => $partDescription,
|
||||||
':part_description' => $partDescription,
|
':mix' => $mix,
|
||||||
':mix' => $mix,
|
':idmatrice' => $idmatrice,
|
||||||
':idmatrice' => $idmatrice,
|
':note' => $note,
|
||||||
':note' => $note,
|
':dateexpiry' => $dateexpiry,
|
||||||
':dateexpiry' => $dateexpiry,
|
]);
|
||||||
]);
|
|
||||||
$newId = $pdo->lastInsertId();
|
// Save extra custom field (if provided)
|
||||||
$results[] = ['part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo'];
|
if ($extraFieldId !== null) {
|
||||||
|
if ($extraValueId === null && $extraValueText === null) {
|
||||||
|
$stmtDeleteCF->execute([
|
||||||
|
':part_id' => $partId,
|
||||||
|
':field_id' => $extraFieldId,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$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())");
|
||||||
|
$stmt->execute([
|
||||||
|
':iddatadb' => $iddatadb,
|
||||||
|
':part_number' => $partNumber,
|
||||||
|
':part_description' => $partDescription,
|
||||||
|
':mix' => $mix,
|
||||||
|
':idmatrice' => $idmatrice,
|
||||||
|
':note' => $note,
|
||||||
|
':dateexpiry' => $dateexpiry,
|
||||||
|
]);
|
||||||
|
$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();
|
$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) {
|
} catch (PDOException $e) {
|
||||||
$pdo->rollBack();
|
$pdo->rollBack();
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
|
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -21,11 +21,37 @@ $value = ($value === 1) ? 1 : 0;
|
|||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE template_mapping SET is_visible_parts = ? WHERE id = ? AND template_id = ?");
|
try {
|
||||||
$result = $stmt->execute([$value, $mapping_id, $template_id]);
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
if ($result) {
|
if ($value === 1) {
|
||||||
echo json_encode(['success' => true]);
|
// 1) Force ONLY ONE row visible_parts per template:
|
||||||
} else {
|
// set all to 0 for this template
|
||||||
echo json_encode(['success' => false, 'message' => 'Failed to update is_visible_parts']);
|
$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()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user