Start integrating the onboarding flow with backend 4

This commit is contained in:
Urtzi Alfaro
2025-09-05 12:55:26 +02:00
parent 0faaa25e58
commit 3fe1f17610
26 changed files with 2161 additions and 1002 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { OnboardingWizard, OnboardingStep } from '../../../components/domain/onboarding/OnboardingWizard';
import { onboardingApiService } from '../../../services/api/onboarding.service';
import { useOnboarding } from '../../../hooks/business/useOnboarding';
import { useAuthUser, useIsAuthenticated } from '../../../stores/auth.store';
import { LoadingSpinner } from '../../../components/shared/LoadingSpinner';
@@ -18,162 +18,138 @@ const OnboardingPage: React.FC = () => {
const navigate = useNavigate();
const user = useAuthUser();
const isAuthenticated = useIsAuthenticated();
const [isLoading, setIsLoading] = useState(false);
const [globalData, setGlobalData] = useState<any>({});
// Use the onboarding business hook
const {
currentStep,
steps,
data,
isLoading,
error,
isInitialized,
onboardingStatus,
nextStep,
previousStep,
goToStep,
updateStepData,
validateCurrentStep,
createTenant,
processSalesFile,
generateInventorySuggestions,
createInventoryFromSuggestions,
getBusinessModelGuide,
downloadTemplate,
completeOnboarding,
clearError,
reset
} = useOnboarding();
// Define the 8 onboarding steps (simplified by merging data upload + analysis)
const steps: OnboardingStep[] = [
{
id: 'setup',
title: '🏢 Setup',
description: 'Configuración básica de tu panadería y creación del tenant',
component: BakerySetupStep,
isRequired: true,
validation: (data) => {
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
if (!data.bakery?.business_model) return 'El modelo de negocio es requerido';
if (!data.bakery?.address) return 'La dirección es requerida';
if (!data.bakery?.city) return 'La ciudad es requerida';
if (!data.bakery?.postal_code) return 'El código postal es requerido';
if (!data.bakery?.phone) return 'El teléfono es requerido';
// Tenant creation will happen automatically when validation passes
return null;
}
},
{
id: 'data-processing',
title: '📊 Historial de Ventas',
description: 'Sube tus datos de ventas para obtener insights personalizados',
component: DataProcessingStep,
isRequired: true,
validation: (data) => {
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar';
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
return null;
}
},
{
id: 'review',
title: '📋 Revisión',
description: 'Revisión de productos detectados por IA y resultados',
component: ReviewStep,
isRequired: true,
validation: (data) => {
if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados';
return null;
}
},
{
id: 'inventory',
title: '⚙️ Inventario',
description: 'Configuración de inventario (stock, fechas de vencimiento)',
component: InventorySetupStep,
isRequired: true,
validation: (data) => {
if (!data.inventoryConfigured) return 'Debes configurar el inventario básico';
return null;
}
},
{
id: 'suppliers',
title: '🏪 Proveedores',
description: 'Configuración de proveedores y asociaciones',
component: SuppliersStep,
isRequired: false,
validation: () => null // Optional step
},
{
id: 'ml-training',
title: '🎯 Inteligencia',
description: 'Creación de tu asistente inteligente personalizado',
component: MLTrainingStep,
isRequired: true,
validation: (data) => {
if (data.trainingStatus !== 'completed') return 'El entrenamiento del modelo debe completarse';
return null;
}
},
{
id: 'completion',
title: '🎉 Listo',
description: 'Finalización y preparación para usar la plataforma',
component: CompletionStep,
isRequired: true,
validation: () => null
// Map steps to components
const stepComponents: { [key: string]: React.ComponentType<any> } = {
'setup': BakerySetupStep,
'data-processing': DataProcessingStep,
'review': ReviewStep,
'inventory': InventorySetupStep,
'suppliers': SuppliersStep,
'ml-training': MLTrainingStep,
'completion': CompletionStep
};
// Convert hook steps to OnboardingWizard format
const wizardSteps: OnboardingStep[] = steps.map(step => ({
id: step.id,
title: step.title,
description: step.description,
component: stepComponents[step.id],
isRequired: step.isRequired,
validation: step.validation
}));
const handleStepChange = (stepIndex: number, stepData: any) => {
const stepId = steps[stepIndex]?.id;
if (stepId) {
updateStepData(stepId, stepData);
}
];
};
const handleNext = () => {
return nextStep();
};
const handlePrevious = () => {
return previousStep();
};
const handleComplete = async (allData: any) => {
setIsLoading(true);
try {
// Mark onboarding as complete in the backend
if (user?.tenant_id) {
await onboardingApiService.completeOnboarding(user.tenant_id, {
completedAt: new Date().toISOString(),
data: allData
});
}
// Navigate to dashboard
navigate('/app/dashboard', {
state: {
message: '¡Felicidades! Tu panadería ha sido configurada exitosamente.',
type: 'success'
}
});
} catch (error) {
console.error('Error completing onboarding:', error);
// Still navigate to dashboard but show warning
navigate('/app/dashboard', {
state: {
message: 'Configuración completada. Algunos ajustes finales pueden estar pendientes.',
type: 'warning'
}
});
} finally {
setIsLoading(false);
const success = await completeOnboarding();
if (success) {
// Navigation is handled inside completeOnboarding
return;
}
};
const handleExit = () => {
const confirmExit = window.confirm(
'¿Estás seguro de que quieres salir del proceso de configuración? Tu progreso se guardará automáticamente.'
);
if (confirmExit) {
navigate('/app/dashboard');
}
};
// Redirect to login if not authenticated
// Redirect if user is not authenticated
useEffect(() => {
if (!isAuthenticated) {
navigate('/login', {
state: {
message: 'Debes iniciar sesión para acceder al onboarding.',
returnUrl: '/app/onboarding'
}
});
if (isInitialized && !isAuthenticated) {
navigate('/auth/login');
}
}, [isAuthenticated, navigate]);
}, [isAuthenticated, isInitialized, navigate]);
if (isLoading) {
return <LoadingSpinner overlay text="Completando configuración..." />;
// Clear error when user navigates away
useEffect(() => {
return () => {
if (error) {
clearError();
}
};
}, [error, clearError]);
// Show loading while initializing
if (!isInitialized || isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="lg" message="Inicializando onboarding..." />
</div>
);
}
// Don't render if not authenticated (will redirect)
if (!isAuthenticated || !user) {
return <LoadingSpinner overlay text="Verificando autenticación..." />;
// Show error state
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-xl font-semibold text-red-600 mb-4">Error en Onboarding</h2>
<p className="text-gray-600 mb-4">{error}</p>
<div className="space-x-4">
<button
onClick={clearError}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Reintentar
</button>
<button
onClick={reset}
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
Reiniciar
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[var(--bg-primary)]">
<div className="min-h-screen bg-gray-50">
<OnboardingWizard
steps={steps}
steps={wizardSteps}
currentStep={currentStep}
data={data}
onStepChange={handleStepChange}
onNext={handleNext}
onPrevious={handlePrevious}
onComplete={handleComplete}
onExit={handleExit}
className="py-8"
onGoToStep={goToStep}
/>
</div>
);