diff --git a/public/userarea/class/debug_odata_metadata.php b/public/userarea/class/debug_odata_metadata.php new file mode 100644 index 00000000..a1913834 --- /dev/null +++ b/public/userarea/class/debug_odata_metadata.php @@ -0,0 +1,152 @@ + 'Unauthorized']); + exit; +} + +use Dotenv\Dotenv; + +header('Content-Type: text/plain; charset=utf-8'); +ini_set('display_errors', '1'); +error_reporting(E_ALL); + +$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2)); +$dotenv->load(); + +$baseUrl = $_ENV['API_BASE_URL']; +$username = $_ENV['API_USERNAME']; +$password = $_ENV['API_PASSWORD']; + +$scriptStart = microtime(true); + +// ── 1. Autenticazione (stessa logica della classe originale) ────────────── +$ch = curl_init("{$baseUrl}/api/authentication/authenticate"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'Username' => $username, + 'Password' => $password +])); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json' +]); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + +$authResponse = curl_exec($ch); +$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($authHttpCode !== 200) { + echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n"; + exit; +} + +$tokenData = json_decode($authResponse, true); +$token = null; +if (is_array($tokenData) && isset($tokenData['token'])) { + $token = $tokenData['token']; +} elseif (is_string($tokenData)) { + $token = trim($tokenData, '"'); +} else { + $token = trim($authResponse, '"'); +} + +if (empty($token)) { + echo "TOKEN NOT RECEIVED\n{$authResponse}\n"; + exit; +} + +$authElapsed = microtime(true) - $scriptStart; + +// ── 2. Richiesta $metadata (XML, non JSON) ───────────────────────────────── +// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto. +$cacheBuster = uniqid('cb_', true); +$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}"; + +$metadataStart = microtime(true); + +$ch = curl_init($metadataUrl); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer {$token}", + "Accept: application/xml" +]); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + +curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione + +$rawResponse = curl_exec($ch); +$curlInfo = curl_getinfo($ch); +$httpCode = $curlInfo['http_code']; +$curlTotalTime = $curlInfo['total_time']; +$curlError = curl_error($ch); +$headerSize = $curlInfo['header_size']; +curl_close($ch); + +$metadataElapsed = microtime(true) - $metadataStart; + +if ($rawResponse === false || $httpCode !== 200) { + echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n"; + exit; +} + +$responseHeaders = substr($rawResponse, 0, $headerSize); +$xml = substr($rawResponse, $headerSize); + +echo "==================== TIMING (prova che NON è cache) ====================\n"; +echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n"; +echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n"; +echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n"; +echo "Cache-buster usato: {$cacheBuster}\n"; +echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n"; + +echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n"; +echo $responseHeaders . "\n"; + +// ── 3. Estrai tutti gli EntitySet disponibili ────────────────────────────── +echo "==================== ENTITY SETS DISPONIBILI ====================\n"; +if (preg_match_all('/]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) { + foreach ($entityMatches as $entity) { + $entityName = $entity[1]; + $entityBody = $entity[2]; + echo "\n--- EntityType: {$entityName} ---\n"; + if (preg_match_all('/ 'Unauthorized']); + exit; +} + +use Dotenv\Dotenv; + +header('Content-Type: text/plain; charset=utf-8'); +ini_set('display_errors', '1'); +error_reporting(E_ALL); + +$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2)); +$dotenv->load(); + +$baseUrl = $_ENV['API_BASE_URL']; +$username = $_ENV['API_USERNAME']; +$password = $_ENV['API_PASSWORD']; + +$scriptStart = microtime(true); + +// ── 1. Autenticazione (stessa logica della classe originale) ────────────── +$ch = curl_init("{$baseUrl}/api/authentication/authenticate"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'Username' => $username, + 'Password' => $password +])); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json' +]); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + +$authResponse = curl_exec($ch); +$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($authHttpCode !== 200) { + echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n"; + exit; +} + +$tokenData = json_decode($authResponse, true); +$token = null; +if (is_array($tokenData) && isset($tokenData['token'])) { + $token = $tokenData['token']; +} elseif (is_string($tokenData)) { + $token = trim($tokenData, '"'); +} else { + $token = trim($authResponse, '"'); +} + +if (empty($token)) { + echo "TOKEN NOT RECEIVED\n{$authResponse}\n"; + exit; +} + +$authElapsed = microtime(true) - $scriptStart; + +// ── 2. Richiesta $metadata (XML, non JSON) ───────────────────────────────── +// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto. +$cacheBuster = uniqid('cb_', true); +$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}"; + +$metadataStart = microtime(true); + +$ch = curl_init($metadataUrl); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer {$token}", + "Accept: application/xml" +]); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + +curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione + +$rawResponse = curl_exec($ch); +$curlInfo = curl_getinfo($ch); +$httpCode = $curlInfo['http_code']; +$curlTotalTime = $curlInfo['total_time']; +$curlError = curl_error($ch); +$headerSize = $curlInfo['header_size']; +curl_close($ch); + +$metadataElapsed = microtime(true) - $metadataStart; + +if ($rawResponse === false || $httpCode !== 200) { + echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n"; + exit; +} + +$responseHeaders = substr($rawResponse, 0, $headerSize); +$xml = substr($rawResponse, $headerSize); + +echo "==================== TIMING (prova che NON è cache) ====================\n"; +echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n"; +echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n"; +echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n"; +echo "Cache-buster usato: {$cacheBuster}\n"; +echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n"; + +echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n"; +echo $responseHeaders . "\n"; + +// ── 3. Estrai tutti gli EntitySet disponibili ────────────────────────────── +echo "==================== ENTITY SETS DISPONIBILI ====================\n"; +if (preg_match_all('/]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) { + foreach ($entityMatches as $entity) { + $entityName = $entity[1]; + $entityBody = $entity[2]; + echo "\n--- EntityType: {$entityName} ---\n"; + if (preg_match_all('/ 'IdCliente,Nominativo,CodiceCliente', + '$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo', '$orderby' => 'Nominativo asc' ]; diff --git a/public/userarea/get_clienti_debug.php b/public/userarea/get_clienti_debug.php new file mode 100644 index 00000000..1d1b0280 --- /dev/null +++ b/public/userarea/get_clienti_debug.php @@ -0,0 +1,127 @@ +getConnection(); + $stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC"); + $ids = $stmt->fetchAll(PDO::FETCH_COLUMN); + $fakeClients = array_map(fn($id) => [ + 'IdCliente' => (int) $id, + 'Nominativo' => "Cliente Simulato {$id}", + 'CodiceCliente' => "SIM_{$id}", + ], $ids); + echo json_encode(['value' => $fakeClients]); + exit; + } + + // ── DEBUG MODE ─────────────────────────────────────────────────────── + // Chiamata: get_clienti.php?debug=1 + // Bypassa cache e $select per vedere TUTTI i campi grezzi restituiti + // dall'API per l'entità Cliente. Usare solo per ispezione manuale, + // NON lasciare linkato/usato in produzione dal frontend. + if (($_GET['debug'] ?? '') === '1') { + $debugParams = [ + '$top' => 5, + '$orderby' => 'Nominativo asc' + // Nessun $select: chiediamo tutti i campi disponibili + ]; + $debugEndpoint = "Cliente?" . http_build_query($debugParams); + $debugData = $api->get($debugEndpoint); + $debugClients = $debugData['value'] ?? $debugData; + + echo json_encode([ + 'mode' => 'debug - no $select, cache bypassed', + 'top_level_keys' => is_array($debugData) ? array_keys($debugData) : null, + 'fields_found_in_first_record' => isset($debugClients[0]) ? array_keys($debugClients[0]) : null, + 'sample_records' => $debugClients, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + exit; + } + // ── FINE DEBUG MODE ────────────────────────────────────────────────── + + // Parametri OData + $params = [ + '$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo', + '$orderby' => 'Nominativo asc' + ]; + + // Costruisce query string con encoding corretto + $queryString = http_build_query($params); + + // Componi endpoint finale + $endpoint = "Cliente?$queryString"; + + // Funzione per eseguire la chiamata con retry + function makeApiRequest($api, $endpoint, $maxRetries = 3) + { + for ($retry = 0; $retry < $maxRetries; $retry++) { + try { + // Tenta la chiamata API + $data = $api->get($endpoint); + // Salva risposta per debug + file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT)); + return $data; + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + // Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico) + if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) { + // Forza il refresh del token + try { + // Assumi che VisualLimsApiClient abbia un metodo per il refresh del token + $api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste + error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint"); + } catch (Exception $refreshEx) { + error_log("Errore durante il refresh del token: " . $refreshEx->getMessage()); + throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage()); + } + // Ritarda leggermente prima del retry + usleep(500000); // 500ms + continue; + } + // Altri errori non gestiti dal retry + throw $e; + } + } + throw new Exception("Massimo numero di tentativi raggiunto per $endpoint"); + } + + // Cache file (1 hour TTL) + $cacheFile = __DIR__ . '/cache/clienti.json'; + if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) { + readfile($cacheFile); + exit; + } + + // Esegui la chiamata con retry + $data = makeApiRequest($api, $endpoint); + + $json = json_encode($data); + if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true); + file_put_contents($cacheFile, $json); + + echo $json; +} catch (Exception $e) { + http_response_code(500); + $errorResponse = [ + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]; + error_log("Errore in get_clienti.php: " . json_encode($errorResponse)); + echo json_encode($errorResponse); +} diff --git a/public/userarea/mapping_template_xls_scheme2.php b/public/userarea/mapping_template_xls_scheme2.php index f36ca483..e516f559 100644 --- a/public/userarea/mapping_template_xls_scheme2.php +++ b/public/userarea/mapping_template_xls_scheme2.php @@ -1874,7 +1874,70 @@ $apiSampleJson = $template['api_sample_json'] ?? ''; document.querySelectorAll('#schemaFieldsBody .main-field-checkbox').forEach(cb => { cb.dataset.originalChecked = cb.checked ? 'true' : 'false'; }); + // ======================= + // SAVE: Main / Import / Parts checkboxes + // ======================= + function saveMappingFlag(checkboxEl, field) { + const mappingId = checkboxEl.getAttribute('data-mapping-id'); + const checked = checkboxEl.checked; + fetch('update_mapping_flag.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: mappingId, + field: field, + value: checked ? 1 : 0 + }) + }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + console.error("❌ Error saving flag:", field, data.message); + + if (data.limit_reached) { + alert(data.message); + } + + // Revert: il server ha rifiutato, riporto la checkbox allo stato precedente + checkboxEl.checked = !checked; + return; + } + + console.log("✅ Flag saved:", field, "=", checked ? 1 : 0, "for id", mappingId); + + // Se il server ha deselezionato un'altra riga (caso "parts"), rifletto anche in UI + if (data.unchecked_others && field === 'is_visible_parts') { + document.querySelectorAll('#schemaFieldsBody .visible-parts-checkbox').forEach(cb => { + if (cb !== checkboxEl) { + cb.checked = false; + } + }); + } + }) + .catch(error => { + console.error("❌ Fetch error saving flag:", field, error); + checkboxEl.checked = !checked; + }); + } + + document.getElementById('schemaFieldsBody').addEventListener('change', function(event) { + const el = event.target; + + if (el.classList.contains('main-field-checkbox')) { + saveMappingFlag(el, 'main_field'); + } + + if (el.classList.contains('visible-import-checkbox')) { + saveMappingFlag(el, 'is_visible_import'); + } + + if (el.classList.contains('visible-parts-checkbox')) { + saveMappingFlag(el, 'is_visible_parts'); + } + }); // AUTO VALUE select change -> save auto_value document.getElementById('schemaFieldsBody').addEventListener('change', function(event) { if (!event.target.classList.contains('auto-value-select')) return; diff --git a/public/userarea/metadata_debug.xml b/public/userarea/metadata_debug.xml new file mode 100644 index 00000000..e4d87f3a --- /dev/null +++ b/public/userarea/metadata_debug.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/userarea/update_mapping_flag.php b/public/userarea/update_mapping_flag.php new file mode 100644 index 00000000..4344798f --- /dev/null +++ b/public/userarea/update_mapping_flag.php @@ -0,0 +1,107 @@ +getConnection(); + +$data = json_decode(file_get_contents("php://input"), true); + +if (!$data || !isset($data['id'], $data['field'])) { + echo json_encode(["success" => false, "message" => "Invalid or missing parameters"]); + exit; +} + +$mappingId = (int)$data['id']; +$field = $data['field']; +$value = isset($data['value']) && $data['value'] ? 1 : 0; + +// Whitelist rigorosa dei campi aggiornabili: evita SQL injection sul nome colonna +$allowedFields = ['main_field', 'is_visible_import', 'is_visible_parts']; + +if (!in_array($field, $allowedFields, true)) { + echo json_encode(["success" => false, "message" => "Invalid field"]); + exit; +} + +try { + // Recupero il template_id della riga per poter applicare i vincoli a livello di template + $stmt = $pdo->prepare("SELECT template_id FROM template_mapping WHERE id = ?"); + $stmt->execute([$mappingId]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$row) { + echo json_encode(["success" => false, "message" => "Mapping not found"]); + exit; + } + + $templateId = (int)$row['template_id']; + + // Vincolo: max 2 righe con main_field = 1 per template + if ($field === 'main_field' && $value === 1) { + $stmt = $pdo->prepare(" + SELECT COUNT(*) + FROM template_mapping + WHERE template_id = ? AND main_field = 1 AND id != ? + "); + $stmt->execute([$templateId, $mappingId]); + $count = (int)$stmt->fetchColumn(); + + if ($count >= 2) { + echo json_encode([ + "success" => false, + "limit_reached" => true, + "message" => "Massimo 2 campi Main consentiti per questo template" + ]); + exit; + } + } + + $pdo->beginTransaction(); + + $uncheckedOthers = false; + + // Vincolo: solo 1 riga con is_visible_parts = 1 per template (comportamento radio) + if ($field === 'is_visible_parts' && $value === 1) { + $stmt = $pdo->prepare(" + UPDATE template_mapping + SET is_visible_parts = 0 + WHERE template_id = ? AND id != ? AND is_visible_parts = 1 + "); + $stmt->execute([$templateId, $mappingId]); + $uncheckedOthers = $stmt->rowCount() > 0; + } + + $stmt = $pdo->prepare("UPDATE template_mapping SET {$field} = ? WHERE id = ?"); + $result = $stmt->execute([$value, $mappingId]); + + if (!$result) { + $pdo->rollBack(); + echo json_encode(["success" => false, "message" => "Database update failed"]); + exit; + } + + $pdo->commit(); + + echo json_encode([ + "success" => true, + "message" => "Flag updated successfully", + "unchecked_others" => $uncheckedOthers, + "saved" => [ + "id" => $mappingId, + "field" => $field, + "value" => $value + ] + ]); +} catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + echo json_encode(["success" => false, "message" => "Error: " . $e->getMessage()]); +} + +exit; diff --git a/public/userarea/update_schemajson.php b/public/userarea/update_schemajson.php index 5b31599f..07e7657c 100644 --- a/public/userarea/update_schemajson.php +++ b/public/userarea/update_schemajson.php @@ -135,25 +135,26 @@ try { // Update existing field AND clear user-entered value if type changed $updateChangedTypeStmt = $pdo->prepare(" - UPDATE template_mapping - SET - schema_id = :schema_id, - data_type = :data_type, - is_required = :is_required, - default_value = :default_value, - has_list = :has_list, - length = :length, - decimals = :decimals, - min_value = :min_value, - max_value = :max_value, - default_curr_date = :default_curr_date, - tablename = :tablename, - field_label = :field_label, + UPDATE template_mapping + SET + schema_id = :schema_id, + field_order = :field_order, + data_type = :data_type, + is_required = :is_required, + default_value = :default_value, + has_list = :has_list, + length = :length, + decimals = :decimals, + min_value = :min_value, + max_value = :max_value, + default_curr_date = :default_curr_date, + tablename = :tablename, + field_label = :field_label, - manual_default = NULL, - auto_value = 'none' + manual_default = NULL, + auto_value = 'none' - WHERE id = :id + WHERE id = :id "); $currentFieldIds = []; diff --git a/public/userarea/validate_export.php b/public/userarea/validate_export.php index b20e4132..0f692e9a 100644 --- a/public/userarea/validate_export.php +++ b/public/userarea/validate_export.php @@ -177,6 +177,8 @@ try { SELECT iddatadb, part_number, part_description, idmatrice FROM identification_parts WHERE iddatadb IN ($placeholders) + AND part_description IS NOT NULL + AND TRIM(part_description) <> '' "); $stmt->execute($iddatadbList); $partsInfo = [];