import React, { useState, useEffect, useMemo } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '../../ui/Button'; import { Card, CardHeader, CardBody } from '../../ui/Card'; import { useAuth } from '../../../contexts/AuthContext'; import { useUserProgress, useMarkStepCompleted } from '../../../api/hooks/onboarding'; import { useTenantActions } from '../../../stores/tenant.store'; import { useTenantInitializer } from '../../../stores/useTenantInitializer'; import { WizardProvider, useWizardContext, BakeryType, DataSource } from './context'; import { BakeryTypeSelectionStep, RegisterTenantStep, FileUploadStep, InventoryReviewStep, ProductCategorizationStep, InitialStockEntryStep, ProductionProcessesStep, MLTrainingStep, CompletionStep } from './steps'; // Import setup wizard steps import { SuppliersSetupStep, RecipesSetupStep, QualitySetupStep, TeamSetupStep, ReviewSetupStep, } from '../setup-wizard/steps'; import { Building2 } from 'lucide-react'; interface StepConfig { id: string; title: string; description: string; component: React.ComponentType; isConditional?: boolean; condition?: (context: any) => boolean; } interface StepProps { onNext?: () => void; onPrevious?: () => void; onComplete?: (data?: any) => void; onUpdate?: (data?: any) => void; isFirstStep?: boolean; isLastStep?: boolean; initialData?: any; } const OnboardingWizardContent: React.FC = () => { const { t } = useTranslation(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { user } = useAuth(); const wizardContext = useWizardContext(); // All possible steps with conditional visibility // All step IDs match backend ONBOARDING_STEPS exactly const ALL_STEPS: StepConfig[] = [ // Phase 1: Discovery { id: 'bakery-type-selection', title: t('onboarding:steps.bakery_type.title', 'Tipo de Panadería'), description: t('onboarding:steps.bakery_type.description', 'Selecciona tu tipo de negocio'), component: BakeryTypeSelectionStep, }, // Phase 2: Core Setup { id: 'setup', title: t('onboarding:steps.setup.title', 'Registrar Panadería'), description: t('onboarding:steps.setup.description', 'Información básica'), component: RegisterTenantStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType !== null, }, // POI Detection removed - now happens automatically in background after tenant registration // Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps) { id: 'upload-sales-data', title: t('onboarding:steps.upload_sales.title', 'Subir Datos de Ventas'), description: t('onboarding:steps.upload_sales.description', 'Cargar archivo con historial de ventas'), component: FileUploadStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, { id: 'inventory-review', title: t('onboarding:steps.inventory_review.title', 'Revisar Inventario'), description: t('onboarding:steps.inventory_review.description', 'Confirmar productos detectados'), component: InventoryReviewStep, isConditional: true, condition: (ctx) => ctx.state.aiAnalysisComplete, }, { id: 'initial-stock-entry', title: t('onboarding:steps.stock.title', 'Niveles de Stock'), description: t('onboarding:steps.stock.description', 'Cantidades iniciales'), component: InitialStockEntryStep, isConditional: true, condition: (ctx) => ctx.state.inventoryReviewCompleted, }, { id: 'suppliers-setup', title: t('onboarding:steps.suppliers.title', 'Proveedores'), description: t('onboarding:steps.suppliers.description', 'Configura tus proveedores'), component: SuppliersSetupStep, // Always show - no conditional }, { id: 'recipes-setup', title: t('onboarding:steps.recipes.title', 'Recetas'), description: t('onboarding:steps.recipes.description', 'Recetas de producción'), component: RecipesSetupStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType === 'production' || ctx.state.bakeryType === 'mixed', }, { id: 'production-processes', title: t('onboarding:steps.processes.title', 'Procesos'), description: t('onboarding:steps.processes.description', 'Procesos de terminado'), component: ProductionProcessesStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType === 'retail' || ctx.state.bakeryType === 'mixed', }, // Phase 3: Advanced Features (Optional) { id: 'quality-setup', title: t('onboarding:steps.quality.title', 'Calidad'), description: t('onboarding:steps.quality.description', 'Estándares de calidad'), component: QualitySetupStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, { id: 'team-setup', title: t('onboarding:steps.team.title', 'Equipo'), description: t('onboarding:steps.team.description', 'Miembros del equipo'), component: TeamSetupStep, isConditional: true, condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, // Phase 4: ML & Finalization { id: 'ml-training', title: t('onboarding:steps.ml_training.title', 'Entrenamiento IA'), description: t('onboarding:steps.ml_training.description', 'Modelo personalizado'), component: MLTrainingStep, // Always show - no conditional }, // Revision step removed - not useful for user, completion step is final step { id: 'completion', title: t('onboarding:steps.completion.title', 'Completado'), description: t('onboarding:steps.completion.description', '¡Todo listo!'), component: CompletionStep, }, ]; // Filter visible steps based on wizard context // useMemo ensures VISIBLE_STEPS recalculates when wizard context state changes // This fixes the bug where conditional steps (suppliers, ml-training) weren't showing const VISIBLE_STEPS = useMemo(() => { const visibleSteps = ALL_STEPS.filter(step => { if (!step.isConditional) return true; if (!step.condition) return true; return step.condition(wizardContext); }); console.log('🔄 VISIBLE_STEPS recalculated:', visibleSteps.map(s => s.id)); console.log('📊 Wizard state:', { stockEntryCompleted: wizardContext.state.stockEntryCompleted, aiAnalysisComplete: wizardContext.state.aiAnalysisComplete, categorizationCompleted: wizardContext.state.categorizationCompleted, }); return visibleSteps; }, [wizardContext.state]); const isNewTenant = searchParams.get('new') === 'true'; const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isInitialized, setIsInitialized] = useState(isNewTenant); const [canContinue, setCanContinue] = useState(true); // Track if current step allows continuation useTenantInitializer(); const { data: userProgress, isLoading: isLoadingProgress, error: progressError } = useUserProgress( user?.id || '', { enabled: !!user?.id } ); const markStepCompleted = useMarkStepCompleted(); const { setCurrentTenant } = useTenantActions(); const [autoCompletionAttempted, setAutoCompletionAttempted] = React.useState(false); // Auto-complete user_registered step useEffect(() => { if (userProgress && user?.id && !autoCompletionAttempted && !markStepCompleted.isPending) { const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); if (!userRegisteredStep?.completed) { console.log('🔄 Auto-completing user_registered step for new user...'); setAutoCompletionAttempted(true); const existingData = userRegisteredStep?.data || {}; markStepCompleted.mutate({ userId: user.id, stepName: 'user_registered', data: { ...existingData, auto_completed: true, completed_at: new Date().toISOString(), source: 'onboarding_wizard_auto_completion' } }, { onSuccess: () => console.log('✅ user_registered step auto-completed successfully'), onError: (error) => { console.error('❌ Failed to auto-complete user_registered step:', error); setAutoCompletionAttempted(false); } }); } } }, [userProgress, user?.id, autoCompletionAttempted, markStepCompleted.isPending]); // Initialize step index based on backend progress useEffect(() => { if (isNewTenant) return; if (userProgress && !isInitialized) { console.log('🔄 Initializing onboarding progress:', userProgress); const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); if (!userRegisteredStep?.completed) { console.log('⏳ Waiting for user_registered step to be auto-completed...'); return; } let stepIndex = 0; if (isNewTenant) { console.log('🆕 New tenant creation - starting from first step'); stepIndex = 0; } else { const currentStepFromBackend = userProgress.current_step; stepIndex = VISIBLE_STEPS.findIndex(step => step.id === currentStepFromBackend); console.log(`🎯 Backend current step: "${currentStepFromBackend}", found at index: ${stepIndex}`); if (stepIndex === -1) { for (let i = 0; i < VISIBLE_STEPS.length; i++) { const step = VISIBLE_STEPS[i]; const stepProgress = userProgress.steps.find(s => s.step_name === step.id); if (!stepProgress?.completed) { stepIndex = i; console.log(`📍 Found first incomplete step: "${step.id}" at index ${i}`); break; } } if (stepIndex === -1) { stepIndex = VISIBLE_STEPS.length - 1; console.log('✅ All steps completed, going to last step'); } } const firstIncompleteStepIndex = VISIBLE_STEPS.findIndex(step => { const stepProgress = userProgress.steps.find(s => s.step_name === step.id); return !stepProgress?.completed; }); if (firstIncompleteStepIndex !== -1 && stepIndex > firstIncompleteStepIndex) { console.log(`🚫 User trying to skip ahead. Redirecting to first incomplete step at index ${firstIncompleteStepIndex}`); stepIndex = firstIncompleteStepIndex; } } console.log(`🎯 Final step index: ${stepIndex} ("${VISIBLE_STEPS[stepIndex]?.id}")`); if (stepIndex !== currentStepIndex) { setCurrentStepIndex(stepIndex); } setIsInitialized(true); } }, [userProgress, isInitialized, currentStepIndex, isNewTenant, VISIBLE_STEPS]); const currentStep = VISIBLE_STEPS[currentStepIndex]; const handleStepComplete = async (data?: any) => { if (!user?.id) { console.error('User ID not available'); return; } if (markStepCompleted.isPending) { console.warn(`⚠️ Step completion already in progress for "${currentStep.id}", skipping duplicate call`); return; } console.log(`🎯 Completing step: "${currentStep.id}" with data:`, data); try { // Update wizard context based on step if (currentStep.id === 'bakery-type-selection' && data?.bakeryType) { wizardContext.updateBakeryType(data.bakeryType as BakeryType); } if (currentStep.id === 'data-source-choice' && data?.dataSource) { wizardContext.updateDataSource(data.dataSource as DataSource); } // REFACTORED: Handle new split steps for AI-assisted inventory if (currentStep.id === 'upload-sales-data' && data?.aiSuggestions) { wizardContext.updateAISuggestions(data.aiSuggestions); wizardContext.updateUploadedFile(data.uploadedFile, data.validationResult); wizardContext.setAIAnalysisComplete(true); } if (currentStep.id === 'inventory-review') { wizardContext.markStepComplete('inventoryReviewCompleted'); // Store inventory items in context for the next step if (data?.inventoryItems) { wizardContext.updateInventoryItems(data.inventoryItems); } } if (currentStep.id === 'product-categorization' && data?.categorizedProducts) { wizardContext.updateCategorizedProducts(data.categorizedProducts); wizardContext.markStepComplete('categorizationCompleted'); } if (currentStep.id === 'initial-stock-entry' && data?.productsWithStock) { wizardContext.updateProductsWithStock(data.productsWithStock); wizardContext.markStepComplete('stockEntryCompleted'); } if (currentStep.id === 'inventory-setup') { wizardContext.markStepComplete('inventoryCompleted'); } if (currentStep.id === 'setup' && data?.tenant) { setCurrentTenant(data.tenant); // If tenant info and location are available in data, update the wizard context if (data.tenantId && data.bakeryLocation) { wizardContext.updateTenantInfo(data.tenantId, data.bakeryLocation); } } // Mark step as completed in backend console.log(`📤 Sending API request to complete step: "${currentStep.id}"`); await markStepCompleted.mutateAsync({ userId: user.id, stepName: currentStep.id, data }); console.log(`✅ Successfully completed step: "${currentStep.id}"`); // Special handling for inventory-review - auto-complete suppliers if requested if (currentStep.id === 'inventory-review' && data?.shouldAutoCompleteSuppliers) { try { console.log('🔄 Auto-completing suppliers-setup step...'); await markStepCompleted.mutateAsync({ userId: user.id, stepName: 'suppliers-setup', data: { auto_completed: true, completed_at: new Date().toISOString(), source: 'inventory_creation_auto_completion', } }); console.log('✅ Suppliers-setup step auto-completed successfully'); } catch (supplierError) { console.warn('⚠️ Could not auto-complete suppliers-setup step:', supplierError); } } if (currentStep.id === 'completion') { wizardContext.resetWizard(); navigate(isNewTenant ? '/app/dashboard' : '/app'); } else { if (currentStepIndex < VISIBLE_STEPS.length - 1) { setCurrentStepIndex(currentStepIndex + 1); } } } catch (error: any) { console.error(`❌ Error completing step "${currentStep.id}":`, error); const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error'; alert(`${t('onboarding:errors.step_failed', 'Error al completar paso')} "${currentStep.title}": ${errorMessage}`); } }; const handleStepUpdate = (data?: any) => { // Handle canContinue state updates from setup wizard steps if (data?.canContinue !== undefined) { setCanContinue(data.canContinue); } // Handle intermediate updates without marking step complete if (currentStep.id === 'bakery-type-selection' && data?.bakeryType) { wizardContext.updateBakeryType(data.bakeryType as BakeryType); } if (currentStep.id === 'data-source-choice' && data?.dataSource) { wizardContext.updateDataSource(data.dataSource as DataSource); } if (currentStep.id === 'product-categorization' && data?.categorizedProducts) { wizardContext.updateCategorizedProducts(data.categorizedProducts); } if (currentStep.id === 'initial-stock-entry' && data?.productsWithStock) { wizardContext.updateProductsWithStock(data.productsWithStock); } }; // Show loading state if (!isNewTenant && (isLoadingProgress || !isInitialized)) { return (

