Files
bakery-ia/frontend/src/api/hooks/dataImport.ts
2025-09-08 17:19:00 +02:00

365 lines
12 KiB
TypeScript

/**
* 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
// 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,
};
};