diff --git a/.gitignore b/.gitignore index 38634bf..0bb0088 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,14 @@ yarn-error.log /public/userarea/logaspi/ /public/userarea/logs/api/ /public/userarea/logs/api/** +/public/userarea/logs/ +/public/userarea/logs/** /public/userarea/photostrf/ /public/userarea/class/*.log /public/userarea/class/curl_auth_debug.log /public/userarea/class/curl_request_debug.log +/public/userarea/schema_dettagli_response.json +public/userarea/schemi_base_response.json # File XLSX temporanei importati /public/userarea/imported_trf/*.xlsx diff --git a/public/userarea/analysisModal.js b/public/userarea/analysisModal.js index bdd1e7d..791121b 100644 --- a/public/userarea/analysisModal.js +++ b/public/userarea/analysisModal.js @@ -431,7 +431,7 @@ const emptyEl = modal.querySelector("#analysisEmptyBox"); const errorEl = modal.querySelector("#analysisErrorBox"); - const webOnly = webOnlyEl ? webOnlyEl.checked : false; + const webOnly = true; const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : ""; let visibleCount = 0; @@ -496,8 +496,10 @@ emptyEl.classList.add("d-none"); } - if (analysisLoadedCache[String(matrixId)]) { - renderAnalysesList(analysisLoadedCache[String(matrixId)]); + const cacheKey = String(matrixId) + "_WEB_ONLY"; + + if (analysisLoadedCache[cacheKey]) { + renderAnalysesList(analysisLoadedCache[cacheKey]); return; } @@ -509,13 +511,21 @@ dataType: "json", data: { id_matrice: matrixId, + web_only: 1, }, }) .done(function (response) { const analyses = Array.isArray(response.value) - ? response.value + ? response.value.filter(function (item) { + return ( + item.SelezionabileSuWeb === true || + item.SelezionabileSuWeb === 1 || + item.SelezionabileSuWeb === "1" + ); + }) : []; - analysisLoadedCache[String(matrixId)] = analyses; + + analysisLoadedCache[cacheKey] = analyses; renderAnalysesList(analyses); }) .fail(function (xhr) { @@ -674,12 +684,7 @@ }); } - const webOnlyEl = modal.querySelector("#analysisWebOnly"); - if (webOnlyEl) { - webOnlyEl.addEventListener("change", function () { - filterAnalysisList(); - }); - } + // WEB only is now fixed by default const searchEl = modal.querySelector("#analysisSearchInput"); if (searchEl) { diff --git a/public/userarea/annotationsModal.js b/public/userarea/annotationsModal.js index ad66692..a9443f1 100644 --- a/public/userarea/annotationsModal.js +++ b/public/userarea/annotationsModal.js @@ -22,6 +22,8 @@ $(document).ready(function () { let partsListData = []; // DIMENSIONE GLOBALE MARKER let globalMarkerSize = 16; + // COLORE TESTO LISTA DESCRIZIONI + let globalDescriptionColor = "#000000"; // =================== // MODAL INITIALIZATION @@ -138,6 +140,7 @@ $(document).ready(function () { descriptionTextbox = null; markerObjects = {}; globalMarkerSize = 16; + globalDescriptionColor = "#000000"; $("#photoSelectorContainerAnnotations").empty().hide(); $("#samplePhotoAnnotations").attr("src", ""); $("#partsListAnnotations").empty(); @@ -177,7 +180,25 @@ $(document).ready(function () { updateMarkers(); markUnsaved(); }); + // =================== + // COLORE LISTA DESCRIZIONI + // =================== + $(document) + .off("input.descriptionColor", "#descriptionColorPickerAnnotations") + .on( + "input.descriptionColor", + "#descriptionColorPickerAnnotations", + function () { + globalDescriptionColor = $(this).val(); + if (descriptionTextbox && fabricCanvas) { + descriptionTextbox.set("fill", globalDescriptionColor); + fabricCanvas.renderAll(); + } + + markUnsaved(); + }, + ); // =================== // PHOTO LOADERS // =================== @@ -614,17 +635,11 @@ $(document).ready(function () { partsListData.forEach((part) => { const partNumber = part.part_number; const partDescription = part.part_description; + const isMixPart = String(part.mix || "N").toUpperCase() === "Y"; + const partColor = - partColors[partNumber] || - (partDescription.toLowerCase().startsWith("mix") - ? "#0000ff" - : "#ff0000"); - if ( - partNumber && - partDescription && - (showMixParts || - !partDescription.toLowerCase().startsWith("mix")) - ) { + partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000"); + if (partNumber && partDescription && (showMixParts || !isMixPart)) { const colorOptions = predefinedColors .map( (color) => @@ -922,11 +937,10 @@ $(document).ready(function () { ) { partsListData = response.parts; response.parts.forEach((part) => { - const defaultColor = part.part_description - .toLowerCase() - .startsWith("mix") - ? "#0000ff" - : "#ff0000"; + const isMixPart = + String(part.mix || "N").toUpperCase() === "Y"; + const defaultColor = isMixPart ? "#0000ff" : "#ff0000"; + partColors[part.part_number] = defaultColor; }); updatePartsList(); @@ -1003,11 +1017,10 @@ $(document).ready(function () { (p) => p.part_number == marker.partNumber, ); const partDescription = part ? part.part_description : ""; - if ( - !showMixParts && - partDescription && - partDescription.toLowerCase().startsWith("mix") - ) { + const isMixPart = + part && String(part.mix || "N").toUpperCase() === "Y"; + + if (!showMixParts && isMixPart) { console.log("Ignoro marker per parte Mix:", marker.partNumber); return; } @@ -1115,11 +1128,10 @@ $(document).ready(function () { } const partsList = partsListData - .filter( - (part) => - showMixParts || - !part.part_description.toLowerCase().startsWith("mix"), - ) + .filter((part) => { + const isMixPart = String(part.mix || "N").toUpperCase() === "Y"; + return showMixParts || !isMixPart; + }) .map((part) => `${part.part_number} ${part.part_description}`); const text = partsList.join("\n"); @@ -1139,7 +1151,7 @@ $(document).ready(function () { backgroundColor: "transparent", fontFamily: "Arial", fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)), - fill: "#000000", + fill: globalDescriptionColor, padding: 10, editable: false, selectable: true, diff --git a/public/userarea/apply_routine.php b/public/userarea/apply_routine.php index 601fb8f..64670f3 100644 --- a/public/userarea/apply_routine.php +++ b/public/userarea/apply_routine.php @@ -3,6 +3,9 @@ ob_start(); session_start(); require_once '../../vendor/autoload.php'; +Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad(); +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + $response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []]; try { diff --git a/public/userarea/class/VisualLimsApiClient.class.php b/public/userarea/class/VisualLimsApiClient.class.php index 61bd945..369d585 100644 --- a/public/userarea/class/VisualLimsApiClient.class.php +++ b/public/userarea/class/VisualLimsApiClient.class.php @@ -255,4 +255,39 @@ class VisualLimsApiClient { return $this->baseUrl; } + + /** + * Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi + */ + public function getRaw($endpoint) + { + $token = $this->getToken(); + + // IMPORTANTE: usa /odata/ e NON /api/odata/ + $url = "{$this->baseUrl}/odata/{$endpoint}"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer {$token}", + "Accept: */*" + ]); + 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 cURL: " . $curl_error); + } + + if ($http_code !== 200) { + throw new Exception("HTTP {$http_code} su endpoint: " . $url); + } + + return $response; + } } diff --git a/public/userarea/class/db-functions.php b/public/userarea/class/db-functions.php index e16888a..18ff684 100644 --- a/public/userarea/class/db-functions.php +++ b/public/userarea/class/db-functions.php @@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php'; use Dotenv\Dotenv; +Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad(); +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + class DBHandlerSelect { private static $instance = null; diff --git a/public/userarea/clone_parts_to_visible.php b/public/userarea/clone_parts_to_visible.php index b3fa6a0..6c8eb27 100644 --- a/public/userarea/clone_parts_to_visible.php +++ b/public/userarea/clone_parts_to_visible.php @@ -28,11 +28,13 @@ try { // 1. Load source parts $stmtParts = $pdo->prepare(" - SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry - FROM identification_parts - WHERE iddatadb = ? - ORDER BY part_number ASC, id ASC - "); + SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry + FROM identification_parts + WHERE iddatadb = ? + AND part_description IS NOT NULL + AND TRIM(part_description) <> '' + ORDER BY part_number ASC, id ASC + "); $stmtParts->execute([$sourceIddatadb]); $sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC); diff --git a/public/userarea/clone_template.php b/public/userarea/clone_template.php new file mode 100644 index 0000000..4f75640 --- /dev/null +++ b/public/userarea/clone_template.php @@ -0,0 +1,228 @@ +getConnection(); + + $pdo->beginTransaction(); + + // 1. Get source template + $stmt = $pdo->prepare(" + SELECT + name, + source_type, + header_row, + start_column, + description, + target_table, + sample_xlsx, + button_size, + button_bg_color, + button_text_color, + button_label, + status, + client_specific_fields, + idclient, + clientname, + schemaname, + idschema, + schemajson, + xls_headers, + api_sample_json, + json_nodes, + idroutine + FROM excel_templates + WHERE id = ? + LIMIT 1 + "); + $stmt->execute([$sourceTemplateId]); + $template = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$template) { + throw new Exception("Template not found."); + } + + // 2. Generate unique clone name + $baseName = 'Copia di ' . $template['name']; + $newName = $baseName; + + $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?"); + $checkStmt->execute([$newName]); + + if ((int)$checkStmt->fetchColumn() > 0) { + $counter = 2; + + do { + $newName = $baseName . ' (' . $counter . ')'; + $checkStmt->execute([$newName]); + $exists = (int)$checkStmt->fetchColumn() > 0; + $counter++; + } while ($exists); + } + + // 3. Insert cloned template + $insertTemplate = $pdo->prepare(" + INSERT INTO excel_templates ( + name, + source_type, + created_at, + updated_at, + header_row, + start_column, + description, + target_table, + sample_xlsx, + button_size, + button_bg_color, + button_text_color, + button_label, + status, + client_specific_fields, + idclient, + clientname, + schemaname, + idschema, + schemajson, + xls_headers, + api_sample_json, + json_nodes, + idroutine + ) VALUES ( + :name, + :source_type, + NOW(), + NOW(), + :header_row, + :start_column, + :description, + :target_table, + :sample_xlsx, + :button_size, + :button_bg_color, + :button_text_color, + :button_label, + :status, + :client_specific_fields, + :idclient, + :clientname, + :schemaname, + :idschema, + :schemajson, + :xls_headers, + :api_sample_json, + :json_nodes, + :idroutine + ) + "); + + $insertTemplate->execute([ + ':name' => $newName, + ':source_type' => $template['source_type'], + ':header_row' => $template['header_row'], + ':start_column' => $template['start_column'], + ':description' => $template['description'], + ':target_table' => $template['target_table'], + ':sample_xlsx' => $template['sample_xlsx'], + ':button_size' => $template['button_size'], + ':button_bg_color' => $template['button_bg_color'], + ':button_text_color' => $template['button_text_color'], + ':button_label' => $template['button_label'], + ':status' => $template['status'], + ':client_specific_fields' => $template['client_specific_fields'], + ':idclient' => $template['idclient'], + ':clientname' => $template['clientname'], + ':schemaname' => $template['schemaname'], + ':idschema' => $template['idschema'], + ':schemajson' => $template['schemajson'], + ':xls_headers' => $template['xls_headers'], + ':api_sample_json' => $template['api_sample_json'], + ':json_nodes' => $template['json_nodes'], + ':idroutine' => $template['idroutine'] + ]); + + $newTemplateId = (int)$pdo->lastInsertId(); + + if ($newTemplateId <= 0) { + throw new Exception("Unable to create cloned template."); + } + + // 4. Clone template_mapping rows + $cloneMappings = $pdo->prepare(" + INSERT INTO template_mapping ( + template_id, + schema_id, + field_id, + excel_column, + json_node, + is_manual, + manual_default, + auto_value, + data_type, + is_required, + default_value, + has_list, + length, + decimals, + min_value, + max_value, + default_curr_date, + tablename, + field_label, + main_field, + is_visible_import, + is_visible_parts + ) + SELECT + :new_template_id, + schema_id, + field_id, + excel_column, + json_node, + is_manual, + manual_default, + auto_value, + data_type, + is_required, + default_value, + has_list, + length, + decimals, + min_value, + max_value, + default_curr_date, + tablename, + field_label, + main_field, + is_visible_import, + is_visible_parts + FROM template_mapping + WHERE template_id = :source_template_id + "); + + $cloneMappings->execute([ + ':new_template_id' => $newTemplateId, + ':source_template_id' => $sourceTemplateId + ]); + + $pdo->commit(); + + header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId); + exit; +} catch (Exception $e) { + if (isset($pdo) && $pdo->inTransaction()) { + $pdo->rollBack(); + } + + die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')); +} diff --git a/public/userarea/cronfiles/get_matrici_cron.php b/public/userarea/cronfiles/get_matrici_cron.php index b118694..bce2c7b 100644 --- a/public/userarea/cronfiles/get_matrici_cron.php +++ b/public/userarea/cronfiles/get_matrici_cron.php @@ -17,6 +17,8 @@ try { exit(1); } +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + // Recupera le variabili d'ambiente $dbHost = $_ENV['DB_HOST']; $dbName = $_ENV['DB_DATABASE']; diff --git a/public/userarea/download_rapporto_file.php b/public/userarea/download_rapporto_file.php new file mode 100644 index 0000000..14ea0ae --- /dev/null +++ b/public/userarea/download_rapporto_file.php @@ -0,0 +1,112 @@ +get("RapportoFile({$id})", []); + + file_put_contents( + $debugDir . "rapporto_file_{$id}.json", + json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + + /** + * Provo vari nomi campo possibili per il contenuto PDF. + * Dopo il primo test potremo adattarlo al nome esatto. + */ + $possibleContentFields = [ + 'File', + 'file', + 'Content', + 'content', + 'FileContent', + 'fileContent', + 'Contenuto', + 'contenuto', + 'Bytes', + 'bytes' + ]; + + $base64 = null; + + foreach ($possibleContentFields as $field) { + if (!empty($data[$field])) { + $base64 = $data[$field]; + break; + } + } + + if (!$base64) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'success' => false, + 'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.', + 'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.', + 'data' => $data + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + + /** + * Se il contenuto ha prefisso data URI, lo pulisco. + */ + if (strpos($base64, 'base64,') !== false) { + $parts = explode('base64,', $base64); + $base64 = end($parts); + } + + $pdfBinary = base64_decode($base64, true); + + if ($pdfBinary === false) { + throw new Exception("Il contenuto trovato non è un base64 valido"); + } + + $filename = $data['NomeFile'] + ?? $data['nomeFile'] + ?? $data['FileName'] + ?? $data['fileName'] + ?? "rapporto_{$id}.pdf"; + + if (!str_ends_with(strtolower($filename), '.pdf')) { + $filename .= '.pdf'; + } + + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="' . basename($filename) . '"'); + header('Content-Length: ' . strlen($pdfBinary)); + + echo $pdfBinary; +} catch (Exception $e) { + http_response_code(500); + header('Content-Type: application/json; charset=utf-8'); + + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); +} diff --git a/public/userarea/edit_template_xls.php b/public/userarea/edit_template_xls.php index ef817d2..9677ba6 100644 --- a/public/userarea/edit_template_xls.php +++ b/public/userarea/edit_template_xls.php @@ -25,6 +25,51 @@ if (!$template) { $stmt = $pdo->prepare("SELECT * FROM routine"); $stmt->execute(); $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Retrieve active API/JSON configurations +$stmt = $pdo->prepare(" + SELECT id, name, provider_code, api_type, php_class_name + FROM api_configurations + WHERE is_active = 1 + ORDER BY name ASC +"); +$stmt->execute(); +$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$buttonBgPalette = [ + '#0d6efd' => 'Blue', + '#6610f2' => 'Indigo', + '#6f42c1' => 'Purple', + '#d63384' => 'Pink', + '#dc3545' => 'Red', + '#fd7e14' => 'Orange', + '#ffc107' => 'Yellow', + '#198754' => 'Green', + '#20c997' => 'Teal', + '#0dcaf0' => 'Cyan', + '#212529' => 'Dark', + '#6c757d' => 'Gray', + '#495057' => 'Slate', + '#795548' => 'Brown', + '#2f5d50' => 'Sage Green', +]; + +$buttonTextPalette = [ + '#ffffff' => 'White', + '#6c757d' => 'Gray', + '#000000' => 'Black', +]; + +$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff'); +$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff'); + +if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) { + $currentButtonBgColor = '#0d6efd'; +} + +if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) { + $currentButtonTextColor = '#ffffff'; +} ?> @@ -35,6 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); + Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> @@ -88,6 +191,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); Choose the source used by this template @@ -102,6 +207,60 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); +
+ + + + Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on. + +
+ + +
@@ -123,12 +282,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
- + +
+ $colorLabel): ?> + + + + +
- + +
+ $colorLabel): ?> + + + + +
@@ -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
- -
+ + + +
+
+
+
Loading XLS templates...
+
+
+ +
+
+
Loading API templates...
+
+
+ +
+
+
Loading PDF templates...
+
+
+
+
@@ -104,46 +204,137 @@ diff --git a/public/userarea/import_edit2.php b/public/userarea/import_edit2.php index f2dbcfa..acd2f35 100644 --- a/public/userarea/import_edit2.php +++ b/public/userarea/import_edit2.php @@ -1,10 +1,6 @@
- Template ID: , Start Row: , Start Column: + + Template ID: , + Sheet Number: , + Start Row: , + Start Column: +
@@ -244,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true)); const templateId = ; console.log('Template ID passed to formData:', templateId); formData.append('template_id', templateId); - formData.append('header_row', ); - formData.append('start_column', ); + formData.append('header_row', ); + formData.append('start_column', ); + formData.append('xls_sheet_index', ); fetch('process_import_xls2.php', { method: 'POST', @@ -331,8 +337,8 @@ error_log("Loaded template: " . print_r($template, true));
- - + + @@ -383,6 +389,42 @@ error_log("Loaded template: " . print_r($template, true)); `; tableContainer.innerHTML = html; + const selectRowsForm = document.getElementById('selectRowsForm'); + + selectRowsForm.addEventListener('submit', function(e) { + const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked')); + + if (checkedBoxes.length === 0) { + e.preventDefault(); + alert('Seleziona almeno una riga.'); + return; + } + + const selectedRows = []; + const selectedExcelRows = []; + + checkedBoxes.forEach((cb, newIndex) => { + const originalIndex = parseInt(cb.value, 10); + + if (data.rows && data.rows[originalIndex]) { + selectedRows.push(data.rows[originalIndex]); + } + + if (data.excel_data && data.excel_data[originalIndex]) { + selectedExcelRows.push(data.excel_data[originalIndex].excelrow); + } + + // Reindex selected_rows so import_insert.php receives only the reduced rows array + cb.value = newIndex; + }); + + document.getElementById('selectedRowsData').value = + encodeURIComponent(JSON.stringify(selectedRows)); + + document.getElementById('selectedExcelRowsData').value = + encodeURIComponent(JSON.stringify(selectedExcelRows)); + }); + const topTableScrollbar = document.getElementById('topTableScrollbar'); const topTableScrollbarInner = document.getElementById('topTableScrollbarInner'); const mainTableContainer = document.getElementById('mainTableContainer'); diff --git a/public/userarea/imported.php b/public/userarea/imported.php index dac2e22..3ba13d9 100644 --- a/public/userarea/imported.php +++ b/public/userarea/imported.php @@ -1,10 +1,6 @@ getConnection(); // Recupera tutti i mapping dal template, includendo is_visible_import -$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, is_visible_import, auto_value +$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, field_order, main_field, is_visible_import, auto_value FROM template_mapping -WHERE template_id = ?"); +WHERE template_id = ? +ORDER BY field_order ASC, id ASC"); $stmt->execute([$template_id]); $allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -59,15 +56,22 @@ if (empty($allMappings)) { exit; } -// Trova il campo main_field -$mainFieldMapping = null; +// Find up to 2 main fields +$mainFieldMappings = []; + foreach ($allMappings as $mapping) { - if ($mapping['main_field'] == 1 && $mapping['is_visible_import'] == 1) { - $mainFieldMapping = $mapping; + if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) { + $mainFieldMappings[] = $mapping; + } + + if (count($mainFieldMappings) >= 2) { break; } } +// Backward compatibility: first main field +$mainFieldMapping = $mainFieldMappings[0] ?? null; + // Recupera l'idclient di default dal template (se presente) $template_stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?"); $template_stmt->execute([$template_id]); @@ -95,7 +99,7 @@ $stmt = $pdo->prepare(" FROM datadb d LEFT JOIN auth_users u ON d.user_id = u.id {$baseWhere} - ORDER BY d.iddatadb DESC + ORDER BY d.excelrow ASC, d.iddatadb ASC {$limitClause} "); $stmt->execute($baseParams); @@ -228,11 +232,18 @@ foreach ($importedData as $index => $row) { $rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? ''; } - // Main field value + // Main field values + foreach ($mainFieldMappings as $mainMapping) { + $mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainMapping['id']); + $mainDetail = reset($mainDetail) ?: ['field_value' => $mainMapping['manual_default'] ?? '']; + + $rowObj['details'][(string)$mainMapping['id']] = + $mainDetail['field_value'] ?? $mainMapping['manual_default'] ?? ''; + } + + // Backward compatibility: first main value if ($mainFieldMapping) { - $mainDetail = array_filter($rowDetails, fn($d) => $d['mapping_id'] == $mainFieldMapping['id']); - $mainDetail = reset($mainDetail) ?: ['field_value' => $mainFieldMapping['manual_default'] ?? '']; - $rowObj['mainFieldValue'] = $mainDetail['field_value'] ?? $mainFieldMapping['manual_default'] ?? ''; + $rowObj['mainFieldValue'] = $rowObj['details'][(string)$mainFieldMapping['id']] ?? ''; } $rowObj['_dirty'] = false; @@ -242,18 +253,27 @@ foreach ($importedData as $index => $row) { // Build columns in display order $gridColumns = []; -// 1. Main field -if ($mainFieldMapping) { - $gridColumns[] = [ - 'type' => 'main_field', - 'key' => (string)$mainFieldMapping['id'], - 'label' => $mainFieldMapping['field_label'], - 'dataType' => $mainFieldMapping['data_type'], - 'isManual' => (bool)$mainFieldMapping['is_manual'], - 'isRequired' => (bool)$mainFieldMapping['is_required'], - 'fieldId' => $mainFieldMapping['field_id'] ?? null, - 'width' => 150, - ]; +// 1. Main fields first, immediately after buttons +foreach ($allMappings as $mapping) { + if ( + (int)$mapping['is_visible_import'] === 1 + && (string)$mapping['main_field'] === '1' + && trim((string)$mapping['field_label']) !== 'Tested Component:' + ) { + $gridColumns[] = [ + 'type' => 'main_field', + 'key' => (string)$mapping['id'], + 'label' => $mapping['field_label'], + 'dataType' => $mapping['data_type'], + 'isManual' => (bool)$mapping['is_manual'], + 'isRequired' => (bool)$mapping['is_required'], + 'fieldId' => $mapping['field_id'] ?? null, + 'fieldOrder' => (int)($mapping['field_order'] ?? 9999), + 'manualDefault' => $mapping['manual_default'] ?? '', + 'autoValue' => $mapping['auto_value'] ?? 'none', + 'width' => 150, + ]; + } } // 2. Status @@ -265,50 +285,30 @@ $gridColumns[] = ['type' => 'idclient', 'key' => 'idclient', 'label' => 'Client' // 4. Cliente Fornitore $gridColumns[] = ['type' => 'cliente_fornitore_id', 'key' => 'cliente_fornitore_id', 'label' => $slugMapping['ClienteFornitore'] ?? 'ClienteFornitore', 'width' => 300]; -// 5. Auto fields +// 5. Other custom fields in schema order foreach ($allMappings as $mapping) { if ( - !$mapping['is_manual'] - && $mapping['main_field'] != 1 - && $mapping['is_visible_import'] == 1 + (int)$mapping['is_visible_import'] === 1 + && (string)$mapping['main_field'] !== '1' && trim((string)$mapping['field_label']) !== 'Tested Component:' ) { + $isMainField = ((string)$mapping['main_field'] === '1'); + $gridColumns[] = [ - 'type' => 'detail', + 'type' => $isMainField ? 'main_field' : 'detail', 'key' => (string)$mapping['id'], 'label' => $mapping['field_label'], 'dataType' => $mapping['data_type'], - 'isManual' => false, + 'isManual' => (bool)$mapping['is_manual'], 'isRequired' => (bool)$mapping['is_required'], 'fieldId' => $mapping['field_id'] ?? null, + 'fieldOrder' => (int)($mapping['field_order'] ?? 9999), + 'manualDefault' => $mapping['manual_default'] ?? '', 'autoValue' => $mapping['auto_value'] ?? 'none', 'width' => 150, ]; } } - -// 6. Manual fields -foreach ($allMappings as $mapping) { - if ( - $mapping['is_manual'] - && $mapping['main_field'] != 1 - && $mapping['is_visible_import'] == 1 - && trim((string)$mapping['field_label']) !== 'Tested Component:' - ) { - $gridColumns[] = [ - 'type' => 'detail', - 'key' => (string)$mapping['id'], - 'label' => $mapping['field_label'], - 'dataType' => $mapping['data_type'], - 'isManual' => true, - 'isRequired' => (bool)$mapping['is_required'], - 'fieldId' => $mapping['field_id'] ?? null, - 'manualDefault' => $mapping['manual_default'] ?? '', - 'width' => 150, - ]; - } -} - // 7. Tested Component $gridColumns[] = ['type' => 'tested_component', 'key' => 'tested_component', 'label' => 'Tested Component', 'width' => 150]; @@ -346,7 +346,8 @@ $gridMeta = [ 'slugMapping' => $slugMapping, 'timeLabels' => $timeLabels, 'columns' => $gridColumns, - 'mainFieldMapping' => $mainFieldMapping, + 'mainFieldMapping' => $mainFieldMapping, + 'mainFieldMappings' => $mainFieldMappings, 'totalRows' => count($gridDataArray), ]; @@ -354,6 +355,9 @@ $gridMeta = [ @@ -374,20 +378,10 @@ $gridMeta = [ transition: background-color 0.3s ease; } - input.auto-input, - select.auto-input { - background-color: #d4edda; - } - input.manual-input, - select.manual-input { - background-color: #fff3cd; - } - input.required-input, - select.required-input { - background-color: #f8d7da; - } + + input.required-input, select.required-input { @@ -426,17 +420,9 @@ $gridMeta = [ outline: none !important; } - textarea.auto-input { - background-color: #d4edda; - } - textarea.manual-input { - background-color: #fff3cd; - } - textarea.required-input { - background-color: #f8d7da; - } + .status-badge { display: inline-block; @@ -688,7 +674,33 @@ $gridMeta = [ flex-shrink: 0; } - .grid-row .grid-header:nth-child(2) { + = 2): ?> + + /* Sticky second Main column - only when the template has 2 Main fields */ + .grid-top .grid-cell:nth-child(3), + #gridHeaderContainer .grid-header:nth-child(3), + .grid-row .grid-cell:nth-child(3) { + position: sticky !important; + left: 360px; + z-index: 7; + background: white; + overflow: visible; + flex-shrink: 0; + } + + #gridHeaderContainer .grid-header:nth-child(3) { + background-color: #e9ecef; + } + + .grid-row:nth-child(even) .grid-cell:nth-child(3) { + background-color: #f8f9fa; + } + + .grid-row:hover .grid-cell:nth-child(3) { + background-color: #e9ecef; + } + + .grid-row .grid-header:nth-child(2) { background-color: #e9ecef; } @@ -1044,11 +1056,7 @@ $gridMeta = [ font-style: italic; } - .api-fixed-select.required-input:invalid, - .api-fixed-select[required]:not([value]):not([data-select2-id]) { - background-color: #f8d7da !important; - border-color: #dc3545 !important; - } + /* ── Pagination bar ── */ .pager-bar { @@ -1161,6 +1169,128 @@ $gridMeta = [ color: #adb5bd; user-select: none; } + + /* ========================================================= + FINAL GRID COLORS + Schema/customfield fields = green + Fixed/standard fields = white + Required fields = red border only + ========================================================= */ + + /* Default: all fields white */ + .grid-container input, + .grid-container select, + .grid-container textarea, + .grid-top input, + .grid-top select, + .grid-top textarea { + background-color: #ffffff !important; + color: #333 !important; + } + + /* Schema/customfield fields: green background */ + .grid-container .schema-field input, + .grid-container .schema-field select, + .grid-container .schema-field textarea, + .grid-top .schema-field input, + .grid-top .schema-field select, + .grid-top .schema-field textarea { + background-color: #dff3e3 !important; + } + + /* Fixed and standard fields: white background */ + .grid-container .fixed-field input, + .grid-container .fixed-field select, + .grid-container .fixed-field textarea, + .grid-container .standard-field input, + .grid-container .standard-field select, + .grid-container .standard-field textarea, + .grid-top .fixed-field input, + .grid-top .fixed-field select, + .grid-top .fixed-field textarea, + .grid-top .standard-field input, + .grid-top .standard-field select, + .grid-top .standard-field textarea { + background-color: #ffffff !important; + } + + /* Required fields: red border only */ + .grid-container .required-field input, + .grid-container .required-field select, + .grid-container .required-field textarea, + .grid-top .required-field input, + .grid-top .required-field select, + .grid-top .required-field textarea { + border: 2px solid #dc3545 !important; + box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important; + } + + /* Required schema/customfield fields: green + red border */ + .grid-container .schema-field.required-field input, + .grid-container .schema-field.required-field select, + .grid-container .schema-field.required-field textarea, + .grid-top .schema-field.required-field input, + .grid-top .schema-field.required-field select, + .grid-top .schema-field.required-field textarea { + background-color: #dff3e3 !important; + border: 2px solid #dc3545 !important; + } + + /* Required fixed/standard fields: white + red border */ + .grid-container .fixed-field.required-field input, + .grid-container .fixed-field.required-field select, + .grid-container .fixed-field.required-field textarea, + .grid-container .standard-field.required-field input, + .grid-container .standard-field.required-field select, + .grid-container .standard-field.required-field textarea, + .grid-top .fixed-field.required-field input, + .grid-top .fixed-field.required-field select, + .grid-top .fixed-field.required-field textarea, + .grid-top .standard-field.required-field input, + .grid-top .standard-field.required-field select, + .grid-top .standard-field.required-field textarea { + background-color: #ffffff !important; + border: 2px solid #dc3545 !important; + } + + /* Select2 - schema/customfield fields: green */ + .grid-container .schema-field .select2-container--default .select2-selection--single, + .grid-top .schema-field .select2-container--default .select2-selection--single { + background-color: #dff3e3 !important; + } + + /* Select2 - fixed/standard fields: white */ + .grid-container .fixed-field .select2-container--default .select2-selection--single, + .grid-container .standard-field .select2-container--default .select2-selection--single, + .grid-top .fixed-field .select2-container--default .select2-selection--single, + .grid-top .standard-field .select2-container--default .select2-selection--single { + background-color: #ffffff !important; + } + + /* Select2 - required fields: red border only */ + .grid-container .required-field .select2-container--default .select2-selection--single, + .grid-top .required-field .select2-container--default .select2-selection--single { + border: 2px solid #dc3545 !important; + box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.15) !important; + } + + /* Remove old red background from required classes */ + .grid-container input.required-input, + .grid-container select.required-input, + .grid-container textarea.required-input, + .grid-top input.required-input, + .grid-top select.required-input, + .grid-top textarea.required-input { + background-color: inherit !important; + } + + /* Missing required cell: red outline only */ + .grid-cell.missing-required { + background-color: inherit !important; + border-right: 1px solid #dee2e6 !important; + outline: 2px solid #dc3545 !important; + outline-offset: -2px; + } Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> @@ -1311,6 +1441,9 @@ $gridMeta = [ const topScrollbar = document.getElementById('topScrollbar'); const topScrollbarInner = document.getElementById('topScrollbarInner'); const gridContainer = document.getElementById('gridContainer'); + const gridRowContainer = document.getElementById('gridRowContainer'); + const gridHeaderContainer = document.getElementById('gridHeaderContainer'); + const gridTopContainer = document.getElementById('gridTopContainer'); if (!topScrollbar || !topScrollbarInner || !gridContainer) return; @@ -1318,14 +1451,22 @@ $gridMeta = [ let syncingFromGrid = false; function updateTopScrollbarWidth() { - topScrollbarInner.style.width = gridContainer.scrollWidth + 'px'; + const realWidth = Math.max( + gridContainer.scrollWidth, + gridHeaderContainer ? gridHeaderContainer.scrollWidth : 0, + gridTopContainer ? gridTopContainer.scrollWidth : 0, + gridRowContainer ? gridRowContainer.scrollWidth : 0 + ); - // Mostra la barra solo se serve davvero - if (gridContainer.scrollWidth > gridContainer.clientWidth) { + topScrollbarInner.style.width = realWidth + 'px'; + + if (realWidth > gridContainer.clientWidth) { topScrollbar.style.display = 'block'; } else { topScrollbar.style.display = 'none'; } + + topScrollbar.scrollLeft = gridContainer.scrollLeft; } topScrollbar.addEventListener('scroll', function() { @@ -1342,14 +1483,24 @@ $gridMeta = [ syncingFromGrid = false; }); - updateTopScrollbarWidth(); - window.addEventListener('resize', updateTopScrollbarWidth); - // Ritarda un attimo per sicurezza, visto che la griglia viene renderizzata via JS - setTimeout(updateTopScrollbarWidth, 200); - setTimeout(updateTopScrollbarWidth, 600); - setTimeout(updateTopScrollbarWidth, 1200); + // Recalculate after JS grid rendering + setTimeout(updateTopScrollbarWidth, 100); + setTimeout(updateTopScrollbarWidth, 300); + setTimeout(updateTopScrollbarWidth, 700); + setTimeout(updateTopScrollbarWidth, 1500); + + // Recalculate automatically when rows/header/top controls are rendered or changed + const observer = new MutationObserver(updateTopScrollbarWidth); + + if (gridContainer) { + observer.observe(gridContainer, { + childList: true, + subtree: true, + attributes: true + }); + } }); diff --git a/public/userarea/include/headscript.php b/public/userarea/include/headscript.php index f74cb8b..ab35e05 100644 --- a/public/userarea/include/headscript.php +++ b/public/userarea/include/headscript.php @@ -10,6 +10,9 @@ error_reporting(E_ALL | E_STRICT); include('../../extra/auth.php'); //require_once __DIR__ . '/extra/auth.php'; +// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + // Here we just check if user is not // logged in, and in that case we redirect // the user to vanguard login page. diff --git a/public/userarea/insert_template_xls.php b/public/userarea/insert_template_xls.php index f3f34dd..9217b25 100644 --- a/public/userarea/insert_template_xls.php +++ b/public/userarea/insert_template_xls.php @@ -3,9 +3,20 @@ // Retrieve all routines from database $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); + $stmt = $pdo->prepare("SELECT * FROM routine"); $stmt->execute(); $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Retrieve active API/JSON configurations +$stmt = $pdo->prepare(" + SELECT id, name, provider_code, api_type, php_class_name + FROM api_configurations + WHERE is_active = 1 + ORDER BY name ASC +"); +$stmt->execute(); +$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> @@ -40,7 +51,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
  • Template Name
  • Source Type
  • Schema and Client
  • -
  • Row Header and Column Header only for XLS templates
  • +
  • Row Header, Column Header and Sheet Number only for XLS templates
  • +
  • API / JSON Configuration only for API / JSON templates
  • @@ -67,7 +79,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); Choose the source used by this template @@ -82,6 +95,58 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); +
    + + + + Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on. + +
    + + +
    @@ -185,10 +250,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"); if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) { alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore."); @@ -210,27 +281,57 @@ $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 isApiJson = selectedSource === 'API'; - 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; + xlsSheetIndex.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; + xlsSheetIndex.required = false; + + headerRow.disabled = true; + startColumn.disabled = true; + xlsSheetIndex.disabled = true; + + if (isApiJson) { + 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'); + } } } @@ -261,7 +362,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); data.value.forEach(client => { const nome = client.Nominativo || "Nome non disponibile"; const id = client.IdCliente || "ID non disponibile"; - const option = new Option(`${nome.trim()} (ID: ${id})`, id); + + const codiceCliente = (client.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); select.add(option); }); @@ -388,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC); let formData = new FormData(this); + const selectedSource = sourceType.value; + + if (selectedSource === 'XLS' && xlsSheetIndex.value === '') { + Swal.fire({ + title: "Errore!", + text: "Inserisci il numero del foglio XLS.", + icon: "error", + confirmButtonText: "OK" + }); + return; + } + + if (selectedSource === 'API' && !apiConfigSelect.value) { + Swal.fire({ + title: "Errore!", + text: "Seleziona una configurazione API / JSON.", + icon: "error", + confirmButtonText: "OK" + }); + return; + } + const clientSelect = document.getElementById("clientSelect"); const clientId = clientSelect.value; const selectedClientOption = clientSelect.options[clientSelect.selectedIndex]; diff --git a/public/userarea/load_active_templates.php b/public/userarea/load_active_templates.php index b0d9fb7..29680c9 100644 --- a/public/userarea/load_active_templates.php +++ b/public/userarea/load_active_templates.php @@ -13,7 +13,10 @@ try { } // Recupera solo i template attivi - $stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'"); + $stmt = $pdo->query("SELECT id, button_label, button_size, button_bg_color, button_text_color, source_type +FROM excel_templates +WHERE status = 'active' +ORDER BY button_label ASC"); $templates = $stmt->fetchAll(PDO::FETCH_ASSOC); $response["success"] = true; diff --git a/public/userarea/load_parts.php b/public/userarea/load_parts.php index 6fb0076..59bef0b 100644 --- a/public/userarea/load_parts.php +++ b/public/userarea/load_parts.php @@ -33,9 +33,9 @@ try { 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 + p.id, p.iddatadb, p.part_number, p.part_description, p.mix, 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 @@ -48,8 +48,8 @@ try { ]); } else { $stmt = $pdo->prepare(" - SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry, - NULL AS extra_value_id, NULL AS extra_value_text + SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, + NULL AS extra_value_id, NULL AS extra_value_text FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC diff --git a/public/userarea/mapping_template_xls_scheme2.php b/public/userarea/mapping_template_xls_scheme2.php index c298501..f36ca48 100644 --- a/public/userarea/mapping_template_xls_scheme2.php +++ b/public/userarea/mapping_template_xls_scheme2.php @@ -7,7 +7,27 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) { $id = intval($_GET['id']); $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); -$stmt = $pdo->prepare("SELECT name, header_row, start_column, target_table, sample_xlsx, idclient, clientname, idschema, schemaname, schemajson, xls_headers FROM excel_templates WHERE id = ?"); + +$stmt = $pdo->prepare(" + SELECT + name, + header_row, + start_column, + target_table, + source_type, + xls_sheet_index, + sample_xlsx, + idclient, + clientname, + idschema, + schemaname, + schemajson, + xls_headers, + api_sample_json, + json_nodes + FROM excel_templates + WHERE id = ? +"); $stmt->execute([$id]); $template = $stmt->fetch(PDO::FETCH_ASSOC); @@ -15,13 +35,53 @@ if (!$template) { die("Template not found"); } +$sourceType = strtoupper($template['source_type'] ?? 'XLS'); +if (!in_array($sourceType, ['XLS', 'API', 'PDF'], true)) { + $sourceType = 'XLS'; +} + +$xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null + ? (int)$template['xls_sheet_index'] + : 0; + +if ($xlsSheetIndex < 0) { + $xlsSheetIndex = 0; +} + $clientName = $template['clientname'] ?: ''; $schemaName = $template['schemaname'] ?: ''; $schemajson = $template['schemajson'] ? json_decode($template['schemajson'], true) : []; -$isSchemajsonEmpty = empty(trim($template['schemajson'])); +$isSchemajsonEmpty = empty(trim($template['schemajson'] ?? '')); // Recupera i campi dalla tabella template_mapping -$stmt = $pdo->prepare("SELECT id, field_id, excel_column, is_manual, manual_default, auto_value, data_type, is_required, default_value, has_list, length, decimals, min_value, max_value, default_curr_date, tablename, field_label, main_field, is_visible_import, is_visible_parts FROM template_mapping WHERE template_id = ?"); +$stmt = $pdo->prepare(" + SELECT + id, + field_id, + field_order, + excel_column, + json_node, + is_manual, + manual_default, + auto_value, + data_type, + is_required, + default_value, + has_list, + length, + decimals, + min_value, + max_value, + default_curr_date, + tablename, + field_label, + main_field, + is_visible_import, + is_visible_parts + FROM template_mapping + WHERE template_id = ? + ORDER BY field_order ASC, id ASC +"); $stmt->execute([$id]); $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -37,11 +97,25 @@ $fixedMappings = $stmt->fetchAll(PDO::FETCH_ASSOC); $hasFixedMappings = !empty($fixedMappings); -// Recupera le colonne già associate nel database +// Recupera le colonne XLS già associate nel database $usedColumnsFromDB = array_filter(array_column($mappings, 'excel_column')); +// Recupera i nodi JSON già associati nel database +$usedJsonNodesFromDB = array_filter(array_column($mappings, 'json_node')); + // Decodifica l'header XLS salvato, se presente $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], true) : []; +if (!is_array($xlsHeaders)) { + $xlsHeaders = []; +} + +// Decodifica nodi JSON API salvati, se presenti +$jsonNodes = $template['json_nodes'] ? json_decode($template['json_nodes'], true) : []; +if (!is_array($jsonNodes)) { + $jsonNodes = []; +} + +$apiSampleJson = $template['api_sample_json'] ?? ''; ?> @@ -133,28 +207,37 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t } /* Type */ + /* Order */ #schemaFieldsTable th:nth-child(5), #schemaFieldsTable td:nth-child(5) { + width: 70px; + text-align: center; + white-space: nowrap; + } + + /* Type */ + #schemaFieldsTable th:nth-child(6), + #schemaFieldsTable td:nth-child(6) { width: 120px; white-space: nowrap; } /* Mapping = wide but NOT insane */ - #schemaFieldsTable th:nth-child(6), - #schemaFieldsTable td:nth-child(6) { + #schemaFieldsTable th:nth-child(7), + #schemaFieldsTable td:nth-child(7) { width: 380px; } /* Default Value = wider */ - #schemaFieldsTable th:nth-child(7), - #schemaFieldsTable td:nth-child(7) { + #schemaFieldsTable th:nth-child(8), + #schemaFieldsTable td:nth-child(8) { 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 { + #schemaFieldsTable td:nth-child(7) .form-select, + #schemaFieldsTable td:nth-child(8) .form-control, + #schemaFieldsTable td:nth-child(8) .form-select { width: 100% !important; } @@ -163,7 +246,8 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t width: 100% !important; } - .xls-columns option.used-option { + .xls-columns option.used-option, + .json-nodes option.used-option { background-color: #fff3cd; color: #856404; font-weight: 600; @@ -176,6 +260,52 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t overflow: hidden; text-overflow: ellipsis; } + + #apiJsonExample { + font-family: Consolas, Monaco, monospace; + font-size: 13px; + } + + .select2-container--default .select2-results__option { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 520px; + } + + .select2-json-node { + font-weight: 600; + } + + .select2-json-value { + color: #6c757d; + font-size: 12px; + margin-left: 8px; + } + + .select2-json-row { + display: block; + margin: -6px; + padding: 6px; + } + + .select2-results__option:has(.select2-json-row.used-option) { + background-color: #fff3cd !important; + color: #856404 !important; + font-weight: 600; + } + + .select2-results__option:has(.select2-json-row.used-option).select2-results__option--highlighted { + background-color: #ffe69c !important; + color: #856404 !important; + } + + .select2-json-row.used-option .select2-json-node::after { + content: " (already used)"; + color: #856404; + font-size: 12px; + font-weight: 600; + } @@ -195,30 +325,64 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t

    Client: | Schema: | - Header Row: | - Start Column: + Source: + + | + Sheet Number: | + Header Row: | + Start Column: +

    -
    - -
    - - + + +
    + +
    + + +
    + + + ✅ Current file: + + No file uploaded yet. + +
    - - - ✅ Current file: - - No file uploaded yet. - - -
    + +
    + + + +
    + + + + + ✅ JSON nodes already loaded: + + No JSON nodes loaded yet. + + +
    +
    + +
    + PDF source type is not implemented yet. +
    +
    @@ -235,16 +399,13 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t Import Parts Title + Order Type - Mapping + Default Value - - - - @@ -268,7 +429,6 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t > - @@ -276,6 +436,11 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t + + + + + @@ -286,9 +451,19 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t $autoValue = $mapping['auto_value'] ?? 'none'; $hasAuto = ($autoValue && $autoValue !== 'none'); - $mappingValue = $isSceltaMultipla - ? 'manual' - : ($hasAuto ? 'auto' : ($mapping['excel_column'] ? 'xls' : ((int)$mapping['is_manual'] === 1 ? 'manual' : ''))); + if ($isSceltaMultipla) { + $mappingValue = 'manual'; + } elseif ($hasAuto) { + $mappingValue = 'auto'; + } elseif ($sourceType === 'XLS' && !empty($mapping['excel_column'])) { + $mappingValue = 'xls'; + } elseif ($sourceType === 'API' && !empty($mapping['json_node'])) { + $mappingValue = 'json'; + } elseif ((int)$mapping['is_manual'] === 1) { + $mappingValue = 'manual'; + } else { + $mappingValue = ''; + } ?> - + + + + + + + - + () @@ -330,6 +521,15 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t style="margin-left:5px;"> X + + + () + + @@ -412,7 +612,6 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t - @@ -461,8 +660,6 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
    - - @@ -479,38 +676,47 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t - - + \ No newline at end of file diff --git a/public/userarea/modal_analysis.php b/public/userarea/modal_analysis.php index ff5b223..50895b6 100644 --- a/public/userarea/modal_analysis.php +++ b/public/userarea/modal_analysis.php @@ -259,11 +259,9 @@ $matrixGroups = array_values($matrixGroups);
    -
    - - + +
    + Showing WEB analyses only
    diff --git a/public/userarea/modal_annotations.php b/public/userarea/modal_annotations.php index 35f5135..47fb077 100644 --- a/public/userarea/modal_annotations.php +++ b/public/userarea/modal_annotations.php @@ -46,6 +46,16 @@
    Foto del Campione
    @@ -290,28 +319,20 @@ white-space: nowrap; } - /* Select delle righe (colonna Matrice) = 150px */ + /* Select delle righe Matrice: si adatta alla colonna */ .part-matrice { - width: 300px !important; - min-width: 300px !important; - max-width: 300px !important; - flex: 0 0 300px !important; + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; } .part-matrice.select2-hidden-accessible+.select2 { - width: 300px !important; - min-width: 300px !important; - max-width: 300px !important; - flex: 0 0 300px !important; + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; } - /* Colonna Descrizione (2ª colonna) = 420px */ - #partsTable th:nth-child(2), - #partsTable td:nth-child(2) { - width: 300px !important; - min-width: 300px !important; - max-width: 300px !important; - } + #partsTable .part-number { text-align: center; @@ -342,7 +363,11 @@ border: 1px solid #aaa !important; border-radius: 4px !important; background: #fff !important; - max-height: 200px !important; + overflow: visible !important; + } + + .select2-container--open .select2-results__options { + max-height: 220px !important; overflow-y: auto !important; } @@ -516,6 +541,202 @@ } } + /* Resizable parts table - stable */ + .parts-table-scroll { + width: 100%; + overflow-x: auto; + overflow-y: visible; + contain: inline-size; + } + + #partsTable { + table-layout: fixed !important; + width: max-content !important; + min-width: unset !important; + } + + #partsTable col.parts-col-num { + width: 55px; + } + + #partsTable col.parts-col-description { + width: 320px; + } + + #partsTable col.parts-col-matrice { + width: 360px; + } + + #partsTable col.parts-col-date { + width: 150px; + } + + #partsTable col.parts-col-actions { + width: 230px; + } + + #partsTable th, + #partsTable td { + position: relative; + box-sizing: border-box; + } + + #partsTable th { + user-select: none; + } + + #partsTable th .parts-resizer { + position: absolute; + top: 0; + right: 0; + width: 9px; + height: 100%; + cursor: col-resize; + z-index: 50; + background: rgba(108, 117, 125, 0.12); + border-right: 1px solid rgba(108, 117, 125, 0.45); + } + + #partsTable th .parts-resizer:hover { + background: rgba(108, 117, 125, 0.25); + border-right: 2px solid rgba(73, 80, 87, 0.7); + } + + #partsTable td { + overflow: hidden; + text-overflow: ellipsis; + } + + #partsTable td input, + #partsTable td select, + #partsTable td .select2-container { + max-width: 100% !important; + } + + /* Propagation control for dynamic extra field column */ + #partsTable th.extra-field-th { + vertical-align: middle; + } + + #partsTable th.extra-field-th .extra-propagate-wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + } + + #partsTable th.extra-field-th .extra-propagate-label { + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 95px; + } + + #partsTable th.extra-field-th .extra-propagate-input, + #partsTable th.extra-field-th .extra-propagate-select { + height: 24px !important; + font-size: 0.75rem !important; + padding: 0.1rem 0.25rem !important; + min-width: 90px; + flex: 1 1 auto; + } + + #partsTable th.extra-field-th .extra-propagate-btn { + height: 24px; + min-width: 26px; + padding: 0.1rem 0.3rem !important; + font-size: 0.75rem !important; + flex: 0 0 auto; + } + + #partsTable th.extra-field-th .select2-container { + flex: 1 1 auto; + min-width: 90px; + max-width: 100% !important; + } + + #partsTable th.extra-field-th .select2-selection--single { + height: 24px !important; + } + + #partsTable th.extra-field-th .select2-selection__rendered { + line-height: 22px !important; + font-size: 0.75rem !important; + padding-left: 4px !important; + } + + #partsTable th.extra-field-th .select2-selection__arrow { + height: 22px !important; + } + + /* Extra field propagation header - full column width */ + #partsTable th.extra-field-th { + width: auto !important; + min-width: 220px !important; + max-width: none !important; + padding: 0.25rem !important; + } + + #partsTable .extra-propagate-wrapper { + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; + display: flex; + flex-direction: column; + gap: 4px; + box-sizing: border-box; + } + + #partsTable .extra-propagate-label { + width: 100% !important; + max-width: 100% !important; + display: block; + font-size: 0.82rem; + font-weight: 700; + line-height: 1.15; + color: #222; + white-space: nowrap !important; + overflow: visible !important; + text-overflow: unset !important; + text-align: center; + } + + #partsTable .extra-propagate-control { + width: 100% !important; + display: flex; + align-items: center; + gap: 5px; + box-sizing: border-box; + } + + #partsTable .extra-propagate-control .extra-propagate-input, + #partsTable .extra-propagate-control .extra-propagate-select { + flex: 1 1 auto !important; + width: 100% !important; + min-width: 0 !important; + max-width: none !important; + } + + #partsTable .extra-propagate-control .select2-container { + flex: 1 1 auto !important; + width: 100% !important; + min-width: 0 !important; + max-width: none !important; + } + + #partsTable .extra-propagate-control .select2-selection { + width: 100% !important; + } + + #partsTable .extra-propagate-btn { + flex: 0 0 30px !important; + width: 30px !important; + height: 24px !important; + min-width: 30px !important; + padding: 0.1rem 0.25rem !important; + } /* rosso */ \ No newline at end of file diff --git a/public/userarea/partsTable.js b/public/userarea/partsTable.js index b8b148a..a4bba99 100644 --- a/public/userarea/partsTable.js +++ b/public/userarea/partsTable.js @@ -9,6 +9,7 @@ $(document).ready(function () { let quotations = []; let partsExtraField = null; // {field_id, field_label} oppure null let extraFieldOptions = []; // [{id,label}] + let isLoadingPartsRecord = false; // --- ROW ID helpers: niente più cache impazzita di jQuery .data() --- function getPartId($row) { @@ -107,8 +108,45 @@ $(document).ready(function () { if (!partsExtraField) return ""; const selectedValueId = $row ? $row.data("extra-value-id") || "" : ""; + const selectedValueText = $row + ? $row.data("extra-value-text") || "" + : ""; // SceltaMultipla -> select + hidden value id + if ( + (partsExtraField.data_type || "").toLowerCase() === "sceltamultipla" + ) { + const opts = [``] + .concat( + extraFieldOptions.map( + (o) => + ``, + ), + ) + .join(""); + + return ` + + + + + `; + } + + // Testo -> input + hidden field_id + return ` + + + + `; + } + + function buildExtraFieldHeaderHtml() { + if (!partsExtraField) return ""; + + const label = partsExtraField.field_label || "Extra"; + + // SceltaMultipla: titolo sopra, select + bottone sotto if ( (partsExtraField.data_type || "").toLowerCase() === "sceltamultipla" ) { @@ -121,19 +159,38 @@ $(document).ready(function () { .join(""); return ` - - - - - `; + +
    +
    + ${label} +
    +
    + + +
    +
    + `; } - // Testo -> input + hidden field_id + // Campo testo: titolo sopra, input + bottone sotto return ` - - - - `; + +
    +
    + ${label} +
    +
    + + +
    +
    + `; } function initializeExtraFieldSelect2($context) { @@ -191,6 +248,79 @@ $(document).ready(function () { saveRow($row); }); + $(document).on("click", ".extra-propagate-btn", function (e) { + e.preventDefault(); + + if (!partsExtraField) return; + + const isSelect = + (partsExtraField.data_type || "").toLowerCase() === + "sceltamultipla"; + + let propagateValueId = null; + let propagateValueText = null; + + if (isSelect) { + propagateValueId = + $("#partsTable thead .extra-propagate-select").val() || ""; + + if (!propagateValueId) { + const errorMsg = $( + '', + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 3500); + return; + } + } else { + propagateValueText = ( + $("#partsTable thead .extra-propagate-input").val() || "" + ).trim(); + + if (!propagateValueText) { + const errorMsg = $( + '', + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 3500); + return; + } + } + + $("#partsTableBody tr").each(function () { + const $row = $(this); + + if (isSelect) { + const $select = $row.find(".part-extra-select"); + + if ($select.length) { + $select.val(propagateValueId).trigger("change.select2"); + + $row.data("extra-value-id", propagateValueId); + $row.find(".part-extra-value-id").val(propagateValueId); + } + } else { + const $input = $row.find(".part-extra-field"); + + if ($input.length) { + $input.val(propagateValueText); + + $row.data("extra-value-text", propagateValueText); + } + } + + saveRow($row); + }); + }); + function applyExtraFieldColumn() { const $theadRow = $("#partsTable thead tr"); @@ -203,16 +333,17 @@ $(document).ready(function () { }); // 3) se non c'è campo extra -> fine - if (!partsExtraField) return; + if (!partsExtraField) { + if (typeof window.applyPartsColumnWidths === "function") { + setTimeout(window.applyPartsColumnWidths, 100); + } + return; + } - // 4) inserisci header prima di "Azioni" (ultima colonna) - $theadRow - .find("th:last") - .before( - `${partsExtraField.field_label}`, - ); + // 4) inserisci header propagabile prima di "Azioni" ultima colonna + $theadRow.find("th:last").before(buildExtraFieldHeaderHtml()); - // 5) aggiungi cella a ogni riga già presente (una sola volta) + // 5) aggiungi cella a ogni riga già presente $("#partsTableBody tr").each(function () { const $row = $(this); $row.find("td:last").before(buildExtraFieldCellHtml($row)); @@ -225,6 +356,33 @@ $(document).ready(function () { }); initializeExtraFieldSelect2($("#partsTableBody")); + + // Select2 anche sul campo di propagazione in testata + if ( + (partsExtraField.data_type || "").toLowerCase() === "sceltamultipla" + ) { + const $headerSelect = $( + "#partsTable thead .extra-propagate-select", + ); + + if ($headerSelect.length && typeof $.fn.select2 !== "undefined") { + if ($headerSelect.hasClass("select2-hidden-accessible")) { + $headerSelect.select2("destroy"); + } + + $headerSelect.select2({ + placeholder: "Select…", + allowClear: true, + width: "100%", + dropdownParent: $("#partsModal"), + minimumResultsForSearch: 0, + }); + } + } + + if (typeof window.applyPartsColumnWidths === "function") { + setTimeout(window.applyPartsColumnWidths, 100); + } } function setPartId($row, id) { @@ -235,6 +393,17 @@ $(document).ready(function () { $row.data("__pid", id); } + function setRowMix($row, isMix) { + const mixValue = isMix === true || isMix === "Y" ? "Y" : "N"; + $row.attr("data-is-mix", mixValue); + $row.data("is-mix", mixValue); + } + + function getRowMix($row) { + const value = $row.attr("data-is-mix") || $row.data("is-mix"); + return value === "Y" ? "Y" : "N"; + } + // =================== // VOICE RECOGNITION SETUP // =================== @@ -341,21 +510,175 @@ $(document).ready(function () { // MODAL HANDLING // =================== function loadParts(iddatadb, idquotations, callback = null) { + isLoadingPartsRecord = true; + unsavedChanges = false; + + // Store current modal context + $("#partsModal").data("iddatadb", iddatadb || null); + $("#partsModal").data("idquotations", idquotations || null); + + // Store the visible record list from the main grid + if (Array.isArray(window.visibleIddatadbList)) { + $("#partsModal").data( + "visible-iddatadb-list", + window.visibleIddatadbList, + ); + } + + updatePartsRecordHeader(iddatadb); + + const finishLoading = function () { + unsavedChanges = false; + isLoadingPartsRecord = false; + + if (callback) callback(); + }; + if (iddatadb) { loadMacroMatrici(); initializeGlobalSelect2(); loadPartsExtraField(iddatadb, function () { loadPhoto(iddatadb, idquotations); - loadExistingParts(iddatadb, idquotations, callback); + loadExistingParts(iddatadb, idquotations, finishLoading); }); } else { loadPartsExtraField(iddatadb, function () { loadPhoto(iddatadb, idquotations); - loadExistingParts(iddatadb, idquotations, callback); + loadExistingParts(iddatadb, idquotations, finishLoading); }); } } + // =================== + // PARTS MODAL RECORD NAVIGATION + // =================== + + function getVisiblePartsRecordList() { + const listFromModal = $("#partsModal").data("visible-iddatadb-list"); + + if (Array.isArray(listFromModal) && listFromModal.length > 0) { + return listFromModal.map((v) => parseInt(v, 10)).filter(Boolean); + } + + if ( + Array.isArray(window.visibleIddatadbList) && + window.visibleIddatadbList.length > 0 + ) { + return window.visibleIddatadbList + .map((v) => parseInt(v, 10)) + .filter(Boolean); + } + + if (Array.isArray(window.gridData) && window.gridData.length > 0) { + return window.gridData + .map((row) => parseInt(row.iddatadb, 10)) + .filter(Boolean); + } + + return []; + } + + function getGridRecordById(iddatadb) { + if (!Array.isArray(window.gridData)) return null; + + return ( + window.gridData.find((row) => { + return parseInt(row.iddatadb, 10) === parseInt(iddatadb, 10); + }) || null + ); + } + + function getRecordHeaderLabel(iddatadb) { + const record = getGridRecordById(iddatadb); + + if (!record) { + return iddatadb ? "#" + iddatadb : ""; + } + + // Prefer main field value if available + if (record.mainFieldValue) { + return record.mainFieldValue; + } + + // Fallbacks + if (record.importreferencecode) { + return record.importreferencecode; + } + + if (record.filename_import) { + return record.filename_import; + } + + return "#" + iddatadb; + } + + function updatePartsRecordHeader(iddatadb) { + const list = getVisiblePartsRecordList(); + const currentId = parseInt(iddatadb, 10); + const currentIndex = list.indexOf(currentId); + + $("#trfHeader").text(getRecordHeaderLabel(currentId)); + + if (list.length <= 1 || currentIndex === -1) { + $("#partsRecordCounter").text("-"); + $("#prevPartsRecordBtn").prop("disabled", true); + $("#nextPartsRecordBtn").prop("disabled", true); + return; + } + + $("#partsRecordCounter").text(currentIndex + 1 + " / " + list.length); + + $("#prevPartsRecordBtn").prop("disabled", currentIndex <= 0); + $("#nextPartsRecordBtn").prop( + "disabled", + currentIndex >= list.length - 1, + ); + } + + function goToAdjacentPartsRecord(direction) { + const list = getVisiblePartsRecordList(); + const currentId = parseInt($("#partsModal").data("iddatadb"), 10); + const currentIndex = list.indexOf(currentId); + + if (currentIndex === -1) return; + + const nextIndex = currentIndex + direction; + + if (nextIndex < 0 || nextIndex >= list.length) return; + + if ( + !isLoadingPartsRecord && + unsavedChanges && + !confirm( + "Hai modifiche non salvate. Vuoi cambiare record senza salvare?", + ) + ) { + return; + } + + const nextIddatadb = list[nextIndex]; + + // Reset local modal state before loading the next record + partMatrice = {}; + unsavedChanges = false; + $("#partsTableBody").empty(); + $("#photoSelectorContainer").empty().hide(); + $("#samplePhoto").attr("src", ""); + $(".temp-alert").remove(); + + loadParts(nextIddatadb, null); + } + + $(document).on("click", "#prevPartsRecordBtn", function (e) { + e.preventDefault(); + goToAdjacentPartsRecord(-1); + }); + + $(document).on("click", "#nextPartsRecordBtn", function (e) { + e.preventDefault(); + goToAdjacentPartsRecord(1); + }); + // EVENTO PER APRIRE IL SECONDO MODALE $(document).on("click", "#openAnnotationsBtn", function () { console.log("Clic su Apri Annotazioni..."); @@ -698,7 +1021,7 @@ $(document).ready(function () { const $saveLoading = $row.find(".save-loading"); const iddatadb = $("#partsModal").data("iddatadb"); const idquotations = $("#partsModal").data("idquotations"); - const isMix = partDescription.startsWith("Mix") ? "Y" : "N"; + const isMix = getRowMix($row); // EXTRA FIELD (0/1) const extra_field_id = $row.find(".part-extra-field-id").val() || null; @@ -837,11 +1160,7 @@ $(document).ready(function () { let $mixRow = $("#partsTableBody tr") .filter(function () { - return $(this) - .find(".part-description") - .val() - .trim() - .startsWith("Mix"); + return getRowMix($(this)) === "Y"; }) .last(); @@ -897,7 +1216,7 @@ $(document).ready(function () { function addNewRow(nextPartNumber, isMix = false) { const description = isMix ? "Mix" : ""; const newRow = ` - + @@ -918,6 +1237,7 @@ $(document).ready(function () { `; $("#partsTableBody").append(newRow); const $newRow = $("#partsTableBody tr:last"); + setRowMix($newRow, isMix ? "Y" : "N"); const $select = $newRow.find(".part-matrice"); const selectedMacro = $("#macro-matrice-filter").val() || ""; @@ -925,7 +1245,10 @@ $(document).ready(function () { initializeExtraFieldSelect2($newRow); updateRowButtons(); - markUnsaved(); + + if (!isLoadingPartsRecord) { + markUnsaved(); + } } // =================== @@ -963,7 +1286,7 @@ $(document).ready(function () { // Raccogli tutti i dati della riga per evitare sovrascritture const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val().trim(); - const mix = partDescription.startsWith("Mix") ? "Y" : "N"; + const mix = getRowMix($row); const idmatrice = $row.find(".part-matrice").val() || null; const dateexpiry = $row.find(".part-dateexpiry").val() || null; @@ -1062,7 +1385,7 @@ $(document).ready(function () { // Raccogli tutti i dati della riga per evitare sovrascritture const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val().trim(); - const mix = partDescription.startsWith("Mix") ? "Y" : "N"; + const mix = getRowMix($row); const idmatrice = $row.find(".part-matrice").val() || null; const note = $row.data("note") || null; @@ -1394,7 +1717,7 @@ $(document).ready(function () { ? $("
    ").text(part.note).html() : ""; const newRow = ` - + @@ -1418,7 +1741,7 @@ $(document).ready(function () { const $row = $( `#partsTableBody tr[data-part-id="${part.id}"]`, ); - + setRowMix($row, part.mix === "Y" ? "Y" : "N"); if ( part.extra_value_id !== undefined && part.extra_value_id !== null @@ -1432,6 +1755,19 @@ $(document).ready(function () { ); } + if ( + part.extra_value_text !== undefined && + part.extra_value_text !== null + ) { + $row.data( + "extra-value-text", + part.extra_value_text, + ); + $row.find(".part-extra-field").val( + part.extra_value_text, + ); + } + initializeExtraFieldSelect2($row); const $select = $("#partsTableBody").find( @@ -1883,7 +2219,7 @@ $(document).ready(function () { id: part.partId, part_number: index + 1, part_description: part.partDescription, - mix: part.partDescription.startsWith("Mix") ? "Y" : "N", + mix: getRowMix($rows.eq(index)), idmatrice: partMatrice[index + 1] || null, note: part.note, dateexpiry: part.dateexpiry, @@ -1964,6 +2300,8 @@ $(document).ready(function () { }); function markUnsaved() { + if (isLoadingPartsRecord) return; + if (!unsavedChanges) { unsavedChanges = true; } @@ -2238,7 +2576,7 @@ $(document).on("change", ".propagate-date-input", function () { const partId = $row.data("part-id"); const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val().trim(); - const mix = partDescription.startsWith("Mix") ? "Y" : "N"; + const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N"; const idmatrice = $row.find(".part-matrice").val() || null; const note = $row.data("note") || null; @@ -2349,7 +2687,7 @@ $(document).on("click", ".save-common-note-btn", function () { const partId = $row.data("part-id"); const partNumber = $row.find(".part-number").val(); const partDescription = $row.find(".part-description").val().trim(); - const mix = partDescription.startsWith("Mix") ? "Y" : "N"; + const mix = $row.attr("data-is-mix") === "Y" ? "Y" : "N"; const idmatrice = $row.find(".part-matrice").val() || null; const dateexpiry = $row.find(".part-dateexpiry").val() || null; @@ -2463,4 +2801,142 @@ $(document).on("click", "#showHideImageBtn", function () { "", ); } + + if (typeof window.applyPartsColumnWidths === "function") { + setTimeout(window.applyPartsColumnWidths, 150); + } }); +// =================== +// RESIZABLE PARTS TABLE COLUMNS - FIXED COLGROUP VERSION +// =================== +(function () { + // Larghezze default per indice colonna (0-based) + const defaultWidths = [55, 320, 360, 150, 220, 230]; + const savedWidths = [...defaultWidths]; + + function getColgroup() { + return $("#partsTableColgroup"); + } + + function syncColgroupToHeaders() { + const $table = $("#partsTable"); + const $ths = $table.find("thead tr:first th"); + const $colgroup = getColgroup(); + const thCount = $ths.length; + + // Assicura che ci siano esattamente tante quante + while ($colgroup.find("col").length < thCount) { + $colgroup.append(""); + } + while ($colgroup.find("col").length > thCount) { + $colgroup.find("col:last").remove(); + } + + // Applica le larghezze salvate + $colgroup.find("col").each(function (i) { + const w = savedWidths[i] !== undefined ? savedWidths[i] : 150; + $(this).css("width", w + "px"); + }); + + // Imposta larghezza totale della tabella = somma colonne (evita reflow) + const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0); + $table.css("width", total + "px"); + } + + function applyColumnWidth(colIndex, newWidth) { + const w = Math.max(40, Math.round(newWidth)); + savedWidths[colIndex] = w; + + const $col = getColgroup().find("col").eq(colIndex); + if ($col.length) { + $col.css("width", w + "px"); + } + + // Aggiorna larghezza totale tabella senza toccare le altre colonne + const thCount = $("#partsTable thead tr:first th").length; + const total = savedWidths.slice(0, thCount).reduce((a, b) => a + b, 0); + $("#partsTable").css("width", total + "px"); + } + + function addResizers() { + const $table = $("#partsTable"); + if (!$table.length) return; + + $table.find("thead tr:first th").each(function (colIndex) { + const $th = $(this); + + // Salta colonna Num (indice 0) — non ridimensionabile + if (colIndex === 0) return; + + // Non aggiungere due volte + if ($th.find(".parts-resizer").length) return; + + const $resizer = $(""); + $th.css("position", "relative"); // necessario per il posizionamento assoluto + $th.append($resizer); + + $resizer.on("mousedown", function (e) { + e.preventDefault(); + e.stopPropagation(); + + const startX = e.pageX; + // Leggi la larghezza ESATTA dalla , non dal + const startWidth = + savedWidths[colIndex] !== undefined + ? savedWidths[colIndex] + : parseInt( + getColgroup() + .find("col") + .eq(colIndex) + .css("width"), + 10, + ) || 150; + + $("body") + .css("user-select", "none") + .css("cursor", "col-resize"); + + $(document).on("mousemove.partsResize", function (ev) { + const delta = ev.pageX - startX; + applyColumnWidth(colIndex, startWidth + delta); + }); + + $(document).on("mouseup.partsResize", function () { + $("body").css("user-select", "").css("cursor", ""); + $(document).off(".partsResize"); + }); + }); + }); + } + + function init() { + syncColgroupToHeaders(); + addResizers(); + } + + // Init al primo show del modale + $(document).on("shown.bs.modal", "#partsModal", function () { + setTimeout(init, 120); + }); + + // Re-init dopo ogni AJAX (nuove righe, caricamenti) + $(document).ajaxComplete(function () { + if ($("#partsModal").hasClass("show")) { + setTimeout(syncColgroupToHeaders, 200); + setTimeout(addResizers, 220); + } + }); + + // Re-init dopo aggiunta/rimozione righe + $(document).on( + "click", + ".add-row-global, .add-mix-global, .add-mix-row, .remove-row, #renumberPartsBtn, #clonePartsBtn", + function () { + setTimeout(syncColgroupToHeaders, 120); + setTimeout(addResizers, 140); + }, + ); + + window.initPartsResizableColumns = init; + window.applyPartsColumnWidths = syncColgroupToHeaders; +})(); diff --git a/public/userarea/ping_lims_api.php b/public/userarea/ping_lims_api.php new file mode 100644 index 0000000..ab020b0 --- /dev/null +++ b/public/userarea/ping_lims_api.php @@ -0,0 +1,36 @@ +get('Rapporto', [ + '$top' => 1, + '$select' => 'IdRapporto' + ]); + + $elapsed = round(microtime(true) - $start, 3); + + echo json_encode([ + 'success' => true, + 'elapsed_seconds' => $elapsed, + 'data' => $data + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} 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/process_edit_template_xls.php b/public/userarea/process_edit_template_xls.php index becdcfe..e333d9a 100644 --- a/public/userarea/process_edit_template_xls.php +++ b/public/userarea/process_edit_template_xls.php @@ -13,8 +13,21 @@ try { $id = intval($_POST['id'] ?? 0); $name = trim($_POST['name'] ?? ''); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS')); - $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null; + + $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' + ? intval($_POST['header_row']) + : null; + $start_column = trim($_POST['start_column'] ?? ''); + + $xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== '' + ? intval($_POST['xls_sheet_index']) + : 0; + + $api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== '' + ? intval($_POST['api_config_id']) + : null; + $description = trim($_POST['description'] ?? ''); $target_table = trim($_POST['target_table'] ?? 'datadb'); $idclient = intval($_POST['client_id'] ?? 0); @@ -27,7 +40,8 @@ try { $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); $button_label = trim($_POST['button_label'] ?? 'Click Me'); - if (!in_array($source_type, ['XLS', 'API'], true)) { + // Allowed source types + if (!in_array($source_type, ['XLS', 'API', 'JSON', 'PDF'], true)) { $source_type = 'XLS'; } @@ -41,18 +55,52 @@ try { if ($header_row === null || $header_row <= 0 || $start_column === '') { throw new Exception("Header Row and Start Column are required for XLS templates."); } + + if ($xls_sheet_index < 0) { + throw new Exception("XLS Sheet Number cannot be negative."); + } + + $api_config_id = null; } - // API templates do not require XLS coordinates - if ($source_type === 'API') { + // API/JSON validation + if ($source_type === 'API' || $source_type === 'JSON') { + if (empty($api_config_id)) { + throw new Exception("API/JSON configuration is required for API or JSON templates."); + } + $header_row = null; $start_column = null; + $xls_sheet_index = null; + } + + // PDF currently does not require XLS coordinates or API configuration + if ($source_type === 'PDF') { + $header_row = null; + $start_column = null; + $xls_sheet_index = null; + $api_config_id = null; } // Database connection $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); + // Optional check: verify API configuration exists and is active + if ($api_config_id !== null) { + $stmt = $pdo->prepare(" + SELECT COUNT(*) + FROM api_configurations + WHERE id = ? + AND is_active = 1 + "); + $stmt->execute([$api_config_id]); + + if ((int)$stmt->fetchColumn() === 0) { + throw new Exception("Selected API/JSON configuration does not exist or is not active."); + } + } + // Update template $stmt = $pdo->prepare(" UPDATE excel_templates @@ -61,6 +109,8 @@ try { source_type = ?, header_row = ?, start_column = ?, + xls_sheet_index = ?, + api_config_id = ?, description = ?, target_table = ?, idclient = ?, @@ -81,6 +131,8 @@ try { $source_type, $header_row, $start_column, + $xls_sheet_index, + $api_config_id, $description, $target_table, $idclient, diff --git a/public/userarea/process_import_xls.php b/public/userarea/process_import_xls.php index 6ec258f..6640ac5 100644 --- a/public/userarea/process_import_xls.php +++ b/public/userarea/process_import_xls.php @@ -10,6 +10,9 @@ session_start(); // Includi PHPSpreadsheet require_once '../../vendor/autoload.php'; +Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad(); +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + $response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '']; try { diff --git a/public/userarea/process_import_xls2.php b/public/userarea/process_import_xls2.php index afd0cf0..407ea08 100644 --- a/public/userarea/process_import_xls2.php +++ b/public/userarea/process_import_xls2.php @@ -11,17 +11,105 @@ session_start(); require_once '../../vendor/autoload.php'; require_once __DIR__ . '/class/db-functions.php'; -$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'apply_routine' => false]; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; + +$response = [ + 'error' => '', + 'rows' => [], + 'columns' => [], + 'template_id' => 0, + 'filename' => '', + 'apply_routine' => false +]; + +/** + * Converts a column value to a PhpSpreadsheet 1-based column index. + * Accepted values: + * - "A" => 1 + * - "B" => 2 + * - "AA" => 27 + * - "1" => 1 + * - 1 => 1 + */ +function normalizeColumnIndex($value): int +{ + $value = trim((string)$value); + + if ($value === '') { + return 1; + } + + if (ctype_digit($value)) { + return max(1, (int)$value); + } + + $value = strtoupper($value); + + if (preg_match('/^[A-Z]+$/', $value)) { + return Coordinate::columnIndexFromString($value); + } + + return 1; +} try { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['excel_file'])) { $template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0; - $header_row = isset($_POST['header_row']) ? intval($_POST['header_row']) : 1; - $start_column = isset($_POST['start_column']) ? intval($_POST['start_column']) : 1; + + if ($template_id <= 0) { + throw new Exception("Template ID non valido."); + } + + // Connessione al database + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + /* + * Recuperiamo i parametri direttamente dal template. + * Così non dipendiamo solo dal form e siamo sicuri di usare i dati salvati. + */ + $stmt = $pdo->prepare(" + SELECT + id, + header_row, + start_column, + xls_sheet_index, + idroutine, + idclient + FROM excel_templates + WHERE id = ? + "); + $stmt->execute([$template_id]); + $template = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$template) { + throw new Exception("Template non trovato."); + } + + $header_row = isset($template['header_row']) && $template['header_row'] !== null + ? (int)$template['header_row'] + : 1; + + $start_column_raw = $template['start_column'] ?? 'A'; + $start_column = normalizeColumnIndex($start_column_raw); + + $xlsSheetIndex = isset($template['xls_sheet_index']) && $template['xls_sheet_index'] !== null + ? (int)$template['xls_sheet_index'] + : 0; + + if ($header_row <= 0) { + $header_row = 1; + } + + if ($xlsSheetIndex < 0) { + $xlsSheetIndex = 0; + } // Debug del template_id ricevuto error_log("Received template_id from POST: " . print_r($_POST['template_id'], true)); error_log("Converted template_id: $template_id"); + error_log("Template XLS settings - header_row: $header_row, start_column_raw: $start_column_raw, start_column_index: $start_column, xls_sheet_index: $xlsSheetIndex"); $file = $_FILES['excel_file']; $fileError = $file['error']; @@ -38,23 +126,32 @@ try { $originalFilename = basename($file['name']); $newFilename = "{$iduserlogin}-{$timestamp}-{$originalFilename}"; $importFolder = __DIR__ . '/imported_trf/'; + if (!file_exists($importFolder)) { mkdir($importFolder, 0777, true); } + $destination = $importFolder . $newFilename; // Sposta il file if (!move_uploaded_file($file['tmp_name'], $destination)) { throw new Exception("Errore durante lo spostamento del file in $destination"); } + error_log("File spostato con successo in: $destination"); - // Connessione al database - $db = DBHandlerSelect::getInstance(); - $pdo = $db->getConnection(); - // Recupera il mapping da template_mapping - $stmt = $pdo->prepare("SELECT field_id AS excel_column, field_id AS mysql_column, data_type, is_required, default_value, is_manual FROM template_mapping WHERE template_id = ?"); + $stmt = $pdo->prepare(" + SELECT + field_id AS excel_column, + field_id AS mysql_column, + data_type, + is_required, + default_value, + is_manual + FROM template_mapping + WHERE template_id = ? + "); $stmt->execute([$template_id]); $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -65,19 +162,45 @@ try { $response['error'] = "Nessun mapping trovato per il template con ID $template_id"; } else { // Carica il file rinominato con PHPSpreadsheet - $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($destination); - $worksheet = $spreadsheet->getActiveSheet(); + $spreadsheet = IOFactory::load($destination); + + $sheetCount = $spreadsheet->getSheetCount(); + $sheetNames = $spreadsheet->getSheetNames(); + + if ($sheetCount <= 0) { + throw new Exception("Il file XLS non contiene fogli."); + } + + if ($xlsSheetIndex >= $sheetCount) { + throw new Exception( + "Il foglio XLS selezionato non esiste. " . + "Sheet Number selezionato: {$xlsSheetIndex}. " . + "Fogli disponibili: " . implode(", ", array_map( + fn($name, $index) => "{$index}={$name}", + $sheetNames, + array_keys($sheetNames) + )) + ); + } + + // Usa il foglio configurato nel template + $worksheet = $spreadsheet->getSheet($xlsSheetIndex); + $selectedSheetName = $worksheet->getTitle(); + + error_log("Selected XLS sheet - index: {$xlsSheetIndex}, name: {$selectedSheetName}"); + $highestRow = $worksheet->getHighestRow(); $highestColumn = $worksheet->getHighestColumn(); - $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); $startRow = max(1, $header_row); $startColumn = max(1, $start_column); - // Advance startColumn to first non-empty cell in header row (match JS behavior) + // Advance startColumn to first non-empty cell in header row, matching JS behavior for ($sc = $startColumn; $sc <= $highestColumnIndex; $sc++) { - $cl = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($sc); + $cl = Coordinate::stringFromColumnIndex($sc); $cv = trim((string)($worksheet->getCell($cl . $header_row)->getCalculatedValue() ?? '')); + if ($cv !== '') { $startColumn = $sc; break; @@ -85,24 +208,32 @@ try { } // Debug dei parametri - error_log("Processing - template_id: $template_id, startRow: $startRow, startColumn: $startColumn, highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex"); + error_log( + "Processing - template_id: $template_id, " . + "sheetIndex: $xlsSheetIndex, sheetName: $selectedSheetName, " . + "startRow: $startRow, startColumn: $startColumn, " . + "highestRow: $highestRow, highestColumn: $highestColumn, highestColumnIndex: $highestColumnIndex" + ); // Validazione degli indici if ($startRow > $highestRow) { - $response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow)."; + $response['error'] = "La riga di partenza ($startRow) supera il numero totale di righe ($highestRow) del foglio '$selectedSheetName'."; } elseif ($startColumn > $highestColumnIndex) { - $response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex)."; + $response['error'] = "La colonna di partenza ($startColumn) supera il numero totale di colonne ($highestColumnIndex) del foglio '$selectedSheetName'."; } else { $excelData = []; // Build merge map for header row: physCol -> mergeStartCol $mergeStartMap = []; + foreach ($worksheet->getMergeCells() as $range) { [$startCell, $endCell] = explode(':', $range); - $mStartCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell)); - $mEndCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell)); + + $mStartCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $startCell)); + $mEndCol = Coordinate::columnIndexFromString(preg_replace('/\d+/', '', $endCell)); $mStartRow = (int)preg_replace('/[A-Z]+/i', '', $startCell); $mEndRow = (int)preg_replace('/[A-Z]+/i', '', $endCell); + if ($header_row >= $mStartRow && $header_row <= $mEndRow) { for ($c = $mStartCol; $c <= $mEndCol; $c++) { $mergeStartMap[$c] = $mStartCol; @@ -111,12 +242,17 @@ try { } // Build logical columns: each merge = one column - $logicalCols = []; // array of physical column indices (one per logical column) + $logicalCols = []; // array of physical column indices, one per logical column $seen = []; + for ($col = $startColumn; $col <= $highestColumnIndex; $col++) { if (isset($mergeStartMap[$col])) { $ms = $mergeStartMap[$col]; - if (in_array($ms, $seen, true)) continue; + + if (in_array($ms, $seen, true)) { + continue; + } + $seen[] = $ms; $logicalCols[] = $ms; } else { @@ -127,38 +263,48 @@ try { // Build header row using logical columns $headerRowData = []; $logicalNum = 0; + foreach ($logicalCols as $physCol) { $logicalNum++; - $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol); + + $columnLetter = Coordinate::stringFromColumnIndex($physCol); $cell = $worksheet->getCell($columnLetter . $header_row); $cellValue = trim((string)($cell ? $cell->getCalculatedValue() : '')); $cellValue = preg_replace('/[\r\n\t]+/', ' ', $cellValue); + // Empty headers get __empty_N__ to match mapping page $headerRowData[] = ($cellValue !== '') ? $cellValue : '__empty_' . $logicalNum . '__'; } error_log("Logical headers: " . json_encode($headerRowData)); - error_log("Logical cols (physical indices): " . json_encode($logicalCols)); + error_log("Logical cols physical indices: " . json_encode($logicalCols)); // Find which logical columns have real headers $headerFilledIndices = []; + foreach ($headerRowData as $idx => $hVal) { - if (!str_starts_with($hVal, '__empty_')) $headerFilledIndices[] = $idx; + if (!str_starts_with($hVal, '__empty_')) { + $headerFilledIndices[] = $idx; + } } + $minFilled = max(1, min(2, count($headerFilledIndices))); // Extract data rows using logical columns for ($row = $startRow + 1; $row <= $highestRow; $row++) { $rowData = []; + foreach ($logicalCols as $physCol) { - $columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($physCol); + $columnLetter = Coordinate::stringFromColumnIndex($physCol); $cell = $worksheet->getCell($columnLetter . $row); $cellValue = $cell ? $cell->getCalculatedValue() : ''; + $rowData[] = $cellValue ?: ''; } // Count how many header columns have data in this row $filledCount = 0; + foreach ($headerFilledIndices as $idx) { if (isset($rowData[$idx]) && trim((string)$rowData[$idx]) !== '') { $filledCount++; @@ -166,17 +312,25 @@ try { } if ($filledCount >= $minFilled) { - $excelData[] = ['data' => $rowData, 'excelrow' => $row]; + $excelData[] = [ + 'data' => $rowData, + 'excelrow' => $row + ]; } } // Recupera routine dal template - $stmt = $pdo->prepare("SELECT idroutine, idclient FROM excel_templates WHERE id = ?"); - $stmt->execute([$template_id]); - $template = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($template && $template['idroutine']) { - $stmtRoutine = $pdo->prepare("SELECT idroutine, name, filename, headerrow, instruction FROM routine WHERE idroutine = ?"); + if ($template && !empty($template['idroutine'])) { + $stmtRoutine = $pdo->prepare(" + SELECT + idroutine, + name, + filename, + headerrow, + instruction + FROM routine + WHERE idroutine = ? + "); $stmtRoutine->execute([$template['idroutine']]); $routineData = $stmtRoutine->fetch(PDO::FETCH_ASSOC); @@ -188,6 +342,7 @@ try { 'filename' => $routineData['filename'] ?? '', 'headerrow' => $routineData['headerrow'] ?? $header_row ]; + error_log("Routine rilevata per template {$template_id}: " . print_r($routineData, true)); } else { error_log("Errore: Nessuna routine trovata per idroutine {$template['idroutine']}"); @@ -204,6 +359,8 @@ try { $_SESSION['template_id'] = $template_id; $_SESSION['headers'] = $headerRowData; $_SESSION['mappings'] = $mappings; + $_SESSION['xls_sheet_index'] = $xlsSheetIndex; + $_SESSION['xls_sheet_name'] = $selectedSheetName; // Includi excel_data nella risposta JSON in ogni caso $response['excel_data'] = $excelData; @@ -211,6 +368,8 @@ try { $response['columns'] = $headerRowData; $response['template_id'] = $template_id; $response['filename'] = $newFilename; + $response['xls_sheet_index'] = $xlsSheetIndex; + $response['xls_sheet_name'] = $selectedSheetName; } } } else { diff --git a/public/userarea/process_insert_template_xls.php b/public/userarea/process_insert_template_xls.php index ddc0a5b..e257656 100644 --- a/public/userarea/process_insert_template_xls.php +++ b/public/userarea/process_insert_template_xls.php @@ -12,22 +12,39 @@ try { // Retrieve and sanitize form data $name = trim($_POST['name'] ?? ''); $source_type = strtoupper(trim($_POST['source_type'] ?? 'XLS')); - $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' ? intval($_POST['header_row']) : null; + + $header_row = isset($_POST['header_row']) && $_POST['header_row'] !== '' + ? intval($_POST['header_row']) + : null; + $start_column = trim($_POST['start_column'] ?? ''); + + $xls_sheet_index = isset($_POST['xls_sheet_index']) && $_POST['xls_sheet_index'] !== '' + ? intval($_POST['xls_sheet_index']) + : 0; + + $api_config_id = isset($_POST['api_config_id']) && $_POST['api_config_id'] !== '' + ? intval($_POST['api_config_id']) + : null; + $description = trim($_POST['description'] ?? ''); $target_table = trim($_POST['target_table'] ?? 'datadb'); $idclient = intval($_POST['client_id'] ?? 0); $clientname = trim($_POST['client_name'] ?? ''); $idschema = intval($_POST['idschema'] ?? 0); $schemaname = trim($_POST['schemaname'] ?? ''); - $idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' ? intval($_POST['idroutine']) : null; + $idroutine = isset($_POST['idroutine']) && $_POST['idroutine'] !== '' + ? intval($_POST['idroutine']) + : null; + $button_size = trim($_POST['button_size'] ?? 'medium'); $button_bg_color = trim($_POST['button_bg_color'] ?? '#007bff'); $button_text_color = trim($_POST['button_text_color'] ?? '#ffffff'); $button_label = trim($_POST['button_label'] ?? 'Click Me'); // Normalize source type - if (!in_array($source_type, ['XLS', 'API'], true)) { + // API / JSON is saved as API + if (!in_array($source_type, ['XLS', 'API', 'PDF'], true)) { $source_type = 'XLS'; } @@ -41,26 +58,62 @@ try { if ($header_row === null || $header_row <= 0 || $start_column === '') { throw new Exception("Header Row and Start Column are required for XLS templates."); } + + if ($xls_sheet_index < 0) { + throw new Exception("XLS Sheet Number cannot be negative."); + } + + $api_config_id = null; } - // API templates do not require XLS coordinates + // API / JSON validation if ($source_type === 'API') { + if (empty($api_config_id)) { + throw new Exception("API / JSON configuration is required for API / JSON templates."); + } + $header_row = null; $start_column = null; + $xls_sheet_index = null; + } + + // PDF currently does not require XLS coordinates or API configuration + if ($source_type === 'PDF') { + $header_row = null; + $start_column = null; + $xls_sheet_index = null; + $api_config_id = null; } // Database connection $db = DBHandlerSelect::getInstance(); $pdo = $db->getConnection(); + // Optional check: verify API configuration exists and is active + if ($api_config_id !== null) { + $stmt = $pdo->prepare(" + SELECT COUNT(*) + FROM api_configurations + WHERE id = ? + AND is_active = 1 + "); + $stmt->execute([$api_config_id]); + + if ((int)$stmt->fetchColumn() === 0) { + throw new Exception("Selected API / JSON configuration does not exist or is not active."); + } + } + // Insert the new template $stmt = $pdo->prepare(" - INSERT INTO excel_templates + INSERT INTO excel_templates ( name, source_type, header_row, start_column, + xls_sheet_index, + api_config_id, description, target_table, idclient, @@ -75,7 +128,13 @@ try { created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + VALUES + ( + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + NOW(), NOW() + ) "); $stmt->execute([ @@ -83,6 +142,8 @@ try { $source_type, $header_row, $start_column, + $xls_sheet_index, + $api_config_id, $description, $target_table, $idclient, diff --git a/public/userarea/quotations.php b/public/userarea/quotations.php index f2d9b43..067fe1c 100644 --- a/public/userarea/quotations.php +++ b/public/userarea/quotations.php @@ -1,15 +1,15 @@ getConnection(); -// Recupera l'ID dell'utente loggato $user_id = $iduserlogin ?? 1; -// Gestione creazione nuova quotation (crea record vuoto su conferma) -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create') { - $description = ''; - $customer = ''; +/** + * Helper redirect + */ +function redirectWithMessage($status, $message, $extraQuery = '') +{ + $url = 'quotations.php?status=' . urlencode($status) . '&message=' . urlencode($message); + + if ($extraQuery !== '') { + $url .= '&' . ltrim($extraQuery, '&'); + } + + header("Location: " . $url); + exit; +} + +/** + * CREATE quotation + * Ora description e customer vengono salvati subito dal modale. + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create') { + $description = trim($_POST['description'] ?? ''); + $customer = trim($_POST['customer'] ?? ''); + + if ($description === '' || $customer === '') { + redirectWithMessage('error', 'Descrizione e Cliente sono obbligatori'); + } try { - $stmt = $pdo->prepare("INSERT INTO quotations (description, customer, iduser) VALUES (?, ?, ?)"); - $success = $stmt->execute([$description, $customer, $user_id]); + $stmt = $pdo->prepare(" + INSERT INTO quotations + (description, customer, iduser) + VALUES + (?, ?, ?) + "); + + $success = $stmt->execute([ + $description, + $customer, + $user_id + ]); + if ($success) { $newId = $pdo->lastInsertId(); - error_log("Creata nuova quotation ID: $newId"); - header("Location: quotations.php?edit_id=" . $newId . "&status=success&message=" . urlencode("Quotation creata con successo")); - } else { - error_log("Errore: Impossibile creare la quotation, nessun ID generato."); - header("Location: quotations.php?status=error&message=" . urlencode("Errore durante la creazione della quotation")); + error_log("Creata nuova quotation ID: " . $newId); + + redirectWithMessage('success', 'Quotation creata con successo'); } + + error_log("Errore: impossibile creare la quotation"); + redirectWithMessage('error', 'Errore durante la creazione della quotation'); } catch (PDOException $e) { error_log("Errore PDO durante la creazione della quotation: " . $e->getMessage()); - header("Location: quotations.php?status=error&message=" . urlencode("Errore database: " . $e->getMessage())); + redirectWithMessage('error', 'Errore database: ' . $e->getMessage()); } - exit; } -// Gestione modifica quotation -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update' && isset($_POST['id'])) { +/** + * UPDATE quotation + * Gestisce sia form normale sia salvataggio inline AJAX. + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update' && isset($_POST['id'])) { $id = intval($_POST['id']); - $description = $_POST['description'] ?? ''; - $customer = $_POST['customer'] ?? ''; + $description = trim($_POST['description'] ?? ''); + $customer = trim($_POST['customer'] ?? ''); + $isAjax = isset($_POST['ajax']) && $_POST['ajax'] === '1'; + + if ($description === '' || $customer === '') { + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => 'Descrizione e Cliente sono obbligatori' + ]); + exit; + } + + redirectWithMessage('error', 'Descrizione e Cliente sono obbligatori'); + } try { - $stmt = $pdo->prepare("UPDATE quotations SET description = ?, customer = ? WHERE id = ? AND iduser = ?"); - $stmt->execute([$description, $customer, $id, $user_id]); - error_log("Modificata quotation ID: $id"); - header("Location: quotations.php?status=success&message=" . urlencode("Quotation modificata con successo")); + $stmt = $pdo->prepare(" + UPDATE quotations + SET description = ?, customer = ? + WHERE id = ? AND iduser = ? + "); + + $stmt->execute([ + $description, + $customer, + $id, + $user_id + ]); + + error_log("Modificata quotation ID: " . $id); + + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => true, + 'message' => 'Quotation modificata con successo' + ]); + exit; + } + + redirectWithMessage('success', 'Quotation modificata con successo'); } catch (PDOException $e) { error_log("Errore PDO durante la modifica della quotation: " . $e->getMessage()); - header("Location: quotations.php?status=error&message=" . urlencode("Errore database: " . $e->getMessage())); + + if ($isAjax) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => 'Errore database: ' . $e->getMessage() + ]); + exit; + } + + redirectWithMessage('error', 'Errore database: ' . $e->getMessage()); } - exit; } -// Gestione cancellazione quotation -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete' && isset($_POST['id'])) { +/** + * DELETE quotation + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete' && isset($_POST['id'])) { $id = intval($_POST['id']); try { - $stmt = $pdo->prepare("DELETE FROM quotations WHERE id = ? AND iduser = ?"); - $stmt->execute([$id, $user_id]); - error_log("Cancellata quotation ID: $id"); - header("Location: quotations.php?status=success&message=" . urlencode("Quotation cancellata con successo")); + $stmt = $pdo->prepare(" + DELETE FROM quotations + WHERE id = ? AND iduser = ? + "); + + $stmt->execute([ + $id, + $user_id + ]); + + error_log("Cancellata quotation ID: " . $id); + + redirectWithMessage('success', 'Quotation cancellata con successo'); } catch (PDOException $e) { error_log("Errore PDO durante la cancellazione della quotation: " . $e->getMessage()); - header("Location: quotations.php?status=error&message=" . urlencode("Errore database: " . $e->getMessage())); + redirectWithMessage('error', 'Errore database: ' . $e->getMessage()); } - exit; } -// Recupera tutte le quotations per l'utente +/** + * Recupera tutte le quotations dell'utente. + * Ultima creata in alto. + */ try { - $stmt = $pdo->prepare("SELECT * FROM quotations WHERE iduser = ? ORDER BY creation_date DESC"); - $stmt->execute([$user_id]); + $stmt = $pdo->prepare(" + SELECT * + FROM quotations + WHERE iduser = ? + ORDER BY creation_date DESC, id DESC + "); + + $stmt->execute([ + $user_id + ]); + $quotations = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { error_log("Errore PDO durante il recupero delle quotations: " . $e->getMessage()); $quotations = []; } -// Verifica se è richiesta la modifica di una quotation +/** + * Recupera quotation in modifica dettagliata + */ $editQuotation = null; + if (isset($_GET['edit_id'])) { $editId = intval($_GET['edit_id']); + try { - $stmt = $pdo->prepare("SELECT * FROM quotations WHERE id = ? AND iduser = ?"); - $stmt->execute([$editId, $user_id]); + $stmt = $pdo->prepare(" + SELECT * + FROM quotations + WHERE id = ? AND iduser = ? + "); + + $stmt->execute([ + $editId, + $user_id + ]); + $editQuotation = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$editQuotation) { - error_log("Nessuna quotation trovata per id: $editId"); + error_log("Nessuna quotation trovata per id: " . $editId); } } catch (PDOException $e) { error_log("Errore PDO durante il recupero della quotation per modifica: " . $e->getMessage()); @@ -112,10 +227,14 @@ if (isset($_GET['edit_id'])) { + - + + + + + Gestione Quotations - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> @@ -271,50 +444,100 @@ if (isset($_GET['edit_id'])) {
    +
    -
    +
    Gestione Quotations
    + + + +
    +
    + -
    - +
    +
    + - -
    Modifica Quotation ID:
    - + +
    + Modifica Quotation ID: +
    + + - + +
    - +
    +
    - +
    - - Torna alla Lista + + + + + Torna alla Lista + +
    Azioni
    - - + + + +
    + - -
    - -
    +
    Quotations Esistenti
    + @@ -325,110 +548,246 @@ if (isset($_GET['edit_id'])) { + $row): ?> - - - + + + + + + +
    Azioni
    - + - + - - - - - + + + + + + + + + + +
    + +
    - + + +
    + + + - - - \ No newline at end of file diff --git a/public/userarea/routines/burberry.php b/public/userarea/routines/burberry.php new file mode 100644 index 0000000..7e866e0 --- /dev/null +++ b/public/userarea/routines/burberry.php @@ -0,0 +1,71 @@ + &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("Routine burberry: invalid row structure at index {$rowIndex}."); + continue; + } + + $valueS = trim((string)($row['data'][$columnSIndex] ?? '')); + $valueT = trim((string)($row['data'][$columnTIndex] ?? '')); + + /* + * Merge values, ignoring empty values. + */ + $mergedValues = []; + + if ($valueS !== '') { + $mergedValues[] = $valueS; + } + + if ($valueT !== '') { + $mergedValues[] = $valueT; + } + + /* + * Save final value into column S. + */ + $row['data'][$targetColumnIndex] = implode(' ', $mergedValues); + + error_log( + "Routine burberry: row " . + ($row['excelrow'] ?? $rowIndex) . + " generated value in column S: " . + $row['data'][$targetColumnIndex] + ); + } + + unset($row); + + error_log("Routine burberry completed."); +} diff --git a/public/userarea/routines/fendi.php b/public/userarea/routines/fendi.php new file mode 100644 index 0000000..e744ce2 --- /dev/null +++ b/public/userarea/routines/fendi.php @@ -0,0 +1,67 @@ + &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("Routine merge T+U: invalid row structure at index {$rowIndex}."); + continue; + } + + $valueT = trim((string)($row['data'][$firstColumnIndex] ?? '')); + $valueU = trim((string)($row['data'][$secondColumnIndex] ?? '')); + + /* + * Merge values, ignoring empty values. + */ + $mergedValues = []; + + if ($valueT !== '') { + $mergedValues[] = $valueT; + } + + if ($valueU !== '') { + $mergedValues[] = $valueU; + } + + /* + * Save final value into column T. + */ + $row['data'][$targetColumnIndex] = implode(' ', $mergedValues); + + error_log( + "Routine merge T+U: row " . + ($row['excelrow'] ?? $rowIndex) . + " generated value in column T: " . + $row['data'][$targetColumnIndex] + ); + } + + unset($row); + + error_log("Routine merge T+U completed."); +} diff --git a/public/userarea/routines/paulshark.php b/public/userarea/routines/paulshark.php new file mode 100644 index 0000000..6ee05c9 --- /dev/null +++ b/public/userarea/routines/paulshark.php @@ -0,0 +1,76 @@ + &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("Routine paulshark: invalid row structure at index {$rowIndex}."); + continue; + } + + $valueD = trim((string)($row['data'][$columnDIndex] ?? '')); + $valueE = trim((string)($row['data'][$columnEIndex] ?? '')); + $valueJ = trim((string)($row['data'][$columnJIndex] ?? '')); + + /* + * Merge values, ignoring empty values. + */ + $mergedValues = []; + + if ($valueD !== '') { + $mergedValues[] = $valueD; + } + + if ($valueE !== '') { + $mergedValues[] = $valueE; + } + + if ($valueJ !== '') { + $mergedValues[] = $valueJ; + } + + /* + * Save final value into column D. + */ + $row['data'][$targetColumnIndex] = implode(' ', $mergedValues); + + error_log( + "Routine paulshark: row " . + ($row['excelrow'] ?? $rowIndex) . + " generated value in column D: " . + $row['data'][$targetColumnIndex] + ); + } + + unset($row); + + error_log("Routine paulshark completed."); +} diff --git a/public/userarea/routines/richemont_pelletteria.php b/public/userarea/routines/richemont_pelletteria.php new file mode 100644 index 0000000..d496aa7 --- /dev/null +++ b/public/userarea/routines/richemont_pelletteria.php @@ -0,0 +1,71 @@ + &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("Routine Richemont Pelletteria: invalid row structure at index {$rowIndex}."); + continue; + } + + $valueD = trim((string)($row['data'][$columnDIndex] ?? '')); + $valueE = trim((string)($row['data'][$columnEIndex] ?? '')); + + /* + * Merge values, ignoring empty values. + */ + $mergedValues = []; + + if ($valueD !== '') { + $mergedValues[] = $valueD; + } + + if ($valueE !== '') { + $mergedValues[] = $valueE; + } + + /* + * Save final value into column D. + */ + $row['data'][$targetColumnIndex] = implode(' ', $mergedValues); + + error_log( + "Routine Richemont Pelletteria: row " . + ($row['excelrow'] ?? $rowIndex) . + " generated value in column D: " . + $row['data'][$targetColumnIndex] + ); + } + + unset($row); + + error_log("Routine Richemont Pelletteria completed."); +} diff --git a/public/userarea/routines/valentino_compliance.php b/public/userarea/routines/valentino_compliance.php new file mode 100644 index 0000000..5bbc219 --- /dev/null +++ b/public/userarea/routines/valentino_compliance.php @@ -0,0 +1,114 @@ + &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("Routine field_id 347: invalid row structure at index {$rowIndex}."); + continue; + } + + $selectedValues = []; + + /* + * Check columns from P to AT. + * If the cell contains x, take the related column header. + */ + for ($columnIndex = $startColumnIndex; $columnIndex <= $endColumnIndex; $columnIndex++) { + $cellValue = strtolower(trim((string)($row['data'][$columnIndex] ?? ''))); + + if ($cellValue === 'x') { + $headerTitle = trim((string)($headers[$columnIndex] ?? '')); + + if ($headerTitle !== '') { + $selectedValues[] = $headerTitle; + } + } + } + + /* + * Add free text from column AU. + */ + $extraText = ''; + + if (isset($row['data'][$extraColumnIndex])) { + $extraText = trim((string)$row['data'][$extraColumnIndex]); + } elseif (isset($row['data']['AU'])) { + $extraText = trim((string)$row['data']['AU']); + } + + error_log( + "Routine field_id 347: row " . + ($row['excelrow'] ?? $rowIndex) . + " AU index {$extraColumnIndex} value: " . + print_r($row['data'][$extraColumnIndex] ?? null, true) . + " | AU key value: " . + print_r($row['data']['AU'] ?? null, true) + ); + + if ($extraText !== '') { + $selectedValues[] = $extraText; + } + + /* + * Remove empty and duplicate values. + */ + $selectedValues = array_values(array_unique(array_filter($selectedValues, function ($value) { + return trim((string)$value) !== ''; + }))); + + /* + * Save final value into column P. + * Column P must be mapped to field_id 347 in the template mapping. + */ + $row['data'][$targetColumnIndex] = implode(', ', $selectedValues); + + error_log( + "Routine field_id 347: row " . + ($row['excelrow'] ?? $rowIndex) . + " generated value: " . + $row['data'][$targetColumnIndex] + ); + } + + unset($row); + + error_log("Routine field_id 347 completed."); +} diff --git a/public/userarea/save_mapping_json.php b/public/userarea/save_mapping_json.php index 3139e02..d907c78 100644 --- a/public/userarea/save_mapping_json.php +++ b/public/userarea/save_mapping_json.php @@ -16,9 +16,11 @@ if (!$data || !isset($data['id'])) { exit; } -$mappingId = $data['id']; +$mappingId = (int)$data['id']; $mappingType = $data['mapping_type'] ?? ''; + $excelColumn = $data['excel_column'] ?? null; +$jsonNode = $data['json_node'] ?? null; $manualDefault = $data['manual_default'] ?? null; $autoValue = $data['auto_value'] ?? 'none'; @@ -26,7 +28,7 @@ $tablename = $data['tablename'] ?? ''; try { // Normalize mapping type - $allowedTypes = ['', 'xls', 'manual', 'auto']; + $allowedTypes = ['', 'xls', 'json', 'manual', 'auto']; if (!in_array($mappingType, $allowedTypes, true)) { echo json_encode(["success" => false, "message" => "Invalid mapping_type"]); exit; @@ -41,43 +43,67 @@ try { // Decide what to persist based on mapping_type $isManual = 0; $excelToSave = null; + $jsonNodeToSave = null; $manualToSave = null; $autoToSave = 'none'; if ($mappingType === 'xls') { + $isManual = 0; $excelToSave = $excelColumn ?: null; + $jsonNodeToSave = null; + $manualToSave = null; + $autoToSave = 'none'; + } elseif ($mappingType === 'json') { + + $isManual = 0; + $excelToSave = null; + $jsonNodeToSave = $jsonNode ?: null; $manualToSave = null; $autoToSave = 'none'; } elseif ($mappingType === 'manual') { + $isManual = 1; $excelToSave = null; + $jsonNodeToSave = null; $manualToSave = $manualDefault; $autoToSave = 'none'; } elseif ($mappingType === 'auto') { + $isManual = 0; $excelToSave = null; + $jsonNodeToSave = null; $manualToSave = null; $autoToSave = $autoValue ?: 'none'; } else { - // reset + + // Reset mapping $isManual = 0; $excelToSave = null; + $jsonNodeToSave = null; $manualToSave = null; $autoToSave = 'none'; } $stmt = $pdo->prepare(" - UPDATE template_mapping - SET - is_manual = ?, - excel_column = ?, - manual_default = ?, - auto_value = ? - WHERE id = ? -"); + UPDATE template_mapping + SET + is_manual = ?, + excel_column = ?, + json_node = ?, + manual_default = ?, + auto_value = ? + WHERE id = ? + "); - $result = $stmt->execute([$isManual, $excelToSave, $manualToSave, $autoToSave, $mappingId]); + $result = $stmt->execute([ + $isManual, + $excelToSave, + $jsonNodeToSave, + $manualToSave, + $autoToSave, + $mappingId + ]); if (!$result) { echo json_encode(["success" => false, "message" => "Database update failed"]); @@ -88,15 +114,17 @@ try { "success" => true, "message" => "Mapping updated successfully", "saved" => [ - "id" => (int)$mappingId, + "id" => $mappingId, "mapping_type" => $mappingType, "is_manual" => $isManual, "excel_column" => $excelToSave, + "json_node" => $jsonNodeToSave, "manual_default" => $manualToSave, "auto_value" => $autoToSave ] ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]); } + exit; diff --git a/public/userarea/schema_dettagli_response.json b/public/userarea/schema_dettagli_response.json deleted file mode 100644 index ae042f3..0000000 --- a/public/userarea/schema_dettagli_response.json +++ /dev/null @@ -1 +0,0 @@ -{"@odata.context":"https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#SchemaCustomField(SchemiCustomFieldsDettagli(CustomField()))\/$entity","IdSchemaCustomFields":82,"ConteggioClienti":0,"Nome":"MONCLER Brand","Descrizione":"Da utilizzare solo per il Brand Diretto\r\nGR 19\/03\/2024","SchemiCustomFieldsDettagli":[{"IdSchemaCustomFieldsDettaglio":2124,"Ordine":1,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":1083,"TitoloTraduzione":"MONCLER_Analisi Commissionate da: ","Titolo":"MONCLER_Analisi Commissionate da: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2125,"Ordine":2,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":214,"TitoloTraduzione":"Season","Titolo":"Season","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2126,"Ordine":3,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":467,"TitoloTraduzione":"Sample Code:","Titolo":"Sample Code:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2127,"Ordine":4,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":210,"TitoloTraduzione":"Sample Description ","Titolo":"Sample Description ","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2128,"Ordine":5,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":264,"TitoloTraduzione":"Color Code:","Titolo":"Color Code:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2129,"Ordine":6,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":263,"TitoloTraduzione":"Style Code:","Titolo":"Style Code:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2130,"Ordine":9,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":468,"TitoloTraduzione":"Style Description:","Titolo":"Style Description:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":5787,"Ordine":12,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":1014,"TitoloTraduzione":"CDC","Titolo":"CDC","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4259,"Ordine":13,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":207,"TitoloTraduzione":"Composition Claimed","Titolo":"Composition Claimed","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":6544,"Ordine":14,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":543,"TitoloTraduzione":"Collezione:","Titolo":"Collezione:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2132,"Ordine":15,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":167,"TitoloTraduzione":"Capitolato di riferimento","Titolo":"Capitolato di riferimento","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2133,"Ordine":16,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":161,"TitoloTraduzione":"Categoria:","Titolo":"Categoria:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2134,"Ordine":17,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":150,"TitoloTraduzione":"Destinazione d'uso: ","Titolo":"Destinazione d'uso: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2135,"Ordine":18,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":156,"TitoloTraduzione":"Tipologia di Materiale:","Titolo":"Tipologia di Materiale:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2136,"Ordine":19,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":146,"TitoloTraduzione":"Tipologia di Trattamento Superficiale:","Titolo":"Tipologia di Trattamento Superficiale:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2137,"Ordine":20,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":147,"TitoloTraduzione":"Tipologia di concia: ","Titolo":"Tipologia di concia: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2155,"Ordine":21,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":142,"TitoloTraduzione":"Fornitore:","Titolo":"Fornitore:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2156,"Ordine":22,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":194,"TitoloTraduzione":"Fabbricante:","Titolo":"Fabbricante:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4022,"Ordine":23,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":595,"TitoloTraduzione":"Final Customer: ","Titolo":"Final Customer: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":"MONCLER","Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4021,"Ordine":24,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":748,"TitoloTraduzione":"Analisi Richieste:","Titolo":"Analisi Richieste:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2473,"Ordine":25,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":189,"TitoloTraduzione":"Tested Component:","Titolo":"Tested Component:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2176,"Ordine":26,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":166,"TitoloTraduzione":"Note:","Titolo":"Note:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2503,"Ordine":27,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":555,"TitoloTraduzione":"Additional Info:","Titolo":"Additional Info:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2138,"Ordine":28,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":362,"TitoloTraduzione":"Tracciante materiale","Titolo":"Tracciante materiale","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2146,"Ordine":29,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":193,"TitoloTraduzione":"DDT N. ","Titolo":"DDT N. ","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2145,"Ordine":30,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":370,"TitoloTraduzione":"DDT del","Titolo":"DDT del","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2480,"Ordine":31,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":371,"TitoloTraduzione":"Mittente (se diverso da Committente)","Titolo":"Mittente (se diverso da Committente)","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2476,"Ordine":32,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":372,"TitoloTraduzione":"Quantit\u00e0","Titolo":"Quantit\u00e0","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2149,"Ordine":33,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":259,"TitoloTraduzione":"Sample Arrival Date:","Titolo":"Sample Arrival Date:","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2150,"Ordine":34,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":348,"TitoloTraduzione":"Sample Arrival Time:","Titolo":"Sample Arrival Time:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2151,"Ordine":35,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":260,"TitoloTraduzione":"Sample Unlock Date:","Titolo":"Sample Unlock Date:","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2152,"Ordine":36,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":349,"TitoloTraduzione":"Sample Unlock Time:","Titolo":"Sample Unlock Time:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2153,"Ordine":37,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":361,"TitoloTraduzione":"Presa in carico, Data:","Titolo":"Presa in carico, Data:","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2154,"Ordine":38,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":360,"TitoloTraduzione":"Presa in carico, Orario:","Titolo":"Presa in carico, Orario:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2157,"Ordine":39,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":163,"TitoloTraduzione":"Campionamento:","Titolo":"Campionamento:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2158,"Ordine":40,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":165,"TitoloTraduzione":"Condizionamento prima e durante il Test: ","Titolo":"Condizionamento prima e durante il Test: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2159,"Ordine":41,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":181,"TitoloTraduzione":"Preparazione del campione ai test chimici: ","Titolo":"Preparazione del campione ai test chimici: ","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2161,"Ordine":42,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":169,"TitoloTraduzione":"(*Restituzione Materiale Residuo:","Titolo":"(*Restituzione Materiale Residuo:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":"","Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2163,"Ordine":43,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":170,"TitoloTraduzione":"(*Originali:","Titolo":"(*Originali:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":"","Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2164,"Ordine":44,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":171,"TitoloTraduzione":"(*Corriere:","Titolo":"(*Corriere:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":"","Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2165,"Ordine":45,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":241,"TitoloTraduzione":"N. di Prove:","Titolo":"N. di Prove:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2166,"Ordine":46,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":244,"TitoloTraduzione":"Accettatore:","Titolo":"Accettatore:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2167,"Ordine":47,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":258,"TitoloTraduzione":"Login Date:","Titolo":"Login Date:","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2168,"Ordine":48,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":350,"TitoloTraduzione":"Login Time:","Titolo":"Login Time:","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2169,"Ordine":49,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":236,"TitoloTraduzione":"Consegna campione:","Titolo":"Consegna campione:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2170,"Ordine":50,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":246,"TitoloTraduzione":"Lab outsourcing:","Titolo":"Lab outsourcing:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2171,"Ordine":51,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":254,"TitoloTraduzione":"AWB: ","Titolo":"AWB: ","Descrizione":null,"Tipo":"Testo","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2172,"Ordine":52,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":255,"TitoloTraduzione":"Data ricezione risultati test:","Titolo":"Data ricezione risultati test:","Descrizione":null,"Tipo":"Data","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2173,"Ordine":53,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":253,"TitoloTraduzione":"Sample return:","Titolo":"Sample return:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2174,"Ordine":54,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":252,"TitoloTraduzione":"Service required:","Titolo":"Service required:","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":true,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":2175,"Ordine":55,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":261,"TitoloTraduzione":"Tipologia report (solo per Cina)","Titolo":"Tipologia report (solo per Cina)","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4150,"Ordine":56,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":772,"TitoloTraduzione":"RATING CHINA MONCLER","Titolo":"RATING CHINA MONCLER","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4151,"Ordine":57,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":773,"TitoloTraduzione":"RATING MANUAL MONCLER","Titolo":"RATING MANUAL MONCLER","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}},{"IdSchemaCustomFieldsDettaglio":4149,"Ordine":58,"ObbligatorioWeb":"Predefinito","RaggruppamentoWeb":null,"CustomField":{"IdCustomField":771,"TitoloTraduzione":"RATING RSL MONCLER","Titolo":"RATING RSL MONCLER","Descrizione":null,"Tipo":"SceltaMultipla","Decimali":0,"Lunghezza":0,"Minimo":null,"Massimo":null,"ValoreDefault":null,"Elenco":false,"DefaultCurrDate":false,"ObbligatorioWeb":false}}]} \ No newline at end of file diff --git a/public/userarea/schemi_base_response.json b/public/userarea/schemi_base_response.json index 4219d5a..4e39ea7 100644 --- a/public/userarea/schemi_base_response.json +++ b/public/userarea/schemi_base_response.json @@ -46,8 +46,8 @@ { "IdSchemaCustomFields": 48, "ConteggioClienti": 0, - "Nome": "Standard Generico \/ Generic Standard", - "Descrizione": "Schema per tutti i campioni di qualsiasi matrice escluso cuoio\/pelle\r\n\r\n" + "Nome": "Standard \/ Generico", + "Descrizione": "\r\n" }, { "IdSchemaCustomFields": 49, @@ -653,7 +653,7 @@ "IdSchemaCustomFields": 163, "ConteggioClienti": 0, "Nome": "Devred", - "Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024\r\n\r\n" + "Descrizione": "Schema creato per cliente DEVRED\r\nGR 18\/03\/2024 aggiornato il 23\/04\/2026\r\n\r\n" }, { "IdSchemaCustomFields": 164, @@ -882,6 +882,24 @@ "ConteggioClienti": 0, "Nome": "LIMS-CIM - MAX MARA", "Descrizione": "Schema per MAX MARA scambio dati Database" + }, + { + "IdSchemaCustomFields": 203, + "ConteggioClienti": 0, + "Nome": "Vince", + "Descrizione": "Schema per tutti i campioni di VINCE\r\n\r\n" + }, + { + "IdSchemaCustomFields": 204, + "ConteggioClienti": 0, + "Nome": "Max Mara", + "Descrizione": "Schema da usare per Max Mara\r\n" + }, + { + "IdSchemaCustomFields": 205, + "ConteggioClienti": 0, + "Nome": "Chanel Flammability", + "Descrizione": "Schema per Chanel Flammability\r\n" } ] } \ No newline at end of file diff --git a/public/userarea/search_clienti.php b/public/userarea/search_clienti.php index f624c6e..5f00884 100644 --- a/public/userarea/search_clienti.php +++ b/public/userarea/search_clienti.php @@ -2,7 +2,12 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once __DIR__ . '/class/db-functions.php'; include dirname(__DIR__) . '/../extra/auth.php'; -if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } + +if (!Auth::check()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + exit; +} require_once __DIR__ . '/class/VisualLimsApiClient.class.php'; @@ -14,44 +19,95 @@ $q = mb_strtolower(trim($_GET['q'] ?? '')); $limit = max(1, min(50, intval($_GET['limit'] ?? 20))); $id = isset($_GET['id']) ? intval($_GET['id']) : null; +function formatClientLabel(array $client): string +{ + $name = trim($client['Nominativo'] ?? ''); + $id = trim((string)($client['IdCliente'] ?? '')); + $code = trim((string)($client['CodiceCliente'] ?? '')); + + $parts = explode('_', $code); + $suffix = trim($parts[1] ?? ''); + + if ($suffix === '' && $code !== '') { + $suffix = substr($code, 0, 1); + } + + if ($suffix === '') { + $suffix = '--'; + } + + return $name . ' - ' . $suffix . ' (ID: ' . $id . ')'; +} + try { // Load from cache or API $cacheFile = __DIR__ . '/cache/clienti.json'; + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) { $data = json_decode(file_get_contents($cacheFile), true); } else { $api = VisualLimsApiClient::getInstance(); + $params = [ '$select' => 'IdCliente,Nominativo,CodiceCliente', '$orderby' => 'Nominativo asc' ]; + $data = $api->get("Cliente?" . http_build_query($params)); - if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true); + + if (!is_dir(__DIR__ . '/cache')) { + mkdir(__DIR__ . '/cache', 0777, true); + } + file_put_contents($cacheFile, json_encode($data)); } $clients = $data['value'] ?? []; - // If requesting by specific ID (for loading selected value) + // If requesting by specific ID, used for loading selected value if ($id !== null) { foreach ($clients as $c) { if ((int)$c['IdCliente'] === $id) { - echo json_encode(['results' => [['id' => $c['IdCliente'], 'text' => trim($c['Nominativo'] ?? '')]]]); + echo json_encode([ + 'results' => [[ + 'id' => $c['IdCliente'], + 'text' => formatClientLabel($c), + 'IdCliente' => $c['IdCliente'], + 'Nominativo' => trim($c['Nominativo'] ?? ''), + 'CodiceCliente' => trim($c['CodiceCliente'] ?? '') + ]] + ]); exit; } } + echo json_encode(['results' => []]); exit; } // Search by query $results = []; + foreach ($clients as $c) { $name = trim($c['Nominativo'] ?? ''); $code = trim($c['CodiceCliente'] ?? ''); - if ($q === '' || mb_strpos(mb_strtolower($name), $q) !== false || mb_strpos(mb_strtolower($code), $q) !== false) { - $results[] = ['id' => $c['IdCliente'], 'text' => $name]; - if (count($results) >= $limit) break; + + if ( + $q === '' || + mb_strpos(mb_strtolower($name), $q) !== false || + mb_strpos(mb_strtolower($code), $q) !== false + ) { + $results[] = [ + 'id' => $c['IdCliente'], + 'text' => formatClientLabel($c), + 'IdCliente' => $c['IdCliente'], + 'Nominativo' => $name, + 'CodiceCliente' => $code + ]; + + if (count($results) >= $limit) { + break; + } } } diff --git a/public/userarea/search_customfield_values.php b/public/userarea/search_customfield_values.php index f0c8273..6e4205a 100644 --- a/public/userarea/search_customfield_values.php +++ b/public/userarea/search_customfield_values.php @@ -2,7 +2,11 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once __DIR__ . '/class/db-functions.php'; include dirname(__DIR__) . '/../extra/auth.php'; -if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } +if (!Auth::check()) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + exit; +} require_once __DIR__ . '/class/VisualLimsApiClient.class.php'; @@ -13,7 +17,8 @@ error_reporting(E_ALL); $fieldId = intval($_GET['field_id'] ?? 0); $q = mb_strtolower(trim($_GET['q'] ?? '')); $id = isset($_GET['id']) ? intval($_GET['id']) : null; -$limit = max(1, min(50, intval($_GET['limit'] ?? 20))); +$rawLimit = intval($_GET['limit'] ?? 20); +$limit = $rawLimit <= 0 ? 0 : max(1, min(500, $rawLimit)); if (!$fieldId) { echo json_encode(['results' => []]); @@ -52,7 +57,7 @@ try { $text = $v['Valore'] ?? ''; if ($q === '' || mb_strpos(mb_strtolower($text), $q) !== false) { $results[] = ['id' => $v['IdCustomFieldsValue'], 'text' => $text]; - if (count($results) >= $limit) break; + if ($limit > 0 && count($results) >= $limit) break; } } diff --git a/public/userarea/search_rapporto_min.php b/public/userarea/search_rapporto_min.php new file mode 100644 index 0000000..1f8487c --- /dev/null +++ b/public/userarea/search_rapporto_min.php @@ -0,0 +1,73 @@ + "Codice eq '{$codice}'", + 'CodiceRapporto' => "CodiceRapporto eq '{$codice}'", + 'Numero' => "Numero eq '{$codice}'" + ]; + + foreach ($filters as $label => $filter) { + try { + $data = $api->get('Rapporto', [ + '$filter' => $filter, + '$top' => 5, + '$select' => 'IdRapporto,Codice,CodiceRapporto,Numero,Data,Versione,Cliente' + ]); + + $attempts[$label] = [ + 'success' => true, + 'filter' => $filter, + 'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null, + 'data' => $data + ]; + + } catch (Exception $e) { + $attempts[$label] = [ + 'success' => false, + 'filter' => $filter, + 'error' => $e->getMessage() + ]; + } + } + + echo json_encode([ + 'success' => true, + 'input_codice' => $codice, + 'attempts' => $attempts + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +} catch (Exception $e) { + http_response_code(500); + + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); +} \ No newline at end of file diff --git a/public/userarea/templates_dashboard.php b/public/userarea/templates_dashboard.php index 15cc5de..38fe986 100644 --- a/public/userarea/templates_dashboard.php +++ b/public/userarea/templates_dashboard.php @@ -181,6 +181,20 @@