From c4d4aeb449a544f0f5bd43730e7645fae19bf03f Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 11 Aug 2025 07:01:08 +0200 Subject: [PATCH] Add onboardin steps improvements --- frontend/src/App.tsx | 155 ++++- frontend/src/api/hooks/index.ts | 12 + frontend/src/api/hooks/useOnboarding.ts | 194 ++++++ frontend/src/api/index.ts | 1 + frontend/src/api/services/index.ts | 5 +- .../src/api/services/onboarding.service.ts | 92 +++ .../src/pages/onboarding/OnboardingPage.tsx | 557 +++++++++++++++--- .../utils/__tests__/onboardingRouter.test.md | 39 ++ frontend/src/utils/onboardingRouter.ts | 215 +++++++ services/auth/app/api/onboarding.py | 418 +++++++++++++ services/auth/app/main.py | 3 +- services/auth/app/models/__init__.py | 14 + services/auth/app/models/onboarding.py | 91 +++ services/auth/app/repositories/__init__.py | 4 +- .../app/repositories/onboarding_repository.py | 211 +++++++ .../versions/001_initial_onboarding_tables.py | 107 ++++ 16 files changed, 2015 insertions(+), 103 deletions(-) create mode 100644 frontend/src/api/hooks/useOnboarding.ts create mode 100644 frontend/src/api/services/onboarding.service.ts create mode 100644 frontend/src/utils/__tests__/onboardingRouter.test.md create mode 100644 frontend/src/utils/onboardingRouter.ts create mode 100644 services/auth/app/api/onboarding.py create mode 100644 services/auth/app/models/onboarding.py create mode 100644 services/auth/app/repositories/onboarding_repository.py create mode 100644 services/auth/migrations/versions/001_initial_onboarding_tables.py diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index da7e180b..8c47370b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; +import toast from 'react-hot-toast'; // Components import LoadingSpinner from './components/ui/LoadingSpinner'; @@ -19,6 +20,9 @@ import Layout from './components/layout/Layout'; import { store } from './store'; import { Provider } from 'react-redux'; +// Onboarding utilities +import { OnboardingRouter, type NextAction, type RoutingDecision } from './utils/onboardingRouter'; + // i18n import './i18n'; @@ -33,6 +37,7 @@ interface User { fullName: string; role: string; isOnboardingComplete: boolean; + tenant_id?: string; } interface AppState { @@ -40,6 +45,7 @@ interface AppState { isLoading: boolean; user: User | null; currentPage: CurrentPage; + routingDecision: RoutingDecision | null; } const LoadingFallback = () => ( @@ -56,9 +62,25 @@ const App: React.FC = () => { isAuthenticated: false, isLoading: true, user: null, - currentPage: 'landing' // 👈 Start with landing page + currentPage: 'landing', + routingDecision: null }); + // Helper function to map NextAction to CurrentPage + const mapActionToPage = (action: NextAction): CurrentPage => { + const actionPageMap: Record = { + 'register': 'register', + 'login': 'login', + 'onboarding_bakery': 'onboarding', + 'onboarding_data': 'onboarding', + 'onboarding_training': 'onboarding', + 'dashboard': 'dashboard', + 'landing': 'landing' + }; + + return actionPageMap[action] || 'landing'; + }; + // Initialize app and check authentication useEffect(() => { const initializeApp = async () => { @@ -69,17 +91,43 @@ const App: React.FC = () => { if (token && userData) { const user = JSON.parse(userData); - setAppState({ - isAuthenticated: true, - isLoading: false, - user, - currentPage: user.isOnboardingComplete ? 'dashboard' : 'onboarding' - }); + + try { + // Use enhanced onboarding router to determine next action + const routingDecision = await OnboardingRouter.getNextActionForUser(); + const nextPage = mapActionToPage(routingDecision.nextAction); + + setAppState({ + isAuthenticated: true, + isLoading: false, + user, + currentPage: nextPage, + routingDecision + }); + + // Show welcome message with progress + if (routingDecision.message && routingDecision.completionPercentage > 0) { + toast.success(`Welcome back! ${routingDecision.message} (${Math.round(routingDecision.completionPercentage)}% complete)`); + } + } catch (onboardingError) { + // Fallback to legacy logic if onboarding API fails + console.warn('Onboarding API failed, using legacy logic:', onboardingError); + setAppState({ + isAuthenticated: true, + isLoading: false, + user, + currentPage: user.isOnboardingComplete ? 'dashboard' : 'onboarding', + routingDecision: null + }); + } } else { + // Unauthenticated user + const routingDecision = OnboardingRouter.getNextActionForGuest(); setAppState(prev => ({ ...prev, isLoading: false, - currentPage: 'landing' // 👈 Show landing page for non-authenticated users + currentPage: 'landing', + routingDecision })); } } catch (error) { @@ -87,7 +135,8 @@ const App: React.FC = () => { setAppState(prev => ({ ...prev, isLoading: false, - currentPage: 'landing' // 👈 Fallback to landing page + currentPage: 'landing', + routingDecision: null })); } }; @@ -95,16 +144,45 @@ const App: React.FC = () => { initializeApp(); }, []); - const handleLogin = (user: User, token: string) => { + const handleLogin = async (user: User, token: string) => { localStorage.setItem('auth_token', token); localStorage.setItem('user_data', JSON.stringify(user)); - setAppState({ - isAuthenticated: true, - isLoading: false, - user, - currentPage: user.isOnboardingComplete ? 'dashboard' : 'onboarding' - }); + try { + // Mark user registration as complete + await OnboardingRouter.completeStep('user_registered', { + user_id: user.id, + email: user.email, + login_type: 'existing_user' + }); + + // Determine next action based on current progress + const routingDecision = await OnboardingRouter.getNextActionForUser(); + const nextPage = mapActionToPage(routingDecision.nextAction); + + setAppState({ + isAuthenticated: true, + isLoading: false, + user, + currentPage: nextPage, + routingDecision + }); + + // Show progress message + if (routingDecision.message) { + toast.success(routingDecision.message); + } + } catch (error) { + console.warn('Enhanced login routing failed, using fallback:', error); + // Fallback to legacy logic + setAppState({ + isAuthenticated: true, + isLoading: false, + user, + currentPage: user.isOnboardingComplete ? 'dashboard' : 'onboarding', + routingDecision: null + }); + } }; const handleLogout = () => { @@ -119,15 +197,42 @@ const App: React.FC = () => { }); }; - const handleOnboardingComplete = () => { - const updatedUser = { ...appState.user!, isOnboardingComplete: true }; - localStorage.setItem('user_data', JSON.stringify(updatedUser)); - - setAppState(prev => ({ - ...prev, - user: updatedUser, - currentPage: 'dashboard' - })); + const handleOnboardingComplete = async () => { + try { + // Mark all onboarding steps as complete + await OnboardingRouter.completeStep('dashboard_accessible', { + completion_time: new Date().toISOString(), + user_id: appState.user?.id + }); + + const updatedUser = { ...appState.user!, isOnboardingComplete: true }; + localStorage.setItem('user_data', JSON.stringify(updatedUser)); + + setAppState(prev => ({ + ...prev, + user: updatedUser, + currentPage: 'dashboard', + routingDecision: { + nextAction: 'dashboard', + currentStep: 'dashboard_accessible', + completionPercentage: 100, + message: 'Welcome to your PanIA dashboard!' + } + })); + + toast.success('¡Configuración completada! Bienvenido a tu dashboard de PanIA 🎉'); + } catch (error) { + console.warn('Enhanced onboarding completion failed, using fallback:', error); + // Fallback logic + const updatedUser = { ...appState.user!, isOnboardingComplete: true }; + localStorage.setItem('user_data', JSON.stringify(updatedUser)); + + setAppState(prev => ({ + ...prev, + user: updatedUser, + currentPage: 'dashboard' + })); + } }; const navigateTo = (page: CurrentPage) => { diff --git a/frontend/src/api/hooks/index.ts b/frontend/src/api/hooks/index.ts index 016de4e2..60cdfa0c 100644 --- a/frontend/src/api/hooks/index.ts +++ b/frontend/src/api/hooks/index.ts @@ -9,6 +9,16 @@ export { useData } from './useData'; export { useTraining } from './useTraining'; export { useForecast } from './useForecast'; export { useNotification } from './useNotification'; +export { useOnboarding, useOnboardingStep } from './useOnboarding'; + +// Import hooks for combined usage +import { useAuth } from './useAuth'; +import { useTenant } from './useTenant'; +import { useData } from './useData'; +import { useTraining } from './useTraining'; +import { useForecast } from './useForecast'; +import { useNotification } from './useNotification'; +import { useOnboarding } from './useOnboarding'; // Combined hook for common operations export const useApiHooks = () => { @@ -18,6 +28,7 @@ export const useApiHooks = () => { const training = useTraining(); const forecast = useForecast(); const notification = useNotification(); + const onboarding = useOnboarding(); return { auth, @@ -26,5 +37,6 @@ export const useApiHooks = () => { training, forecast, notification, + onboarding, }; }; \ No newline at end of file diff --git a/frontend/src/api/hooks/useOnboarding.ts b/frontend/src/api/hooks/useOnboarding.ts new file mode 100644 index 00000000..492a55bf --- /dev/null +++ b/frontend/src/api/hooks/useOnboarding.ts @@ -0,0 +1,194 @@ +// frontend/src/api/hooks/useOnboarding.ts +/** + * Onboarding Hook + * React hook for managing user onboarding flow and progress + */ + +import { useState, useEffect } from 'react'; +import { onboardingService } from '../services/onboarding.service'; +import type { UserProgress, UpdateStepRequest } from '../services/onboarding.service'; + +export interface UseOnboardingReturn { + progress: UserProgress | null; + isLoading: boolean; + error: string | null; + currentStep: string | null; + nextStep: string | null; + completionPercentage: number; + isFullyComplete: boolean; + + // Actions + updateStep: (data: UpdateStepRequest) => Promise; + completeStep: (stepName: string, data?: Record) => Promise; + resetStep: (stepName: string) => Promise; + getNextStep: () => Promise; + completeOnboarding: () => Promise; + canAccessStep: (stepName: string) => Promise; + refreshProgress: () => Promise; +} + +export const useOnboarding = (): UseOnboardingReturn => { + const [progress, setProgress] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Derived state + const currentStep = progress?.current_step || null; + const nextStep = progress?.next_step || null; + const completionPercentage = progress?.completion_percentage || 0; + const isFullyComplete = progress?.fully_completed || false; + + // Load initial progress + const loadProgress = async () => { + setIsLoading(true); + setError(null); + + try { + const userProgress = await onboardingService.getUserProgress(); + setProgress(userProgress); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to load onboarding progress'; + setError(message); + console.error('Onboarding progress load error:', err); + } finally { + setIsLoading(false); + } + }; + + // Update step + const updateStep = async (data: UpdateStepRequest) => { + setIsLoading(true); + setError(null); + + try { + const updatedProgress = await onboardingService.updateStep(data); + setProgress(updatedProgress); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to update step'; + setError(message); + throw err; // Re-throw so calling component can handle it + } finally { + setIsLoading(false); + } + }; + + // Complete step with data + const completeStep = async (stepName: string, data?: Record) => { + await updateStep({ + step_name: stepName, + completed: true, + data + }); + }; + + // Reset step + const resetStep = async (stepName: string) => { + await updateStep({ + step_name: stepName, + completed: false + }); + }; + + // Get next step + const getNextStep = async (): Promise => { + try { + const result = await onboardingService.getNextStep(); + return result.step; + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to get next step'; + setError(message); + throw err; + } + }; + + // Complete entire onboarding + const completeOnboarding = async () => { + setIsLoading(true); + setError(null); + + try { + await onboardingService.completeOnboarding(); + await loadProgress(); // Refresh progress after completion + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to complete onboarding'; + setError(message); + throw err; + } finally { + setIsLoading(false); + } + }; + + // Check if user can access step + const canAccessStep = async (stepName: string): Promise => { + try { + const result = await onboardingService.canAccessStep(stepName); + return result.can_access; + } catch (err) { + console.error('Can access step check failed:', err); + return false; + } + }; + + // Refresh progress + const refreshProgress = async () => { + await loadProgress(); + }; + + // Load progress on mount + useEffect(() => { + loadProgress(); + }, []); + + return { + progress, + isLoading, + error, + currentStep, + nextStep, + completionPercentage, + isFullyComplete, + updateStep, + completeStep, + resetStep, + getNextStep, + completeOnboarding, + canAccessStep, + refreshProgress, + }; +}; + +// Helper hook for specific steps +export const useOnboardingStep = (stepName: string) => { + const onboarding = useOnboarding(); + + const stepStatus = onboarding.progress?.steps.find( + step => step.step_name === stepName + ); + + const isCompleted = stepStatus?.completed || false; + const stepData = stepStatus?.data || {}; + const completedAt = stepStatus?.completed_at; + + const completeThisStep = async (data?: Record) => { + await onboarding.completeStep(stepName, data); + }; + + const resetThisStep = async () => { + await onboarding.resetStep(stepName); + }; + + const canAccessThisStep = async (): Promise => { + return await onboarding.canAccessStep(stepName); + }; + + return { + ...onboarding, + stepName, + isCompleted, + stepData, + completedAt, + completeThisStep, + resetThisStep, + canAccessThisStep, + }; +}; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index cfbd73b6..8867fa3e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -28,6 +28,7 @@ export { useForecast, useNotification, useApiHooks, + useOnboarding, } from './hooks'; // Export WebSocket functionality diff --git a/frontend/src/api/services/index.ts b/frontend/src/api/services/index.ts index 820f69fe..8a5076ce 100644 --- a/frontend/src/api/services/index.ts +++ b/frontend/src/api/services/index.ts @@ -11,6 +11,7 @@ import { DataService } from './data.service'; import { TrainingService } from './training.service'; import { ForecastingService } from './forecasting.service'; import { NotificationService } from './notification.service'; +import { OnboardingService } from './onboarding.service'; // Create service instances export const authService = new AuthService(); @@ -19,9 +20,10 @@ export const dataService = new DataService(); export const trainingService = new TrainingService(); export const forecastingService = new ForecastingService(); export const notificationService = new NotificationService(); +export const onboardingService = new OnboardingService(); // Export the classes as well -export { AuthService, TenantService, DataService, TrainingService, ForecastingService, NotificationService }; +export { AuthService, TenantService, DataService, TrainingService, ForecastingService, NotificationService, OnboardingService }; // Import base client export { apiClient } from '../client'; @@ -37,6 +39,7 @@ export const api = { training: trainingService, forecasting: forecastingService, notification: notificationService, + onboarding: onboardingService, } as const; // Service status checking diff --git a/frontend/src/api/services/onboarding.service.ts b/frontend/src/api/services/onboarding.service.ts new file mode 100644 index 00000000..98de8870 --- /dev/null +++ b/frontend/src/api/services/onboarding.service.ts @@ -0,0 +1,92 @@ +// frontend/src/api/services/onboarding.service.ts +/** + * Onboarding Service + * Handles user progress tracking and onboarding flow management + */ + +import { apiClient } from '../client'; + +export interface OnboardingStepStatus { + step_name: string; + completed: boolean; + completed_at?: string; + data?: Record; +} + +export interface UserProgress { + user_id: string; + steps: OnboardingStepStatus[]; + current_step: string; + next_step?: string; + completion_percentage: number; + fully_completed: boolean; + last_updated: string; +} + +export interface UpdateStepRequest { + step_name: string; + completed: boolean; + data?: Record; +} + +export class OnboardingService { + private baseEndpoint = '/users/me/onboarding'; + + /** + * Get user's current onboarding progress + */ + async getUserProgress(): Promise { + return apiClient.get(`${this.baseEndpoint}/progress`); + } + + /** + * Update a specific onboarding step + */ + async updateStep(data: UpdateStepRequest): Promise { + return apiClient.put(`${this.baseEndpoint}/step`, data); + } + + /** + * Mark step as completed with optional data + */ + async completeStep(stepName: string, data?: Record): Promise { + return this.updateStep({ + step_name: stepName, + completed: true, + data + }); + } + + /** + * Reset a step (mark as incomplete) + */ + async resetStep(stepName: string): Promise { + return this.updateStep({ + step_name: stepName, + completed: false + }); + } + + /** + * Get next required step for user + */ + async getNextStep(): Promise<{ step: string; data?: Record }> { + return apiClient.get(`${this.baseEndpoint}/next-step`); + } + + /** + * Complete entire onboarding process + */ + async completeOnboarding(): Promise<{ success: boolean; message: string }> { + return apiClient.post(`${this.baseEndpoint}/complete`); + } + + /** + * Check if user can access a specific step + */ + async canAccessStep(stepName: string): Promise<{ can_access: boolean; reason?: string }> { + return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`); + } +} + +export const onboardingService = new OnboardingService(); \ No newline at end of file diff --git a/frontend/src/pages/onboarding/OnboardingPage.tsx b/frontend/src/pages/onboarding/OnboardingPage.tsx index f833e7eb..66e91256 100644 --- a/frontend/src/pages/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/onboarding/OnboardingPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react'; import toast from 'react-hot-toast'; @@ -9,10 +9,13 @@ import { useTraining, useData, useTrainingWebSocket, + useOnboarding, TenantCreate, TrainingJobRequest } from '../../api'; +import { OnboardingRouter } from '../../utils/onboardingRouter'; + interface OnboardingPageProps { user: any; onComplete: () => void; @@ -47,6 +50,15 @@ const MADRID_PRODUCTS = [ const OnboardingPage: React.FC = ({ user, onComplete }) => { const [currentStep, setCurrentStep] = useState(1); const [isLoading, setIsLoading] = useState(false); + const manualNavigation = useRef(false); + + // Enhanced onboarding with progress tracking + const { + progress, + isLoading: onboardingLoading, + completeStep, + refreshProgress + } = useOnboarding(); const [bakeryData, setBakeryData] = useState({ name: '', address: '', @@ -56,9 +68,12 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => }); // Training progress state - const [tenantId, setTenantId] = useState(''); + const [tenantId, setTenantId] = useState(() => { + // Initialize from localStorage + return localStorage.getItem('current_tenant_id') || ''; + }); const [trainingJobId, setTrainingJobId] = useState(''); - const { createTenant, isLoading: tenantLoading } = useTenant(); + const { createTenant, getUserTenants, isLoading: tenantLoading } = useTenant(); const { startTrainingJob } = useTraining({ disablePolling: true }); const { uploadSalesHistory, validateSalesData } = useData(); @@ -78,6 +93,60 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => estimatedTimeRemaining: 0 }); + // Sales data validation state + const [validationStatus, setValidationStatus] = useState<{ + status: 'idle' | 'validating' | 'valid' | 'invalid'; + message?: string; + records?: number; + }>({ + status: 'idle' + }); + + // Initialize current step from onboarding progress (only when not manually navigating) + useEffect(() => { + const initializeStepFromProgress = async () => { + if (progress && !manualNavigation.current) { + // Only initialize from progress if we haven't manually navigated + try { + const { step } = await OnboardingRouter.getOnboardingStepFromProgress(); + console.log('🎯 Initializing step from progress:', step); + setCurrentStep(step); + } catch (error) { + console.warn('Failed to get step from progress, using default:', error); + } + } + }; + + initializeStepFromProgress(); + }, [progress]); + + // Fetch tenant ID from backend if not available locally + useEffect(() => { + const fetchTenantIdFromBackend = async () => { + if (!tenantId && user) { + console.log('🏢 No tenant ID in localStorage, fetching from backend...'); + try { + const userTenants = await getUserTenants(); + console.log('📋 User tenants:', userTenants); + + if (userTenants.length > 0) { + // Use the first (most recent) tenant for onboarding + const primaryTenant = userTenants[0]; + console.log('✅ Found tenant from backend:', primaryTenant.id); + setTenantId(primaryTenant.id); + storeTenantId(primaryTenant.id); + } else { + console.log('ℹ️ No tenants found for user - will be created in step 1'); + } + } catch (error) { + console.warn('⚠️ Failed to fetch user tenants:', error); + // This is not a critical error - tenant will be created in step 1 if needed + } + } + }; + + fetchTenantIdFromBackend(); + }, [tenantId, user, getUserTenants]); // WebSocket connection for real-time training updates const { @@ -118,9 +187,19 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => currentStep: 'Entrenamiento completado', estimatedTimeRemaining: 0 })); + + // Mark training step as completed in onboarding API + completeStep('training_completed', { + training_completed_at: new Date().toISOString(), + user_id: user?.id, + tenant_id: tenantId + }).catch(error => { + console.warn('Failed to mark training as completed in API:', error); + }); // Auto-advance to final step after 2 seconds setTimeout(() => { + manualNavigation.current = true; setCurrentStep(4); }, 2000); @@ -203,32 +282,238 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => } }; - const handleNext = () => { - if (validateCurrentStep()) { - if (currentStep === 2) { - // Always proceed to training step after CSV upload - startTraining(); - } else { - setCurrentStep(prev => Math.min(prev + 1, steps.length)); + const handleNext = async () => { + console.log(`🚀 HandleNext called for step ${currentStep}`); + + if (!validateCurrentStep()) { + console.log('❌ Validation failed for current step'); + return; + } + + setIsLoading(true); + + try { + if (currentStep === 1) { + console.log('📝 Processing step 1: Register bakery'); + await createBakeryAndTenant(); + console.log('✅ Step 1 completed successfully'); + } else if (currentStep === 2) { + console.log('📂 Processing step 2: Upload sales data'); + await uploadAndValidateSalesData(); + console.log('✅ Step 2 completed successfully'); } + } catch (error) { + console.error('❌ Error in step:', error); + toast.error('Error al procesar el paso. Por favor, inténtalo de nuevo.'); + } finally { + setIsLoading(false); + console.log(`🔄 Loading state set to false, current step: ${currentStep}`); } }; const handlePrevious = () => { + manualNavigation.current = true; setCurrentStep(prev => Math.max(prev - 1, 1)); }; + // Create bakery and tenant for step 1 + const createBakeryAndTenant = async () => { + try { + console.log('🏪 Creating bakery tenant...'); + + let currentTenantId = tenantId; + + // Check if user already has a tenant + if (currentTenantId) { + console.log('ℹ️ User already has tenant ID:', currentTenantId); + console.log('✅ Using existing tenant, skipping creation'); + } else { + console.log('📝 Creating new tenant'); + const tenantData: TenantCreate = { + name: bakeryData.name, + address: bakeryData.address, + business_type: "bakery", + postal_code: "28010", + phone: "+34655334455", + coordinates: bakeryData.coordinates, + products: bakeryData.products, + has_historical_data: bakeryData.hasHistoricalData, + }; + + const newTenant = await createTenant(tenantData); + console.log('✅ Tenant created:', newTenant.id); + + currentTenantId = newTenant.id; + setTenantId(newTenant.id); + storeTenantId(newTenant.id); + } + + // Mark step as completed in onboarding API (non-blocking) + try { + await completeStep('bakery_registered', { + bakery_name: bakeryData.name, + bakery_address: bakeryData.address, + business_type: bakeryData.businessType, + tenant_id: currentTenantId, // Use the correct tenant ID + user_id: user?.id + }); + console.log('✅ Step marked as completed'); + } catch (stepError) { + console.warn('⚠️ Failed to mark step as completed, but continuing:', stepError); + // Don't throw here - step completion is not critical for UI flow + } + + toast.success('Panadería registrada correctamente'); + console.log('🎯 Advancing to step 2'); + manualNavigation.current = true; + setCurrentStep(2); + + } catch (error) { + console.error('❌ Error creating tenant:', error); + throw new Error('Error al registrar la panadería'); + } + }; + + // Validate sales data without importing + const validateSalesFile = async () => { + console.log('🔍 validateSalesFile called'); + console.log('tenantId:', tenantId); + console.log('bakeryData.csvFile:', bakeryData.csvFile); + + if (!tenantId || !bakeryData.csvFile) { + console.log('❌ Missing tenantId or csvFile'); + toast.error('Falta información del tenant o archivo'); + return; + } + + console.log('⏳ Setting validation status to validating'); + setValidationStatus({ status: 'validating' }); + + try { + console.log('📤 Calling validateSalesData API'); + const validationResult = await validateSalesData(tenantId, bakeryData.csvFile); + console.log('📥 Validation result:', validationResult); + + if (validationResult.is_valid) { + console.log('✅ Validation successful'); + setValidationStatus({ + status: 'valid', + message: validationResult.message, + records: validationResult.details?.total_records || 0 + }); + toast.success('¡Archivo validado correctamente!'); + } else { + console.log('❌ Validation failed'); + setValidationStatus({ + status: 'invalid', + message: validationResult.message + }); + toast.error(`Error en validación: ${validationResult.message}`); + } + } catch (error: any) { + console.error('❌ Error validating sales data:', error); + setValidationStatus({ + status: 'invalid', + message: 'Error al validar el archivo' + }); + toast.error('Error al validar el archivo'); + } + }; + + // Upload and validate sales data for step 2 + const uploadAndValidateSalesData = async () => { + if (!tenantId || !bakeryData.csvFile) { + throw new Error('Falta información del tenant o archivo'); + } + + try { + // If not already validated, validate first + if (validationStatus.status !== 'valid') { + const validationResult = await validateSalesData(tenantId, bakeryData.csvFile); + + if (!validationResult.is_valid) { + toast.error(`Error en validación: ${validationResult.message}`); + return; + } + } + + // If validation passes, upload the data + const uploadResult = await uploadSalesHistory(tenantId, bakeryData.csvFile); + + // Mark step as completed + await completeStep('sales_data_uploaded', { + file_name: bakeryData.csvFile.name, + file_size: bakeryData.csvFile.size, + records_imported: uploadResult.records_imported || 0, + validation_status: 'success', + tenant_id: tenantId, + user_id: user?.id + }); + + toast.success(`Datos importados correctamente (${uploadResult.records_imported} registros)`); + + // Automatically start training after successful upload + await startTraining(); + } catch (error: any) { + console.error('Error uploading sales data:', error); + const message = error?.response?.data?.detail || error.message || 'Error al subir datos de ventas'; + throw new Error(message); + } + }; + + // Helper function to mark current step as completed + const markStepCompleted = async () => { + const stepMap: Record = { + 1: 'bakery_registered', + 2: 'sales_data_uploaded', + 3: 'training_completed', + 4: 'dashboard_accessible' + }; + + const stepName = stepMap[currentStep]; + if (stepName) { + const stepData: Record = { + completed_at: new Date().toISOString(), + user_id: user?.id + }; + + // Add step-specific data + if (currentStep === 1) { + stepData.bakery_name = bakeryData.name; + stepData.bakery_address = bakeryData.address; + stepData.business_type = bakeryData.businessType; + stepData.products_count = bakeryData.products.length; + } else if (currentStep === 2) { + stepData.file_name = bakeryData.csvFile?.name; + stepData.file_size = bakeryData.csvFile?.size; + stepData.file_type = bakeryData.csvFile?.type; + stepData.has_historical_data = bakeryData.hasHistoricalData; + } + + await completeStep(stepName, stepData); + // Note: Not calling refreshProgress() here to avoid step reset + + toast.success(`✅ Paso ${currentStep} completado`); + } + }; + const validateCurrentStep = (): boolean => { + console.log(`🔍 Validating step ${currentStep}`); + console.log('Bakery data:', { name: bakeryData.name, address: bakeryData.address }); + switch (currentStep) { case 1: if (!bakeryData.name.trim()) { + console.log('❌ Bakery name is empty'); toast.error('El nombre de la panadería es obligatorio'); return false; } if (!bakeryData.address.trim()) { + console.log('❌ Bakery address is empty'); toast.error('La dirección es obligatoria'); return false; } + console.log('✅ Step 1 validation passed'); return true; case 2: if (!bakeryData.csvFile) { @@ -252,6 +537,12 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => toast.error('El archivo es demasiado grande. Máximo 10MB'); return false; } + + // Check if validation was successful + if (validationStatus.status !== 'valid') { + toast.error('Por favor, valida el archivo antes de continuar'); + return false; + } return true; default: @@ -260,6 +551,7 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => }; const startTraining = async () => { + manualNavigation.current = true; setCurrentStep(3); setIsLoading(true); @@ -270,48 +562,9 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => return; } - // Create tenant first - const tenantData: TenantCreate = { - name: bakeryData.name, - address: bakeryData.address, - business_type: "bakery", - postal_code: "28010", - phone: "+34655334455", - coordinates: bakeryData.coordinates, - products: bakeryData.products, - has_historical_data: bakeryData.hasHistoricalData, - }; - - const tenant = await createTenant(tenantData); - setTenantId(tenant.id); - storeTenantId(tenant.id); - - // Step 2: Validate and Upload CSV file if provided - if (bakeryData.csvFile) { - try { - const validationResult = await validateSalesData(tenant.id, bakeryData.csvFile); - if (!validationResult.is_valid) { - toast.error(`Error en los datos: ${validationResult.message}`); - setTrainingProgress(prev => ({ - ...prev, - status: 'failed', - error: 'Error en la validación de datos históricos' - })); - return; - } - - await uploadSalesHistory(tenant.id, bakeryData.csvFile); - toast.success('Datos históricos validados y subidos correctamente'); - } catch (error) { - console.error('CSV validation/upload error:', error); - toast.error('Error al procesar los datos históricos'); - setTrainingProgress(prev => ({ - ...prev, - status: 'failed', - error: 'Error al procesar los datos históricos' - })); - return; - } + if (!tenantId) { + toast.error('Error: No se ha registrado la panadería correctamente.'); + return; } // Prepare training job request - always use uploaded data since CSV is required @@ -323,7 +576,7 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => }; // Start training job using the proper API - const trainingJob = await startTrainingJob(tenant.id, trainingRequest); + const trainingJob = await startTrainingJob(tenantId, trainingRequest); setTrainingJobId(trainingJob.job_id); setTrainingProgress({ @@ -357,9 +610,24 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => // Start training process await startTraining(); } else { - // Complete onboarding - toast.success('¡Configuración completada exitosamente!'); - onComplete(); + try { + // Mark final step as completed + await completeStep('dashboard_accessible', { + completion_time: new Date().toISOString(), + user_id: user?.id, + tenant_id: tenantId, + final_step: true + }); + + // Complete onboarding + toast.success('¡Configuración completada exitosamente!'); + onComplete(); + } catch (error) { + console.warn('Failed to mark final step as completed:', error); + // Continue anyway for better UX + toast.success('¡Configuración completada exitosamente!'); + onComplete(); + } } }; @@ -380,6 +648,7 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => icon: 'ℹ️', duration: 4000 }); + manualNavigation.current = true; setCurrentStep(4); }; @@ -573,6 +842,8 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => csvFile: file, hasHistoricalData: true })); + // Reset validation status when new file is selected + setValidationStatus({ status: 'idle' }); toast.success(`Archivo ${file.name} seleccionado correctamente`); } }} @@ -582,25 +853,130 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => {bakeryData.csvFile ? ( -
-
-
- -
-

