/** * Data Import React Query hooks * Provides data fetching, caching, and state management for data import operations */ import { useMutation, useQuery, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query'; import { dataImportService } from '../services/dataImport'; import { ApiError } from '../client/apiClient'; import type { ImportValidationResponse, ImportProcessResponse, ImportStatusResponse, } from '../types/dataImport'; // Query Keys Factory export const dataImportKeys = { all: ['data-import'] as const, status: (tenantId: string, importId: string) => [...dataImportKeys.all, 'status', tenantId, importId] as const, } as const; // Status Query export const useImportStatus = ( tenantId: string, importId: string, options?: Omit, 'queryKey' | 'queryFn'> ) => { return useQuery({ queryKey: dataImportKeys.status(tenantId, importId), queryFn: () => dataImportService.getImportStatus(tenantId, importId), enabled: !!tenantId && !!importId, refetchInterval: 5000, // Poll every 5 seconds for active imports staleTime: 1000, // Consider data stale after 1 second ...options, }); }; // Validation Mutations export const useValidateJsonData = ( options?: UseMutationOptions< ImportValidationResponse, ApiError, { tenantId: string; data: any } > ) => { return useMutation< ImportValidationResponse, ApiError, { tenantId: string; data: any } >({ mutationFn: ({ tenantId, data }) => dataImportService.validateJsonData(tenantId, data), ...options, }); }; export const useValidateCsvFile = ( options?: UseMutationOptions< ImportValidationResponse, ApiError, { tenantId: string; file: File } > ) => { return useMutation< ImportValidationResponse, ApiError, { tenantId: string; file: File } >({ mutationFn: ({ tenantId, file }) => dataImportService.validateCsvFile(tenantId, file), ...options, }); }; // Import Mutations export const useImportJsonData = ( options?: UseMutationOptions< ImportProcessResponse, ApiError, { tenantId: string; data: any; options?: { skip_validation?: boolean; chunk_size?: number } } > ) => { return useMutation< ImportProcessResponse, ApiError, { tenantId: string; data: any; options?: { skip_validation?: boolean; chunk_size?: number } } >({ mutationFn: ({ tenantId, data, options: importOptions }) => dataImportService.importJsonData(tenantId, data, importOptions), ...options, }); }; export const useImportCsvFile = ( options?: UseMutationOptions< ImportProcessResponse, ApiError, { tenantId: string; file: File; options?: { skip_validation?: boolean; chunk_size?: number } } > ) => { return useMutation< ImportProcessResponse, ApiError, { tenantId: string; file: File; options?: { skip_validation?: boolean; chunk_size?: number } } >({ mutationFn: ({ tenantId, file, options: importOptions }) => dataImportService.importCsvFile(tenantId, file, importOptions), ...options, }); }; // Combined validation and import hook for easier use // Validation-only hook for onboarding export const useValidateFileOnly = () => { const validateCsv = useValidateCsvFile(); const validateJson = useValidateJsonData(); const validateFile = async ( tenantId: string, file: File, options?: { onProgress?: (stage: string, progress: number, message: string) => void; } ): Promise<{ validationResult?: ImportValidationResponse; success: boolean; error?: string; }> => { try { let validationResult: ImportValidationResponse | undefined; options?.onProgress?.('validating', 20, 'Validando estructura del archivo...'); const fileExtension = file.name.split('.').pop()?.toLowerCase(); if (fileExtension === 'csv') { validationResult = await validateCsv.mutateAsync({ tenantId, file }); } else if (fileExtension === 'json') { const jsonData = await file.text().then(text => JSON.parse(text)); validationResult = await validateJson.mutateAsync({ tenantId, data: jsonData }); } else { throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); } options?.onProgress?.('validating', 50, 'Verificando integridad de datos...'); if (!validationResult.is_valid) { const errorMessage = validationResult.errors && validationResult.errors.length > 0 ? validationResult.errors.join(', ') : 'Error de validación desconocido'; throw new Error(`Archivo inválido: ${errorMessage}`); } // Report validation success with details options?.onProgress?.('completed', 100, `Archivo validado: ${validationResult.valid_records} registros válidos de ${validationResult.total_records} totales` ); return { validationResult, success: true, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error validando archivo'; options?.onProgress?.('error', 0, errorMessage); return { success: false, error: errorMessage, }; } }; return { validateFile, }; }; // Full validation + import hook (for later use) export const useValidateAndImportFile = () => { const validateCsv = useValidateCsvFile(); const validateJson = useValidateJsonData(); const importCsv = useImportCsvFile(); const importJson = useImportJsonData(); const processFile = async ( tenantId: string, file: File, options?: { skipValidation?: boolean; chunkSize?: number; onProgress?: (stage: string, progress: number, message: string) => void; } ): Promise<{ validationResult?: ImportValidationResponse; importResult?: ImportProcessResponse; success: boolean; error?: string; }> => { try { let validationResult: ImportValidationResponse | undefined; // Step 1: Validation (unless skipped) if (!options?.skipValidation) { options?.onProgress?.('validating', 20, 'Validando estructura del archivo...'); const fileExtension = file.name.split('.').pop()?.toLowerCase(); if (fileExtension === 'csv') { validationResult = await validateCsv.mutateAsync({ tenantId, file }); } else if (fileExtension === 'json') { const jsonData = await file.text().then(text => JSON.parse(text)); validationResult = await validateJson.mutateAsync({ tenantId, data: jsonData }); } else { throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); } options?.onProgress?.('validating', 50, 'Verificando integridad de datos...'); if (!validationResult.is_valid) { const errorMessage = validationResult.errors && validationResult.errors.length > 0 ? validationResult.errors.join(', ') : 'Error de validación desconocido'; throw new Error(`Archivo inválido: ${errorMessage}`); } // Report validation success with details options?.onProgress?.('validating', 60, `Archivo validado: ${validationResult.valid_records} registros válidos de ${validationResult.total_records} totales` ); } // Step 2: Import options?.onProgress?.('importing', 70, 'Importando datos...'); const importOptions = { skip_validation: options?.skipValidation || false, chunk_size: options?.chunkSize, }; let importResult: ImportProcessResponse; const fileExtension = file.name.split('.').pop()?.toLowerCase(); if (fileExtension === 'csv') { importResult = await importCsv.mutateAsync({ tenantId, file, options: importOptions }); } else if (fileExtension === 'json') { const jsonData = await file.text().then(text => JSON.parse(text)); importResult = await importJson.mutateAsync({ tenantId, data: jsonData, options: importOptions }); } else { throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); } // Report completion with details const completionMessage = importResult.success ? `Importación completada: ${importResult.records_processed} registros procesados` : `Importación fallida: ${importResult.errors?.join(', ') || 'Error desconocido'}`; options?.onProgress?.('completed', 100, completionMessage); return { validationResult, importResult, success: importResult.success, error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'), }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error procesando archivo'; options?.onProgress?.('error', 0, errorMessage); return { success: false, error: errorMessage, }; } }; return { processFile, validateCsv, validateJson, importCsv, importJson, isValidating: validateCsv.isPending || validateJson.isPending, isImporting: importCsv.isPending || importJson.isPending, isLoading: validateCsv.isPending || validateJson.isPending || importCsv.isPending || importJson.isPending, error: validateCsv.error || validateJson.error || importCsv.error || importJson.error, }; }; // Import-only hook (for when validation has already been done) export const useImportFileOnly = () => { const importCsv = useImportCsvFile(); const importJson = useImportJsonData(); const importFile = async ( tenantId: string, file: File, options?: { chunkSize?: number; onProgress?: (stage: string, progress: number, message: string) => void; } ): Promise<{ importResult?: ImportProcessResponse; success: boolean; error?: string; }> => { try { options?.onProgress?.('importing', 10, 'Iniciando importación de datos...'); const fileExtension = file.name.split('.').pop()?.toLowerCase(); let importResult: ImportProcessResponse; if (fileExtension === 'csv') { importResult = await importCsv.mutateAsync({ tenantId, file, options: { skip_validation: true, // Skip validation since already done chunk_size: options?.chunkSize } }); } else if (fileExtension === 'json') { const jsonData = await file.text().then(text => JSON.parse(text)); importResult = await importJson.mutateAsync({ tenantId, data: jsonData, options: { skip_validation: true, // Skip validation since already done chunk_size: options?.chunkSize } }); } else { throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); } options?.onProgress?.('completed', 100, `Importación completada: ${importResult.records_processed} registros procesados` ); return { importResult, success: importResult.success, error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'), }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Error importando archivo'; options?.onProgress?.('error', 0, errorMessage); return { success: false, error: errorMessage, }; } }; return { importFile, isImporting: importCsv.isPending || importJson.isPending, error: importCsv.error || importJson.error, }; };