2025-09-05 22:46:28 +02:00
|
|
|
/**
|
|
|
|
|
* 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<UseQueryOptions<ImportStatusResponse, ApiError>, 'queryKey' | 'queryFn'>
|
|
|
|
|
) => {
|
|
|
|
|
return useQuery<ImportStatusResponse, ApiError>({
|
|
|
|
|
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
|
2025-09-06 19:40:47 +02:00
|
|
|
// 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)
|
2025-09-05 22:46:28 +02:00
|
|
|
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...');
|
|
|
|
|
|
2025-09-06 19:40:47 +02:00
|
|
|
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}`);
|
2025-09-05 22:46:28 +02:00
|
|
|
}
|
2025-09-06 19:40:47 +02:00
|
|
|
|
|
|
|
|
// Report validation success with details
|
|
|
|
|
options?.onProgress?.('validating', 60,
|
|
|
|
|
`Archivo validado: ${validationResult.valid_records} registros válidos de ${validationResult.total_records} totales`
|
|
|
|
|
);
|
2025-09-05 22:46:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.');
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-06 19:40:47 +02:00
|
|
|
// 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);
|
2025-09-05 22:46:28 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
validationResult,
|
|
|
|
|
importResult,
|
2025-09-06 19:40:47 +02:00
|
|
|
success: importResult.success,
|
|
|
|
|
error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'),
|
2025-09-05 22:46:28 +02:00
|
|
|
};
|
|
|
|
|
} 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,
|
|
|
|
|
};
|
|
|
|
|
};
|