getConnection(); header("Content-Type: application/json"); // 🔹 Configura directory log (creala se non esiste) $logDir = __DIR__ . '/logs/api/'; if (!is_dir($logDir)) { mkdir($logDir, 0755, true); } $uploadDir = realpath(__DIR__ . '/../photostrf') . '/'; // 🔹 Base URL API $apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/'; // 🔹 Batch UUID — if present, all logs go to a single file $batchUuid = $_POST['batch_uuid'] ?? null; $writeLog = (function () use ($batchUuid, $logDir) { $batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null; return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) { if ($batchLogFile) { $header = "\n" . str_repeat("=", 60) . "\n"; if ($stepLabel) { $header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n"; } $header .= str_repeat("=", 60) . "\n"; file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND); } else { file_put_contents($individualPath, $content); } }; })(); // 🔹 Funzione per validare e convertire date function validateDate($value) { // Prova a validare come data (accetta formati comuni) $date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value); if ($date) { return $date->format('Y-m-d\TH:i:sP'); // Formato ISO 8601 } return null; // Imposta null se non è una data valida } // 🔹 Funzione per validare e convertire date function formatDateToExport($value) { $date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value); if ($date) { return $date->format('d/m/Y'); } return null; // Imposta null se non è una data valida } try { $iddatadb = $_POST['iddatadb'] ?? null; if (!$iddatadb) { throw new Exception("Missing iddatadb"); } // TEMP: simulate error on every other row for testing if (env('SIMULATE_EXPORT_LIMS') && $iddatadb % 2 === 0) { throw new Exception("Simulated error for iddatadb $iddatadb"); } // 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates // Also fetch fixed fields stored in datadb $stmt = $pdo->prepare(" SELECT d.idclient AS clienteId, et.idschema AS schemaId, d.cliente_responsabile_id, d.moltiplicatore_prezzo_id, d.anagrafica_certest_object_id, d.anagrafica_certest_service_id, d.cliente_fornitore_id, d.consegna_richiesta FROM datadb as d INNER JOIN excel_templates as et ON d.templateid = et.id WHERE d.iddatadb = :iddatadb LIMIT 1 "); $stmt->execute(['iddatadb' => $iddatadb]); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!$result) { throw new Exception("No Cliente/Schema found for iddatadb {$iddatadb}"); } $clienteId = (int) $result['clienteId']; $schemaId = (int) $result['schemaId']; // Extract fixed fields (nullable INTs) $clienteResponsabile = !empty($result['cliente_responsabile_id']) ? (int) $result['cliente_responsabile_id'] : null; $moltiplicatorePrezzo = !empty($result['moltiplicatore_prezzo_id']) ? (int) $result['moltiplicatore_prezzo_id'] : null; $anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null; $anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null; $clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null; $clienteAnalisi = !empty($result['clienteAnalisi']) ? (int) $result['clienteAnalisi'] : null; $consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null; // 🔹 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 "); $stmt->execute(['iddatadb' => $iddatadb]); $parts = $stmt->fetchAll(PDO::FETCH_ASSOC); // 🔹 STEP 3.1: Fetch custom field values per part from identification_parts_customfields $partIds = array_column($parts, 'part_id'); $partsCustomFields = []; // part_id => [ { field_id, value_id, value_text }, ... ] if (!empty($partIds)) { $placeholders = implode(',', array_fill(0, count($partIds), '?')); $cfStmt = $pdo->prepare(" SELECT part_id, field_id, value_id, value_text FROM identification_parts_customfields WHERE part_id IN ({$placeholders}) "); $cfStmt->execute($partIds); foreach ($cfStmt->fetchAll(PDO::FETCH_ASSOC) as $cfRow) { $partsCustomFields[(int)$cfRow['part_id']][] = $cfRow; } } // 🔹 STEP 4a: Auto-populate export_date / export_time fields $stmt = $pdo->prepare(" UPDATE import_data_details idd JOIN template_mapping m ON idd.mapping_id = m.id SET idd.field_value = CASE m.auto_value WHEN 'export_date' THEN :export_date WHEN 'export_time' THEN :export_time END WHERE idd.id = :iddatadb AND m.auto_value IN ('export_date', 'export_time') "); $stmt->execute([ 'iddatadb' => $iddatadb, 'export_date' => date('Y-m-d'), 'export_time' => date('H:i'), ]); // 🔹 STEP 4: Fetch Field Values with Labels $stmt = $pdo->prepare(" SELECT idd.field_value, m.field_label, m.schema_id, m.field_id FROM import_data_details as idd JOIN template_mapping m ON idd.mapping_id = m.id WHERE idd.id = :iddatadb "); $stmt->execute(['iddatadb' => $iddatadb]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $fieldValues = []; $valueMap = []; foreach ($rows as $row) { $fieldValues[] = [ "IdCommesseCustomFields" => (int) $row['field_id'], "Valore" => $row['field_value'], "FieldLabel" => $row['field_label'] ]; $valueMap[(int) $row['field_id']] = $row['field_value']; } // Logga i fieldValues in error_log $logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT); error_log($logFieldValues); // 🔹 Initialize API client $api = VisualLimsApiClient::getInstance(); // 🔹 STEP 5: Create CommessaWeb // Fixed fields are sent as direct properties — they are navigation properties on Commessa // accepted by the XAF API even though not explicitly listed in OData $metadata $commessaWebPayload = [ "Cliente" => $clienteId, "SchemaCustomField" => $schemaId, "Richiedente" => "From TRFSmart Application", // TODO: replace with real value "Descrizione" => "From TRFSmart Application", // TODO: replace with real value "ClienteResponsabile" => $clienteResponsabile, "MoltiplicatorePrezzo" => $moltiplicatorePrezzo, "AnagraficaCertestObject" => $anagraficaObject, "AnagraficaCertestService" => $anagraficaService, "ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented "ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented // DeliveryRequest goes to Campione, not CommessaWeb ]; // Costruisci log curl-like per STEP 5 $jsonPayload = json_encode($commessaWebPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $logContentStep5 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$jsonPayload}'"; $commessaWeb = $api->post("CommessaWeb", $commessaWebPayload); $logContentStep5 .= "\n\nRESPONSE:\n" . json_encode($commessaWeb, JSON_PRETTY_PRINT); // Salva log $logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt"; $writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})"); $commessaId = $commessaWeb["IdCommessa"]; $commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri // 🔹 STEP 6: Create Campioni (Samples) for each part $campioni = []; $logContentStep6 = ""; foreach ($parts as $index => $part) { $matriceId = (int) ($part["idmatrice"] ?? 0); if ($matriceId <= 0) { throw new Exception("Invalid or missing idmatrice for part: " . ($part["part_number"] ?? "Unknown")); } $campionePayload = [ "Commessa" => $commessaId, "Matrice" => $matriceId, "SottoMatrice" => null, "SchemaCustomField" => $schemaId, // "Riferimento" => $part["part_description"] ?? "", "NoteWeb" => $part["part_description"] ?? "", "ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta, ]; // Costruisci curl-like per questo campione $jsonPayload = json_encode($campionePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $logContentStep6 .= "CAMPIONE #{$index}\n" . "curl --location --request POST '{$apiBaseUrl}Campione' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$jsonPayload}'\n\n"; $campione = $api->post("Campione", $campionePayload); $logContentStep6 .= "RESPONSE:\n" . json_encode($campione, JSON_PRETTY_PRINT) . "\n\n---\n"; $campione["PartNumber"] = $part["part_number"] ?? ""; $campione["Material"] = $part["material"] ?? ""; $campione["Color"] = $part["color"] ?? ""; $campione["Mix"] = $part["mix"] ?? ""; $campioni[] = $campione; } // Salva log per STEP 6 $logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt"; $writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})"); // 🔹 STEP 6.0: PATCH each Campione custom fields: // - field 189 (Tested Component) = part_description // - additional fields from identification_parts_customfields (field_id → value_id/value_text) $logContentStep63 = ""; foreach ($campioni as $index => $campione) { $campioneId = (int)($campione['IdCampione'] ?? 0); $partDescription = $parts[$index]['part_description'] ?? ''; $partId = (int)($parts[$index]['part_id'] ?? 0); if ($campioneId <= 0) { continue; } // Build a map of overrides: IdCustomField => value $overrides = []; // Override 1: Tested Component (field 189) = part_description if ($partDescription !== '') { $overrides[189] = $partDescription; } // Override 2: values from identification_parts_customfields $partCFs = $partsCustomFields[$partId] ?? []; foreach ($partCFs as $pcf) { $cfFieldId = (int)$pcf['field_id']; $cfValue = $pcf['value_text'] ?? $pcf['value_id'] ?? null; if ($cfFieldId > 0 && $cfValue !== null && $cfValue !== '') { $overrides[$cfFieldId] = (string)$cfValue; } } // Skip if nothing to override if (empty($overrides)) { continue; } // GET campione custom fields $campioneWithFields = $api->get("Campione({$campioneId})?\$expand=CampioniCustomFields(\$expand=CustomField)"); $logContentStep63 .= "GET Campione({$campioneId}) CustomFields:\n" . json_encode($campioneWithFields['CampioniCustomFields'] ?? [], JSON_PRETTY_PRINT) . "\n\n"; $logContentStep63 .= "Overrides for part {$partId}: " . json_encode($overrides) . "\n\n"; // Build PATCH payload — apply overrides where IdCustomField matches $campioniCustomFields = []; foreach ($campioneWithFields["CampioniCustomFields"] ?? [] as $cf) { $definitionId = (int)($cf["CustomField"]["IdCustomField"] ?? 0); $fieldInstanceId = (int)$cf["IdCampioniCustomFields"]; $currentValue = $cf["Valore"] ?? ''; $newValue = $overrides[$definitionId] ?? $currentValue; $campioniCustomFields[] = [ "IdCampioniCustomFields" => $fieldInstanceId, "Valore" => $newValue ]; } if (!empty($campioniCustomFields)) { $patchPayload = ["CampioniCustomFields" => $campioniCustomFields]; $patchJson = json_encode($patchPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $logContentStep63 .= "PATCH Campione({$campioneId}):\n" . "curl --location --request PATCH '{$apiBaseUrl}Campione({$campioneId})' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$patchJson}'\n\n"; $patchResult = $api->patch("Campione({$campioneId})", $patchPayload); $logContentStep63 .= "RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n---\n"; } } $logFileStep63 = $logDir . "commessa_{$commessaId}_campioni_customfields_step60_" . time() . ".txt"; $writeLog($logFileStep63, $logContentStep63, "STEP 6.0 - Campioni CustomFields (commessa={$commessaId})"); // 🔹 STEP 6.1: Fetch photos linked to this iddatadb $stmtPhotos = $pdo->prepare(" SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina FROM datadb_photos WHERE iddatadb = :iddatadb ORDER BY id ASC "); $stmtPhotos->execute(['iddatadb' => $iddatadb]); $photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC); // 🔹 STEP 6.2: Upload photos to Campione .01 (fetched from API) $photosUploaded = 0; $logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n"; $logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n"; if (!empty($campioni) && !empty($photos)) { // Fetch campioni list from API to find the .01 campione $commessaCampioni = $api->get("CommessaWeb({$commessaId})?\$expand=Campioni"); $apiCampioni = $commessaCampioni['Campioni'] ?? []; // Sort by CodiceCampione to find .01 usort($apiCampioni, fn($a, $b) => strcmp($a['CodiceCampione'] ?? '', $b['CodiceCampione'] ?? '')); $mainCampione = $apiCampioni[0] ?? null; $campioneId = (int)($mainCampione['IdCampione'] ?? 0); $logContentPhotos .= "API Campioni order:\n"; foreach ($apiCampioni as $ac) { $logContentPhotos .= " - {$ac['CodiceCampione']} (IdCampione: {$ac['IdCampione']})\n"; } $logContentPhotos .= "Selected .01 campione: IdCampione={$campioneId}\n\n"; if ($campioneId > 0) { $logContentPhotos .= "=== Campione {$campioneId} (main) ===\n"; foreach ($photos as $photo) { $photoPath = $uploadDir . '/' . ltrim($photo['file_path'], './'); $fullPath = realpath($photoPath); if (!$fullPath || !file_exists($fullPath)) { $logContentPhotos .= "SKIP (file not found): {$photoPath}\n"; continue; } $photoEndpoint = "Campione({$campioneId})/UploadCampioneFile"; $stampaNelRapporto = !empty($photo['StampaNelRapporto']); $primaPagina = !empty($photo['PrimaPagina']); $logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--form 'file=@{$fullPath}'\n\n"; // Step 1: Upload file (flags are ignored by API during upload) $photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']); $logContentPhotos .= "UPLOAD RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n"; // Step 2: PATCH CampioneFile to set flags (StampaNelRapporto, PrimaPagina) $campioneFileId = (int)($photoResult['IdCampioneFile'] ?? 0); if ($campioneFileId > 0 && ($stampaNelRapporto || $primaPagina)) { $patchPayload = []; if ($stampaNelRapporto) { $patchPayload['StampaNelRapporto'] = true; } if ($primaPagina) { $patchPayload['PrimaPagina'] = true; } $patchEndpoint = "CampioneFile({$campioneFileId})"; $patchJsonLog = json_encode($patchPayload, JSON_PRETTY_PRINT); $logContentPhotos .= "curl --location --request PATCH '{$apiBaseUrl}{$patchEndpoint}' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$patchJsonLog}'\n\n"; $patchResult = $api->patch($patchEndpoint, $patchPayload); $logContentPhotos .= "PATCH RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n"; } $logContentPhotos .= "---\n"; $photosUploaded++; } } else { $logContentPhotos .= "SKIP: main campione has invalid IdCampione\n"; } } elseif (empty($campioni)) { $logContentPhotos .= "SKIP: no campioni created, cannot upload photos\n"; } $logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt"; $writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})"); // 🔹 STEP 7: Update Custom Fields for CommessaWeb if (!empty($fieldValues)) { // GET con espansione per CustomField $expand = "CommesseCustomFields(\$expand=CustomField)"; $commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}"); // Logga il GET $logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" . "--header 'Authorization: Bearer ••••••'\n\n" . "RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT); $logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt"; $writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})"); // Prepara payload PATCH $commessaCustomFields = []; foreach ($commessaWithFields["CommesseCustomFields"] as $customField) { $definitionId = (int) ($customField["CustomField"]["IdCustomField"] ?? 0); $fieldId = (int) $customField["IdCommesseCustomFields"]; $currentValue = $customField["Valore"] ?? ''; $fieldType = $customField["CustomField"]["Tipo"] ?? ''; $newValue = isset($valueMap[$definitionId]) ? $valueMap[$definitionId] : $currentValue; // Valida se il campo è di tipo Data if ($fieldType === 'Data' && $newValue !== $currentValue) { $newValue = formatDateToExport($newValue); } $commessaCustomFields[] = [ "IdCommesseCustomFields" => $fieldId, "Valore" => $newValue ]; } $logFileStep7 = null; if (!empty($commessaCustomFields)) { $updatePayload = ["CommesseCustomFields" => $commessaCustomFields]; // Logga payload e response $jsonPayload = json_encode($updatePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $logContentStep7 = "curl --location --request PATCH '{$apiBaseUrl}CommessaWeb({$commessaId})' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$jsonPayload}'"; $patchResponse = $api->patch("CommessaWeb({$commessaId})", $updatePayload); $logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT); $logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt"; $writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})"); } } // 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status $stmt = $pdo->prepare(" UPDATE datadb SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l' WHERE iddatadb = :iddatadb "); $stmt->execute([ 'idcommessaweb' => $commessaId, 'commessaweb' => $commessaWebCode, 'iddatadb' => $iddatadb ]); // 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto) $sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []); // Logga il POST $logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{}'\n\n" . "RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT); $logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt"; $writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})"); // 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (commentato come richiesto) // Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa $importPayload = ["IdUtente" => 285]; // user-id $importResult = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $importPayload); $importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); // Logga il POST $logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" . "--header 'Content-Type: application/json' \\\n" . "--header 'Authorization: Bearer ••••••' \\\n" . "--data '{$importPayloadLog}'\n\n" . "RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT); $logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt"; $writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId})"); // 🔹 STEP 10: GET di controllo post-PATCH $expand = "CommesseCustomFields(\$expand=CustomField)"; $commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand); // Logga il GET di controllo $logContentStep10 = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" . "--header 'Authorization: Bearer ••••••'\n\n" . "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 11: Prepare final response $finalCommessa = [ "Cliente" => $clienteId, "SchemaCustomField" => $schemaId, "Richiedente" => $commessaWeb["Richiedente"] ?? "Web Import", "Descrizione" => $commessaWeb["Descrizione"] ?? "", "CommesseCustomFields" => $commessaAfterPatch["CommesseCustomFields"] ?? [], "Campioni" => $campioni, "Inviata" => 0 // Non inviato, come richiesto ]; echo json_encode([ "success" => true, "idcommessaweb" => $commessaId, "commessaweb" => $commessaWebCode, "commessaWeb" => $finalCommessa, "commessaWebApiResponse" => $commessaWeb, // Incluso per debug "totalCampioni" => count($campioni), "totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []), "totalPhotos" => count($photos), "message" => "Export successful", "logFiles" => [ "step5_create" => $logFileStep5, "step5_2_photos" => $logFilePhotos, "step6_campioni" => $logFileStep6, "step7_patch" => $logFileStep7 ?? null, "step9_1_importa" => $logFileStep91, "step10_get" => $logFileStep10 ] ]); } catch (Exception $e) { error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString()); echo json_encode([ "success" => false, "message" => "Export failed: " . $e->getMessage(), "logFiles" => [ "step5_create" => $logFileStep5 ?? null, "step5_2_photos" => $logFilePhotos ?? null, "step6_campioni" => $logFileStep6 ?? null, "step7_patch" => $logFileStep7 ?? null, "step9_1_importa" => $logFileStep91 ?? null, "step10_get" => $logFileStep10 ?? null ] ]); }