Start integrating the onboarding flow with backend 17

This commit is contained in:
Urtzi Alfaro
2025-09-08 16:02:20 +02:00
parent 54102f7f4b
commit 201817a1be
6 changed files with 353 additions and 103 deletions

View File

@@ -85,6 +85,7 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
// Use onboarding hooks
const {
processSalesFile,
generateProductSuggestions, // New separated function
createInventoryFromSuggestions,
importSalesData,
salesProcessing: {
@@ -120,8 +121,11 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
const result = Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding));
// If user has already uploaded a file or has processing data, assume tenant is available
// This prevents blocking the UI due to temporary state inconsistencies after successful progress
const hasProgressData = !!(data.files?.salesData || data.processingResults || data.processingStage);
const result = Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding || isAlreadyInStep));
return result;
};
@@ -152,20 +156,55 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
productsLength: products.length,
suggestionsIsArray: Array.isArray(suggestions),
suggestionsType: typeof suggestions,
shouldConvert: suggestions && suggestions.length > 0 && products.length === 0
});
if (suggestions && suggestions.length > 0 && products.length === 0) {
if (suggestions && Array.isArray(suggestions) && suggestions.length > 0 && products.length === 0) {
console.log('✅ Converting suggestions to products and setting stage to review');
const newProducts = convertSuggestionsToCards(suggestions);
setProducts(newProducts);
setLocalStage('review');
try {
const newProducts = convertSuggestionsToCards(suggestions);
console.log('📦 Converted products:', newProducts.length);
setProducts(newProducts);
setLocalStage('review');
// Force update parent data immediately
const updatedData = {
...data,
detectedProducts: newProducts,
processingStage: 'review'
};
onDataChange(updatedData);
} catch (error) {
console.error('❌ Error converting suggestions to products:', error);
}
}
}, [suggestions, products.length]);
}, [suggestions, products.length, data, onDataChange]);
// Derive current stage
const stage = (localStage === 'completed' || localStage === 'error')
? localStage
: (onboardingStage === 'completed' ? 'review' : onboardingStage || localStage);
const stage = (() => {
// If local stage is explicitly set to completed or error, use it
if (localStage === 'completed' || localStage === 'error') {
return localStage;
}
// If we have products to review, always show review stage
if (products.length > 0) {
return 'review';
}
// If onboarding processing completed but no products yet, wait for conversion
if (onboardingStage === 'completed' && suggestions && suggestions.length > 0) {
return 'review';
}
// If file is validated but no suggestions generated yet, show confirmation stage
if (onboardingStage === 'validated' || localStage === 'validated') {
return 'validated';
}
// Otherwise use the onboarding stage or local stage
return onboardingStage || localStage;
})();
const progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || '';
@@ -176,7 +215,10 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
onboardingStage,
finalStage: stage,
productsLength: products.length,
suggestionsLength: suggestions?.length || 0
suggestionsLength: suggestions?.length || 0,
hasSuggestions: !!suggestions,
suggestionsArray: Array.isArray(suggestions),
willShowReview: stage === 'review' && products.length > 0
});
}, [localStage, onboardingStage, stage, products.length, suggestions?.length]);
@@ -275,22 +317,14 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
console.log('🔄 SmartInventorySetup - Processing result:', { success });
if (success) {
console.log('✅ File processed successfully, setting stage to review');
setLocalStage('review');
console.log('✅ File validation completed successfully');
// Don't set to review stage anymore - let the 'validated' stage show first
setLocalStage('validated');
// Check if there was a suggestion error (AI service timeout)
const stepData = data.allStepData?.['smart-inventory-setup'];
if (stepData?.suggestionError) {
toast.addToast(`Archivo procesado. ${stepData.suggestionError} Se crearon sugerencias básicas que puedes editar.`, {
title: 'Procesamiento completado con advertencias',
type: 'warning'
});
} else {
toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', {
title: 'Procesamiento completado',
type: 'success'
});
}
toast.addToast(`Archivo validado correctamente. Se encontraron ${validationResults?.product_list?.length || 0} productos.`, {
title: 'Validación completada',
type: 'success'
});
} else {
console.error('❌ File processing failed - processSalesFile returned false');
throw new Error('Error procesando el archivo');
@@ -364,6 +398,40 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
setEditingProduct(null);
};
// Handle generating suggestions after file validation
const handleGenerateSuggestions = async () => {
if (!validationResults?.product_list?.length) {
toast.addToast('No se encontraron productos para analizar', {
title: 'Error',
type: 'error'
});
return;
}
try {
setLocalStage('analyzing');
const success = await generateProductSuggestions(validationResults.product_list);
if (success) {
toast.addToast('Sugerencias generadas correctamente', {
title: 'Análisis completado',
type: 'success'
});
} else {
toast.addToast('Error generando sugerencias de productos', {
title: 'Error en análisis',
type: 'error'
});
}
} catch (error) {
console.error('Error generating suggestions:', error);
toast.addToast('Error generando sugerencias de productos', {
title: 'Error en análisis',
type: 'error'
});
}
};
// Update parent data
useEffect(() => {
const updatedData = {
@@ -529,6 +597,57 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
</>
)}
{/* File Validated - User Confirmation Required */}
{stage === 'validated' && validationResults && (
<Card className="p-8">
<div className="text-center">
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
</div>
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-4">
¡Archivo Validado Correctamente!
</h3>
<p className="text-[var(--text-secondary)] text-lg mb-6 max-w-2xl mx-auto">
Hemos encontrado <strong>{validationResults.product_list?.length || 0} productos únicos</strong> en tu archivo de ventas.
</p>
<div className="bg-[var(--bg-secondary)] rounded-lg p-6 mb-8 max-w-2xl mx-auto">
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Lo que haremos a continuación:</h4>
<ul className="text-[var(--text-secondary)] text-left space-y-2">
<li>🤖 <strong>Análisis con IA:</strong> Clasificaremos automáticamente tus productos</li>
<li>📦 <strong>Configuración inteligente:</strong> Calcularemos niveles de stock óptimos</li>
<li> <strong>Tu aprobación:</strong> Podrás revisar y aprobar cada sugerencia</li>
<li>🎯 <strong>Inventario personalizado:</strong> Crearemos tu inventario final</li>
</ul>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Button
onClick={handleGenerateSuggestions}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white shadow-lg px-8 py-3 text-lg"
>
<Brain className="w-5 h-5 mr-2" />
Generar Sugerencias con IA
</Button>
<Button
variant="outline"
onClick={() => {
setLocalStage('upload');
setUploadedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}}
className="text-[var(--text-secondary)] border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] px-6 py-3"
>
Subir otro archivo
</Button>
</div>
</div>
</Card>
)}
{/* Processing Stages */}
{(stage === 'validating' || stage === 'analyzing') && (
<Card className="p-8">
@@ -589,6 +708,23 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
</Card>
)}
{/* Waiting for Suggestions to Load */}
{(stage === 'review' || (onboardingStage === 'completed' && suggestions)) && products.length === 0 && suggestions && suggestions.length > 0 && (
<Card className="p-8">
<div className="text-center">
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
<Brain className="w-8 h-8 text-[var(--color-info)]" />
</div>
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
Preparando sugerencias...
</h3>
<p className="text-[var(--text-secondary)]">
Convirtiendo {suggestions.length} productos en sugerencias personalizadas
</p>
</div>
</Card>
)}
{/* Review & Configure Stage */}
{(stage === 'review') && products.length > 0 && (
<div className="space-y-8">

View File

@@ -184,6 +184,25 @@ export const useOnboardingActions = () => {
return result.success;
}, [store, salesProcessing]);
const generateProductSuggestions = useCallback(async (productList: string[]): Promise<boolean> => {
console.log('🎬 Actions - generateProductSuggestions started for', productList.length, 'products');
store.setLoading(true);
const result = await salesProcessing.generateProductSuggestions(productList);
console.log('🎬 Actions - generateProductSuggestions result:', result);
store.setLoading(false);
if (!result.success) {
console.error('❌ Actions - Suggestions generation failed:', result.error);
store.setError(result.error || 'Error generating product suggestions');
} else {
console.log('✅ Actions - Product suggestions generated successfully');
}
return result.success;
}, [store, salesProcessing]);
const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[]
): Promise<boolean> => {
@@ -306,6 +325,7 @@ export const useOnboardingActions = () => {
// Step-specific actions
createTenant,
processSalesFile,
generateProductSuggestions, // New function for separated suggestion generation
createInventoryFromSuggestions,
importSalesData,
startTraining,

View File

@@ -73,7 +73,7 @@ export interface OnboardingData {
files?: {
salesData?: File;
};
processingStage?: 'upload' | 'validating' | 'analyzing' | 'review' | 'completed' | 'error';
processingStage?: 'upload' | 'validating' | 'validated' | 'analyzing' | 'review' | 'completed' | 'error';
processingResults?: {
is_valid: boolean;
total_records: number;
@@ -161,7 +161,7 @@ export interface TenantCreationState extends ServiceState<BakeryRegistration> {
}
export interface SalesProcessingState extends ServiceState<any> {
stage: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error';
stage: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error';
progress: number;
currentMessage: string;
validationResults: any | null;

View File

@@ -17,15 +17,15 @@ export const useSalesProcessing = () => {
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[] | null>(null);
const [stage, setStage] = useState<'idle' | 'validating' | 'analyzing' | 'completed' | 'error'>('idle');
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[]>([]);
const [stage, setStage] = useState<'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error'>('idle');
const [progress, setProgress] = useState(0);
const [currentMessage, setCurrentMessage] = useState('');
const [validationResults, setValidationResults] = useState<any | null>(null);
const updateProgress = useCallback((
progressValue: number,
stageValue: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error',
stageValue: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error',
message: string,
onProgress?: ProgressCallback
) => {
@@ -158,18 +158,68 @@ export const useSalesProcessing = () => {
throw new Error('No se encontraron productos válidos en el archivo');
}
// Stage 2: Generate AI suggestions
updateProgress(60, 'analyzing', 'Identificando productos únicos...', onProgress);
updateProgress(70, 'analyzing', 'Generando sugerencias de IA...', onProgress);
// Stage 2: File validation completed - WAIT FOR USER CONFIRMATION
updateProgress(100, 'validated', 'Archivo validado correctamente. Esperando confirmación del usuario...', onProgress);
// Store validation results and wait for user action
setValidationResults(validationResult);
console.log('✅ File validation completed:', validationResult.product_list?.length, 'products found');
// Update onboarding store - ONLY with validation results
setStepData('smart-inventory-setup', {
files: { salesData: file },
processingStage: 'validated', // Changed from 'completed'
processingResults: validationResult,
// DON'T set suggestions here - they will be generated later
});
console.log('📊 Updated onboarding store with suggestions');
return {
success: true,
validationResults: validationResult,
// No suggestions returned from processFile - they will be generated separately
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
setError(errorMessage);
updateProgress(0, 'error', errorMessage, onProgress);
return {
success: false,
};
} finally {
setIsLoading(false);
}
}, [updateProgress, currentTenant, validateFile, extractProductList, setStepData]);
const generateProductSuggestions = useCallback(async (
productList: string[],
onProgress?: ProgressCallback
): Promise<{
success: boolean;
suggestions?: ProductSuggestionResponse[];
error?: string;
}> => {
console.log('🚀 Generating product suggestions for', productList.length, 'products');
setIsLoading(true);
setError(null);
try {
updateProgress(10, 'analyzing', 'Iniciando análisis de productos...', onProgress);
updateProgress(30, 'analyzing', 'Identificando productos únicos...', onProgress);
updateProgress(50, 'analyzing', 'Generando sugerencias de IA...', onProgress);
let suggestions: ProductSuggestionResponse[] = [];
let suggestionError: string | null = null;
try {
updateProgress(70, 'analyzing', 'Generando sugerencias de IA...', onProgress);
suggestions = await generateSuggestions(validationResult.product_list);
updateProgress(70, 'analyzing', 'Consultando servicios de IA...', onProgress);
suggestions = await generateSuggestions(productList);
console.log('🔍 After generateSuggestions call:', {
console.log('🔍 Generated suggestions:', {
suggestionsReceived: suggestions?.length || 0,
});
} catch (error) {
@@ -177,11 +227,10 @@ export const useSalesProcessing = () => {
suggestionError = errorMessage;
console.error('❌ Suggestions generation failed:', errorMessage);
// Still continue with empty suggestions - user can manually add products later
// Create basic suggestions from product names as fallback
updateProgress(80, 'analyzing', 'Preparando productos básicos...', onProgress);
// Create basic suggestions from product names as fallback
suggestions = validationResult.product_list.map((productName, index) => ({
suggestions = productList.map((productName, index) => ({
suggestion_id: `manual-${index}`,
original_name: productName,
suggested_name: productName,
@@ -200,42 +249,40 @@ export const useSalesProcessing = () => {
}
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
updateProgress(100, 'completed', 'Procesamiento completado exitosamente', onProgress);
updateProgress(100, 'completed', 'Sugerencias generadas correctamente', onProgress);
// Update state with suggestions (even if empty or fallback)
// Update state with suggestions
setSuggestions(suggestions || []);
setValidationResults(validationResult);
console.log('✅ Processing completed:', suggestions?.length || 0, 'suggestions generated');
console.log('✅ Suggestions generation completed:', suggestions?.length || 0, 'suggestions');
// Update onboarding store
setStepData('smart-inventory-setup', {
files: { salesData: file },
// Update onboarding store with suggestions
setStepData('smart-inventory-setup', (prevData) => ({
...prevData,
processingStage: 'completed',
processingResults: validationResult,
suggestions: suggestions || [],
suggestionError: suggestionError,
});
console.log('📊 Updated onboarding store with suggestions');
}));
return {
success: true,
validationResults: validationResult,
suggestions: suggestions || [],
error: suggestionError || undefined,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
const errorMessage = error instanceof Error ? error.message : 'Error generando sugerencias';
setError(errorMessage);
updateProgress(0, 'error', errorMessage, onProgress);
return {
success: false,
error: errorMessage,
};
} finally {
setIsLoading(false);
}
}, [updateProgress, currentTenant, validateFile, extractProductList, generateSuggestions, setStepData]);
}, [updateProgress, generateSuggestions, setStepData]);
const clearError = useCallback(() => {
setError(null);
@@ -244,7 +291,7 @@ export const useSalesProcessing = () => {
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setSuggestions(null);
setSuggestions([]);
setStage('idle');
setProgress(0);
setCurrentMessage('');
@@ -263,7 +310,7 @@ export const useSalesProcessing = () => {
// Actions
processFile,
generateSuggestions,
generateProductSuggestions, // New separated function
clearError,
reset,
};

View File

@@ -67,7 +67,7 @@ export const useOnboarding = () => {
progress: salesProcessing.progress,
currentMessage: salesProcessing.currentMessage,
validationResults: salesProcessing.validationResults,
suggestions: salesProcessing.suggestions,
suggestions: Array.isArray(salesProcessing.suggestions) ? salesProcessing.suggestions : [],
},
inventorySetup: {
@@ -123,6 +123,7 @@ export const useOnboarding = () => {
// Step-specific actions
createTenant: actions.createTenant,
processSalesFile: actions.processSalesFile,
generateProductSuggestions: actions.generateProductSuggestions, // New separated function
createInventoryFromSuggestions: actions.createInventoryFromSuggestions,
importSalesData: actions.importSalesData,
startTraining: actions.startTraining,