Start integrating the onboarding flow with backend 7
This commit is contained in:
@@ -4,7 +4,7 @@ import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
import { useModal } from '../../../../hooks/ui/useModal';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { salesService } from '../../../../api';
|
||||
import { useOnboarding } from '../../../../hooks/business/onboarding';
|
||||
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
|
||||
|
||||
@@ -31,92 +31,7 @@ interface ProcessingResult {
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
// Data processing utility function
|
||||
const processDataFile = async (
|
||||
file: File,
|
||||
onProgress: (progress: number, stage: string, message: string) => void,
|
||||
validateSalesData: any,
|
||||
generateInventorySuggestions: any
|
||||
) => {
|
||||
try {
|
||||
// Stage 1: Validate file with sales service
|
||||
onProgress(20, 'validating', 'Validando estructura del archivo...');
|
||||
const validationResult = await validateSalesData(file);
|
||||
|
||||
onProgress(40, 'validating', 'Verificando integridad de datos...');
|
||||
|
||||
if (!validationResult.is_valid) {
|
||||
throw new Error('Archivo de datos inválido');
|
||||
}
|
||||
|
||||
if (!validationResult.product_list || validationResult.product_list.length === 0) {
|
||||
throw new Error('No se encontraron productos en el archivo');
|
||||
}
|
||||
|
||||
// Stage 2: Store validation result for later import (after inventory setup)
|
||||
onProgress(50, 'validating', 'Procesando datos identificados...');
|
||||
|
||||
// Stage 3: Generate AI suggestions with inventory service
|
||||
onProgress(60, 'analyzing', 'Identificando productos únicos...');
|
||||
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
|
||||
|
||||
console.log('DataProcessingStep - Validation result:', validationResult);
|
||||
console.log('DataProcessingStep - Product list:', validationResult.product_list);
|
||||
console.log('DataProcessingStep - Product list length:', validationResult.product_list?.length);
|
||||
|
||||
// Extract product list from validation result
|
||||
const productList = validationResult.product_list || [];
|
||||
|
||||
console.log('DataProcessingStep - Generating AI suggestions with:', {
|
||||
fileName: file.name,
|
||||
productList: productList,
|
||||
productListLength: productList.length
|
||||
});
|
||||
|
||||
let suggestionsResult;
|
||||
if (productList.length > 0) {
|
||||
suggestionsResult = await generateInventorySuggestions(productList);
|
||||
} else {
|
||||
console.warn('DataProcessingStep - No products found, creating default suggestions');
|
||||
suggestionsResult = {
|
||||
suggestions: [],
|
||||
total_products: validationResult.unique_products || 0,
|
||||
business_model_analysis: {
|
||||
model: 'production' as const,
|
||||
recommendations: []
|
||||
},
|
||||
high_confidence_count: 0
|
||||
};
|
||||
}
|
||||
|
||||
console.log('DataProcessingStep - AI suggestions result:', suggestionsResult);
|
||||
|
||||
onProgress(90, 'analyzing', 'Generando recomendaciones con IA...');
|
||||
onProgress(100, 'completed', 'Procesamiento completado');
|
||||
|
||||
// Combine results
|
||||
const combinedResult = {
|
||||
...validationResult,
|
||||
salesDataFile: file, // Store file for later import after inventory setup
|
||||
productsIdentified: suggestionsResult.total_products || validationResult.unique_products,
|
||||
categoriesDetected: suggestionsResult.suggestions ?
|
||||
new Set(suggestionsResult.suggestions.map(s => s.category)).size : 4,
|
||||
businessModel: suggestionsResult.business_model_analysis?.model || 'production',
|
||||
confidenceScore: suggestionsResult.high_confidence_count && suggestionsResult.total_products ?
|
||||
Math.round((suggestionsResult.high_confidence_count / suggestionsResult.total_products) * 100) : 85,
|
||||
recommendations: suggestionsResult.business_model_analysis?.recommendations || [],
|
||||
aiSuggestions: suggestionsResult.suggestions || []
|
||||
};
|
||||
|
||||
console.log('DataProcessingStep - Combined result:', combinedResult);
|
||||
console.log('DataProcessingStep - Combined result aiSuggestions:', combinedResult.aiSuggestions);
|
||||
console.log('DataProcessingStep - Combined result aiSuggestions length:', combinedResult.aiSuggestions?.length);
|
||||
return combinedResult;
|
||||
} catch (error) {
|
||||
console.error('Data processing error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// This function has been replaced by the onboarding hooks
|
||||
|
||||
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
@@ -130,15 +45,25 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
const authLoading = useAuthLoading();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantLoading = useTenantLoading();
|
||||
const createAlert = (alert: any) => {
|
||||
console.log('Alert:', alert);
|
||||
};
|
||||
|
||||
// Use hooks for UI and direct service calls for now (until we extend hooks)
|
||||
const { isLoading: inventoryLoading } = useInventory();
|
||||
const { isLoading: salesLoading } = useSales();
|
||||
// Use the new onboarding hooks
|
||||
const {
|
||||
processSalesFile,
|
||||
generateInventorySuggestions,
|
||||
salesProcessing: {
|
||||
stage: onboardingStage,
|
||||
progress: onboardingProgress,
|
||||
currentMessage: onboardingMessage,
|
||||
validationResults,
|
||||
suggestions
|
||||
},
|
||||
isLoading,
|
||||
error,
|
||||
clearError
|
||||
} = useOnboarding();
|
||||
|
||||
const errorModal = useModal();
|
||||
const { showToast } = useToast();
|
||||
const toast = useToast();
|
||||
|
||||
// Check if we're still loading user or tenant data
|
||||
const isLoadingUserData = authLoading || tenantLoading;
|
||||
@@ -162,11 +87,25 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
const isTenantAvailable = (): boolean => {
|
||||
return !isLoadingUserData && getTenantId() !== null;
|
||||
};
|
||||
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
|
||||
// Use onboarding hook state when available, fallback to local state
|
||||
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
|
||||
const [progress, setProgress] = useState(data.processingProgress || 0);
|
||||
const [currentMessage, setCurrentMessage] = useState(data.currentMessage || '');
|
||||
const [results, setResults] = useState<ProcessingResult | null>(data.processingResults || null);
|
||||
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
|
||||
|
||||
// Derive current state from onboarding hooks or local state
|
||||
const stage = onboardingStage || localStage;
|
||||
const progress = onboardingProgress || 0;
|
||||
const currentMessage = onboardingMessage || '';
|
||||
const results = (validationResults && suggestions) ? {
|
||||
...validationResults,
|
||||
aiSuggestions: suggestions,
|
||||
// Add calculated fields
|
||||
productsIdentified: validationResults.product_list?.length || 0,
|
||||
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
|
||||
businessModel: 'production',
|
||||
confidenceScore: 85,
|
||||
recommendations: []
|
||||
} : localResults;
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -179,12 +118,13 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
processingProgress: progress,
|
||||
currentMessage: currentMessage,
|
||||
processingResults: results,
|
||||
suggestions: suggestions,
|
||||
files: {
|
||||
...data.files,
|
||||
salesData: uploadedFile
|
||||
}
|
||||
});
|
||||
}, [stage, progress, currentMessage, results, uploadedFile]);
|
||||
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data]);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -214,13 +154,12 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
// Validate file type
|
||||
const validExtensions = ['.csv', '.xlsx', '.xls'];
|
||||
const validExtensions = ['.csv', '.xlsx', '.xls', '.json'];
|
||||
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
||||
|
||||
if (!validExtensions.includes(fileExtension)) {
|
||||
showToast({
|
||||
toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', {
|
||||
title: 'Formato inválido',
|
||||
message: 'Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
@@ -228,187 +167,96 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
|
||||
// Check file size (max 10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
showToast({
|
||||
toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', {
|
||||
title: 'Archivo muy grande',
|
||||
message: 'El archivo es demasiado grande. Máximo 10MB permitido.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadedFile(file);
|
||||
setStage('validating');
|
||||
setProgress(0);
|
||||
setLocalStage('validating');
|
||||
|
||||
try {
|
||||
// Wait for user data to load if still loading
|
||||
if (!isTenantAvailable()) {
|
||||
createAlert({
|
||||
type: 'info',
|
||||
category: 'system',
|
||||
priority: 'low',
|
||||
title: 'Cargando datos de usuario',
|
||||
message: 'Por favor espere mientras cargamos su información...',
|
||||
source: 'onboarding'
|
||||
});
|
||||
// Reset file state since we can't process it yet
|
||||
console.log('Tenant not available, waiting...');
|
||||
setUploadedFile(null);
|
||||
setStage('upload');
|
||||
setLocalStage('upload');
|
||||
toast.addToast('Por favor espere mientras cargamos su información...', {
|
||||
title: 'Esperando datos de usuario',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantId = getTenantId();
|
||||
if (!tenantId) {
|
||||
console.error('DataProcessingStep - No tenant ID available:', {
|
||||
user,
|
||||
currentTenant,
|
||||
userTenantId: user?.tenant_id,
|
||||
currentTenantId: currentTenant?.id,
|
||||
isLoadingUserData,
|
||||
authLoading,
|
||||
tenantLoading
|
||||
console.log('DataProcessingStep - Starting file processing');
|
||||
|
||||
// Use the onboarding hook for file processing
|
||||
const success = await processSalesFile(file, (progress, stage, message) => {
|
||||
console.log(`Processing: ${progress}% - ${stage} - ${message}`);
|
||||
});
|
||||
|
||||
if (success) {
|
||||
setLocalStage('completed');
|
||||
toast.addToast('El archivo se procesó correctamente', {
|
||||
title: 'Procesamiento completado',
|
||||
type: 'success'
|
||||
});
|
||||
throw new Error('No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.');
|
||||
} else {
|
||||
throw new Error('Error procesando el archivo');
|
||||
}
|
||||
|
||||
console.log('DataProcessingStep - Starting file processing with tenant:', tenantId);
|
||||
|
||||
const result = await processDataFile(
|
||||
file,
|
||||
(newProgress, newStage, message) => {
|
||||
setProgress(newProgress);
|
||||
setStage(newStage as ProcessingStage);
|
||||
setCurrentMessage(message);
|
||||
},
|
||||
salesService.validateSalesData.bind(salesService),
|
||||
inventoryService.generateInventorySuggestions.bind(inventoryService)
|
||||
);
|
||||
|
||||
setResults(result);
|
||||
setStage('completed');
|
||||
|
||||
// Store results for next steps
|
||||
onDataChange({
|
||||
...data,
|
||||
files: { ...data.files, salesData: file },
|
||||
processingResults: result,
|
||||
processingStage: 'completed',
|
||||
processingProgress: 100
|
||||
});
|
||||
|
||||
console.log('DataProcessingStep - File processing completed:', result);
|
||||
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'medium',
|
||||
title: 'Procesamiento completado',
|
||||
message: `Se procesaron ${result.total_records} registros y se identificaron ${result.unique_products} productos únicos.`,
|
||||
source: 'onboarding'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('DataProcessingStep - Processing error:', error);
|
||||
console.error('DataProcessingStep - Error details:', {
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
||||
errorStack: error instanceof Error ? error.stack : null,
|
||||
tenantInfo: {
|
||||
user: user ? { id: user.id, tenant_id: user.tenant_id } : null,
|
||||
currentTenant: currentTenant ? { id: currentTenant.id } : null
|
||||
}
|
||||
});
|
||||
|
||||
setStage('error');
|
||||
setLocalStage('error');
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
|
||||
setCurrentMessage(errorMessage);
|
||||
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
toast.addToast(errorMessage, {
|
||||
title: 'Error en el procesamiento',
|
||||
message: errorMessage,
|
||||
source: 'onboarding'
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
if (!isTenantAvailable()) {
|
||||
createAlert({
|
||||
type: 'info',
|
||||
category: 'system',
|
||||
priority: 'low',
|
||||
title: 'Cargando datos de usuario',
|
||||
message: 'Por favor espere mientras cargamos su información...',
|
||||
source: 'onboarding'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantId = getTenantId();
|
||||
if (!tenantId) {
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: 'Error',
|
||||
message: 'No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.',
|
||||
source: 'onboarding'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Template download functionality can be implemented later if needed
|
||||
console.warn('Template download not yet implemented in reorganized structure');
|
||||
createAlert({
|
||||
type: 'info',
|
||||
category: 'system',
|
||||
title: 'Descarga de plantilla no disponible',
|
||||
message: 'Esta funcionalidad se implementará próximamente.'
|
||||
});
|
||||
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'low',
|
||||
title: 'Plantilla descargada',
|
||||
message: 'La plantilla de ventas se ha descargado correctamente',
|
||||
source: 'onboarding'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error downloading template:', error);
|
||||
// Fallback to static template
|
||||
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
||||
const downloadTemplate = () => {
|
||||
// Provide a static CSV template
|
||||
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
|
||||
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
|
||||
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
|
||||
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
|
||||
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
|
||||
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'plantilla_ventas.csv');
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'plantilla_ventas.csv');
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
toast.addToast('La plantilla se descargó correctamente.', {
|
||||
title: 'Plantilla descargada',
|
||||
type: 'success'
|
||||
});
|
||||
};
|
||||
|
||||
const resetProcess = () => {
|
||||
setStage('upload');
|
||||
setLocalStage('upload');
|
||||
setUploadedFile(null);
|
||||
setProgress(0);
|
||||
setCurrentMessage('');
|
||||
setResults(null);
|
||||
setLocalResults(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
if (error) {
|
||||
clearError();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -429,7 +277,7 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
||||
)}
|
||||
|
||||
{/* Improved Upload Stage */}
|
||||
{stage === 'upload' && isTenantAvailable() && (
|
||||
{(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
|
||||
Reference in New Issue
Block a user