ValidateLoadArchivesController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use Illuminate\Support\Facades\Validator;
  5. use Illuminate\Support\Facades\Log;
  6. use PhpOffice\PhpSpreadsheet\IOFactory;
  7. use ZipArchive;
  8. /**
  9. * Controller for validating Excel and ZIP files for equipment documentation loading
  10. * Validates Excel headers structure and compares file listings with ZIP contents
  11. */
  12. class ValidateLoadArchivesController extends Controller
  13. {
  14. private $responseController;
  15. // Expected headers in row 7 of Excel file with their corresponding column letters
  16. private $expectedHeaders = [
  17. ['letter' => 'D', 'category' => 'CÓDIGO DE EQUIPO'],
  18. ['letter' => 'H', 'category' => 'FICHA DE SEGURIDAD DE PRODUCTOS QUÍMICOS'],
  19. ['letter' => 'J', 'category' => 'FICHA TÉCNICA'],
  20. ['letter' => 'L', 'category' => 'FOTOGRAFÍAS - DIAGRAMAS'],
  21. ['letter' => 'N', 'category' => 'DATOS DEL PROVEEDOR'],
  22. ['letter' => 'P', 'category' => 'MANUALES DE OPERACIÓN'],
  23. ['letter' => 'R', 'category' => 'MANUALES DE MANTENIMIENTO PREVENTIVO'],
  24. ['letter' => 'T', 'category' => 'MANUALES DE MANTENIMIENTO CORRECTIVO'],
  25. ['letter' => 'V', 'category' => 'DOCUMENTOS ADICIONALES']
  26. ];
  27. // Mapping of file extensions to their types for validation
  28. private $extensionTypes = [
  29. 'pdf' => 'PDF',
  30. 'jpg' => 'IMG', 'jpeg' => 'IMG', 'png' => 'IMG', 'gif' => 'IMG', 'bmp' => 'IMG',
  31. 'xlsx' => 'EXCEL', 'xls' => 'EXCEL'
  32. ];
  33. public function __construct()
  34. {
  35. $this->responseController = new ResponseController();
  36. }
  37. /**
  38. * Main validation endpoint that validates both Excel and ZIP files
  39. * Checks Excel headers, ZIP integrity, file listings, and compares them
  40. */
  41. public function validateFiles(Request $request)
  42. {
  43. Log::info('=== INICIANDO VALIDACIÓN DE ARCHIVOS ===');
  44. // Validate request inputs
  45. Log::info('Paso 1: Validando tipos de archivo');
  46. $validator = Validator::make($request->all(), [
  47. 'excel_file' => 'required|file|mimes:xlsx,xls',
  48. 'zip_file' => 'required|file|mimes:zip'
  49. ]);
  50. if ($validator->fails()) {
  51. Log::error('Error en validación de tipos de archivo', $validator->errors()->toArray());
  52. return $this->responseController->makeResponse(
  53. true,
  54. 'Se encontraron uno o más errores.',
  55. $this->responseController->makeErrors($validator->errors()->messages()),
  56. 400
  57. );
  58. }
  59. Log::info('✓ Tipos de archivo válidos');
  60. // Validate Excel file headers structure
  61. Log::info('Paso 2: Validando encabezados de Excel');
  62. $excelValidation = $this->validateExcelHeaders($request->file('excel_file'));
  63. if ($excelValidation['error']) {
  64. Log::error('Error en encabezados Excel: ' . $excelValidation['message']);
  65. return $this->responseController->makeResponse(true, $excelValidation['message'], [], 400);
  66. }
  67. Log::info('✓ Encabezados de Excel válidos');
  68. // Extract and validate file listings from Excel
  69. Log::info('Paso 3: Extrayendo listado de archivos del Excel');
  70. $filesValidation = $this->extractAndValidateFiles($request->file('excel_file'));
  71. if ($filesValidation['error']) {
  72. Log::error('Error extrayendo archivos Excel: ' . $filesValidation['message']);
  73. return $this->responseController->makeResponse(true, $filesValidation['message'], [], 400);
  74. }
  75. Log::info('✓ Archivos extraídos: ' . count($filesValidation['files']) . ' archivos encontrados');
  76. // Validate ZIP file integrity
  77. Log::info('Paso 4: Validando integridad del archivo ZIP');
  78. $zipValidation = $this->validateZipFile($request->file('zip_file'));
  79. if ($zipValidation['error']) {
  80. Log::error('Error validando ZIP: ' . $zipValidation['message']);
  81. return $this->responseController->makeResponse(true, $zipValidation['message'], [], 400);
  82. }
  83. Log::info('✓ Archivo ZIP válido');
  84. // Compare Excel file listings with ZIP contents
  85. Log::info('Paso 5: Comparando listados Excel vs ZIP');
  86. $comparison = $this->compareExcelWithZip($filesValidation['files'], $request->file('zip_file'));
  87. Log::info('✓ Comparación completada', [
  88. 'valid' => $comparison['valid'],
  89. 'missing_in_zip' => count($comparison['missing_in_zip']),
  90. 'extra_in_zip' => count($comparison['extra_in_zip'])
  91. ]);
  92. Log::info('=== VALIDACIÓN COMPLETADA EXITOSAMENTE ===');
  93. return $this->responseController->makeResponse(false, 'Validación completada.', $comparison);
  94. }
  95. /**
  96. * Validates that Excel file has the correct headers in row 7
  97. * Each column must match the expected category name exactly
  98. */
  99. private function validateExcelHeaders($file)
  100. {
  101. try {
  102. Log::info(' - Cargando archivo Excel: ' . $file->getClientOriginalName());
  103. $spreadsheet = IOFactory::load($file->getPathname());
  104. $worksheet = $spreadsheet->getActiveSheet();
  105. Log::info(' - Excel cargado, validando encabezados en fila 7');
  106. // Check each expected header in row 7
  107. foreach ($this->expectedHeaders as $header) {
  108. $cellValue = $worksheet->getCell($header['letter'] . '7')->getValue();
  109. if (trim($cellValue) !== $header['category']) {
  110. return [
  111. 'error' => true,
  112. 'message' => "El encabezado en la columna {$header['letter']} no coincide. Se esperaba: '{$header['category']}', se encontró: '{$cellValue}'"
  113. ];
  114. }
  115. }
  116. return ['error' => false];
  117. } catch (\Exception $e) {
  118. return ['error' => true, 'message' => 'Error al procesar el archivo Excel: ' . $e->getMessage()];
  119. }
  120. }
  121. /**
  122. * Validates ZIP file integrity and ensures it's not empty
  123. */
  124. private function validateZipFile($file)
  125. {
  126. Log::info(' - Abriendo archivo ZIP: ' . $file->getClientOriginalName());
  127. $zip = new ZipArchive();
  128. $result = $zip->open($file->getPathname());
  129. if ($result !== TRUE) {
  130. return ['error' => true, 'message' => 'No se pudo abrir el archivo ZIP.'];
  131. }
  132. if ($zip->numFiles === 0) {
  133. $zip->close();
  134. return ['error' => true, 'message' => 'El archivo ZIP está vacío.'];
  135. }
  136. Log::info(' - ZIP contiene ' . $zip->numFiles . ' archivos');
  137. $zip->close();
  138. return ['error' => false];
  139. }
  140. /**
  141. * Extracts file listings from Excel starting from row 8
  142. * Validates file extensions and creates structured file list
  143. * Only processes rows with equipment code (column D)
  144. */
  145. private function extractAndValidateFiles($file)
  146. {
  147. try {
  148. $spreadsheet = IOFactory::load($file->getPathname());
  149. $worksheet = $spreadsheet->getActiveSheet();
  150. $highestRow = $worksheet->getHighestRow();
  151. Log::info(' - Procesando ' . ($highestRow - 7) . ' filas de datos (filas 8-' . $highestRow . ')');
  152. $files = [];
  153. // Process each row starting from row 8 (data rows)
  154. for ($row = 8; $row <= $highestRow; $row++) {
  155. $equipmentCode = trim($worksheet->getCell('D' . $row)->getValue());
  156. if (empty($equipmentCode)) continue; // Skip rows without equipment code
  157. $hasFiles = false;
  158. $rowFiles = [];
  159. // Check each file category column (excluding equipment code column D)
  160. foreach ($this->expectedHeaders as $header) {
  161. if ($header['letter'] === 'D') continue;
  162. $cellValue = trim($worksheet->getCell($header['letter'] . $row)->getValue());
  163. if (empty($cellValue)) continue;
  164. // Handle multiple files separated by commas
  165. $fileNames = explode(',', $cellValue);
  166. foreach ($fileNames as $fileName) {
  167. $fileName = trim($fileName);
  168. if (empty($fileName)) continue;
  169. // Validate file extension
  170. $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
  171. if (!isset($this->extensionTypes[$extension])) {
  172. return [
  173. 'error' => true,
  174. 'message' => "Extensión inválida '{$extension}' en archivo '{$fileName}' para equipo '{$equipmentCode}'"
  175. ];
  176. }
  177. $rowFiles[] = [
  178. 'equipment_code' => $equipmentCode,
  179. 'category' => $header['category'],
  180. 'file_name' => $fileName,
  181. 'type' => $this->extensionTypes[$extension]
  182. ];
  183. $hasFiles = true;
  184. }
  185. }
  186. if ($hasFiles) {
  187. $files = array_merge($files, $rowFiles);
  188. }
  189. }
  190. return ['error' => false, 'files' => $files];
  191. } catch (\Exception $e) {
  192. return ['error' => true, 'message' => 'Error al extraer archivos del Excel: ' . $e->getMessage()];
  193. }
  194. }
  195. /**
  196. * Compares file listings from Excel with actual files in ZIP
  197. * Returns validation status and lists of missing/extra files
  198. */
  199. private function compareExcelWithZip($excelFiles, $zipFile)
  200. {
  201. $zip = new ZipArchive();
  202. $zip->open($zipFile->getPathname());
  203. // Extract all file names from ZIP
  204. Log::info(' - Extrayendo nombres de archivos del ZIP');
  205. $zipFiles = [];
  206. for ($i = 0; $i < $zip->numFiles; $i++) {
  207. $fullPath = $zip->getNameIndex($i);
  208. // Skip directories (entries ending with /)
  209. if (substr($fullPath, -1) === '/') {
  210. continue;
  211. }
  212. // Extract only filename without path
  213. $fileName = basename($fullPath);
  214. if (!empty($fileName)) {
  215. $zipFiles[] = $fileName;
  216. }
  217. }
  218. $zip->close();
  219. Log::info(' - ZIP contiene ' . count($zipFiles) . ' archivos (excluyendo directorios)');
  220. // Get file names from Excel listings
  221. $excelFileNames = array_column($excelFiles, 'file_name');
  222. Log::info(' - Excel lista ' . count($excelFileNames) . ' archivos');
  223. // Find discrepancies between Excel and ZIP
  224. $missingInZip = array_diff($excelFileNames, $zipFiles);
  225. $extraInZip = array_diff($zipFiles, $excelFileNames);
  226. Log::info(' - Archivos faltantes en ZIP: ' . count($missingInZip));
  227. Log::info(' - Archivos extra en ZIP: ' . count($extraInZip));
  228. return [
  229. 'valid' => empty($missingInZip) && empty($extraInZip),
  230. 'missing_in_zip' => array_values($missingInZip),
  231. 'extra_in_zip' => array_values($extraInZip)
  232. ];
  233. }
  234. }