- {bakeryData.csvFile.name} -

-

- {(bakeryData.csvFile.size / 1024).toFixed(1)} KB • {bakeryData.csvFile.type || 'Archivo de datos'} -

+
+
+
+
+ +
+

+ {bakeryData.csvFile.name} +

+

+ {(bakeryData.csvFile.size / 1024).toFixed(1)} KB • {bakeryData.csvFile.type || 'Archivo de datos'} +

+
+
- +
+ + {/* Validation Section */} +
+
+

+ Validación de datos +

+ {validationStatus.status === 'validating' ? ( + + ) : validationStatus.status === 'valid' ? ( + + ) : validationStatus.status === 'invalid' ? ( + + ) : ( + + )} +
+ + {validationStatus.status === 'idle' && tenantId ? ( +
+

+ Valida tu archivo para verificar que tiene el formato correcto. +

+ +
+ ) : ( +
+

+ Debug: validationStatus = "{validationStatus.status}", tenantId = "{tenantId || 'not set'}" +

+ {!tenantId ? ( +
+

+ ⚠️ No se ha encontrado la panadería registrada. +

+

+ Ve al paso anterior para registrar tu panadería o espera mientras se carga desde el servidor. +

+
+ ) : validationStatus.status !== 'idle' ? ( + + ) : null} +
+ )} + + {validationStatus.status === 'validating' && ( +

+ Validando archivo... Por favor espera. +

+ )} + + {validationStatus.status === 'valid' && ( +
+

+ ✅ Archivo validado correctamente +

+ {validationStatus.records && ( +

+ {validationStatus.records} registros encontrados +

+ )} + {validationStatus.message && ( +

+ {validationStatus.message} +

+ )} +
+ )} + + {validationStatus.status === 'invalid' && ( +
+

+ ❌ Error en validación +

+ {validationStatus.message && ( +

+ {validationStatus.message} +

+ )} + +
+ )}
) : ( @@ -748,6 +1124,18 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => } }; + // Show loading while onboarding data is being fetched + if (onboardingLoading && !progress) { + return ( +
+
+ +

Cargando progreso de configuración...

+
+
+ ); + } + return (
@@ -759,6 +1147,20 @@ const OnboardingPage: React.FC = ({ user, onComplete }) =>

Configuremos tu panadería para obtener predicciones precisas de demanda

+ + {/* Progress Summary */} + {progress && ( +
+
+
+ Progreso: {Math.round(progress.completion_percentage)}% +
+
+ ({progress.steps.filter((s: any) => s.completed).length}/{progress.steps.length} pasos completados) +
+
+
+ )}
{/* Progress Indicator */} @@ -790,7 +1192,9 @@ const OnboardingPage: React.FC = ({ user, onComplete }) =>
@@ -857,7 +1261,10 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => ) : trainingProgress.status === 'completed' ? (