{t('common:loading', 'Cargando tu progreso...')}

); } // Show error state if (!isNewTenant && progressError) { return (

{t('onboarding:errors.network_error', 'Error al cargar progreso')}

{t('onboarding:errors.try_again', 'No pudimos cargar tu progreso. Puedes continuar desde el inicio.')}

); } const StepComponent = currentStep.component; const progressPercentage = isNewTenant ? ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100 : userProgress?.completion_percentage || ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100; return (
{/* Progress Header */}

{isNewTenant ? t('onboarding:wizard.title_new', 'Nueva Panadería') : t('onboarding:wizard.title', 'Configuración Inicial')}

{t('onboarding:wizard.subtitle', 'Configura tu sistema paso a paso')}

{t('onboarding:wizard.progress.step_of', 'Paso {{current}} de {{total}}', { current: currentStepIndex + 1, total: VISIBLE_STEPS.length })}
{Math.round(progressPercentage)}% {t('onboarding:wizard.progress.completed', 'completado')}
{/* Progress Bar */}
{/* Step Content */}
{currentStepIndex + 1}

{currentStep.title}

{currentStep.description}

{}} onPrevious={() => {}} onComplete={handleStepComplete} onUpdate={handleStepUpdate} isFirstStep={currentStepIndex === 0} isLastStep={currentStepIndex === VISIBLE_STEPS.length - 1} canContinue={canContinue} initialData={ // Pass AI data and file to InventoryReviewStep currentStep.id === 'inventory-review' ? { uploadedFile: wizardContext.state.uploadedFile, validationResult: wizardContext.state.uploadedFileValidation, aiSuggestions: wizardContext.state.aiSuggestions, uploadedFileName: wizardContext.state.uploadedFileName || '', uploadedFileSize: wizardContext.state.uploadedFileSize || 0, } : // Pass inventory items to InitialStockEntryStep currentStep.id === 'initial-stock-entry' && wizardContext.state.inventoryItems ? { productsWithStock: wizardContext.state.inventoryItems.map(item => ({ id: item.id, name: item.name, type: item.product_type === 'ingredient' ? 'ingredient' : 'finished_product', category: item.category, unit: item.unit_of_measure, initialStock: undefined, })) } : undefined } />
); }; export const UnifiedOnboardingWizard: React.FC = () => { return ( ); }; export default UnifiedOnboardingWizard;