@@ -123,12 +282,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -208,10 +401,16 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
+
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
+ const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
+ const apiConfigWrapper = document.getElementById("apiConfigWrapper");
+
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
+ const xlsSheetIndex = document.getElementById("xlsSheetIndex");
+ const apiConfigSelect = document.getElementById("apiConfigSelect");
const selectedClientId = ;
const selectedSchemaId = ;
@@ -231,33 +430,71 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
+ $('#apiConfigSelect').select2({
+ placeholder: "Select an API configuration...",
+ allowClear: true
+ });
+
function updateSourceFields() {
const selectedSource = sourceType.value;
- if (selectedSource === 'API') {
- headerRowWrapper.style.opacity = '0.6';
- startColumnWrapper.style.opacity = '0.6';
+ const isXls = selectedSource === 'XLS';
+ const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
- headerRow.required = false;
- startColumn.required = false;
-
- headerRow.disabled = true;
- startColumn.disabled = true;
- } else {
- headerRowWrapper.style.opacity = '1';
- startColumnWrapper.style.opacity = '1';
+ if (isXls) {
+ headerRowWrapper.style.display = 'block';
+ startColumnWrapper.style.display = 'block';
+ xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
+ xlsSheetIndex.disabled = false;
+
+ apiConfigWrapper.style.display = 'none';
+ apiConfigSelect.required = false;
+ apiConfigSelect.disabled = true;
+ $('#apiConfigSelect').val(null).trigger('change');
+ } else {
+ headerRowWrapper.style.display = 'none';
+ startColumnWrapper.style.display = 'none';
+ xlsSheetNumberWrapper.style.display = 'none';
+
+ headerRow.required = false;
+ startColumn.required = false;
+
+ headerRow.disabled = true;
+ startColumn.disabled = true;
+ xlsSheetIndex.disabled = true;
+
+ if (isApiOrJson) {
+ apiConfigWrapper.style.display = 'block';
+ apiConfigSelect.required = true;
+ apiConfigSelect.disabled = false;
+ } else {
+ apiConfigWrapper.style.display = 'none';
+ apiConfigSelect.required = false;
+ apiConfigSelect.disabled = true;
+ $('#apiConfigSelect').val(null).trigger('change');
+ }
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
+ function formatClientLabel(client) {
+ const nome = client.Nominativo || "Name not available";
+ const id = client.IdCliente || "";
+ const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
+ const suffix = (codiceCliente.split("_")[1] || "").trim();
+ const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
+
+ return `${nome.trim()} - ${shortCode} (ID: ${id})`;
+ }
+
async function loadClients() {
try {
clientLoadingStatus.style.display = 'inline';
@@ -280,9 +517,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
select.innerHTML = '
';
data.value.forEach(client => {
- const nome = client.Nominativo || "Name not available";
const id = client.IdCliente || "";
- const option = new Option(`${nome.trim()} (ID: ${id})`, id);
+ const option = new Option(formatClientLabel(client), id);
if (parseInt(id) === parseInt(selectedClientId)) {
option.selected = true;
@@ -468,6 +704,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineId = routineSelect.value;
formData.append("idroutine", routineId);
+ const selectedSource = sourceType.value;
+
+ if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
+ Swal.fire({
+ title: "Error!",
+ text: "Please select an API/JSON configuration.",
+ icon: "error",
+ confirmButtonText: "OK"
+ });
+ return;
+ }
+
+ if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
+ Swal.fire({
+ title: "Error!",
+ text: "Please enter the XLS sheet number.",
+ icon: "error",
+ confirmButtonText: "OK"
+ });
+ return;
+ }
+
fetch("process_edit_template_xls.php", {
method: "POST",
body: formData
diff --git a/public/userarea/error_log.txt b/public/userarea/error_log.txt
index 4c5140a..9d43509 100644
--- a/public/userarea/error_log.txt
+++ b/public/userarea/error_log.txt
@@ -328,3 +328,6 @@
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
+2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
+2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
+2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
diff --git a/public/userarea/export_to_lims.php b/public/userarea/export_to_lims.php
index 7b7d375..3ec2720 100644
--- a/public/userarea/export_to_lims.php
+++ b/public/userarea/export_to_lims.php
@@ -59,6 +59,49 @@ function formatDateToExport($value)
return null; // Imposta null se non è una data valida
}
+// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
+// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
+// Riprova con backoff esponenziale finché non passa a "Elaborata".
+function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
+{
+ $result = null;
+ $stato = null;
+ $succeeded = false;
+ $log = "";
+ $backoff = $initialBackoff;
+
+ set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
+
+ for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
+ try {
+ $result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
+ $stato = $result['StatoCommessaWeb'] ?? null;
+ $log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
+ } catch (Exception $e) {
+ $stato = null;
+ $log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
+ }
+
+ if ($stato === 'Elaborata') {
+ $succeeded = true;
+ break;
+ }
+
+ if ($attempt <= $maxRetries) {
+ $log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
+ sleep($backoff);
+ $backoff *= 2;
+ }
+ }
+
+ return [
+ 'succeeded' => $succeeded,
+ 'stato' => $stato,
+ 'result' => $result,
+ 'log' => $log,
+ ];
+}
+
try {
$iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) {
@@ -107,11 +150,13 @@ try {
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare("
- SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
- FROM identification_parts
- WHERE iddatadb = :iddatadb
- ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
- ");
+ SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry
+ FROM identification_parts
+ WHERE iddatadb = :iddatadb
+ AND part_description IS NOT NULL
+ AND TRIM(part_description) <> ''
+ ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
+ ");
$stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -575,9 +620,8 @@ try {
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
- // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto)
+ // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
-
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id
: 285;
@@ -585,17 +629,23 @@ try {
$importPayload = [
"IdUtente" => $importUserId
];
- $importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload);
-
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- // Logga il POST
+
+ $importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
+ $importResult = $importOutcome['result'];
+ $importStato = $importOutcome['stato'];
+ $importSucceeded = $importOutcome['succeeded'];
+
+ // Logga il POST (tutti i tentativi)
$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);
+ "ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
+ "SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
+ "LAST 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})");
+ $writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -607,7 +657,23 @@ try {
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
+ // 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
+ // After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
+ // Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
+ $finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
+ if ($finalCodiceCommessa !== '') {
+ $stmt = $pdo->prepare("
+ UPDATE datadb
+ SET commessaweb = :commessaweb,
+ status = 'l'
+ WHERE iddatadb = :iddatadb
+ ");
+ $stmt->execute([
+ 'commessaweb' => substr($finalCodiceCommessa, 0, 30),
+ 'iddatadb' => $iddatadb
+ ]);
+ }
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -622,7 +688,7 @@ try {
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
- "commessaweb" => $commessaWebCode,
+ "commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
diff --git a/public/userarea/get_analisi_matrice_filter.php b/public/userarea/get_analisi_matrice_filter.php
index 6d1f6d7..ce5d038 100644
--- a/public/userarea/get_analisi_matrice_filter.php
+++ b/public/userarea/get_analisi_matrice_filter.php
@@ -18,7 +18,15 @@ try {
$api = VisualLimsApiClient::getInstance();
- $filter = rawurlencode("Matrice/IdMatrice eq $idMatrice");
+ $webOnly = isset($_GET['web_only']) ? (int)$_GET['web_only'] : 1;
+
+ $filterString = "Matrice/IdMatrice eq $idMatrice";
+
+ if ($webOnly === 1) {
+ $filterString .= " and SelezionabileSuWeb eq true";
+ }
+
+ $filter = rawurlencode($filterString);
$endpoint = "Analisi?\$filter={$filter}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
diff --git a/public/userarea/get_analisiabilitate.php b/public/userarea/get_analisiabilitate.php
new file mode 100644
index 0000000..d7db8ce
--- /dev/null
+++ b/public/userarea/get_analisiabilitate.php
@@ -0,0 +1,95 @@
+ 'AnalisiAbilitate'
+ ];
+
+ // Debug: salva URL usato
+ $base_url = 'https://93.43.5.102/limsapi/api/odata/';
+ $query = http_build_query($options);
+ $query = urldecode($query); // rende leggibile $expand invece di %24expand
+
+ $full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
+ file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
+
+ // Chiamata API
+ $data = $api->get($endpoint, $options);
+
+ // Recupera AnalisiAbilitate dalla risposta
+ $analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
+
+ // Alcune API OData possono restituire collection dentro "value"
+ if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
+ $analisiAbilitate = $analisiAbilitate['value'];
+ }
+
+ // Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
+ $matches = [];
+
+ foreach ($analisiAbilitate as $analisi) {
+ $recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
+ $idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
+
+ if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
+ $matches[] = $analisi;
+ }
+ }
+
+ // Output diagnostico
+ $output = [
+ 'success' => true,
+ 'idCampione' => $idCampione,
+ 'request_url' => $full_url,
+ 'targetRecordKey' => $targetRecordKey,
+ 'targetIdAnalisi' => $targetIdAnalisi,
+ 'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
+ 'target_found' => count($matches) > 0,
+ 'target_matches' => $matches,
+ 'analisi_abilitate' => $analisiAbilitate,
+ 'raw_response' => $data
+ ];
+
+ // Salva il JSON in locale
+ file_put_contents(
+ __DIR__ . '/analisi_abilitate_campione_749027_response.json',
+ json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
+ );
+
+ echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+} catch (Exception $e) {
+ file_put_contents(
+ __DIR__ . '/error_log_analisi_abilitate.txt',
+ date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
+ FILE_APPEND
+ );
+
+ http_response_code(500);
+
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+}
diff --git a/public/userarea/get_campionematrice.php b/public/userarea/get_campionematrice.php
new file mode 100644
index 0000000..74aef0e
--- /dev/null
+++ b/public/userarea/get_campionematrice.php
@@ -0,0 +1,88 @@
+ 'Matrice'
+ ];
+
+ // Debug URL
+ $base_url = 'https://93.43.5.102/limsapi/api/odata/';
+ $query = http_build_query($options);
+ $queryReadable = urldecode($query);
+
+ $full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
+
+ file_put_contents(
+ __DIR__ . '/last_url_check_matrice.txt',
+ $full_url . PHP_EOL,
+ FILE_APPEND
+ );
+
+ // Chiamata API
+ $data = $api->get($endpoint, $options);
+
+ // Recupero Matrice dalla response
+ $matrice = $data['Matrice'] ?? null;
+
+ $actualMatriceId = null;
+
+ if (is_array($matrice)) {
+ // Provo i nomi più probabili
+ $actualMatriceId = $matrice['IdMatrice']
+ ?? $matrice['idMatrice']
+ ?? $matrice['Id']
+ ?? $matrice['ID']
+ ?? null;
+ }
+
+ $matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
+
+ $output = [
+ 'success' => true,
+ 'idCampione' => $idCampione,
+ 'expectedMatriceId' => $expectedMatriceId,
+ 'actualMatriceId' => $actualMatriceId,
+ 'matrice_ok' => $matrice_ok,
+ 'request_url' => $full_url,
+ 'matrice' => $matrice,
+ 'raw_response' => $data
+ ];
+
+ // Salva JSON completo
+ file_put_contents(
+ __DIR__ . '/check_matrice_campione_749027_response.json',
+ json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
+ );
+
+ echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+} catch (Exception $e) {
+ file_put_contents(
+ __DIR__ . '/error_log_check_matrice.txt',
+ date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
+ FILE_APPEND
+ );
+
+ http_response_code(500);
+
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+}
diff --git a/public/userarea/get_rapporto_full_by_codice.php b/public/userarea/get_rapporto_full_by_codice.php
new file mode 100644
index 0000000..9d311e0
--- /dev/null
+++ b/public/userarea/get_rapporto_full_by_codice.php
@@ -0,0 +1,256 @@
+ "CodiceRapporto eq '{$codiceRapportoSafe}'",
+ '$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
+ '$top' => 1
+ ];
+
+ $searchEndpoint = "Rapporto?" . http_build_query($searchParams);
+ $searchData = $api->get($searchEndpoint);
+
+ $rapporti = $searchData['value'] ?? [];
+
+ if (empty($rapporti)) {
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
+ 'codice_rapporto' => $codiceRapporto
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
+ $rapportoBase = $rapporti[0];
+ $idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
+
+ if (!$idRapporto) {
+ throw new Exception("IdRapporto non trovato nella risposta.");
+ }
+
+ /**
+ * STEP 2 - Dettaglio Rapporto
+ */
+ $clienteExpandError = null;
+ $detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
+
+ try {
+ $detailData = $api->get($detailEndpoint);
+ } catch (Exception $e) {
+ $clienteExpandError = $e->getMessage();
+ $detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
+ $detailData = $api->get($detailEndpoint);
+ }
+
+ file_put_contents(
+ $debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
+ json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
+ );
+
+ $cliente = $detailData['Cliente'] ?? null;
+ $rapportiFiles = $detailData['RapportiFiles'] ?? [];
+ $campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
+
+ // ====================== CLIENTE ======================
+ $clienteInfo = [
+ 'found' => is_array($cliente),
+ 'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
+ 'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
+ 'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
+ 'partita_iva' => $cliente['PartitaIva'] ?? null,
+ 'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
+ ];
+
+ // ====================== FILE PDF ======================
+ $selectedRapportoFile = null;
+ $idRapportoFile = null;
+ $pdfFileName = $codiceRapporto . '.pdf';
+ $pdfFullPath = $pdfDir . $pdfFileName;
+
+ if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
+ foreach ($rapportiFiles as $file) {
+ $fileName = $file['FileName'] ?? '';
+ $tipoRapporto = $file['TipoRapporto'] ?? '';
+ $categoria = $file['Categoria'] ?? '';
+
+ if (
+ stripos($fileName, '.pdf') !== false ||
+ stripos($tipoRapporto, 'Rapporto') !== false ||
+ stripos($categoria, 'Rapporti') !== false
+ ) {
+ $selectedRapportoFile = $file;
+ $idRapportoFile = $file['IdRapportoFile'] ?? null;
+ break;
+ }
+ }
+
+ if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
+ $selectedRapportoFile = $rapportiFiles[0];
+ $idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
+ }
+ }
+
+ // ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
+ $pdfDownloaded = false;
+ $pdfError = null;
+ $endpointUsato = null;
+
+ if ($downloadPdf && $idRapportoFile) {
+
+ $tentativi = [
+ "RapportoFile({$idRapportoFile})/\$value",
+ "RapportoFile({$idRapportoFile})/Contenuto/\$value",
+ "RapportoFile({$idRapportoFile})/File/\$value",
+ "Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
+ "Rapporto({$idRapporto})/RapportoFile/\$value",
+ "GetRapportoFile?IdRapportoFile={$idRapportoFile}",
+ "RapportoFile/Download?Id={$idRapportoFile}",
+ "Rapporto/DownloadFile?IdRapporto={$idRapporto}"
+ ];
+
+ foreach ($tentativi as $ep) {
+ try {
+ $pdfContent = $api->getRaw($ep);
+
+ if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
+ file_put_contents($pdfFullPath, $pdfContent);
+ $pdfDownloaded = true;
+ $endpointUsato = $ep;
+ $pdfError = "SUCCESSO con: " . $ep;
+ break;
+ } else {
+ $pdfError = "Contenuto troppo piccolo con: " . $ep;
+ }
+ } catch (Exception $e) {
+ $pdfError = "Fallito con " . $ep . " → " . $e->getMessage();
+ }
+ }
+ }
+
+ // ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
+ $campioniSintesi = [];
+ $ratingFinale = null;
+ $ratingSource = null;
+ $hasIrregolare = false;
+ $hasPositiveResults = false;
+
+ if (is_array($campioniDatiRapporto)) {
+ foreach ($campioniDatiRapporto as $campione) {
+ $giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
+ $giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
+ $esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
+ $esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
+ $risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
+ $risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
+
+ $campioniSintesi[] = [
+ 'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
+ 'codice_campione' => $campione['CodiceCampione'] ?? null,
+ 'stato_campione' => $campione['StatoCampione'] ?? null,
+ 'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
+ 'matrice' => $campione['Matrice'] ?? null,
+ 'macro_matrice' => $campione['MacroMatrice'] ?? null,
+ 'giudizio_rapporto' => $giudizioRapporto,
+ 'giudizio_certificato' => $giudizioCertificato,
+ 'esito_giudizio_lmr' => $esitoGiudizioLMR,
+ 'esito_giudizio_impiego' => $esitoGiudizioImpiego,
+ 'risultati_positivi' => $risultatiPositivi,
+ 'risultati_irregolari' => $risultatiIrregolari
+ ];
+
+ if (!$ratingFinale && !empty($giudizioRapporto)) {
+ $ratingFinale = $giudizioRapporto;
+ $ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
+ }
+
+ if (!$ratingFinale && !empty($giudizioCertificato)) {
+ $ratingFinale = $giudizioCertificato;
+ $ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
+ }
+
+ if ($risultatiIrregolari) $hasIrregolare = true;
+ if ($risultatiPositivi) $hasPositiveResults = true;
+ }
+ }
+
+ // Fallback rating
+ if (!$ratingFinale) {
+ if ($hasIrregolare) {
+ $ratingFinale = 'Irregolare';
+ $ratingSource = 'RisultatiIrregolari';
+ } elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
+ $ratingFinale = 'Regolare';
+ $ratingSource = 'fallback';
+ }
+ }
+
+ // ====================== RISPOSTA FINALE ======================
+ echo json_encode([
+ 'success' => true,
+
+ 'codice_rapporto' => $codiceRapporto,
+ 'id_rapporto' => $idRapporto,
+
+ 'search_endpoint' => $searchEndpoint,
+ 'detail_endpoint' => $detailEndpoint,
+ 'cliente_expand_error' => $clienteExpandError,
+
+ 'rapporto_base' => $rapportoBase,
+ 'cliente' => $clienteInfo,
+
+ 'rating_finale' => $ratingFinale,
+ 'rating_source' => $ratingSource,
+
+ 'rapporti_files_count' => count($rapportiFiles),
+ 'selected_rapporto_file' => $selectedRapportoFile,
+
+ 'pdf_file_name' => $pdfFileName,
+ 'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
+ 'pdf_downloaded' => $pdfDownloaded,
+ 'pdf_error' => $pdfError,
+
+ 'campioni_count' => count($campioniSintesi),
+ 'campioni_sintesi' => $campioniSintesi,
+
+ 'download_requested' => $downloadPdf
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+}
diff --git a/public/userarea/get_rapporto_lims.php b/public/userarea/get_rapporto_lims.php
new file mode 100644
index 0000000..a3eff2e
--- /dev/null
+++ b/public/userarea/get_rapporto_lims.php
@@ -0,0 +1,118 @@
+ "CodiceCommessa eq '{$commessa}'",
+ 'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
+ 'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
+ 'Codice' => "Codice eq '{$commessa}'"
+ ];
+
+ foreach ($filters as $label => $filter) {
+ if (!$filter) {
+ continue;
+ }
+
+ try {
+ $options = [
+ '$filter' => $filter,
+ '$top' => 10
+ ];
+
+ $data = $api->get('Rapporto', $options);
+
+ $attempts[$label] = [
+ 'success' => true,
+ 'filter' => $filter,
+ 'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
+ 'data' => $data
+ ];
+
+ file_put_contents(
+ $debugDir . "rapporto_{$commessa}_{$label}.json",
+ json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
+ );
+ } catch (Exception $e) {
+ $attempts[$label] = [
+ 'success' => false,
+ 'filter' => $filter,
+ 'error' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * STEP 2
+ * Prendo solo eventuali rapporti trovati.
+ */
+ $rapportiFound = [];
+
+ foreach ($attempts as $label => $attempt) {
+ if (!$attempt['success']) {
+ continue;
+ }
+
+ $items = $attempt['data']['value'] ?? [];
+
+ if (!is_array($items)) {
+ continue;
+ }
+
+ foreach ($items as $item) {
+ $rapportiFound[] = [
+ 'matched_by' => $label,
+ 'rapporto' => $item
+ ];
+ }
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'input_commessa' => $commessa,
+ 'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
+ 'rapporti_found_count' => count($rapportiFound),
+ 'rapporti_found' => $rapportiFound,
+ 'attempts_summary' => array_map(function ($a) {
+ return [
+ 'success' => $a['success'],
+ 'filter' => $a['filter'],
+ 'records' => $a['records'] ?? null,
+ 'error' => $a['error'] ?? null
+ ];
+ }, $attempts)
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+} catch (Exception $e) {
+ http_response_code(500);
+
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+}
diff --git a/public/userarea/get_rapporto_prova.php b/public/userarea/get_rapporto_prova.php
index 16e8020..0ae0cd9 100644
--- a/public/userarea/get_rapporto_prova.php
+++ b/public/userarea/get_rapporto_prova.php
@@ -2,25 +2,140 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
-header('Content-Type: application/json');
+header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
- $rapporto_id = 533329;
- // Costruzione manuale dell'endpoint con espansione annidata
- $endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
+ // Esempi:
+ // rapporto_by_codice_expand_step.php?codice=2541111&step=base
+ // rapporto_by_codice_expand_step.php?codice=2541111&step=files
+ // rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
+ // rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
- // Non passiamo options, già incluso nell'endpoint
- $data = $api->get($endpoint);
+ $codiceRapporto = trim($_GET['codice'] ?? '');
+ $step = trim($_GET['step'] ?? 'base');
- file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
- echo json_encode($data);
+ if ($codiceRapporto === '') {
+ throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
+ }
+
+ $allowedSteps = [
+ 'base' => '',
+ 'files' => 'RapportiFiles',
+ 'allegati' => 'RapportiAllegati',
+ 'campioni' => 'CampioniDatiRapporto',
+ 'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
+ ];
+
+ if (!array_key_exists($step, $allowedSteps)) {
+ throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
+ }
+
+ // Escape OData per eventuali apostrofi
+ $codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
+
+ /*
+ * STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
+ * Query leggera, con $select e $top=1.
+ */
+ $searchParams = [
+ '$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
+ '$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
+ '$top' => 1
+ ];
+
+ $searchEndpoint = "Rapporto?" . http_build_query($searchParams);
+
+ $searchData = $api->get($searchEndpoint);
+
+ $items = $searchData['value'] ?? [];
+
+ if (!is_array($items) || count($items) === 0) {
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
+ 'codice_rapporto' => $codiceRapporto,
+ 'search_endpoint' => $searchEndpoint,
+ 'search_data' => $searchData
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
+ $rapportoBase = $items[0];
+ $rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
+
+ if (!$rapportoId) {
+ throw new Exception("IdRapporto non trovato nella risposta.");
+ }
+
+ /*
+ * STEP 2 - Se step=base, restituisco solo la ricerca base.
+ */
+ if ($step === 'base') {
+ echo json_encode([
+ 'success' => true,
+ 'codice_rapporto' => $codiceRapporto,
+ 'id_rapporto' => $rapportoId,
+ 'step' => $step,
+ 'search_endpoint' => $searchEndpoint,
+ 'rapporto_base' => $rapportoBase
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
+ /*
+ * STEP 3 - Espande SOLO il rapporto trovato.
+ */
+ $expandValue = $allowedSteps[$step];
+
+ $detailParams = [
+ '$expand' => $expandValue
+ ];
+
+ $detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
+
+ file_put_contents(
+ __DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
+ '[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
+ '[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
+ FILE_APPEND
+ );
+
+ $detailData = $api->get($detailEndpoint);
+
+ file_put_contents(
+ __DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
+ json_encode([
+ 'search' => $searchData,
+ 'detail' => $detailData
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
+ );
+
+ echo json_encode([
+ 'success' => true,
+ 'codice_rapporto' => $codiceRapporto,
+ 'id_rapporto' => $rapportoId,
+ 'step' => $step,
+ 'search_endpoint' => $searchEndpoint,
+ 'detail_endpoint' => $detailEndpoint,
+ 'rapporto_base' => $rapportoBase,
+ 'data' => $detailData
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
- file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
+ file_put_contents(
+ __DIR__ . '/error_log.txt',
+ date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
+ FILE_APPEND
+ );
+
http_response_code(500);
- echo json_encode(['error' => $e->getMessage()]);
+
+ echo json_encode([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
diff --git a/public/userarea/gridRenderer.js b/public/userarea/gridRenderer.js
index 59743a4..44d6ee5 100644
--- a/public/userarea/gridRenderer.js
+++ b/public/userarea/gridRenderer.js
@@ -36,6 +36,15 @@
return d.innerHTML;
}
+ function escAttr(str) {
+ if (str === null || str === undefined) return "";
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+ }
function getDetailValue(rowIndex, mappingId) {
return data[rowIndex].details[String(mappingId)] ?? "";
}
@@ -57,7 +66,20 @@
// ── Client data (AJAX Select2, no bulk loading) ──────────────────────
function formatClientLabel(client) {
- return (client.Nominativo || "").trim();
+ const nome = client.Nominativo || client.text || "Nome non disponibile";
+ const id = client.IdCliente || client.id || "";
+ const codiceCliente = (
+ client.CodiceCliente ??
+ client.codiceCliente ??
+ ""
+ )
+ .toString()
+ .trim();
+ const suffix = (codiceCliente.split("_")[1] || "").trim();
+ const shortCode =
+ suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
+
+ return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
// Cache of resolved client names: id → name
@@ -102,7 +124,9 @@
);
const json = await resp.json();
const item = (json.results || [])[0];
- if (item) clientNameCache[id] = item.text;
+ if (item) {
+ clientNameCache[id] = formatClientLabel(item);
+ }
} catch (e) {
/* ignore */
}
@@ -110,12 +134,35 @@
);
}
+ function sortSelect2ResultsByStart(data) {
+ const term = $(".select2-container--open .select2-search__field").val();
+
+ if (!term) {
+ return data;
+ }
+
+ const search = term.toLowerCase().trim();
+
+ return data.sort(function (a, b) {
+ const textA = (a.text || "").toLowerCase().trim();
+ const textB = (b.text || "").toLowerCase().trim();
+
+ const aStarts = textA.startsWith(search);
+ const bStarts = textB.startsWith(search);
+
+ if (aStarts && !bStarts) return -1;
+ if (!aStarts && bStarts) return 1;
+
+ return textA.localeCompare(textB, "it", { sensitivity: "base" });
+ });
+ }
// Select2 AJAX config for client selects
const clientSelect2Config = {
placeholder: "Search client...",
allowClear: true,
width: "100%",
minimumInputLength: 0,
+ sorter: sortSelect2ResultsByStart,
dropdownCssClass: "select2-dropdown-smaller",
ajax: {
url: "search_clienti.php",
@@ -125,7 +172,12 @@
return { q: params.term || "", limit: 20 };
},
processResults: function (data) {
- return { results: data.results || [] };
+ const results = (data.results || []).map((item) => ({
+ ...item,
+ text: formatClientLabel(item),
+ }));
+
+ return { results: results };
},
cache: true,
},
@@ -222,7 +274,88 @@
return _pendingFixed[cacheKey];
}
+ async function refreshDependentFixedFieldsForRow(rowIndex) {
+ const row = data[rowIndex];
+ if (!row) return;
+
+ const clientId = row.idclient || "";
+
+ // Find fixed fields that depend on idclient
+ const dependentFields = Object.keys(fixedFieldApiConfig).filter(
+ (key) => {
+ return fixedFieldApiConfig[key].dependsOn === "idclient";
+ },
+ );
+
+ if (dependentFields.length === 0) return;
+
+ for (const fieldKey of dependentFields) {
+ // When client changes, the old responsible is no longer reliable
+ if (
+ row.fixedFields &&
+ Object.prototype.hasOwnProperty.call(row.fixedFields, fieldKey)
+ ) {
+ row.fixedFields[fieldKey] = "";
+ row._dirty = true;
+ }
+
+ // Reload options for the new client
+ if (clientId) {
+ await loadFixedFieldOptions(fieldKey, clientId);
+ }
+ }
+
+ // Re-render only this row so ClienteResponsabile select is rebuilt with the new options
+ renderSingleRow(rowIndex);
+
+ // If the first row client changes, update the top propagation select too
+ if (rowIndex === 0) {
+ await refreshTopDependentFixedSelect(
+ "ClienteResponsabile",
+ clientId,
+ );
+ }
+
+ updateDirtyIndicator();
+ }
// ── Custom field dropdown data loading ─────────────────────────────────
+ async function refreshTopDependentFixedSelect(fieldKey, clientId) {
+ if (!topContainer || !fieldKey) return;
+
+ const sel = topContainer.querySelector(
+ `.api-fixed-select[data-fixed-key="${fieldKey}"]`,
+ );
+
+ if (!sel) return;
+
+ // Destroy Select2 if already initialized
+ if ($(sel).hasClass("select2-hidden-accessible")) {
+ $(sel).select2("destroy");
+ }
+
+ sel.innerHTML = '
';
+
+ if (!clientId) {
+ $(sel).select2({
+ placeholder: "Seleziona...",
+ allowClear: true,
+ width: "100%",
+ });
+ return;
+ }
+
+ const items = await loadFixedFieldOptions(fieldKey, clientId);
+
+ items.forEach((item) => {
+ sel.add(new Option(item.text, item.id));
+ });
+
+ $(sel).select2({
+ placeholder: "Seleziona...",
+ allowClear: true,
+ width: "100%",
+ });
+ }
// Select2 AJAX config factory for SceltaMultipla
function sceltaSelect2Config(fieldId) {
@@ -231,6 +364,7 @@
allowClear: true,
width: "100%",
minimumInputLength: 0,
+ sorter: sortSelect2ResultsByStart,
ajax: {
url: "search_customfield_values.php",
dataType: "json",
@@ -239,7 +373,7 @@
return {
field_id: fieldId,
q: params.term || "",
- limit: 10,
+ limit: 0, // 0 = no limit for custom field values
};
},
processResults: function (data) {
@@ -331,6 +465,28 @@
function createCell(col, rowIndex, cellIndex) {
const div = document.createElement("div");
div.className = "grid-cell editable-cell";
+
+ // Field color classification
+ // Schema/customfield fields are green.
+ // Fixed and standard fields stay white.
+ if (col.type === "detail" || col.type === "main_field") {
+ div.classList.add("schema-field");
+ } else if (col.type === "fixed") {
+ div.classList.add("fixed-field");
+ } else {
+ div.classList.add("standard-field");
+ }
+
+ // Required field classification.
+ // This comes from template_mapping.is_required or template_fixed_mapping.is_required.
+ if (
+ col.isRequired === true ||
+ col.isRequired === 1 ||
+ col.isRequired === "1"
+ ) {
+ div.classList.add("required-field");
+ }
+
div.dataset.col = col.key;
div.dataset.colType = col.type;
div.dataset.row = rowIndex;
@@ -340,13 +496,13 @@
const row = data[rowIndex];
switch (col.type) {
- case "main_field":
- div.innerHTML = createInputHTML(
- col,
- row.mainFieldValue || "",
- rowIndex,
- );
+ case "main_field": {
+ const val = getDetailValue(rowIndex, col.key);
+
+ div.innerHTML = createInputHTML(col, val || "", rowIndex);
+
break;
+ }
case "status": {
const st = row.status || "i";
@@ -396,7 +552,7 @@
case "tested_component":
div.style.overflow = "visible";
div.innerHTML = `
-
+
@@ -442,7 +598,7 @@
const cls = col.isManual ? "manual-input" : "auto-input";
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
- const v = esc(value);
+ const v = escAttr(value);
if (col.dataType === "SceltaMultipla") {
const options = buildDropdownOptionsHTML(col.fieldId, value);
@@ -467,7 +623,7 @@
if (col.dataType === "DATE") {
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
- return ``;
+ return ``;
}
// Client-sourced fields → AJAX Select2 (like idclient)
@@ -480,7 +636,7 @@
const label = clientNameCache[value] || value;
opts += ``;
}
- return ``;
+ return ``;
}
// Select — build from cache
@@ -498,7 +654,7 @@
const reqCls = col.isRequired ? " required-input" : "";
const req = col.isRequired ? " required" : "";
- return ``;
+ return ``;
}
function buildDropdownOptionsHTML(fieldId, selectedValue) {
@@ -703,6 +859,143 @@
flatpickr(this, { dateFormat: "Y-m-d", allowInput: true });
});
}
+ function getInputTextWidth(input) {
+ const span = document.createElement("span");
+ const style = window.getComputedStyle(input);
+
+ span.style.position = "absolute";
+ span.style.visibility = "hidden";
+ span.style.whiteSpace = "pre";
+ span.style.font = style.font;
+ span.style.fontSize = style.fontSize;
+ span.style.fontFamily = style.fontFamily;
+ span.style.fontWeight = style.fontWeight;
+ span.textContent = input.value || input.placeholder || "";
+
+ document.body.appendChild(span);
+
+ const width = span.offsetWidth + 60;
+
+ document.body.removeChild(span);
+
+ return width;
+ }
+
+ function autoExpandColumnFromInput(input) {
+ if (!input) return;
+
+ const cell = input.closest(".grid-cell");
+ if (!cell || !cell.dataset.index) return;
+
+ const columnIndex = parseInt(cell.dataset.index, 10);
+ if (!columnIndex) return;
+
+ const wantedWidth = Math.max(120, getInputTextWidth(input));
+ const currentWidth = cell.offsetWidth || 0;
+
+ // Only expand, do not shrink automatically
+ if (wantedWidth <= currentWidth) return;
+
+ const newWidth = Math.min(wantedWidth, 900);
+
+ const header = document.querySelector(
+ `.grid-header[data-index="${columnIndex}"]`,
+ );
+
+ if (header) {
+ header.style.flex = `0 0 ${newWidth}px`;
+ }
+
+ const topCell = document.querySelector(
+ `.grid-top .grid-cell:nth-child(${columnIndex + 1})`,
+ );
+
+ if (topCell) {
+ topCell.style.flex = `0 0 ${newWidth}px`;
+ }
+
+ const cells = document.querySelectorAll(
+ `.grid-row .grid-cell[data-index="${columnIndex}"]`,
+ );
+
+ cells.forEach((c) => {
+ c.style.flex = `0 0 ${newWidth}px`;
+ });
+
+ const colPos = columnIndex - 1;
+
+ if (columns[colPos]) {
+ columns[colPos].width = newWidth;
+ }
+ }
+
+ function syncVisibleRowsToGridData() {
+ if (!rowContainer) return;
+
+ rowContainer
+ .querySelectorAll(".grid-cell[data-row]")
+ .forEach((cell) => {
+ const rowIndex = parseInt(cell.dataset.row, 10);
+ const row = data[rowIndex];
+
+ if (!row) return;
+
+ const colType = cell.dataset.colType;
+ const colKey = cell.dataset.col;
+ const input = cell.querySelector(".cell-input");
+
+ if (!input) return;
+
+ const value = $(input).hasClass("select2-hidden-accessible")
+ ? $(input).val() || ""
+ : input.value || "";
+
+ if (colType === "detail" || colType === "main_field") {
+ if (!row.details) row.details = {};
+
+ if (
+ String(row.details[String(colKey)] ?? "") !==
+ String(value)
+ ) {
+ row.details[String(colKey)] = value;
+
+ if (colType === "main_field") {
+ row.mainFieldValue = value;
+ }
+
+ row._dirty = true;
+ }
+ } else if (colType === "fixed") {
+ if (!row.fixedFields) row.fixedFields = {};
+
+ if (
+ String(row.fixedFields[colKey] ?? "") !== String(value)
+ ) {
+ row.fixedFields[colKey] = value;
+ row._dirty = true;
+ }
+ } else if (colType === "idclient") {
+ if (String(row.idclient ?? "") !== String(value)) {
+ row.idclient = value;
+ row._dirty = true;
+ }
+ } else if (colType === "cliente_fornitore_id") {
+ if (
+ String(row.cliente_fornitore_id ?? "") !== String(value)
+ ) {
+ row.cliente_fornitore_id = value;
+ row._dirty = true;
+ }
+ } else if (colType === "tested_component") {
+ if (String(row.tested_component ?? "") !== String(value)) {
+ row.tested_component = value;
+ row._dirty = true;
+ }
+ }
+ });
+
+ updateDirtyIndicator();
+ }
// ── Headers & Propagate row ────────────────────────────────────────────
@@ -744,6 +1037,27 @@
columns.forEach((col, colIdx) => {
const cell = document.createElement("div");
cell.className = "grid-cell grid-top-cell";
+
+ // Field color classification for top propagation row
+ // Schema/customfield fields are green.
+ // Fixed and standard fields stay white.
+ if (col.type === "detail" || col.type === "main_field") {
+ cell.classList.add("schema-field");
+ } else if (col.type === "fixed") {
+ cell.classList.add("fixed-field");
+ } else {
+ cell.classList.add("standard-field");
+ }
+
+ // Required field classification also for the top propagation row.
+ if (
+ col.isRequired === true ||
+ col.isRequired === 1 ||
+ col.isRequired === "1"
+ ) {
+ cell.classList.add("required-field");
+ }
+
cell.style.flex = `0 0 ${col.width}px`;
if (
@@ -823,23 +1137,28 @@
}
if (config && config.dependsOn) {
- // For dependent fields: merge all cached values across all clientIds
- const allItems = new Map();
- for (const [key, items] of Object.entries(fixedFieldCache)) {
- if (key.startsWith(fieldKey + "_")) {
- items.forEach((item) =>
- allItems.set(String(item.id), item),
- );
- }
- }
+ // Dependent fixed fields, for example ClienteResponsabile:
+ // use the first row client, not all cached clients.
+ const firstClientId =
+ data[0]?.idclient || meta.defaultIdclient || "";
+
sel.innerHTML = '';
- [...allItems.values()]
- .sort((a, b) =>
- String(a.text).localeCompare(String(b.text), "it", {
- sensitivity: "base",
- }),
- )
- .forEach((item) => sel.add(new Option(item.text, item.id)));
+
+ if (firstClientId) {
+ const items =
+ fixedFieldCache[fieldKey + "_" + firstClientId] || [];
+
+ items.forEach((item) => {
+ sel.add(new Option(item.text, item.id));
+ });
+ }
+
+ $(sel).select2({
+ placeholder: "Seleziona...",
+ allowClear: true,
+ width: "100%",
+ sorter: sortSelect2ResultsByStart,
+ });
} else {
const items = fixedFieldCache[fieldKey] || [];
sel.innerHTML = '';
@@ -887,6 +1206,8 @@
} else if (colType === "idclient") {
data[rowIndex].idclient = value;
data[rowIndex]._dirty = true;
+
+ refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true;
@@ -909,6 +1230,10 @@
const cell = e.target.closest(".grid-cell");
if (!cell || !cell.dataset.row) return;
+ if (e.target.classList.contains("cell-input")) {
+ autoExpandColumnFromInput(e.target);
+ }
+
const rowIndex = parseInt(cell.dataset.row, 10);
const colType = cell.dataset.colType;
@@ -920,6 +1245,16 @@
}
});
+ rowContainer.addEventListener("focusin", function (e) {
+ if (!e.target.classList.contains("cell-input")) return;
+
+ autoExpandColumnFromInput(e.target);
+
+ setTimeout(() => {
+ autoExpandColumnFromInput(e.target);
+ }, 50);
+ });
+
// Persist tested_component before clicking +
document.addEventListener("mousedown", function (e) {
const btn = e.target.closest(".add-part-btn");
@@ -948,62 +1283,149 @@
const btn = e.target.closest(".propagate-btn");
if (!btn) return;
- const colIndex = parseInt(btn.dataset.colIndex);
- const column = btn.dataset.column;
- if (isNaN(colIndex) && !column) return;
+ // Before propagating and re-rendering, persist current visible row values into gridData.
+ syncVisibleRowsToGridData();
- // Get value from the input/select in the same cell
- const cell =
- btn.closest(".grid-cell") || btn.closest(".grid-top-cell");
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+
+ const column = btn.dataset.column || "";
+ const colIndex = Number.isNaN(parseInt(btn.dataset.colIndex, 10))
+ ? null
+ : parseInt(btn.dataset.colIndex, 10);
+
+ if (!column && colIndex === null) return;
+
+ // IMPORTANT:
+ // Read ONLY the input/select inside the same top propagation cell.
+ // Do not scan other top fields.
+ const cell = btn.closest(".grid-top-cell");
if (!cell) return;
- const input = cell.querySelector("select, input");
- if (!input) return;
- const value = $(input).hasClass("select2-hidden-accessible")
- ? $(input).val()
- : input.value;
- // Cache Select2 label so re-render shows name not ID
+ const input = cell.querySelector(".custom-field");
+ if (!input) return;
+
+ const value = $(input).hasClass("select2-hidden-accessible")
+ ? $(input).val() || ""
+ : input.value || "";
+
+ // Do not propagate empty dropdown values.
+ // This prevents wiping rows when a top Select2 is empty/not fully initialized.
+ if (input.tagName === "SELECT" && value === "") {
+ console.warn(
+ "[gridRenderer] Empty select propagation blocked:",
+ column,
+ );
+ return;
+ }
+
+ // Cache selected label so re-render can show text instead of only ID.
if (value && $(input).hasClass("select2-hidden-accessible")) {
const label = $(input).find("option:selected").text();
+
if (label && label !== value) {
- clientNameCache[value] = label;
- // Also cache for SceltaMultipla
+ if (
+ column === "idclient" ||
+ column === "cliente_fornitore_id" ||
+ input.classList.contains("searchable-client")
+ ) {
+ clientNameCache[value] = label;
+ }
+
const fieldId = input.dataset?.fieldId;
- if (fieldId)
+ if (fieldId) {
dropdownNameCache[fieldId + "_" + value] = label;
+ }
}
}
- const col = columns[colIndex] || null;
+ const col = colIndex !== null ? columns[colIndex] : null;
+
+ console.log("[gridRenderer] Propagating ONLY:", {
+ column: column,
+ colIndex: colIndex,
+ value: value,
+ label:
+ input.tagName === "SELECT"
+ ? $(input).find("option:selected").text()
+ : value,
+ });
if (column === "idclient") {
data.forEach((row) => {
+ const oldClientId = row.idclient || "";
+
row.idclient = value;
+
+ // Clear ClienteResponsabile only if client really changed.
+ if (
+ String(oldClientId) !== String(value) &&
+ row.fixedFields &&
+ Object.prototype.hasOwnProperty.call(
+ row.fixedFields,
+ "ClienteResponsabile",
+ )
+ ) {
+ row.fixedFields["ClienteResponsabile"] = "";
+ }
+
row._dirty = true;
});
- } else if (column === "cliente_fornitore_id") {
+
+ loadFixedFieldOptions("ClienteResponsabile", value).then(() => {
+ refreshTopDependentFixedSelect(
+ "ClienteResponsabile",
+ value,
+ );
+ renderVisibleRows();
+ updateDirtyIndicator();
+ });
+
+ return;
+ }
+
+ if (column === "cliente_fornitore_id") {
data.forEach((row) => {
row.cliente_fornitore_id = value;
row._dirty = true;
});
- } else if (column && column.startsWith("fixed_")) {
+
+ renderVisibleRows();
+ updateDirtyIndicator();
+ return;
+ }
+
+ if (column && column.startsWith("fixed_")) {
const fixedKey = column.replace("fixed_", "");
+
data.forEach((row) => {
+ if (!row.fixedFields) row.fixedFields = {};
row.fixedFields[fixedKey] = value;
row._dirty = true;
});
- } else if (col) {
- if (col.type === "detail" || col.type === "main_field") {
- data.forEach((row) => {
- row.details[col.key] = value;
- if (col.type === "main_field")
- row.mainFieldValue = value;
- row._dirty = true;
- });
- }
+
+ renderVisibleRows();
+ updateDirtyIndicator();
+ return;
}
- renderVisibleRows();
+ if (col && (col.type === "detail" || col.type === "main_field")) {
+ data.forEach((row) => {
+ if (!row.details) row.details = {};
+ row.details[col.key] = value;
+
+ if (col.type === "main_field") {
+ row.mainFieldValue = value;
+ }
+
+ row._dirty = true;
+ });
+
+ renderVisibleRows();
+ updateDirtyIndicator();
+ return;
+ }
});
// Select2 change events (don't bubble via native addEventListener)
@@ -1023,6 +1445,8 @@
if (colType === "idclient") {
data[rowIndex].idclient = value;
data[rowIndex]._dirty = true;
+
+ refreshDependentFixedFieldsForRow(rowIndex);
} else if (colType === "cliente_fornitore_id") {
data[rowIndex].cliente_fornitore_id = value;
data[rowIndex]._dirty = true;
@@ -1031,14 +1455,39 @@
updateDirtyIndicator();
});
- // Cache labels on SceltaMultipla change
+ // Handle SceltaMultipla changes and persist them into gridData.
+ // Without this, a later renderVisibleRows() can rebuild the row with the old empty value.
$(rowContainer).on("change", ".searchable-dropdown", function () {
- const val = $(this).val();
+ const cell = this.closest(".grid-cell");
+ if (!cell || !cell.dataset.row) return;
+
+ const rowIndex = parseInt(cell.dataset.row, 10);
+ const colType = cell.dataset.colType;
+ const colKey = cell.dataset.col;
+ const val = $(this).val() || "";
const fieldId = this.dataset.fieldId;
+
+ // Cache label so re-render shows the text instead of only the ID.
if (val && fieldId) {
const label = $(this).find("option:selected").text();
- if (label && label !== val)
+ if (label && label !== val) {
dropdownNameCache[fieldId + "_" + val] = label;
+ }
+ }
+
+ // Persist value into gridData.
+ if (colType === "detail" || colType === "main_field") {
+ if (!data[rowIndex].details) data[rowIndex].details = {};
+
+ data[rowIndex].details[String(colKey)] = val;
+
+ if (colType === "main_field") {
+ data[rowIndex].mainFieldValue = val;
+ }
+
+ data[rowIndex]._dirty = true;
+ cell.classList.add("cell-changed");
+ updateDirtyIndicator();
}
});
diff --git a/public/userarea/import_dashboard.php b/public/userarea/import_dashboard.php
index a35f1cb..cae5980 100644
--- a/public/userarea/import_dashboard.php
+++ b/public/userarea/import_dashboard.php
@@ -10,65 +10,125 @@
Template Buttons - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
@@ -87,14 +147,54 @@
Active Templates
-
-
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+