From 2e1e696cb5539f3b088a9a5990cc9035d222a7c9 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 8 Sep 2025 17:19:00 +0200 Subject: [PATCH] Simplify the onboardinf flow components --- frontend/src/api/hooks/dataImport.ts | 73 ++ frontend/src/api/types/tenant.ts | 17 +- .../domain/onboarding/OnboardingWizard.tsx | 505 +++----- .../src/components/domain/onboarding/index.ts | 13 +- .../onboarding/steps/BakerySetupStep.tsx | 329 ----- .../onboarding/steps/CompletionStep.tsx | 576 ++------- .../onboarding/steps/MLTrainingStep.tsx | 602 ++++----- .../onboarding/steps/RegisterTenantStep.tsx | 172 +++ .../steps/SmartInventorySetupStep.tsx | 1095 ----------------- .../domain/onboarding/steps/SuppliersStep.tsx | 816 ------------ .../onboarding/steps/UploadSalesDataStep.tsx | 626 ++++++++++ .../domain/onboarding/steps/index.ts | 4 + .../src/hooks/business/onboarding/README.md | 225 ---- .../hooks/business/onboarding/config/steps.ts | 187 --- .../hooks/business/onboarding/core/actions.ts | 338 ----- .../hooks/business/onboarding/core/store.ts | 200 --- .../hooks/business/onboarding/core/types.ts | 205 --- .../business/onboarding/core/useAutoResume.ts | 20 - .../src/hooks/business/onboarding/index.ts | 29 - .../onboarding/services/useInventorySetup.ts | 255 ---- .../services/useProgressTracking.ts | 167 --- .../onboarding/services/useResumeLogic.ts | 151 --- .../onboarding/services/useSalesProcessing.ts | 317 ----- .../onboarding/services/useTenantCreation.ts | 89 -- .../services/useTrainingOrchestration.ts | 258 ---- .../business/onboarding/useOnboarding.ts | 145 --- .../onboarding/utils/createServiceHook.ts | 103 -- .../pages/app/onboarding/OnboardingPage.tsx | 226 ---- .../src/pages/onboarding/OnboardingPage.tsx | 25 + frontend/src/pages/onboarding/index.ts | 1 + frontend/src/router/AppRouter.tsx | 4 +- frontend/src/stores/tenant.store.ts | 24 +- 32 files changed, 1431 insertions(+), 6366 deletions(-) delete mode 100644 frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx delete mode 100644 frontend/src/components/domain/onboarding/steps/SmartInventorySetupStep.tsx delete mode 100644 frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/index.ts delete mode 100644 frontend/src/hooks/business/onboarding/README.md delete mode 100644 frontend/src/hooks/business/onboarding/config/steps.ts delete mode 100644 frontend/src/hooks/business/onboarding/core/actions.ts delete mode 100644 frontend/src/hooks/business/onboarding/core/store.ts delete mode 100644 frontend/src/hooks/business/onboarding/core/types.ts delete mode 100644 frontend/src/hooks/business/onboarding/core/useAutoResume.ts delete mode 100644 frontend/src/hooks/business/onboarding/index.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useInventorySetup.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useProgressTracking.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useResumeLogic.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useSalesProcessing.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useTenantCreation.ts delete mode 100644 frontend/src/hooks/business/onboarding/services/useTrainingOrchestration.ts delete mode 100644 frontend/src/hooks/business/onboarding/useOnboarding.ts delete mode 100644 frontend/src/hooks/business/onboarding/utils/createServiceHook.ts delete mode 100644 frontend/src/pages/app/onboarding/OnboardingPage.tsx create mode 100644 frontend/src/pages/onboarding/OnboardingPage.tsx create mode 100644 frontend/src/pages/onboarding/index.ts diff --git a/frontend/src/api/hooks/dataImport.ts b/frontend/src/api/hooks/dataImport.ts index 1dcf1ff5..83a87857 100644 --- a/frontend/src/api/hooks/dataImport.ts +++ b/frontend/src/api/hooks/dataImport.ts @@ -289,4 +289,77 @@ export const useValidateAndImportFile = () => { isLoading: validateCsv.isPending || validateJson.isPending || importCsv.isPending || importJson.isPending, error: validateCsv.error || validateJson.error || importCsv.error || importJson.error, }; +}; + +// Import-only hook (for when validation has already been done) +export const useImportFileOnly = () => { + const importCsv = useImportCsvFile(); + const importJson = useImportJsonData(); + + const importFile = async ( + tenantId: string, + file: File, + options?: { + chunkSize?: number; + onProgress?: (stage: string, progress: number, message: string) => void; + } + ): Promise<{ + importResult?: ImportProcessResponse; + success: boolean; + error?: string; + }> => { + try { + options?.onProgress?.('importing', 10, 'Iniciando importación de datos...'); + + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + let importResult: ImportProcessResponse; + + if (fileExtension === 'csv') { + importResult = await importCsv.mutateAsync({ + tenantId, + file, + options: { + skip_validation: true, // Skip validation since already done + chunk_size: options?.chunkSize + } + }); + } else if (fileExtension === 'json') { + const jsonData = await file.text().then(text => JSON.parse(text)); + importResult = await importJson.mutateAsync({ + tenantId, + data: jsonData, + options: { + skip_validation: true, // Skip validation since already done + chunk_size: options?.chunkSize + } + }); + } else { + throw new Error('Formato de archivo no soportado. Use CSV o JSON.'); + } + + options?.onProgress?.('completed', 100, + `Importación completada: ${importResult.records_processed} registros procesados` + ); + + return { + importResult, + success: importResult.success, + error: importResult.success ? undefined : (importResult.errors?.join(', ') || 'Error en la importación'), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Error importando archivo'; + options?.onProgress?.('error', 0, errorMessage); + + return { + success: false, + error: errorMessage, + }; + } + }; + + return { + importFile, + isImporting: importCsv.isPending || importJson.isPending, + error: importCsv.error || importJson.error, + }; }; \ No newline at end of file diff --git a/frontend/src/api/types/tenant.ts b/frontend/src/api/types/tenant.ts index 2dde45ec..0f191b03 100644 --- a/frontend/src/api/types/tenant.ts +++ b/frontend/src/api/types/tenant.ts @@ -4,19 +4,12 @@ export interface BakeryRegistration { name: string; - business_type?: string; - description?: string; - address?: string; + address: string; + postal_code: string; + phone: string; city?: string; - state?: string; - country?: string; - postal_code?: string; - phone?: string; - email?: string; - website?: string; - subdomain?: string; - latitude?: number; - longitude?: number; + business_type?: string; + business_model?: string; } export interface TenantResponse { diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx index b54872d5..a2c9b566 100644 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx @@ -1,384 +1,185 @@ -import React, { useState, useCallback } from 'react'; -import { Card, Button } from '../../ui'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '../../ui/Button'; +import { useAuth } from '../../../contexts/AuthContext'; +import { useMarkStepCompleted } from '../../../api/hooks/onboarding'; +import { useTenantActions } from '../../../stores/tenant.store'; +import { useTenantInitializer } from '../../../stores/useTenantInitializer'; +import { + RegisterTenantStep, + UploadSalesDataStep, + MLTrainingStep, + CompletionStep +} from './steps'; - -export interface OnboardingStep { +interface StepConfig { id: string; title: string; description: string; - component: React.ComponentType; - isCompleted?: boolean; - isRequired?: boolean; - validation?: (data: any) => string | null; + component: React.ComponentType; } -export interface OnboardingStepProps { - data: any; - onDataChange: (data: any) => void; +interface StepProps { onNext: () => void; onPrevious: () => void; + onComplete: (data?: any) => void; isFirstStep: boolean; isLastStep: boolean; } -interface OnboardingWizardProps { - steps: OnboardingStep[]; - currentStep: number; - data: any; - onStepChange: (stepIndex: number, stepData: any) => void; - onNext: () => Promise | boolean; - onPrevious: () => boolean; - onComplete: (data: any) => Promise | void; - onGoToStep: (stepIndex: number) => boolean; - onExit?: () => void; - className?: string; -} +const STEPS: StepConfig[] = [ + { + id: 'setup', + title: 'Registrar Panadería', + description: 'Configura la información de tu panadería', + component: RegisterTenantStep, + }, + { + id: 'smart-inventory-setup', + title: 'Configurar Inventario', + description: 'Sube datos de ventas y configura tu inventario inicial', + component: UploadSalesDataStep, + }, + { + id: 'ml-training', + title: 'Entrenamiento IA', + description: 'Entrena tu modelo de inteligencia artificial', + component: MLTrainingStep, + }, + { + id: 'completion', + title: 'Configuración Completa', + description: 'Bienvenido a tu sistema de gestión de panadería', + component: CompletionStep, + }, +]; -export const OnboardingWizard: React.FC = ({ - steps, - currentStep: currentStepIndex, - data: stepData, - onStepChange, - onNext, - onPrevious, - onComplete, - onGoToStep, - onExit, - className = '', -}) => { - const [completedSteps, setCompletedSteps] = useState>(new Set()); - const [validationErrors, setValidationErrors] = useState>({}); +export const OnboardingWizard: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const navigate = useNavigate(); + const { user } = useAuth(); + + // Initialize tenant data for authenticated users + useTenantInitializer(); + + const markStepCompleted = useMarkStepCompleted(); + const { setCurrentTenant } = useTenantActions(); - const currentStep = steps[currentStepIndex]; + const currentStep = STEPS[currentStepIndex]; - const updateStepData = useCallback((stepId: string, data: any) => { - onStepChange(currentStepIndex, { ...stepData, ...data }); - - // Clear validation error for this step - setValidationErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[stepId]; - return newErrors; - }); - }, [currentStepIndex, stepData, onStepChange]); - - const validateCurrentStep = useCallback(() => { - const step = currentStep; - const data = stepData || {}; - - if (step.validation) { - const error = step.validation(data); - if (error) { - setValidationErrors(prev => ({ - ...prev, - [step.id]: error - })); - return false; - } + const handlePrevious = () => { + if (currentStepIndex > 0) { + setCurrentStepIndex(currentStepIndex - 1); } - - // Mark step as completed - setCompletedSteps(prev => new Set(prev).add(step.id)); - return true; - }, [currentStep, stepData]); - - const goToNextStep = useCallback(async () => { - if (validateCurrentStep()) { - const result = onNext(); - if (result instanceof Promise) { - await result; - } - } - }, [validateCurrentStep, onNext]); - - const goToPreviousStep = useCallback(() => { - onPrevious(); - }, [onPrevious]); - - const goToStep = useCallback((stepIndex: number) => { - onGoToStep(stepIndex); - }, [onGoToStep]); - - const calculateProgress = () => { - return (completedSteps.size / steps.length) * 100; }; - const renderStepIndicator = () => ( -
-
- - {/* Progress summary */} -
-
- Paso - - {currentStepIndex + 1} - - de - - {steps.length} - -
- - {Math.round(calculateProgress())}% completado - -
-
+ const handleStepComplete = async (data?: any) => { + try { + // Special handling for setup step - set the created tenant in tenant store + if (currentStep.id === 'setup' && data?.tenant) { + setCurrentTenant(data.tenant); + } - {/* Progress connections between steps */} -
- - {/* Step circles */} -
- {/* Desktop view */} -
- {steps.map((step, index) => { - const isCompleted = completedSteps.has(step.id); - const isCurrent = index === currentStepIndex; - const hasError = validationErrors[step.id]; - const isAccessible = index <= currentStepIndex + 1; - const nextStepCompleted = index < steps.length - 1 && completedSteps.has(steps[index + 1].id); + // Mark step as completed in backend + await markStepCompleted.mutateAsync({ + userId: user?.id || '', + stepName: currentStep.id, + data + }); - return ( - -
- - - {/* Step label */} -
-
- {step.title.replace(/🏢|📊|🤖|📋|⚙️|🏪|🧠|🎉/g, '').trim()} -
-
- - {/* Error indicator */} - {hasError && ( -
- ! -
- )} -
- - {/* Connection line to next step */} - {index < steps.length - 1 && ( -
-
-
-
-
- )} - - ); - })} -
- - {/* Mobile view - simplified horizontal scroll */} -
-
- {steps.map((step, index) => { - const isCompleted = completedSteps.has(step.id); - const isCurrent = index === currentStepIndex; - const hasError = validationErrors[step.id]; - const isAccessible = index <= currentStepIndex + 1; - - return ( - -
- - - {/* Step label for mobile */} -
-
- {step.title.replace(/🏢|📊|🤖|📋|⚙️|🏪|🧠|🎉/g, '').trim()} -
-
-
- - {/* Connection line for mobile */} - {index < steps.length - 1 && ( -
-
-
- )} - - ); - })} -
-
-
-
- -
-
- ); - - - if (!currentStep) { - return ( - -

No hay pasos de onboarding configurados.

-
- ); - } + if (currentStep.id === 'completion') { + navigate('/app'); + } else { + // Auto-advance to next step after successful completion + if (currentStepIndex < STEPS.length - 1) { + setCurrentStepIndex(currentStepIndex + 1); + } + } + } catch (error) { + console.error('Error marking step as completed:', error); + } + }; const StepComponent = currentStep.component; return ( -
-
- {/* Header with clean design */} -
-

- Configuración inicial +
+ {/* Progress Bar */} +
+
+

+ Bienvenido a Bakery IA

-

- Completa estos pasos para comenzar a usar la plataforma -

+ + Paso {currentStepIndex + 1} de {STEPS.length} + +
+ +
+
+
- {onExit && ( - - )} -
- - {renderStepIndicator()} - - {/* Main Content - Single clean card */} -
- {/* Step header */} -
-

- {currentStep.title} -

-

- {currentStep.description} -

-
- - {/* Step content */} -
- { - updateStepData(currentStep.id, data); - }} - onNext={goToNextStep} - onPrevious={goToPreviousStep} - isFirstStep={currentStepIndex === 0} - isLastStep={currentStepIndex === steps.length - 1} - /> -
- - {/* Navigation footer */} -
-
- - - +
+ {step.title} +
+
+ {step.description} +
-
+ ))}
+ + {/* Step Content */} +
+
+

+ {currentStep.title} +

+

+ {currentStep.description} +

+
+ + {}} // No-op - steps must use onComplete instead + onPrevious={handlePrevious} + onComplete={handleStepComplete} + isFirstStep={currentStepIndex === 0} + isLastStep={currentStepIndex === STEPS.length - 1} + /> +
+ + {/* Navigation */} +
+ + +
+ Puedes pausar y reanudar este proceso en cualquier momento +
+ + {/* No skip button - all steps are required */} +
+
); -}; - -export default OnboardingWizard; \ No newline at end of file +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/index.ts b/frontend/src/components/domain/onboarding/index.ts index 60cc2829..8d2d6185 100644 --- a/frontend/src/components/domain/onboarding/index.ts +++ b/frontend/src/components/domain/onboarding/index.ts @@ -1,12 +1 @@ -// Onboarding domain components -export { default as OnboardingWizard } from './OnboardingWizard'; - -// Individual step components -export { BakerySetupStep } from './steps/BakerySetupStep'; -export { SmartInventorySetupStep } from './steps/SmartInventorySetupStep'; -export { SuppliersStep } from './steps/SuppliersStep'; -export { MLTrainingStep } from './steps/MLTrainingStep'; -export { CompletionStep } from './steps/CompletionStep'; - -// Re-export types from wizard -export type { OnboardingStep, OnboardingStepProps } from './OnboardingWizard'; \ No newline at end of file +export { OnboardingWizard } from './OnboardingWizard'; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx deleted file mode 100644 index 8d89d6bc..00000000 --- a/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Store, MapPin, Phone, Mail, Hash, Building } from 'lucide-react'; -import { Button, Card, Input } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; -import { useAuthUser } from '../../../../stores/auth.store'; -import { useOnboarding } from '../../../../hooks/business/onboarding'; - -// Backend-compatible bakery setup interface -interface BakerySetupData { - name: string; - business_type: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant'; - business_model: 'individual_bakery' | 'central_baker_satellite' | 'retail_bakery' | 'hybrid_bakery'; - address: string; - city: string; - postal_code: string; - phone: string; - email?: string; - description?: string; -} - -export const BakerySetupStep: React.FC = ({ - data, - onDataChange, - onNext, - onPrevious, - isFirstStep, - isLastStep -}) => { - const user = useAuthUser(); - const userId = user?.id; - - // Business onboarding hooks - const { - updateStepData, - tenantCreation, - isLoading, - error, - clearError - } = useOnboarding(); - - const [formData, setFormData] = useState({ - name: data.bakery?.name || '', - business_type: data.bakery?.business_type || 'bakery', - business_model: data.bakery?.business_model || 'individual_bakery', - address: data.bakery?.address || '', - city: data.bakery?.city || 'Madrid', - postal_code: data.bakery?.postal_code || '', - phone: data.bakery?.phone || '', - email: data.bakery?.email || '', - description: data.bakery?.description || '' - }); - - const bakeryModels = [ - { - value: 'individual_bakery', - label: 'Panadería Individual', - description: 'Producción propia tradicional con recetas artesanales', - icon: '🥖' - }, - { - value: 'central_baker_satellite', - label: 'Panadería Central con Satélites', - description: 'Producción centralizada con múltiples puntos de venta', - icon: '🏭' - }, - { - value: 'retail_bakery', - label: 'Panadería Retail', - description: 'Punto de venta que compra productos terminados', - icon: '🏪' - }, - { - value: 'hybrid_bakery', - label: 'Modelo Híbrido', - description: 'Combina producción propia con productos externos', - icon: '🔄' - } - ]; - - const lastFormDataRef = useRef(formData); - - useEffect(() => { - // Only update if formData actually changed and is valid - if (JSON.stringify(formData) !== JSON.stringify(lastFormDataRef.current)) { - lastFormDataRef.current = formData; - - // Update parent data when form changes - const bakeryData = { - ...formData, - tenant_id: data.bakery?.tenant_id - }; - - onDataChange({ bakery: bakeryData }); - } - }, [formData, onDataChange, data.bakery?.tenant_id]); - - // Separate effect for hook updates to avoid circular dependencies - useEffect(() => { - const timeoutId = setTimeout(() => { - if (userId && Object.values(formData).some(value => value.trim() !== '')) { - updateStepData('setup', { bakery: formData }); - } - }, 1000); - - return () => clearTimeout(timeoutId); - }, [formData, userId, updateStepData]); - - const handleInputChange = (field: keyof BakerySetupData, value: string) => { - setFormData(prev => ({ - ...prev, - [field]: value - })); - }; - - - // Validate form data for completion - const isFormValid = () => { - return !!( - formData.name && - formData.address && - formData.city && - formData.postal_code && - formData.phone && - formData.business_type && - formData.business_model - ); - }; - - - - return ( -
- - {/* Form */} -
- {/* Basic Info */} -
-
- - handleInputChange('name', e.target.value)} - placeholder="Ej: Panadería Artesanal El Buen Pan" - className="w-full text-lg py-3" - /> -
- - {/* Business Model Selection */} -
- -
- {bakeryModels.map((model) => ( - - ))} -
-
- - {/* Location and Contact - Backend compatible */} -
-
- -
- - handleInputChange('address', e.target.value)} - placeholder="Dirección completa de tu panadería" - className="w-full pl-12 py-3" - /> -
-
- -
-
- -
- - handleInputChange('city', e.target.value)} - placeholder="Madrid" - className="w-full pl-10" - /> -
-
- -
- -
- - handleInputChange('postal_code', e.target.value)} - placeholder="28001" - pattern="[0-9]{5}" - className="w-full pl-10" - /> -
-
-
- -
-
- -
- - handleInputChange('phone', e.target.value)} - placeholder="+34 600 123 456" - type="tel" - className="w-full pl-10" - /> -
-
- -
- -
- - handleInputChange('email', e.target.value)} - placeholder="contacto@panaderia.com" - className="w-full pl-10" - /> -
-
-
-
-
-
- - {/* Show loading state for tenant creation */} - {tenantCreation.isLoading && ( -
-
-

- Configurando panadería... -

-
- )} - - {/* Show loading state when creating tenant */} - {data.bakery?.isCreating && ( -
-
-

- Creando tu espacio de trabajo... -

-
- )} - - {/* Show error state for business onboarding operations */} - {error && ( -
-

- Error al configurar panadería -

-

- {error} -

- -
- )} - - {/* Show error state if tenant creation fails */} - {data.bakery?.creationError && ( -
-

- Error al crear el espacio de trabajo -

-

- {data.bakery.creationError} -

-
- )} - -
- ); -}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx index f18857d7..b4b57ad9 100644 --- a/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx @@ -1,477 +1,157 @@ -import React, { useState, useEffect } from 'react'; -import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react'; -import { Button, Card, Badge } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; -import { useModal } from '../../../../hooks/ui/useModal'; -import { useAuthUser } from '../../../../stores/auth.store'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '../../../ui/Button'; +import { useCurrentTenant } from '../../../../stores/tenant.store'; -interface CompletionStats { - totalProducts: number; - inventoryItems: number; - suppliersConfigured: number; - mlModelAccuracy: number; - estimatedTimeSaved: string; - completionScore: number; - salesImported: boolean; - salesImportRecords: number; +interface CompletionStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data?: any) => void; + isFirstStep: boolean; + isLastStep: boolean; } -export const CompletionStep: React.FC = ({ - data, - onDataChange, - onNext, - onPrevious, - isFirstStep, - isLastStep +export const CompletionStep: React.FC = ({ + onComplete }) => { - const user = useAuthUser(); - const createAlert = (alert: any) => { - console.log('Alert:', alert); + const navigate = useNavigate(); + const currentTenant = useCurrentTenant(); + + const handleGetStarted = () => { + onComplete({ redirectTo: '/app' }); + navigate('/app'); }; - const certificateModal = useModal(); - const demoModal = useModal(); - const shareModal = useModal(); - - const [showConfetti, setShowConfetti] = useState(false); - const [completionStats, setCompletionStats] = useState(null); - - // Handle final sales import - const handleFinalSalesImport = async () => { - if (!user?.tenant_id || !data.files?.salesData || !data.inventoryMapping) { - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error en importación final', - message: 'Faltan datos necesarios para importar las ventas.', - source: 'onboarding' - }); - return; - } - - try { - // Sales data should already be imported during DataProcessingStep - // Just create inventory items from approved suggestions - // TODO: Implement inventory creation from suggestions - const result = { success: true, successful_imports: 0 }; - - createAlert({ - type: 'success', - category: 'system', - priority: 'medium', - title: 'Importación completada', - message: `Se importaron ${result.successful_imports} registros de ventas exitosamente.`, - source: 'onboarding' - }); - - // Update completion stats - const updatedStats = { - ...completionStats!, - salesImported: true, - salesImportRecords: result.successful_imports || 0 - }; - setCompletionStats(updatedStats); - - onDataChange({ - ...data, - completionStats: updatedStats, - salesImportResult: result, - finalImportCompleted: true - }); - - } catch (error) { - console.error('Sales import error:', error); - const errorMessage = error instanceof Error ? error.message : 'Error al importar datos de ventas'; - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error en importación', - message: errorMessage, - source: 'onboarding' - }); - } - }; - - useEffect(() => { - // Show confetti animation - setShowConfetti(true); - const timer = setTimeout(() => setShowConfetti(false), 3000); - - // Calculate completion stats - const stats: CompletionStats = { - totalProducts: data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0, - inventoryItems: data.inventoryItems?.length || 0, - suppliersConfigured: data.suppliers?.length || 0, - mlModelAccuracy: data.trainingMetrics?.accuracy * 100 || 0, - estimatedTimeSaved: '15-20 horas', - completionScore: calculateCompletionScore(), - salesImported: data.finalImportCompleted || false, - salesImportRecords: data.salesImportResult?.successful_imports || 0 - }; - - setCompletionStats(stats); - - // Update parent data - onDataChange({ - ...data, - completionStats: stats, - onboardingCompleted: true, - completedAt: new Date().toISOString() - }); - - // Trigger final sales import if not already done - if (!data.finalImportCompleted && data.inventoryMapping && data.files?.salesData) { - handleFinalSalesImport(); - } - - return () => clearTimeout(timer); - }, []); - - const calculateCompletionScore = () => { - let score = 0; - - // Base score for completing setup - if (data.bakery?.tenant_id) score += 20; - - // Data upload and analysis - if (data.validation?.is_valid) score += 15; - if (data.analysisStatus === 'completed') score += 15; - - // Product review - const approvedProducts = data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0; - if (approvedProducts > 0) score += 20; - - // Inventory setup - if (data.inventoryItems?.length > 0) score += 15; - - // ML training - if (data.trainingStatus === 'completed') score += 15; - - return Math.min(score, 100); - }; - - const generateCertificate = () => { - // Mock certificate generation - const certificateData = { - bakeryName: data.bakery?.name || 'Tu Panadería', - completionDate: new Date().toLocaleDateString('es-ES'), - score: completionStats?.completionScore || 0, - features: [ - 'Configuración de Tenant Multi-inquilino', - 'Análisis de Datos con IA', - 'Gestión de Inventario Inteligente', - 'Entrenamiento de Modelo ML', - 'Integración de Proveedores' - ] - }; - - console.log('Generating certificate:', certificateData); - certificateModal.openModal({ - title: '🎓 Certificado Generado', - message: `Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100` - }); - }; - - const scheduleDemo = () => { - // Mock demo scheduling - demoModal.openModal({ - title: '📅 Demo Agendado', - message: 'Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.' - }); - }; - - const shareSuccess = () => { - // Mock social sharing - const shareText = `¡Acabo de completar la configuración de mi panadería inteligente con IA! 🥖🤖 Puntuación: ${completionStats?.completionScore}/100`; - navigator.clipboard.writeText(shareText); - shareModal.openModal({ - title: '✅ ¡Compartido!', - message: 'Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!' - }); - }; - - const quickStartActions = [ - { - id: 'dashboard', - title: 'Ir al Dashboard', - description: 'Explora tu panel de control personalizado', - icon: , - action: () => onNext(), - primary: true - }, - { - id: 'inventory', - title: 'Gestionar Inventario', - description: 'Revisa y actualiza tu stock actual', - icon: , - action: () => console.log('Navigate to inventory'), - primary: false - }, - { - id: 'predictions', - title: 'Ver Predicciones IA', - description: 'Consulta las predicciones de demanda', - icon: , - action: () => console.log('Navigate to predictions'), - primary: false - } - ]; - - const achievementBadges = [ - { - title: 'Pionero IA', - description: 'Primera configuración con Machine Learning', - icon: '🤖', - earned: data.trainingStatus === 'completed' - }, - { - title: 'Organizador', - description: 'Inventario completamente configurado', - icon: '📦', - earned: (data.inventoryItems?.length || 0) >= 5 - }, - { - title: 'Analista', - description: 'Análisis de datos exitoso', - icon: '📊', - earned: data.analysisStatus === 'completed' - }, - { - title: 'Perfeccionista', - description: 'Puntuación de configuración 90+', - icon: '⭐', - earned: (completionStats?.completionScore || 0) >= 90 - } - ]; return ( -
- {/* Confetti Effect */} - {showConfetti && ( -
-
-
🎉
-
- {/* Additional confetti elements could be added here */} -
- )} +
+ {/* Success Icon */} +
+ + + +
- {/* Celebration Header */} -
-
- -
-

- ¡Felicidades! 🎉 + {/* Success Message */} +
+

+ ¡Bienvenido a Bakery IA!

-

- {data.bakery?.name} está listo para funcionar -

-

- Has completado exitosamente la configuración inicial de tu panadería inteligente. - Tu sistema está optimizado con IA y listo para transformar la gestión de tu negocio. +

+ Tu panadería {currentTenant?.name} está lista para usar nuestro sistema de gestión inteligente.

- {/* Completion Stats */} - {completionStats && ( - -
-
- {completionStats.completionScore} - /100 -
-

- Puntuación de Configuración -

-

- ¡Excelente trabajo! Has superado el promedio de la industria (78/100) -

+ {/* Summary Cards */} +
+
+
+ + +
+

Panadería Registrada

+

+ Tu información empresarial está configurada y lista +

+
-
-
-

{completionStats.totalProducts}

-

Productos

-
-
-

{completionStats.inventoryItems}

-

Inventario

-
-
-

{completionStats.suppliersConfigured}

-

Proveedores

-
-
-

{completionStats.mlModelAccuracy.toFixed(1)}%

-

Precisión IA

-
-
-

{completionStats.salesImported ? completionStats.salesImportRecords : '⏳'}

-

Ventas {completionStats.salesImported ? 'Importadas' : 'Importando...'}

-
-
-

{completionStats.estimatedTimeSaved}

-

Tiempo Ahorrado

-
+
+
+ + +
- - )} - - {/* Achievement Badges */} - -

- 🏆 Logros Desbloqueados -

-
- {achievementBadges.map((badge, index) => ( -
-
{badge.icon}
-

{badge.title}

-

{badge.description}

- {badge.earned && ( -
- Conseguido -
- )} -
- ))} +

Inventario Creado

+

+ Tus productos base están configurados con datos iniciales +

-
- {/* Quick Actions */} - -

- 🚀 Próximos Pasos -

-
- {quickStartActions.map((action) => ( - - ))} +
+
+ + + +
+

IA Entrenada

+

+ Tu modelo de inteligencia artificial está listo para predecir demanda +

- - - {/* Additional Actions */} -
- - -

Certificado

-

- Descarga tu certificado de configuración -

- -
- - - -

Demo Personal

-

- Agenda una demostración 1-a-1 -

- -
- - - -

Compartir Éxito

-

- Comparte tu logro en redes sociales -

- -
- {/* Summary & Thanks */} - -
-

- 🙏 ¡Gracias por confiar en nuestra plataforma! -

-

- Tu panadería ahora cuenta con tecnología de vanguardia para: -

- -
-
-
- - Predicciones de demanda con IA -
-
- - Gestión inteligente de inventario -
-
- - Optimización de compras -
+ {/* Next Steps */} +
+

Próximos Pasos

+
+
+
+ 1
-
-
- - Alertas automáticas de restock -
-
- - Análisis de tendencias de venta -
-
- - Control multi-tenant seguro -
+
+

Explora el Dashboard

+

+ Revisa las métricas principales y el estado de tu inventario +

- -
-

- 💡 Consejo: Explora el dashboard para descubrir todas las funcionalidades disponibles. - El sistema aprenderá de tus patrones y mejorará sus recomendaciones con el tiempo. -

+ +
+
+ 2 +
+
+

Registra Ventas Diarias

+

+ Mantén tus datos actualizados para mejores predicciones +

+
+
+ +
+
+ 3 +
+
+

Configura Alertas

+

+ Recibe notificaciones sobre inventario bajo y productos próximos a vencer +

+
- +
+ {/* Action Button */} +
+ +
+ + {/* Help Text */} +
+ ¿Necesitas ayuda? Visita nuestra{' '} + + guía de usuario + {' '} + o contacta a nuestro{' '} + + equipo de soporte + +
); }; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx b/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx index 4504c0b3..d89d5f3b 100644 --- a/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx @@ -1,422 +1,264 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp, Upload, Database } from 'lucide-react'; -import { Button, Card, Badge } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; -import { useOnboarding } from '../../../../hooks/business/onboarding'; -import { useAuthUser } from '../../../../stores/auth.store'; -import { useCurrentTenant } from '../../../../stores'; +import React, { useState, useEffect } from 'react'; +import { Button } from '../../../ui/Button'; +import { useCurrentTenant } from '../../../../stores/tenant.store'; +import { useCreateTrainingJob, useTrainingWebSocket } from '../../../../api/hooks/training'; -// Type definitions for training messages (will be moved to API types later) -interface TrainingProgressMessage { - type: 'training_progress'; - progress: number; +interface MLTrainingStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data?: any) => void; + isFirstStep: boolean; + isLastStep: boolean; +} + +interface TrainingProgress { stage: string; - message: string; -} - -interface TrainingCompletedMessage { - type: 'training_completed'; - metrics: TrainingMetrics; -} - -interface TrainingErrorMessage { - type: 'training_error'; - error: string; -} - -interface TrainingMetrics { - accuracy: number; - mape: number; - mae: number; - rmse: number; -} - -interface TrainingLog { - timestamp: string; - message: string; - level: 'info' | 'warning' | 'error' | 'success'; -} - -interface TrainingJob { - id: string; - status: 'pending' | 'running' | 'completed' | 'failed'; progress: number; - started_at?: string; - completed_at?: string; - error_message?: string; - metrics?: TrainingMetrics; + message: string; + currentStep?: string; + estimatedTimeRemaining?: number; } -// Using the proper training service from services/api/training.service.ts - -export const MLTrainingStep: React.FC = ({ - data, - onDataChange, - onNext, +export const MLTrainingStep: React.FC = ({ onPrevious, - isFirstStep, - isLastStep + onComplete, + isFirstStep }) => { - const user = useAuthUser(); + const [trainingProgress, setTrainingProgress] = useState(null); + const [isTraining, setIsTraining] = useState(false); + const [error, setError] = useState(''); + const [jobId, setJobId] = useState(null); + const currentTenant = useCurrentTenant(); - - // Use the onboarding hooks - const { - startTraining, - trainingOrchestration: { - status, - progress, - currentStep, - estimatedTimeRemaining, - job, - logs, - metrics - }, - data: allStepData, - isLoading, - error, - clearError - } = useOnboarding(); - - // Local state for UI-only elements - const [hasStarted, setHasStarted] = useState(false); - const wsRef = useRef(null); + const createTrainingJob = useCreateTrainingJob(); - // Validate that required data is available for training - const validateDataRequirements = (): { isValid: boolean; missingItems: string[] } => { - const missingItems: string[] = []; - - console.log('MLTrainingStep - Validating data requirements'); - console.log('MLTrainingStep - Current allStepData:', allStepData); - - // Check if sales data was processed - const hasProcessingResults = allStepData?.processingResults && - allStepData.processingResults.is_valid && - allStepData.processingResults.total_records > 0; - - // Check if sales data was imported (required for training) - const hasImportResults = allStepData?.salesImportResult && - (allStepData.salesImportResult.records_created > 0 || - allStepData.salesImportResult.success === true || - allStepData.salesImportResult.imported === true); - - if (!hasProcessingResults) { - missingItems.push('Datos de ventas validados'); + // WebSocket for real-time training progress + const trainingWebSocket = useTrainingWebSocket( + currentTenant?.id || '', + jobId || '', + undefined, // token will be handled by the service + { + onProgress: (data) => { + setTrainingProgress({ + stage: 'training', + progress: data.progress?.percentage || 0, + message: data.message || 'Entrenando modelo...', + currentStep: data.progress?.current_step, + estimatedTimeRemaining: data.progress?.estimated_time_remaining + }); + }, + onCompleted: (data) => { + setTrainingProgress({ + stage: 'completed', + progress: 100, + message: 'Entrenamiento completado exitosamente' + }); + setIsTraining(false); + + setTimeout(() => { + onComplete({ + jobId: jobId, + success: true, + message: 'Modelo entrenado correctamente' + }); + }, 2000); + }, + onError: (data) => { + setError(data.error || 'Error durante el entrenamiento'); + setIsTraining(false); + setTrainingProgress(null); + }, + onStarted: (data) => { + setTrainingProgress({ + stage: 'starting', + progress: 5, + message: 'Iniciando entrenamiento del modelo...' + }); + } } - - // Sales data must be imported for ML training to work - if (!hasImportResults) { - missingItems.push('Datos de ventas importados'); - } - - // Check if products were approved in review step - const hasApprovedProducts = allStepData?.approvedProducts && - allStepData.approvedProducts.length > 0 && - allStepData.reviewCompleted; - - if (!hasApprovedProducts) { - missingItems.push('Productos aprobados en revisión'); - } - - // Check if inventory was configured - const hasInventoryConfig = allStepData?.inventoryConfigured && - allStepData?.inventoryItems && - allStepData.inventoryItems.length > 0; - - if (!hasInventoryConfig) { - missingItems.push('Inventario configurado'); - } - - // Check if we have enough data for training - if (dataProcessingData?.processingResults?.total_records && - dataProcessingData.processingResults.total_records < 10) { - missingItems.push('Suficientes registros de ventas (mínimo 10)'); - } - - console.log('MLTrainingStep - Validation result:', { - isValid: missingItems.length === 0, - missingItems, - hasProcessingResults, - hasImportResults, - hasApprovedProducts, - hasInventoryConfig - }); - - return { - isValid: missingItems.length === 0, - missingItems - }; - }; + ); const handleStartTraining = async () => { - // Validate data requirements - const validation = validateDataRequirements(); - if (!validation.isValid) { - console.error('Datos insuficientes para entrenamiento:', validation.missingItems); + if (!currentTenant?.id) { + setError('No se encontró información del tenant'); return; } - setHasStarted(true); - - // Use the onboarding hook for training - const success = await startTraining({ - // You can pass options here if needed - startDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[0], - endDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[1], + setIsTraining(true); + setError(''); + setTrainingProgress({ + stage: 'preparing', + progress: 0, + message: 'Preparando datos para entrenamiento...' }); - if (!success) { - console.error('Error starting training'); - setHasStarted(false); - } + try { + const response = await createTrainingJob.mutateAsync({ + tenantId: currentTenant.id, + request: { + // Use the exact backend schema - all fields are optional + // This will train on all available data + } + }); + setJobId(response.job_id); + + setTrainingProgress({ + stage: 'queued', + progress: 10, + message: 'Trabajo de entrenamiento en cola...' + }); + } catch (err) { + setError('Error al iniciar el entrenamiento del modelo'); + setIsTraining(false); + setTrainingProgress(null); + } }; - // Cleanup WebSocket on unmount - useEffect(() => { - return () => { - if (wsRef.current) { - wsRef.current.disconnect(); - wsRef.current = null; - } - }; - }, []); - - useEffect(() => { - // Auto-start training if all requirements are met and not already started - const validation = validateDataRequirements(); - console.log('MLTrainingStep - useEffect validation:', validation); + const formatTime = (seconds?: number) => { + if (!seconds) return ''; - if (validation.isValid && status === 'idle' && data.autoStartTraining) { - console.log('MLTrainingStep - Auto-starting training...'); - // Auto-start after a brief delay to allow user to see the step - const timer = setTimeout(() => { - handleStartTraining(); - }, 1000); - return () => clearTimeout(timer); - } - }, [allStepData, data.autoStartTraining, status]); - - const getStatusIcon = () => { - switch (status) { - case 'idle': return ; - case 'validating': return ; - case 'training': return ; - case 'completed': return ; - case 'failed': return ; - default: return ; + if (seconds < 60) { + return `${Math.round(seconds)}s`; + } else if (seconds < 3600) { + return `${Math.round(seconds / 60)}m`; + } else { + return `${Math.round(seconds / 3600)}h ${Math.round((seconds % 3600) / 60)}m`; } }; - const getStatusColor = () => { - switch (status) { - case 'completed': return 'text-[var(--color-success)]'; - case 'failed': return 'text-[var(--color-error)]'; - case 'training': - case 'validating': return 'text-[var(--color-info)]'; - default: return 'text-[var(--text-primary)]'; - } - }; - - const getStatusMessage = () => { - switch (status) { - case 'idle': return 'Listo para entrenar tu asistente IA'; - case 'validating': return 'Validando datos para entrenamiento...'; - case 'training': return 'Entrenando modelo de predicción...'; - case 'completed': return '¡Tu asistente IA está listo!'; - case 'failed': return 'Error en el entrenamiento'; - default: return 'Estado desconocido'; - } - }; - - // Check data requirements for display - const validation = validateDataRequirements(); - - if (!validation.isValid) { - return ( -
-
- -

- Datos insuficientes para entrenamiento -

-

- Para entrenar tu modelo de IA, necesitamos que completes los siguientes elementos: -

- - -

Elementos requeridos:

-
    - {validation.missingItems.map((item, index) => ( -
  • - - {item} -
  • - ))} -
-
- -

- Una vez completados estos elementos, el entrenamiento se iniciará automáticamente. -

- - -
-
- ); - } - return ( -
- {/* Header */} +
-
- {getStatusIcon()} -
-

- Entrenamiento de IA -

-

- {getStatusMessage()} -

-

- Creando tu asistente inteligente personalizado con tus datos de ventas e inventario +

+ Ahora entrenaremos tu modelo de inteligencia artificial utilizando los datos de ventas + e inventario que has proporcionado. Este proceso puede tomar varios minutos.

- {/* Progress Bar */} - -
- Progreso del entrenamiento - {progress.toFixed(1)}% -
- - {currentStep && ( -
- Paso actual: - {currentStep} -
- )} - - {estimatedTimeRemaining > 0 && ( -
- Tiempo estimado restante: - - {Math.round(estimatedTimeRemaining / 60)} minutos - -
- )} - -
-
-
- - - {/* Training Logs */} - -

- - Registro de entrenamiento -

-
- {trainingLogs.length === 0 ? ( -

Esperando inicio de entrenamiento...

- ) : ( - trainingLogs.map((log, index) => ( -
-
-
-

{log.message}

-

- {new Date(log.timestamp).toLocaleTimeString()} -

-
+ {/* Training Status Card */} +
+
+ {!isTraining && !trainingProgress && ( +
+
+ + +
- )) +
+

Listo para Entrenar

+

+ Tu modelo está listo para ser entrenado con los datos proporcionados. +

+
+
+ )} + + {trainingProgress && ( +
+
+ {trainingProgress.stage === 'completed' ? ( +
+ + + +
+ ) : ( +
+
+
+ )} + + {trainingProgress.progress > 0 && trainingProgress.stage !== 'completed' && ( +
+ + {trainingProgress.progress}% + +
+ )} +
+ +
+

+ {trainingProgress.stage === 'completed' + ? '¡Entrenamiento Completo!' + : 'Entrenando Modelo IA' + } +

+

+ {trainingProgress.message} +

+ + {trainingProgress.stage !== 'completed' && ( +
+
+
+
+ +
+ {trainingProgress.currentStep || 'Procesando...'} + {trainingProgress.estimatedTimeRemaining && ( + Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)} + )} +
+
+ )} +
+
)}
- +
- {/* Training Metrics */} - {metrics && status === 'completed' && ( - -

- - Métricas del modelo -

-
-
-

- {(metrics.accuracy * 100).toFixed(1)}% -

-

Precisión

-
-
-

- {metrics.mape.toFixed(1)}% -

-

MAPE

-
-
-

- {metrics.mae.toFixed(2)} -

-

MAE

-
-
-

- {metrics.rmse.toFixed(2)} -

-

RMSE

-
-
-
+ {/* Training Info */} +
+

¿Qué sucede durante el entrenamiento?

+
    +
  • • Análisis de patrones de ventas históricos
  • +
  • • Creación de modelos predictivos de demanda
  • +
  • • Optimización de algoritmos de inventario
  • +
  • • Validación y ajuste de precisión
  • +
+
+ + {error && ( +
+

{error}

+
)} - {/* Manual Start Button (if not auto-started) */} - {status === 'idle' && ( - - - - )} - - {/* Navigation */} -
- - + + {!isTraining && !trainingProgress && ( + + )} + + {trainingProgress?.stage === 'completed' && ( + + )}
); diff --git a/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx new file mode 100644 index 00000000..989b5615 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx @@ -0,0 +1,172 @@ +import React, { useState } from 'react'; +import { Button } from '../../../ui/Button'; +import { Input } from '../../../ui/Input'; +import { useRegisterBakery } from '../../../../api/hooks/tenant'; +import { BakeryRegistration } from '../../../../api/types/tenant'; + +interface RegisterTenantStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data?: any) => void; + isFirstStep: boolean; + isLastStep: boolean; +} + +export const RegisterTenantStep: React.FC = ({ + onComplete, + isFirstStep +}) => { + const [formData, setFormData] = useState({ + name: '', + address: '', + postal_code: '', + phone: '', + city: 'Madrid', + business_type: 'bakery', + business_model: 'individual_bakery' + }); + + const [errors, setErrors] = useState>({}); + const registerBakery = useRegisterBakery(); + + const handleInputChange = (field: keyof BakeryRegistration, value: string) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + + if (errors[field]) { + setErrors(prev => ({ + ...prev, + [field]: '' + })); + } + }; + + const validateForm = () => { + const newErrors: Record = {}; + + // Required fields according to backend BakeryRegistration schema + if (!formData.name.trim()) { + newErrors.name = 'El nombre de la panadería es obligatorio'; + } else if (formData.name.length < 2 || formData.name.length > 200) { + newErrors.name = 'El nombre debe tener entre 2 y 200 caracteres'; + } + + if (!formData.address.trim()) { + newErrors.address = 'La dirección es obligatoria'; + } else if (formData.address.length < 10 || formData.address.length > 500) { + newErrors.address = 'La dirección debe tener entre 10 y 500 caracteres'; + } + + if (!formData.postal_code.trim()) { + newErrors.postal_code = 'El código postal es obligatorio'; + } else if (!/^\d{5}$/.test(formData.postal_code)) { + newErrors.postal_code = 'El código postal debe tener exactamente 5 dígitos'; + } + + if (!formData.phone.trim()) { + newErrors.phone = 'El número de teléfono es obligatorio'; + } else if (formData.phone.length < 9 || formData.phone.length > 20) { + newErrors.phone = 'El teléfono debe tener entre 9 y 20 caracteres'; + } else { + // Basic Spanish phone validation + const phone = formData.phone.replace(/[\s\-\(\)]/g, ''); + const patterns = [ + /^(\+34|0034|34)?[6789]\d{8}$/, // Mobile + /^(\+34|0034|34)?9\d{8}$/ // Landline + ]; + if (!patterns.some(pattern => pattern.test(phone))) { + newErrors.phone = 'Introduce un número de teléfono español válido'; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + return; + } + + try { + const tenant = await registerBakery.mutateAsync(formData); + onComplete({ tenant }); + } catch (error) { + console.error('Error registering bakery:', error); + setErrors({ submit: 'Error al registrar la panadería. Por favor, inténtalo de nuevo.' }); + } + }; + + return ( +
+
+ handleInputChange('name', e.target.value)} + error={errors.name} + isRequired + /> + + handleInputChange('phone', e.target.value)} + error={errors.phone} + isRequired + /> + +
+ handleInputChange('address', e.target.value)} + error={errors.address} + isRequired + /> +
+ + handleInputChange('postal_code', e.target.value)} + error={errors.postal_code} + isRequired + maxLength={5} + /> + + handleInputChange('city', e.target.value)} + error={errors.city} + /> +
+ + {errors.submit && ( +
+ {errors.submit} +
+ )} + +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/SmartInventorySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/SmartInventorySetupStep.tsx deleted file mode 100644 index 5af00b2a..00000000 --- a/frontend/src/components/domain/onboarding/steps/SmartInventorySetupStep.tsx +++ /dev/null @@ -1,1095 +0,0 @@ -import React, { useState, useRef, useEffect, useMemo } from 'react'; -import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, Package, Edit } from 'lucide-react'; -import { Button, Card, Badge, Input } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; -import { useToast } from '../../../../hooks/ui/useToast'; -import { useOnboarding } from '../../../../hooks/business/onboarding'; -import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store'; -import { useCurrentTenant } from '../../../../stores'; -import type { ProductSuggestionResponse } from '../../../../api'; - -type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'review' | 'inventory-config' | 'creating' | 'completed' | 'error'; - -interface EnhancedProductCard { - // From AI suggestion - suggestion_id: string; - original_name: string; - suggested_name: string; - product_type: 'ingredient' | 'finished_product'; - category: string; - unit_of_measure: string; - confidence_score: number; - estimated_shelf_life_days?: number; - requires_refrigeration: boolean; - requires_freezing: boolean; - is_seasonal: boolean; - suggested_supplier?: string; - notes?: string; - sales_data?: { - total_quantity: number; - average_daily_sales: number; - peak_day: string; - frequency: number; - }; - - // Inventory configuration (user editable) - current_stock: number; - min_stock: number; - max_stock: number; - cost_per_unit?: number; - custom_expiry_date?: string; - - // UI state - status: 'pending' | 'approved' | 'rejected'; -} - -// Convert API suggestions to enhanced product cards -const convertSuggestionsToCards = (suggestions: ProductSuggestionResponse[]): EnhancedProductCard[] => { - return suggestions.map(suggestion => ({ - // From API - suggestion_id: suggestion.suggestion_id, - original_name: suggestion.original_name, - suggested_name: suggestion.suggested_name, - product_type: suggestion.product_type as 'ingredient' | 'finished_product', - category: suggestion.category, - unit_of_measure: suggestion.unit_of_measure, - confidence_score: suggestion.confidence_score, - estimated_shelf_life_days: suggestion.estimated_shelf_life_days, - requires_refrigeration: suggestion.requires_refrigeration, - requires_freezing: suggestion.requires_freezing, - is_seasonal: suggestion.is_seasonal, - suggested_supplier: suggestion.suggested_supplier, - notes: suggestion.notes, - sales_data: suggestion.sales_data, - - // Smart defaults based on sales data - current_stock: 0, - min_stock: Math.max(1, Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7)), // Week buffer - max_stock: Math.max(10, Math.ceil((suggestion.sales_data?.total_quantity || 10) * 2)), // 2x peak quantity - cost_per_unit: 0, - custom_expiry_date: undefined, - - // Initial state - status: suggestion.confidence_score >= 0.8 ? 'approved' : 'pending' - })); -}; - -export const SmartInventorySetupStep: React.FC = ({ - data, - onDataChange -}) => { - const user = useAuthUser(); - const authLoading = useAuthLoading(); - const currentTenant = useCurrentTenant(); - - // Use onboarding hooks - const { - processSalesFile, - generateProductSuggestions, // New separated function - createInventoryFromSuggestions, - importSalesData, - salesProcessing: { - stage: onboardingStage, - progress: onboardingProgress, - currentMessage: onboardingMessage, - validationResults, - suggestions - }, - inventorySetup: { - createdItems, - inventoryMapping, - salesImportResult - }, - tenantCreation, - error, - clearError - } = useOnboarding(); - - const toast = useToast(); - - // Get tenant ID - const getTenantId = (): string | null => { - const onboardingTenantId = data.bakery?.tenant_id; - const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null; - return tenantId; - }; - - // Check if tenant data is available - const isTenantAvailable = (): boolean => { - const hasAuth = !authLoading && user; - const hasTenantId = getTenantId() !== null; - const tenantCreatedSuccessfully = tenantCreation.isSuccess; - const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true; - - // 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; - }; - - // Local state - const [localStage, setLocalStage] = useState(data.processingStage || 'upload'); - const [uploadedFile, setUploadedFile] = useState(data.files?.salesData || null); - const [products, setProducts] = useState(() => { - if (data.detectedProducts) { - return data.detectedProducts; - } - if (suggestions && suggestions.length > 0) { - return convertSuggestionsToCards(suggestions); - } - return []; - }); - const [selectedCategory, setSelectedCategory] = useState('all'); - const [editingProduct, setEditingProduct] = useState(null); - const [isCreating, setIsCreating] = useState(false); - const [dragActive, setDragActive] = useState(false); - - const fileInputRef = useRef(null); - - // Update products when suggestions change - useEffect(() => { - console.log('🔄 SmartInventorySetup - suggestions effect:', { - suggestionsLength: suggestions?.length || 0, - productsLength: products.length, - suggestionsIsArray: Array.isArray(suggestions), - suggestionsType: typeof suggestions, - shouldConvert: 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'); - 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, data, onDataChange]); - - // Derive current stage - 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 || ''; - - // Debug current stage - useEffect(() => { - console.log('🔄 SmartInventorySetup - Stage debug:', { - localStage, - onboardingStage, - finalStage: stage, - productsLength: products.length, - suggestionsLength: suggestions?.length || 0, - hasSuggestions: !!suggestions, - suggestionsArray: Array.isArray(suggestions), - willShowReview: stage === 'review' && products.length > 0 - }); - }, [localStage, onboardingStage, stage, products.length, suggestions?.length]); - - // Product stats - const stats = useMemo(() => ({ - total: products.length, - approved: products.filter(p => p.status === 'approved').length, - rejected: products.filter(p => p.status === 'rejected').length, - pending: products.filter(p => p.status === 'pending').length - }), [products]); - - const approvedProducts = useMemo(() => - products.filter(p => p.status === 'approved'), - [products] - ); - - const reviewCompleted = useMemo(() => - products.length > 0 && products.every(p => p.status !== 'pending'), - [products] - ); - - const inventoryConfigured = useMemo(() => - approvedProducts.length > 0 && - approvedProducts.every(p => p.min_stock >= 0 && p.max_stock > p.min_stock), - [approvedProducts] - ); - - const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))]; - - const getFilteredProducts = () => { - if (selectedCategory === 'all') return products; - return products.filter(p => p.category === selectedCategory); - }; - - // File handling - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - setDragActive(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - setDragActive(false); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - setDragActive(false); - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - handleFileUpload(files[0]); - } - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - handleFileUpload(e.target.files[0]); - } - }; - - const handleFileUpload = async (file: File) => { - const validExtensions = ['.csv', '.xlsx', '.xls', '.json']; - const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); - - if (!validExtensions.includes(fileExtension)) { - toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', { - title: 'Formato inválido', - type: 'error' - }); - return; - } - - if (file.size > 10 * 1024 * 1024) { - toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', { - title: 'Archivo muy grande', - type: 'error' - }); - return; - } - - if (!isTenantAvailable()) { - toast.addToast('Por favor espere mientras cargamos su información...', { - title: 'Esperando datos de usuario', - type: 'info' - }); - return; - } - - setUploadedFile(file); - setLocalStage('validating'); - - try { - console.log('🔄 SmartInventorySetup - Processing file:', file.name); - const success = await processSalesFile(file); - - console.log('🔄 SmartInventorySetup - Processing result:', { success }); - - if (success) { - console.log('✅ File validation completed successfully'); - // Don't set to review stage anymore - let the 'validated' stage show first - setLocalStage('validated'); - - 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'); - } - } catch (error) { - console.error('Error processing file:', error); - setLocalStage('error'); - const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos'; - - toast.addToast(errorMessage, { - title: 'Error en el procesamiento', - type: 'error' - }); - } - }; - - const downloadTemplate = () => { - 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`; - - 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' - }); - }; - - // Product actions - const handleProductAction = (productId: string, action: 'approve' | 'reject') => { - setProducts(prev => prev.map(product => - product.suggestion_id === productId - ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } - : product - )); - }; - - const handleBulkAction = (action: 'approve' | 'reject') => { - const filteredProducts = getFilteredProducts(); - setProducts(prev => prev.map(product => - filteredProducts.some(fp => fp.suggestion_id === product.suggestion_id) - ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } - : product - )); - }; - - const handleEditProduct = (product: EnhancedProductCard) => { - setEditingProduct({ ...product }); - }; - - const handleSaveProduct = (updatedProduct: EnhancedProductCard) => { - setProducts(prev => prev.map(product => - product.suggestion_id === updatedProduct.suggestion_id ? updatedProduct : product - )); - setEditingProduct(null); - }; - - const handleCancelEdit = () => { - 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 = { - ...data, - files: { ...data.files, salesData: uploadedFile }, - processingStage: stage === 'review' ? 'completed' : (stage === 'completed' ? 'completed' : stage), - processingResults: validationResults, - suggestions: suggestions, - detectedProducts: products, - approvedProducts: approvedProducts, - reviewCompleted: reviewCompleted, - inventoryConfigured: inventoryConfigured, - inventoryItems: createdItems, - inventoryMapping: inventoryMapping, - salesImportResult: salesImportResult - }; - - // Debug logging - console.log('SmartInventorySetupStep - Updating parent data:', { - stage, - reviewCompleted, - inventoryConfigured, - hasSalesImportResult: !!salesImportResult, - salesImportResult, - approvedProductsCount: approvedProducts.length - }); - - // Only update if data actually changed to prevent infinite loops - const dataString = JSON.stringify(updatedData); - const currentDataString = JSON.stringify(data); - if (dataString !== currentDataString) { - onDataChange(updatedData); - } - }, [stage, uploadedFile, validationResults, suggestions, products, approvedProducts, reviewCompleted, inventoryConfigured, createdItems, inventoryMapping, salesImportResult, data, onDataChange]); - - const getConfidenceColor = (confidence: number) => { - if (confidence >= 0.9) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; - if (confidence >= 0.75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20'; - return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; - case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; - default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]'; - } - }; - - return ( -
- {/* Loading state when tenant data is not available */} - {!isTenantAvailable() && ( - -
- -
-

- Cargando datos de usuario... -

-

- Por favor espere mientras cargamos su información de tenant -

-
- )} - - {/* Upload Stage */} - {(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && ( - <> -
-

- 📦 Configuración Inteligente de Inventario -

-

- Sube tu historial de ventas y crearemos automáticamente tu inventario con configuraciones inteligentes -

-
- -
fileInputRef.current?.click()} - > - - -
- {uploadedFile ? ( - <> -
- -
-
-

- ¡Perfecto! Archivo listo -

-
-

- 📄 {uploadedFile.name} -

-

- {(uploadedFile.size / 1024 / 1024).toFixed(2)} MB • Listo para procesar -

-
-
- - ) : ( - <> -
- -
-
-

- Sube tus datos de ventas de forma segura: tú eres el propietario de esos datos, nosotros solo los optimizamos. -

-

- Arrastra y suelta tu archivo aquí, o haz clic para seleccionar -

-
- - )} -
-
- - {/* Template Download Section */} -
-
-
- -
-
-

- ¿Necesitas ayuda con el formato? -

-

- Descarga nuestra plantilla con ejemplos para tus datos de ventas -

- -
-
-
- - )} - - {/* File Validated - User Confirmation Required */} - {stage === 'validated' && validationResults && ( - -
-
- -
-

- ¡Archivo Validado Correctamente! -

-

- Hemos encontrado {validationResults.product_list?.length || 0} productos únicos en tu archivo de ventas. -

- -
-

Lo que haremos a continuación:

-
    -
  • 🤖 Análisis con IA: Clasificaremos automáticamente tus productos
  • -
  • 📦 Configuración inteligente: Calcularemos niveles de stock óptimos
  • -
  • Tu aprobación: Podrás revisar y aprobar cada sugerencia
  • -
  • 🎯 Inventario personalizado: Crearemos tu inventario final
  • -
-
- -
- - - -
-
-
- )} - - {/* Processing Stages */} - {(stage === 'validating' || stage === 'analyzing') && ( - -
-
-
- {stage === 'validating' ? ( - - ) : ( - - )} -
- -

- {stage === 'validating' ? 'Validando datos...' : 'Analizando con IA...'} -

-

- {currentMessage} -

-
- -
-
- - Progreso - - {progress}% -
-
-
-
-
-
- - )} - - {/* Creating Stage */} - {isCreating && ( - -
-
- -
-

- Creando tu inventario... -

-

- Configurando productos e importando datos de ventas -

-
-
- )} - - {/* Waiting for Suggestions to Load */} - {(stage === 'review' || (onboardingStage === 'completed' && suggestions)) && products.length === 0 && suggestions && suggestions.length > 0 && ( - -
-
- -
-

- Preparando sugerencias... -

-

- Convirtiendo {suggestions.length} productos en sugerencias personalizadas -

-
-
- )} - - {/* Review & Configure Stage */} - {(stage === 'review') && products.length > 0 && ( -
-
-
- -
-

- ¡Productos Detectados! -

-

- Revisa, aprueba y configura los niveles de inventario para tus productos -

-
- - {/* Stats */} -
- -
{stats.total}
-
Productos detectados
-
- - -
{stats.approved}
-
Aprobados
-
- - -
{stats.rejected}
-
Rechazados
-
- - -
{stats.pending}
-
Pendientes
-
-
- - {/* Controls */} -
-
- - - - {getFilteredProducts().length} productos - -
- -
- - -
-
- - {/* Products List */} -
- {getFilteredProducts().map((product) => ( - -
-
-
-

- {product.suggested_name} -

- - {product.status === 'approved' ? '✓ Aprobado' : - product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'} - - - {Math.round(product.confidence_score * 100)}% confianza - -
- -
- - {product.category} - - - {product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'} - - {product.requires_refrigeration && ( - - ❄️ Refrigeración - - )} -
- -
- {product.original_name !== product.suggested_name && ( -
- Original: - {product.original_name} -
- )} - -
-
- Stock min: - {product.min_stock} -
-
- Stock max: - {product.max_stock} -
-
- Unidad: - {product.unit_of_measure} -
-
- - {product.sales_data && ( -
-
Datos de Ventas:
-
- Total vendido: {product.sales_data.total_quantity} - Promedio diario: {product.sales_data.average_daily_sales.toFixed(1)} -
-
- )} - - {product.notes && ( -
- Nota: {product.notes} -
- )} -
-
- -
- - - -
-
-
- ))} -
- - {/* Ready to Proceed Status */} - {approvedProducts.length > 0 && reviewCompleted && ( -
- -
-
-
- -
-
-

- ✅ Listo para crear inventario -

-

- {approvedProducts.length} productos aprobados y listos para crear el inventario. -

-
- Haz clic en "Siguiente" para crear automáticamente el inventario e importar los datos de ventas. -
-
-
-
- )} - - {/* Information */} - -

- 📦 Sube tus datos de ventas de forma segura: tú eres el propietario, nosotros solo los optimizamos.: -

-
    -
  • Configuración automática - Los niveles de stock se calculan basándose en tus datos de ventas
  • -
  • Revisa y aprueba - Confirma que cada producto sea correcto para tu negocio
  • -
  • Edita configuración - Ajusta niveles de stock, proveedores y otros detalles
  • -
  • Creación integral - Se creará el inventario e importarán los datos de ventas automáticamente
  • -
-
-
- )} - - {/* Completed Stage */} - {stage === 'completed' && ( -
-
- -
-

- ¡Inventario Creado! -

-

- Tu inventario inteligente ha sido configurado exitosamente con {approvedProducts.length} productos. -

- {salesImportResult?.success && ( -

- Se importaron {salesImportResult.records_created} registros de ventas para entrenar tu IA. -

- )} -
- )} - - {/* Error State */} - {stage === 'error' && ( - -
- -
-

- Error en el procesamiento -

-

- {currentMessage || error} -

- -
- )} - - {/* Product Editor Modal */} - {editingProduct && ( -
- -
-

- Editar Producto: {editingProduct.suggested_name} -

- -
-
- - setEditingProduct({ ...editingProduct, current_stock: Number(e.target.value) })} - min="0" - className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]" - /> -
- -
- - setEditingProduct({ ...editingProduct, min_stock: Number(e.target.value) })} - min="0" - className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]" - /> -
- -
- - setEditingProduct({ ...editingProduct, max_stock: Number(e.target.value) })} - min="1" - className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]" - /> -
- -
- - setEditingProduct({ ...editingProduct, cost_per_unit: Number(e.target.value) })} - min="0" - step="0.01" - className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]" - /> -
- -
- - setEditingProduct({ ...editingProduct, custom_expiry_date: e.target.value })} - className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]" - /> -
-
- -
- - -
-
-
-
- )} -
- ); -}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx b/frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx deleted file mode 100644 index 21cd5d89..00000000 --- a/frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx +++ /dev/null @@ -1,816 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin, AlertCircle, Loader } from 'lucide-react'; -import { Button, Card, Input, Badge } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; -import { useModal } from '../../../../hooks/ui/useModal'; -import { useToast } from '../../../../hooks/ui/useToast'; -import { useAuthUser } from '../../../../stores/auth.store'; -import { useCurrentTenant } from '../../../../stores'; -// TODO: Import procurement service from new API when available - -// Frontend supplier interface that matches the form needs -interface SupplierFormData { - id?: string; - name: string; - contact_name: string; - phone: string; - email: string; - address: string; - payment_terms: string; - delivery_terms: string; - tax_id?: string; - is_active: boolean; -} - -const commonCategories = [ - 'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos', - 'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes' -]; - -const paymentTermsOptions = [ - 'Inmediato', - '15 días', - '30 días', - '45 días', - '60 días', - '90 días' -]; - -export const SuppliersStep: React.FC = ({ - data, - onDataChange, - onNext, - onPrevious, - isFirstStep, - isLastStep -}) => { - const user = useAuthUser(); - const currentTenant = useCurrentTenant(); - const createAlert = (alert: any) => { - console.log('Alert:', alert); - }; - const { showToast } = useToast(); - - // Use modals for confirmations and editing - const deleteModal = useModal(); - const editModal = useModal(); - - const [suppliers, setSuppliers] = useState([]); - const [editingSupplier, setEditingSupplier] = useState(null); - const [isAddingNew, setIsAddingNew] = useState(false); - const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all'); - const [loading, setLoading] = useState(false); - const [creating, setCreating] = useState(false); - const [updating, setUpdating] = useState(false); - const [deleting, setDeleting] = useState(null); - - // Load suppliers from backend on component mount - useEffect(() => { - const loadSuppliers = async () => { - // Check if we already have suppliers loaded - if (data.suppliers && Array.isArray(data.suppliers) && data.suppliers.length > 0) { - setSuppliers(data.suppliers); - return; - } - - if (!currentTenant?.id) { - return; - } - - setLoading(true); - try { - const response = await procurementService.getSuppliers({ size: 100 }); - if (response.success && response.data) { - setSuppliers(response.data.items); - } - } catch (error) { - console.error('Failed to load suppliers:', error); - createAlert({ - type: 'error', - category: 'system', - priority: 'medium', - title: 'Error al cargar proveedores', - message: 'No se pudieron cargar los proveedores existentes.', - source: 'onboarding' - }); - } finally { - setLoading(false); - } - }; - - loadSuppliers(); - }, [currentTenant?.id]); - - // Update parent data when suppliers change - useEffect(() => { - onDataChange({ - ...data, - suppliers: suppliers, - suppliersConfigured: true // This step is optional - }); - }, [suppliers]); - - const handleAddSupplier = () => { - const newSupplier: SupplierFormData = { - name: '', - contact_name: '', - phone: '', - email: '', - address: '', - payment_terms: '30 días', - delivery_terms: 'Recoger en tienda', - tax_id: '', - is_active: true - }; - setEditingSupplier(newSupplier); - setIsAddingNew(true); - }; - - const handleSaveSupplier = async (supplierData: SupplierFormData) => { - if (isAddingNew) { - setCreating(true); - try { - const response = await procurementService.createSupplier({ - name: supplierData.name, - contact_name: supplierData.contact_name, - phone: supplierData.phone, - email: supplierData.email, - address: supplierData.address, - payment_terms: supplierData.payment_terms, - delivery_terms: supplierData.delivery_terms, - tax_id: supplierData.tax_id, - is_active: supplierData.is_active - }); - - if (response.success && response.data) { - setSuppliers(prev => [...prev, response.data]); - createAlert({ - type: 'success', - category: 'system', - priority: 'low', - title: 'Proveedor creado', - message: `El proveedor ${response.data.name} se ha creado exitosamente.`, - source: 'onboarding' - }); - } - } catch (error) { - console.error('Failed to create supplier:', error); - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error al crear proveedor', - message: 'No se pudo crear el proveedor. Intente nuevamente.', - source: 'onboarding' - }); - } finally { - setCreating(false); - } - } else { - // Update existing supplier - if (!supplierData.id) return; - - setUpdating(true); - try { - const response = await procurementService.updateSupplier(supplierData.id, { - name: supplierData.name, - contact_name: supplierData.contact_name, - phone: supplierData.phone, - email: supplierData.email, - address: supplierData.address, - payment_terms: supplierData.payment_terms, - delivery_terms: supplierData.delivery_terms, - tax_id: supplierData.tax_id, - is_active: supplierData.is_active - }); - - if (response.success && response.data) { - setSuppliers(prev => prev.map(s => s.id === response.data.id ? response.data : s)); - createAlert({ - type: 'success', - category: 'system', - priority: 'low', - title: 'Proveedor actualizado', - message: `El proveedor ${response.data.name} se ha actualizado exitosamente.`, - source: 'onboarding' - }); - } - } catch (error) { - console.error('Failed to update supplier:', error); - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error al actualizar proveedor', - message: 'No se pudo actualizar el proveedor. Intente nuevamente.', - source: 'onboarding' - }); - } finally { - setUpdating(false); - } - } - - setEditingSupplier(null); - setIsAddingNew(false); - }; - - const handleDeleteSupplier = async (id: string) => { - deleteModal.openModal({ - title: 'Confirmar eliminación', - message: '¿Estás seguro de eliminar este proveedor? Esta acción no se puede deshacer.', - onConfirm: () => performDelete(id), - onCancel: () => deleteModal.closeModal() - }); - return; - }; - - const performDelete = async (id: string) => { - deleteModal.closeModal(); - - setDeleting(id); - try { - const response = await procurementService.deleteSupplier(id); - if (response.success) { - setSuppliers(prev => prev.filter(s => s.id !== id)); - createAlert({ - type: 'success', - category: 'system', - priority: 'low', - title: 'Proveedor eliminado', - message: 'El proveedor se ha eliminado exitosamente.', - source: 'onboarding' - }); - } - } catch (error) { - console.error('Failed to delete supplier:', error); - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error al eliminar proveedor', - message: 'No se pudo eliminar el proveedor. Intente nuevamente.', - source: 'onboarding' - }); - } finally { - setDeleting(null); - } - }; - - const toggleSupplierStatus = async (id: string, currentStatus: boolean) => { - try { - const response = await procurementService.updateSupplier(id, { - is_active: !currentStatus - }); - - if (response.success && response.data) { - setSuppliers(prev => prev.map(s => - s.id === id ? response.data : s - )); - createAlert({ - type: 'success', - category: 'system', - priority: 'low', - title: 'Estado actualizado', - message: `El proveedor se ha ${!currentStatus ? 'activado' : 'desactivado'} exitosamente.`, - source: 'onboarding' - }); - } - } catch (error) { - console.error('Failed to toggle supplier status:', error); - createAlert({ - type: 'error', - category: 'system', - priority: 'high', - title: 'Error al cambiar estado', - message: 'No se pudo cambiar el estado del proveedor.', - source: 'onboarding' - }); - } - }; - - const getFilteredSuppliers = () => { - if (filterStatus === 'all') { - return suppliers; - } - return suppliers.filter(s => - filterStatus === 'active' ? s.is_active : !s.is_active - ); - }; - - const stats = { - total: suppliers.length, - active: suppliers.filter(s => s.is_active).length, - inactive: suppliers.filter(s => !s.is_active).length, - totalOrders: suppliers.reduce((sum, s) => sum + s.performance_metrics.total_orders, 0) - }; - - if (loading) { - return ( -
-
- -

Cargando proveedores...

-
-
- ); - } - - return ( -
- {/* Optional Step Notice */} - -

- 💡 Paso Opcional: Puedes saltar este paso y configurar proveedores más tarde desde el módulo de compras -

-
- - {/* Quick Actions */} - -
-
-

- {stats.total} proveedores configurados ({stats.active} activos) -

-
-
-
- - {/* Stats */} -
- -

{stats.total}

-

Total

-
- -

{stats.active}

-

Activos

-
- -

{stats.inactive}

-

Inactivos

-
- -

{stats.totalOrders}

-

Órdenes

-
-
- - {/* Filters */} - -
- -
- {[ - { value: 'all', label: 'Todos' }, - { value: 'active', label: 'Activos' }, - { value: 'inactive', label: 'Inactivos' } - ].map(filter => ( - - ))} -
-
-
- - {/* Suppliers List */} -
- {getFilteredSuppliers().map((supplier) => ( - -
-
- {/* Status Icon */} -
- -
- - {/* Supplier Info */} -
-

{supplier.name}

- -
- - {supplier.is_active ? 'Activo' : 'Inactivo'} - - {supplier.rating && ( - ★ {supplier.rating.toFixed(1)} - )} - {supplier.performance_metrics.total_orders > 0 && ( - {supplier.performance_metrics.total_orders} órdenes - )} -
- -
- {supplier.contact_name && ( -
- Contacto: - {supplier.contact_name} -
- )} - - {supplier.delivery_terms && ( -
- Entrega: - {supplier.delivery_terms} -
- )} - - {supplier.payment_terms && ( -
- Pago: - {supplier.payment_terms} -
- )} -
- -
- {supplier.phone && ( -
- - {supplier.phone} -
- )} - {supplier.email && ( -
- - {supplier.email} -
- )} -
- - {supplier.address} -
-
- - {supplier.performance_metrics.on_time_delivery_rate > 0 && ( -
- Rendimiento: - - {supplier.performance_metrics.on_time_delivery_rate}% entregas a tiempo - - {supplier.performance_metrics.quality_score > 0 && ( - - , {supplier.performance_metrics.quality_score}/5 calidad - - )} -
- )} -
-
- - {/* Actions */} -
- - - - - -
-
-
- ))} - - {getFilteredSuppliers().length === 0 && !loading && ( - - -

- {filterStatus === 'all' - ? 'Comienza agregando tu primer proveedor' - : `No hay proveedores ${filterStatus === 'active' ? 'activos' : 'inactivos'}` - } -

-

- {filterStatus === 'all' - ? 'Los proveedores te ayudarán a gestionar tus compras y mantener un control de calidad en tu panadería.' - : 'Ajusta los filtros para ver otros proveedores o agrega uno nuevo.' - } -

- -
- )} - - {/* Floating Action Button for when suppliers exist */} - {suppliers.length > 0 && !editingSupplier && ( -
- -
- )} -
- - {/* Edit Modal */} - {editingSupplier && ( -
- -

- {isAddingNew ? 'Agregar Proveedor' : 'Editar Proveedor'} -

- - { - setEditingSupplier(null); - setIsAddingNew(false); - }} - isCreating={creating} - isUpdating={updating} - /> -
-
- )} - - {/* Information - Only show when there are suppliers */} - {suppliers.length > 0 && ( - -

- 🚚 Gestión de Proveedores: -

-
    -
  • Editar información - Actualiza datos de contacto y términos comerciales
  • -
  • Gestionar estado - Activa o pausa proveedores según necesidades
  • -
  • Revisar rendimiento - Evalúa entregas a tiempo y calidad de productos
  • -
  • Filtrar vista - Usa los filtros para encontrar proveedores específicos
  • -
-
- )} - -
- ); -}; - -// Component for editing suppliers -interface SupplierFormProps { - supplier: SupplierFormData; - onSave: (supplier: SupplierFormData) => void; - onCancel: () => void; - isCreating: boolean; - isUpdating: boolean; -} - -const SupplierForm: React.FC = ({ - supplier, - onSave, - onCancel, - isCreating, - isUpdating -}) => { - const [formData, setFormData] = useState(supplier); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!formData.name.trim()) { - showToast({ - title: 'Error de validación', - message: 'El nombre de la empresa es requerido', - type: 'error' - }); - return; - } - if (!formData.address.trim()) { - showToast({ - title: 'Error de validación', - message: 'La dirección es requerida', - type: 'error' - }); - return; - } - - onSave(formData); - }; - - return ( -
-
-
- - setFormData(prev => ({ ...prev, name: e.target.value }))} - placeholder="Molinos del Sur" - disabled={isCreating || isUpdating} - /> -
- -
- - setFormData(prev => ({ ...prev, contact_name: e.target.value }))} - placeholder="Juan Pérez" - disabled={isCreating || isUpdating} - /> -
-
- -
-
- - setFormData(prev => ({ ...prev, phone: e.target.value }))} - placeholder="+1 555-0123" - disabled={isCreating || isUpdating} - /> -
- -
- - setFormData(prev => ({ ...prev, email: e.target.value }))} - placeholder="ventas@proveedor.com" - disabled={isCreating || isUpdating} - /> -
-
- -
- - setFormData(prev => ({ ...prev, address: e.target.value }))} - placeholder="Av. Industrial 123, Zona Sur" - disabled={isCreating || isUpdating} - /> -
- -
-
- - -
- -
- - setFormData(prev => ({ ...prev, delivery_terms: e.target.value }))} - placeholder="Recoger en tienda" - disabled={isCreating || isUpdating} - /> -
-
- -
- - setFormData(prev => ({ ...prev, tax_id: e.target.value }))} - placeholder="12345678-9" - disabled={isCreating || isUpdating} - /> -
- -
- setFormData(prev => ({ ...prev, is_active: e.target.checked }))} - disabled={isCreating || isUpdating} - className="rounded" - /> - -
- -
- - -
-
- ); -}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx new file mode 100644 index 00000000..3892e229 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx @@ -0,0 +1,626 @@ +import React, { useState, useRef } from 'react'; +import { Button } from '../../../ui/Button'; +import { Input } from '../../../ui/Input'; +import { useValidateFileOnly } from '../../../../api/hooks/dataImport'; +import { ImportValidationResponse } from '../../../../api/types/dataImport'; +import { useCurrentTenant } from '../../../../stores/tenant.store'; +import { useCreateIngredient } from '../../../../api/hooks/inventory'; +import { useImportFileOnly } from '../../../../api/hooks/dataImport'; +import { useClassifyProductsBatch } from '../../../../api/hooks/classification'; + +interface UploadSalesDataStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data?: any) => void; + isFirstStep: boolean; + isLastStep: boolean; +} + +interface ProgressState { + stage: string; + progress: number; + message: string; +} + +interface InventoryItem { + suggestion_id: string; + suggested_name: string; + category: string; + unit_of_measure: string; + selected: boolean; + stock_quantity: number; + expiration_days: number; + cost_per_unit: number; + confidence_score: number; + requires_refrigeration: boolean; + requires_freezing: boolean; + is_seasonal: boolean; + notes?: string; +} + +export const UploadSalesDataStep: React.FC = ({ + onPrevious, + onComplete, + isFirstStep +}) => { + const [selectedFile, setSelectedFile] = useState(null); + const [isValidating, setIsValidating] = useState(false); + const [validationResult, setValidationResult] = useState(null); + const [inventoryItems, setInventoryItems] = useState([]); + const [showInventoryStep, setShowInventoryStep] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [error, setError] = useState(''); + const [progressState, setProgressState] = useState(null); + const fileInputRef = useRef(null); + + const currentTenant = useCurrentTenant(); + const { validateFile } = useValidateFileOnly(); + const createIngredient = useCreateIngredient(); + const { importFile } = useImportFileOnly(); + const classifyProducts = useClassifyProductsBatch(); + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setSelectedFile(file); + setValidationResult(null); + setError(''); + } + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + if (file) { + setSelectedFile(file); + setValidationResult(null); + setError(''); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const handleValidateFile = async () => { + if (!selectedFile || !currentTenant?.id) return; + + setIsValidating(true); + setError(''); + setProgressState({ stage: 'preparing', progress: 0, message: 'Preparando validación del archivo...' }); + + try { + const result = await validateFile( + currentTenant.id, + selectedFile, + { + onProgress: (stage: string, progress: number, message: string) => { + setProgressState({ stage, progress, message }); + } + } + ); + + if (result.success && result.validationResult) { + setValidationResult(result.validationResult); + setProgressState(null); + } else { + setError(result.error || 'Error al validar el archivo'); + setProgressState(null); + } + } catch (error) { + setError('Error validando archivo: ' + (error instanceof Error ? error.message : 'Error desconocido')); + setProgressState(null); + } + + setIsValidating(false); + }; + + const handleContinue = () => { + if (validationResult) { + // Generate inventory suggestions based on validation + generateInventorySuggestions(); + } + }; + + const generateInventorySuggestions = async () => { + if (!currentTenant?.id || !validationResult) { + setError('No hay datos de validación disponibles para generar sugerencias'); + return; + } + + setProgressState({ stage: 'analyzing', progress: 25, message: 'Analizando productos de ventas...' }); + + try { + // Extract product data from validation result + const products = validationResult.product_summary?.map((product: any) => ({ + product_name: product.name, + sales_volume: product.total_quantity, + sales_data: { + total_quantity: product.total_quantity, + average_daily_sales: product.average_daily_sales, + frequency: product.frequency + } + })) || []; + + if (products.length === 0) { + setError('No se encontraron productos en los datos de ventas'); + setProgressState(null); + return; + } + + setProgressState({ stage: 'classifying', progress: 50, message: 'Clasificando productos con IA...' }); + + // Call the classification API + const suggestions = await classifyProducts.mutateAsync({ + tenantId: currentTenant.id, + batchData: { products } + }); + + setProgressState({ stage: 'preparing', progress: 75, message: 'Preparando sugerencias de inventario...' }); + + // Convert API response to InventoryItem format + const items: InventoryItem[] = suggestions.map(suggestion => { + // Calculate default stock quantity based on sales data + const defaultStock = Math.max( + Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7), // 1 week supply + 1 + ); + + // Estimate cost per unit based on category + const estimatedCost = suggestion.category === 'Dairy' ? 5.0 : + suggestion.category === 'Baking Ingredients' ? 2.0 : + 3.0; + + return { + suggestion_id: suggestion.suggestion_id, + suggested_name: suggestion.suggested_name, + category: suggestion.category, + unit_of_measure: suggestion.unit_of_measure, + selected: suggestion.confidence_score > 0.7, // Auto-select high confidence items + stock_quantity: defaultStock, + expiration_days: suggestion.estimated_shelf_life_days || 30, + cost_per_unit: estimatedCost, + confidence_score: suggestion.confidence_score, + requires_refrigeration: suggestion.requires_refrigeration, + requires_freezing: suggestion.requires_freezing, + is_seasonal: suggestion.is_seasonal, + notes: suggestion.notes + }; + }); + + setInventoryItems(items); + setShowInventoryStep(true); + setProgressState(null); + } catch (err) { + console.error('Error generating inventory suggestions:', err); + setError('Error al generar sugerencias de inventario. Por favor, inténtalo de nuevo.'); + setProgressState(null); + } + }; + + const handleToggleSelection = (id: string) => { + setInventoryItems(items => + items.map(item => + item.suggestion_id === id ? { ...item, selected: !item.selected } : item + ) + ); + }; + + const handleUpdateItem = (id: string, field: keyof InventoryItem, value: number) => { + setInventoryItems(items => + items.map(item => + item.suggestion_id === id ? { ...item, [field]: value } : item + ) + ); + }; + + const handleSelectAll = () => { + const allSelected = inventoryItems.every(item => item.selected); + setInventoryItems(items => + items.map(item => ({ ...item, selected: !allSelected })) + ); + }; + + const handleCreateInventory = async () => { + const selectedItems = inventoryItems.filter(item => item.selected); + + if (selectedItems.length === 0) { + setError('Por favor selecciona al menos un artículo de inventario para crear'); + return; + } + + if (!currentTenant?.id) { + setError('No se encontró información del tenant'); + return; + } + + setIsCreating(true); + setError(''); + + try { + const createdIngredients = []; + + for (const item of selectedItems) { + const ingredientData = { + name: item.suggested_name, + category: item.category, + unit_of_measure: item.unit_of_measure, + minimum_stock_level: Math.ceil(item.stock_quantity * 0.2), + maximum_stock_level: item.stock_quantity * 2, + reorder_point: Math.ceil(item.stock_quantity * 0.3), + shelf_life_days: item.expiration_days, + requires_refrigeration: item.requires_refrigeration, + requires_freezing: item.requires_freezing, + is_seasonal: item.is_seasonal, + cost_per_unit: item.cost_per_unit, + notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%` + }; + + const created = await createIngredient.mutateAsync({ + tenantId: currentTenant.id, + ingredientData + }); + + createdIngredients.push({ + ...created, + initialStock: item.stock_quantity + }); + } + + // After inventory creation, import the sales data + console.log('Importing sales data after inventory creation...'); + let salesImportResult = null; + try { + if (selectedFile) { + const result = await importFile( + currentTenant.id, + selectedFile, + { + onProgress: (stage, progress, message) => { + console.log(`Import progress: ${stage} - ${progress}% - ${message}`); + setProgressState({ + stage: 'importing', + progress, + message: `Importando datos de ventas: ${message}` + }); + } + } + ); + + salesImportResult = result; + if (result.success) { + console.log('Sales data imported successfully'); + } else { + console.warn('Sales import completed with issues:', result.error); + } + } + } catch (importError) { + console.error('Error importing sales data:', importError); + // Don't fail the entire process if import fails - the inventory has been created successfully + } + + setProgressState(null); + onComplete({ + createdIngredients, + totalItems: selectedItems.length, + validationResult, + file: selectedFile, + salesImportResult + }); + } catch (err) { + console.error('Error creating inventory items:', err); + setError('Error al crear artículos de inventario. Por favor, inténtalo de nuevo.'); + setIsCreating(false); + setProgressState(null); + } + }; + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const selectedCount = inventoryItems.filter(item => item.selected).length; + const allSelected = inventoryItems.length > 0 && inventoryItems.every(item => item.selected); + + if (showInventoryStep) { + return ( +
+
+

+ Basado en tus datos de ventas, hemos generado estas sugerencias de inventario. + Revisa y selecciona los artículos que te gustaría agregar a tu inventario. +

+
+ + {/* Summary */} +
+
+
+

+ {selectedCount} de {inventoryItems.length} artículos seleccionados +

+

+ Los artículos con alta confianza están preseleccionados +

+
+ +
+
+ + {/* Inventory Items */} +
+ {inventoryItems.map((item) => ( +
+
+
+ handleToggleSelection(item.id)} + className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-[var(--color-primary)]" + /> +
+ +
+
+

+ {item.name} +

+

+ {item.category} • Unidad: {item.unit_of_measure} +

+
+ + Confianza: {Math.round(item.confidence_score * 100)}% + + {item.requires_refrigeration && ( + + Requiere refrigeración + + )} +
+
+ + {item.selected && ( +
+ handleUpdateItem( + item.id, + 'stock_quantity', + Number(e.target.value) + )} + size="sm" + /> + handleUpdateItem( + item.id, + 'cost_per_unit', + Number(e.target.value) + )} + size="sm" + /> + handleUpdateItem( + item.id, + 'expiration_days', + Number(e.target.value) + )} + size="sm" + /> +
+ )} +
+
+
+ ))} +
+ + {error && ( +
+

{error}

+
+ )} + + {/* Actions */} +
+ + + +
+
+ ); + } + + return ( +
+
+

+ Sube tus datos de ventas (formato CSV o JSON) para generar sugerencias de inventario inteligentes. +

+
+ + {/* File Upload Area */} +
+ + + {selectedFile ? ( +
+
+ + + +

Archivo Seleccionado

+

{selectedFile.name}

+

+ {formatFileSize(selectedFile.size)} +

+
+ +
+ ) : ( +
+ + + +
+

Drop your sales data here

+

or click to browse files

+

+ Supported formats: CSV, JSON (max 100MB) +

+
+ +
+ )} +
+ + {/* Progress */} + {progressState && ( +
+
+ {progressState.message} + {progressState.progress}% +
+
+
+
+
+ )} + + {/* Validation Results */} + {validationResult && ( +
+

Validation Successful!

+
+

Total records: {validationResult.total_records}

+

Valid records: {validationResult.valid_records}

+ {validationResult.invalid_records > 0 && ( +

+ Invalid records: {validationResult.invalid_records} +

+ )} + {validationResult.warnings && validationResult.warnings.length > 0 && ( +
+

Warnings:

+
    + {validationResult.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} +
+
+ )} + + {/* Error */} + {error && ( +
+

{error}

+
+ )} + + {/* Actions */} +
+ + +
+ {selectedFile && !validationResult && ( + + )} + + {validationResult && ( + + )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/index.ts b/frontend/src/components/domain/onboarding/steps/index.ts new file mode 100644 index 00000000..2a4836c1 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/index.ts @@ -0,0 +1,4 @@ +export { RegisterTenantStep } from './RegisterTenantStep'; +export { UploadSalesDataStep } from './UploadSalesDataStep'; +export { MLTrainingStep } from './MLTrainingStep'; +export { CompletionStep } from './CompletionStep'; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/README.md b/frontend/src/hooks/business/onboarding/README.md deleted file mode 100644 index d25247b6..00000000 --- a/frontend/src/hooks/business/onboarding/README.md +++ /dev/null @@ -1,225 +0,0 @@ -# Onboarding Hooks - Complete Clean Architecture - -## Overview - -This onboarding system has been **completely refactored** with no legacy code or backwards compatibility. It follows modern clean architecture principles with: - -- **Standardized patterns** across all services -- **Zero circular dependencies** -- **Centralized state management** with Zustand -- **Type-safe interfaces** throughout -- **Service isolation** with standardized APIs -- **Factory pattern** for service hooks - -## Architecture - -### 🏗️ Core Layer (`core/`) - -**`store.ts`** - Centralized Zustand store -- Single source of truth for all onboarding state -- Atomic operations with devtools integration -- Computed getters for derived state - -**`actions.ts`** - Business logic orchestration -- Coordinates between services -- Handles complex step transitions -- Manages validation and error states - -**`types.ts`** - Complete type definitions -- Unified interfaces for all services -- Standardized error and state patterns -- Re-exports for external consumption - -### 🔧 Services Layer (`services/`) - -All services follow the same standardized pattern using `createServiceHook`: - -**`useTenantCreation.ts`** - Bakery registration and tenant setup -**`useSalesProcessing.ts`** - File validation and AI classification -**`useInventorySetup.ts`** - Inventory creation and sales import -**`useTrainingOrchestration.ts`** - ML model training workflow -**`useProgressTracking.ts`** - Backend progress synchronization -**`useResumeLogic.ts`** - Flow resumption management - -### 🛠️ Utils Layer (`utils/`) - -**`createServiceHook.ts`** - Factory for standardized service hooks -- Eliminates duplicate patterns -- Provides consistent async execution -- Standardized error handling - -## New File Structure - -``` -src/hooks/business/onboarding/ -├── core/ -│ ├── store.ts # Zustand centralized store -│ ├── actions.ts # Business logic orchestration -│ ├── types.ts # Complete type definitions -│ └── useAutoResume.ts # Auto-resume wrapper -├── services/ -│ ├── useTenantCreation.ts # Tenant service -│ ├── useSalesProcessing.ts # Sales processing service -│ ├── useInventorySetup.ts # Inventory service -│ ├── useTrainingOrchestration.ts # Training service -│ ├── useProgressTracking.ts # Progress service -│ └── useResumeLogic.ts # Resume service -├── utils/ -│ └── createServiceHook.ts # Service factory -├── config/ -│ └── steps.ts # Step definitions and validation -├── useOnboarding.ts # Main unified hook -└── index.ts # Clean exports -``` - -## Component Usage - -### Primary Interface - useOnboarding - -```typescript -import { useOnboarding } from '../hooks/business/onboarding'; - -const OnboardingComponent = () => { - const { - // Core state - currentStep, - steps, - data, - progress, - isLoading, - error, - - // Service states (when needed) - tenantCreation: { isLoading: tenantLoading, isSuccess }, - salesProcessing: { stage, progress: fileProgress, suggestions }, - inventorySetup: { createdItems, inventoryMapping }, - trainingOrchestration: { status, logs, metrics }, - - // Actions - nextStep, - previousStep, - updateStepData, - createTenant, - processSalesFile, - startTraining, - completeOnboarding, - clearError, - } = useOnboarding(); - - // Clean, consistent API across all functionality -}; -``` - -### Auto-Resume Functionality - -```typescript -import { useAutoResume } from '../hooks/business/onboarding'; - -const OnboardingPage = () => { - const { isCheckingResume, completionPercentage } = useAutoResume(); - - if (isCheckingResume) { - return ; - } - - // Continue with onboarding flow -}; -``` - -## Key Improvements - -### ✅ **Eliminated All Legacy Issues** - -1. **No Circular Dependencies** - Clear dependency hierarchy -2. **No Massive God Hooks** - Focused, single-responsibility services -3. **No Inconsistent Patterns** - Standardized service factory -4. **No Type Confusion** - Clean, unified type system -5. **No Duplicate Code** - DRY principles throughout - -### ✅ **Modern Patterns** - -1. **Zustand Store** - Performant, devtools-enabled state -2. **Factory Pattern** - Consistent service creation -3. **Service Composition** - Clean separation of concerns -4. **Type-Safe** - Full TypeScript coverage -5. **Async-First** - Proper error handling and loading states - -### ✅ **Developer Experience** - -1. **Predictable API** - Same patterns across all services -2. **Easy Testing** - Isolated, mockable services -3. **Clear Documentation** - Self-documenting code structure -4. **Performance** - Optimized renders and state updates -5. **Debugging** - Zustand devtools integration - -## Benefits for Components - -### Before (Legacy) -```typescript -// Inconsistent APIs, complex imports, circular deps -import { useOnboarding } from './useOnboarding'; -import { useAutoResume } from './useAutoResume'; // Circular dependency! - -const { - // 50+ properties mixed together - currentStep, data, isLoading, tenantCreation: { isLoading: tenantLoading }, - salesProcessing: { stage, progress }, // Nested complexity -} = useOnboarding(); -``` - -### After (Clean) -```typescript -// Clean, consistent, predictable -import { useOnboarding, useAutoResume } from '../hooks/business/onboarding'; - -const onboarding = useOnboarding(); -const autoResume = useAutoResume(); - -// Clear separation, no circular deps, standardized patterns -``` - -## Migration Path - -### ❌ Removed (No Backwards Compatibility) -- Old `useOnboarding` (400+ lines) -- Old `useOnboardingData` -- Old `useOnboardingFlow` -- Old `useAutoResume` -- All inconsistent service hooks - -### ✅ New Clean Interfaces -- Unified `useOnboarding` hook -- Standardized service hooks -- Centralized store management -- Factory-created services - -## Advanced Usage - -### Direct Service Access -```typescript -import { useSalesProcessing, useInventorySetup } from '../hooks/business/onboarding'; - -// When you need direct service control -const salesService = useSalesProcessing(); -const inventoryService = useInventorySetup(); -``` - -### Custom Service Creation -```typescript -import { createServiceHook } from '../hooks/business/onboarding'; - -// Create new services following the same pattern -const useMyCustomService = createServiceHook({ - initialState: { customData: null }, - // ... configuration -}); -``` - -## Performance - -- **Zustand** - Minimal re-renders, optimized updates -- **Memoized Selectors** - Computed values cached -- **Service Isolation** - Independent loading states -- **Factory Pattern** - Reduced bundle size - -This is a **complete rewrite** with zero legacy baggage. Modern, maintainable, and built for the future. \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/config/steps.ts b/frontend/src/hooks/business/onboarding/config/steps.ts deleted file mode 100644 index 986c7355..00000000 --- a/frontend/src/hooks/business/onboarding/config/steps.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Onboarding step definitions and validation logic - */ - -import type { OnboardingStep, OnboardingData } from '../core/types'; - -export const DEFAULT_STEPS: OnboardingStep[] = [ - { - id: 'setup', - title: '🏢 Setup', - description: 'Configuración básica de tu panadería y creación del tenant', - isRequired: true, - isCompleted: false, - validation: (data: OnboardingData) => { - if (!data.bakery?.name) return 'El nombre de la panadería es requerido'; - if (!data.bakery?.business_type) return 'El tipo 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'; - return null; - }, - }, - { - id: 'smart-inventory-setup', - title: '📦 Inventario Inteligente', - description: 'Sube datos de ventas, configura inventario y crea tu catálogo de productos', - isRequired: true, - isCompleted: false, - validation: (data: OnboardingData) => { - console.log('Smart Inventory Step Validation - Data:', { - hasSalesData: !!data.files?.salesData, - processingStage: data.processingStage, - isValid: data.processingResults?.is_valid, - reviewCompleted: data.reviewCompleted, - approvedProductsCount: data.approvedProducts?.length || 0, - inventoryConfigured: data.inventoryConfigured, - salesImportResult: data.salesImportResult - }); - - if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas'; - if (data.processingStage !== 'completed' && data.processingStage !== 'review') return 'El procesamiento debe completarse antes de continuar'; - if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar'; - if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados'; - const hasApprovedProducts = data.approvedProducts && data.approvedProducts.length > 0; - if (!hasApprovedProducts) return 'Debes aprobar al menos un producto para continuar'; - - // Check if ready for automatic inventory creation and sales import - // If inventory is already configured, check if sales data was imported - if (data.inventoryConfigured) { - const hasImportResults = data.salesImportResult && - (data.salesImportResult.records_created > 0 || - data.salesImportResult.success === true || - data.salesImportResult.imported === true); - - if (!hasImportResults) { - console.log('Smart Inventory Step Validation - Sales import validation failed:', { - hasSalesImportResult: !!data.salesImportResult, - salesImportResult: data.salesImportResult, - inventoryConfigured: data.inventoryConfigured - }); - - return 'Los datos de ventas históricos deben estar importados para continuar al entrenamiento de IA.'; - } - } else { - // If inventory is not configured yet, ensure all prerequisites are ready - // The actual creation will happen automatically on "Next Step" - console.log('Smart Inventory Step Validation - Ready for automatic inventory creation:', { - hasApprovedProducts: hasApprovedProducts, - reviewCompleted: data.reviewCompleted, - readyForCreation: hasApprovedProducts && data.reviewCompleted - }); - } - return null; - }, - }, - { - id: 'suppliers', - title: '🏪 Proveedores', - description: 'Configuración de proveedores y asociaciones', - isRequired: false, - isCompleted: false, - // Optional step - no strict validation required - }, - { - id: 'ml-training', - title: '🎯 Inteligencia', - description: 'Creación de tu asistente inteligente personalizado', - isRequired: true, - isCompleted: false, - validation: (data: OnboardingData) => { - console.log('ML Training Step Validation - Data:', { - inventoryConfigured: data.inventoryConfigured, - hasSalesFile: !!data.files?.salesData, - processingResults: data.processingResults, - salesImportResult: data.salesImportResult, - trainingStatus: data.trainingStatus - }); - - // CRITICAL PREREQUISITE 1: Inventory must be configured - if (!data.inventoryConfigured) { - return 'Debes configurar el inventario antes de entrenar el modelo de IA'; - } - - // CRITICAL PREREQUISITE 2: Sales file must be uploaded and processed - if (!data.files?.salesData) { - return 'Debes cargar un archivo de datos de ventas históricos para entrenar el modelo'; - } - - // CRITICAL PREREQUISITE 3: Sales data must be processed and valid - if (!data.processingResults?.is_valid) { - return 'Los datos de ventas deben ser procesados y validados antes del entrenamiento'; - } - - // CRITICAL PREREQUISITE 4: Sales data must be imported to backend - const hasSalesDataImported = data.salesImportResult && - (data.salesImportResult.records_created > 0 || - data.salesImportResult.success === true); - - if (!hasSalesDataImported && data.trainingStatus !== 'completed') { - return 'Los datos de ventas históricos deben estar importados en el sistema para iniciar el entrenamiento del modelo de IA'; - } - - // CRITICAL PREREQUISITE 5: Training must be completed to proceed - if (data.trainingStatus !== 'completed') { - return 'El entrenamiento del modelo de IA debe completarse antes de continuar'; - } - - return null; - }, - }, - { - id: 'completion', - title: '🎉 Listo', - description: 'Finalización y preparación para usar la plataforma', - isRequired: true, - isCompleted: false, - // Completion step - no additional validation needed - }, -]; - -/** - * Get step by ID - */ -export const getStepById = (stepId: string): OnboardingStep | undefined => { - return DEFAULT_STEPS.find(step => step.id === stepId); -}; - -/** - * Get step index by ID - */ -export const getStepIndex = (stepId: string): number => { - return DEFAULT_STEPS.findIndex(step => step.id === stepId); -}; - -/** - * Check if all required steps before given step are completed - */ -export const canAccessStep = (stepIndex: number, completedSteps: boolean[]): boolean => { - for (let i = 0; i < stepIndex; i++) { - if (DEFAULT_STEPS[i].isRequired && !completedSteps[i]) { - return false; - } - } - return true; -}; - -/** - * Calculate onboarding progress - */ -export const calculateProgress = (completedSteps: boolean[]): { - completedCount: number; - totalRequired: number; - percentage: number; -} => { - const requiredSteps = DEFAULT_STEPS.filter(step => step.isRequired); - const completedRequired = requiredSteps.filter((step) => { - const stepIndex = getStepIndex(step.id); - return completedSteps[stepIndex]; - }); - - return { - completedCount: completedRequired.length, - totalRequired: requiredSteps.length, - percentage: Math.round((completedRequired.length / requiredSteps.length) * 100), - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/core/actions.ts b/frontend/src/hooks/business/onboarding/core/actions.ts deleted file mode 100644 index 9653cb76..00000000 --- a/frontend/src/hooks/business/onboarding/core/actions.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Core onboarding actions - Business logic orchestration - */ - -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useOnboardingStore } from './store'; -import { useTenantCreation } from '../services/useTenantCreation'; -import { useSalesProcessing } from '../services/useSalesProcessing'; -import { useInventorySetup } from '../services/useInventorySetup'; -import { useTrainingOrchestration } from '../services/useTrainingOrchestration'; -import { useProgressTracking } from '../services/useProgressTracking'; -import { getStepById } from '../config/steps'; -import type { ProductSuggestionResponse } from '../core/types'; -import type { BakeryRegistration } from '../../../../api'; - -export const useOnboardingActions = () => { - const navigate = useNavigate(); - const store = useOnboardingStore(); - - // Service hooks - const tenantCreation = useTenantCreation(); - const salesProcessing = useSalesProcessing(); - const inventorySetup = useInventorySetup(); - const trainingOrchestration = useTrainingOrchestration(); - const progressTracking = useProgressTracking(); - - const validateCurrentStep = useCallback((): string | null => { - const currentStep = store.getCurrentStep(); - if (!currentStep) return null; - - const stepConfig = getStepById(currentStep.id); - if (stepConfig?.validation) { - return stepConfig.validation(store.data); - } - return null; - }, [store]); - - const nextStep = useCallback(async (): Promise => { - try { - const currentStep = store.getCurrentStep(); - - if (!currentStep) { - return false; - } - - // Validate current step - const validation = validateCurrentStep(); - - if (validation) { - store.setError(validation); - return false; - } - - store.setError(null); - - // Handle step-specific actions before moving to next step - if (currentStep.id === 'setup') { - // IMPORTANT: Ensure user_registered step is completed first - const userRegisteredCompleted = await progressTracking.markStepCompleted('user_registered', {}); - if (!userRegisteredCompleted) { - console.error('❌ Failed to mark user_registered as completed'); - store.setError('Failed to verify user registration status'); - return false; - } - - const stepData = store.getStepData('setup'); - const bakeryData = stepData.bakery; - - // Check if tenant creation is needed - const needsTenantCreation = bakeryData && !bakeryData.tenantCreated && !bakeryData.tenant_id; - - if (needsTenantCreation) { - store.setLoading(true); - const success = await tenantCreation.createTenant(bakeryData); - store.setLoading(false); - - if (!success) { - store.setError(tenantCreation.error || 'Error creating tenant'); - return false; - } - - console.log('✅ Tenant created successfully'); - - // Wait a moment for backend to update state - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } - - if (currentStep.id === 'smart-inventory-setup') { - const inventoryData = store.getStepData('smart-inventory-setup'); - - if (inventoryData?.approvedProducts?.length > 0 && !inventoryData?.inventoryConfigured) { - store.setLoading(true); - - // Create inventory from approved products - const inventoryResult = await inventorySetup.createInventoryFromSuggestions(inventoryData.approvedProducts); - - if (!inventoryResult.success) { - store.setLoading(false); - store.setError(inventorySetup.error || 'Error creating inventory'); - return false; - } - - // Import sales data after inventory creation - if (inventoryData?.processingResults && inventoryResult?.inventoryMapping) { - const salesImportResult = await inventorySetup.importSalesData( - inventoryData.processingResults, - inventoryResult.inventoryMapping - ); - - if (!salesImportResult.success) { - store.setLoading(false); - store.setError(inventorySetup.error || 'Error importing sales data'); - return false; - } - } - - store.setLoading(false); - } - } - - // Save progress to backend - const stepData = store.getStepData(currentStep.id); - - const markCompleted = await progressTracking.markStepCompleted(currentStep.id, stepData); - if (!markCompleted) { - console.error(`❌ Failed to mark step "${currentStep.id}" as completed`); - store.setError(`Failed to save progress for step "${currentStep.id}"`); - return false; - } - - // Move to next step - if (store.nextStep()) { - store.markStepCompleted(store.currentStep - 1); - return true; - } - - return false; - } catch (error) { - console.error('Error in nextStep:', error); - store.setError(error instanceof Error ? error.message : 'Error moving to next step'); - store.setLoading(false); - return false; - } - }, [store, validateCurrentStep, tenantCreation, inventorySetup, progressTracking]); - - const previousStep = useCallback((): boolean => { - return store.previousStep(); - }, [store]); - - const goToStep = useCallback((stepIndex: number): boolean => { - return store.goToStep(stepIndex); - }, [store]); - - const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise => { - store.setLoading(true); - const success = await tenantCreation.createTenant(bakeryData); - store.setLoading(false); - - if (!success) { - store.setError(tenantCreation.error || 'Error creating tenant'); - } - - return success; - }, [store, tenantCreation]); - - const processSalesFile = useCallback(async (file: File): Promise => { - console.log('🎬 Actions - processSalesFile started'); - store.setLoading(true); - - const result = await salesProcessing.processFile(file); - console.log('🎬 Actions - processFile result:', result); - - store.setLoading(false); - - if (!result.success) { - console.error('❌ Actions - Processing failed:', salesProcessing.error); - store.setError(salesProcessing.error || 'Error processing sales file'); - } else { - console.log('✅ Actions - Processing succeeded'); - } - - return result.success; - }, [store, salesProcessing]); - - const generateProductSuggestions = useCallback(async (productList: string[]): Promise => { - 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 => { - store.setLoading(true); - const result = await inventorySetup.createInventoryFromSuggestions(suggestions); - store.setLoading(false); - - if (!result.success) { - store.setError(inventorySetup.error || 'Error creating inventory'); - } - - return result.success; - }, [store, inventorySetup]); - - const importSalesData = useCallback(async ( - salesData: any, - inventoryMapping: { [productName: string]: string } - ): Promise => { - store.setLoading(true); - const result = await inventorySetup.importSalesData(salesData, inventoryMapping); - store.setLoading(false); - - if (!result.success) { - store.setError(inventorySetup.error || 'Error importing sales data'); - } - - return result.success; - }, [store, inventorySetup]); - - const startTraining = useCallback(async (options?: { - products?: string[]; - startDate?: string; - endDate?: string; - }): Promise => { - // Validate training prerequisites - const validation = await trainingOrchestration.validateTrainingData(store.getAllStepData()); - if (!validation.isValid) { - store.setError(`Training prerequisites not met: ${validation.missingItems.join(', ')}`); - return false; - } - - store.setLoading(true); - const success = await trainingOrchestration.startTraining(options); - store.setLoading(false); - - if (!success) { - store.setError(trainingOrchestration.error || 'Error starting training'); - } - - return success; - }, [store, trainingOrchestration]); - - const completeOnboarding = useCallback(async (): Promise => { - try { - store.setLoading(true); - - // Mark final completion - const completionStats = { - totalProducts: store.data.processingResults?.unique_products || 0, - inventoryItems: store.data.inventoryItems?.length || 0, - suppliersConfigured: store.data.suppliers?.length || 0, - mlModelAccuracy: store.data.trainingMetrics?.accuracy || 0, - estimatedTimeSaved: '2-3 horas por día', - completionScore: 95, - }; - - store.setStepData('completion', { completionStats }); - - // Complete in backend - const success = await progressTracking.completeOnboarding(); - - store.setLoading(false); - - if (success) { - console.log('✅ Onboarding completed'); - store.markStepCompleted(store.steps.length - 1); - - // Navigate to dashboard after completion - setTimeout(() => { - navigate('/app/dashboard'); - }, 2000); - - return true; - } else { - store.setError('Error completing onboarding'); - return false; - } - } catch (error) { - console.error('Error completing onboarding:', error); - store.setError(error instanceof Error ? error.message : 'Error completing onboarding'); - store.setLoading(false); - return false; - } - }, [store, progressTracking, navigate]); - - const clearError = useCallback(() => { - store.setError(null); - tenantCreation.clearError(); - salesProcessing.clearError(); - inventorySetup.clearError(); - trainingOrchestration.clearError(); - progressTracking.clearError(); - }, [store, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration, progressTracking]); - - const reset = useCallback(() => { - store.reset(); - tenantCreation.reset(); - salesProcessing.reset(); - inventorySetup.reset(); - trainingOrchestration.reset(); - }, [store, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration]); - - return { - // Navigation actions - nextStep, - previousStep, - goToStep, - validateCurrentStep, - - // Step-specific actions - createTenant, - processSalesFile, - generateProductSuggestions, // New function for separated suggestion generation - createInventoryFromSuggestions, - importSalesData, - startTraining, - completeOnboarding, - - // Utility actions - clearError, - reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/core/store.ts b/frontend/src/hooks/business/onboarding/core/store.ts deleted file mode 100644 index d7bc8d9b..00000000 --- a/frontend/src/hooks/business/onboarding/core/store.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Onboarding store - Centralized state management with Zustand - * Handles all onboarding data, steps, and global state - */ - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import type { OnboardingData, OnboardingStep, OnboardingProgress } from './types'; -import { DEFAULT_STEPS } from '../config/steps'; - -interface OnboardingStore { - // Flow state - currentStep: number; - steps: OnboardingStep[]; - - // Data state - data: OnboardingData; - - // UI state - isLoading: boolean; - error: string | null; - - // Progress state - progress: OnboardingProgress | null; - - // Actions - Flow management - setCurrentStep: (step: number) => void; - nextStep: () => boolean; - previousStep: () => boolean; - goToStep: (stepIndex: number) => boolean; - markStepCompleted: (stepIndex: number) => void; - - // Actions - Data management - setStepData: (stepId: string, stepData: Partial) => void; - clearStepData: (stepId: string) => void; - - // Actions - UI state - setLoading: (loading: boolean) => void; - setError: (error: string | null) => void; - - // Actions - Progress - setProgress: (progress: OnboardingProgress) => void; - - // Actions - Utilities - reset: () => void; - - // Getters - getCurrentStep: () => OnboardingStep; - getStepData: (stepId: string) => any; - getAllStepData: () => { [stepId: string]: any }; - getProgress: () => OnboardingProgress; - canNavigateToStep: (stepIndex: number) => boolean; -} - -const initialState = { - currentStep: 0, - steps: DEFAULT_STEPS.map(step => ({ ...step, isCompleted: false })), - data: { allStepData: {} }, - isLoading: false, - error: null, - progress: null, -}; - -// Debug logging for store initialization (only if there's an issue) -if (initialState.steps.length !== DEFAULT_STEPS.length) { - console.error('⚠️ Store initialization issue: steps count mismatch'); -} - -export const useOnboardingStore = create()( - devtools( - (set, get) => ({ - ...initialState, - - // Flow management actions - setCurrentStep: (step: number) => { - set({ currentStep: step }, false, 'setCurrentStep'); - }, - - nextStep: () => { - const { currentStep, steps } = get(); - if (currentStep < steps.length - 1) { - set({ currentStep: currentStep + 1 }, false, 'nextStep'); - return true; - } - return false; - }, - - previousStep: () => { - const { currentStep } = get(); - if (currentStep > 0) { - set({ currentStep: currentStep - 1 }, false, 'previousStep'); - return true; - } - return false; - }, - - goToStep: (stepIndex: number) => { - const { steps, canNavigateToStep } = get(); - if (stepIndex >= 0 && stepIndex < steps.length && canNavigateToStep(stepIndex)) { - set({ currentStep: stepIndex }, false, 'goToStep'); - return true; - } - return false; - }, - - markStepCompleted: (stepIndex: number) => { - set((state) => ({ - steps: state.steps.map((step, index) => - index === stepIndex ? { ...step, isCompleted: true } : step - ), - }), false, 'markStepCompleted'); - }, - - // Data management actions - setStepData: (stepId: string, stepData: Partial) => { - set((state) => ({ - data: { - ...state.data, - ...stepData, - allStepData: { - ...state.data.allStepData, - [stepId]: { - ...state.data.allStepData?.[stepId], - ...stepData, - }, - }, - }, - }), false, 'setStepData'); - }, - - clearStepData: (stepId: string) => { - set((state) => ({ - data: { - ...state.data, - allStepData: { - ...state.data.allStepData, - [stepId]: undefined, - }, - }, - }), false, 'clearStepData'); - }, - - // UI state actions - setLoading: (loading: boolean) => { - set({ isLoading: loading }, false, 'setLoading'); - }, - - setError: (error: string | null) => { - set({ error, isLoading: false }, false, 'setError'); - }, - - // Progress actions - setProgress: (progress: OnboardingProgress) => { - set({ progress }, false, 'setProgress'); - }, - - // Utility actions - reset: () => { - set(initialState, false, 'reset'); - }, - - // Getters - getCurrentStep: () => { - const { steps, currentStep } = get(); - return steps[currentStep]; - }, - - getStepData: (stepId: string) => { - const { data } = get(); - return data.allStepData?.[stepId] || {}; - }, - - getAllStepData: () => { - const { data } = get(); - return data.allStepData || {}; - }, - - getProgress: () => { - const { steps, currentStep } = get(); - const completedSteps = steps.filter(step => step.isCompleted).length; - const requiredSteps = steps.filter(step => step.isRequired).length; - - return { - currentStep, - totalSteps: steps.length, - completedSteps, - isComplete: completedSteps === requiredSteps, - progressPercentage: Math.round((completedSteps / requiredSteps) * 100), - }; - }, - - canNavigateToStep: (stepIndex: number) => { - const { steps } = get(); - // Allow navigation to any step for now - can add more complex logic later - return stepIndex >= 0 && stepIndex < steps.length; - }, - }), - { name: 'onboarding-store' } - ) -); \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/core/types.ts b/frontend/src/hooks/business/onboarding/core/types.ts deleted file mode 100644 index 84f118c5..00000000 --- a/frontend/src/hooks/business/onboarding/core/types.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Core types for the entire onboarding system - */ - -import type { - BakeryRegistration, - TrainingJobResponse, - UserProgress, -} from '../../../../api'; - -// Re-export TrainingMetrics locally -export interface TrainingMetrics { - accuracy: number; - mape: number; - mae: number; - rmse: number; - r2_score: number; -} - -// Re-export ProductSuggestionResponse type locally since it's used across many files -export interface ProductSuggestionResponse { - suggestion_id: string; - original_name: string; - suggested_name: string; - product_type: string; - category: string; - unit_of_measure: string; - confidence_score: number; - estimated_shelf_life_days?: number; - requires_refrigeration: boolean; - requires_freezing: boolean; - is_seasonal: boolean; - suggested_supplier?: string; - notes?: string; - sales_data?: { - total_quantity: number; - average_daily_sales: number; - peak_day: string; - frequency: number; - }; -} - -// Base service state pattern -export interface ServiceState { - data: T | null; - isLoading: boolean; - error: string | null; - isSuccess: boolean; -} - -// Base service actions pattern -export interface ServiceActions { - clearError: () => void; - reset: () => void; -} - -// Onboarding step definitions -export interface OnboardingStep { - id: string; - title: string; - description: string; - isRequired: boolean; - isCompleted: boolean; - validation?: (data: OnboardingData) => string | null; -} - -// Complete onboarding data structure -export interface OnboardingData { - // Step 1: Setup - bakery?: BakeryRegistration; - - // Step 2: Smart Inventory Setup - files?: { - salesData?: File; - }; - processingStage?: 'upload' | 'validating' | 'validated' | 'analyzing' | 'review' | 'completed' | 'error'; - processingResults?: { - is_valid: boolean; - total_records: number; - unique_products: number; - product_list: string[]; - validation_errors: string[]; - validation_warnings: string[]; - summary: { - date_range: string; - total_sales: number; - average_daily_sales: number; - }; - }; - suggestions?: ProductSuggestionResponse[]; - detectedProducts?: any[]; - approvedSuggestions?: ProductSuggestionResponse[]; - approvedProducts?: ProductSuggestionResponse[]; - reviewCompleted?: boolean; - inventoryItems?: any[]; - inventoryMapping?: { [productName: string]: string }; - inventoryConfigured?: boolean; - salesImportResult?: { - success: boolean; - imported: boolean; - records_created: number; - message: string; - }; - - // Step 3: Suppliers - suppliers?: any[]; - supplierMappings?: any[]; - - // Step 4: ML Training - trainingStatus?: 'idle' | 'validating' | 'training' | 'completed' | 'failed'; - trainingProgress?: number; - trainingJob?: TrainingJobResponse; - trainingLogs?: TrainingLog[]; - trainingMetrics?: TrainingMetrics; - autoStartTraining?: boolean; - - // Step 5: Completion - completionStats?: { - totalProducts: number; - inventoryItems: number; - suppliersConfigured: number; - mlModelAccuracy: number; - estimatedTimeSaved: string; - completionScore: number; - }; - - // Cross-step data sharing - allStepData?: { [stepId: string]: any }; -} - -export interface OnboardingProgress { - currentStep: number; - totalSteps: number; - completedSteps: number; - isComplete: boolean; - progressPercentage: number; -} - -export interface OnboardingError { - step?: string; - message: string; - details?: any; -} - -// Training types -export interface TrainingLog { - timestamp: string; - message: string; - level: 'info' | 'warning' | 'error' | 'success'; -} - -// Progress callback -export type ProgressCallback = (progress: number, stage: string, message: string) => void; - -// Step validation function -export type StepValidator = (data: OnboardingData) => string | null; - -// Service-specific state types -export interface TenantCreationState extends ServiceState { - tenantData: BakeryRegistration | null; -} - -export interface SalesProcessingState extends ServiceState { - stage: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error'; - progress: number; - currentMessage: string; - validationResults: any | null; - suggestions: ProductSuggestionResponse[] | null; -} - -export interface InventorySetupState extends ServiceState { - createdItems: any[]; - inventoryMapping: { [productName: string]: string }; - salesImportResult: { - success: boolean; - imported: boolean; - records_created: number; - message: string; - } | null; - isInventoryConfigured: boolean; -} - -export interface TrainingOrchestrationState extends ServiceState { - status: 'idle' | 'validating' | 'training' | 'completed' | 'failed'; - progress: number; - currentStep: string; - estimatedTimeRemaining: number; - job: TrainingJobResponse | null; - logs: TrainingLog[]; - metrics: TrainingMetrics | null; -} - -export interface ProgressTrackingState extends ServiceState { - progress: UserProgress | null; - isInitialized: boolean; - isCompleted: boolean; - completionPercentage: number; - currentBackendStep: string | null; -} - -export interface ResumeState extends ServiceState { - isCheckingResume: boolean; - resumePoint: { stepId: string; stepIndex: number } | null; - shouldResume: boolean; -} \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/core/useAutoResume.ts b/frontend/src/hooks/business/onboarding/core/useAutoResume.ts deleted file mode 100644 index eba1ca6f..00000000 --- a/frontend/src/hooks/business/onboarding/core/useAutoResume.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Auto-resume hook - Simple wrapper around resume logic service - */ - -import { useResumeLogic } from '../services/useResumeLogic'; - -export const useAutoResume = () => { - const resumeLogic = useResumeLogic(); - - return { - // State - isCheckingResume: resumeLogic.isCheckingResume, - isCompleted: resumeLogic.isCompleted, - completionPercentage: resumeLogic.completionPercentage, - - // Actions - checkForSavedProgress: resumeLogic.checkForResume, - resumeFromSavedProgress: resumeLogic.resumeFlow, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/index.ts b/frontend/src/hooks/business/onboarding/index.ts deleted file mode 100644 index 8f804eb2..00000000 --- a/frontend/src/hooks/business/onboarding/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Onboarding hooks index - Complete clean architecture - * No legacy code, no backwards compatibility, clean modern patterns - */ - -// Types (re-export core types for external usage) -export type * from './core/types'; - -// Steps configuration -export { DEFAULT_STEPS, getStepById, getStepIndex, canAccessStep, calculateProgress } from './config/steps'; - -// Core architecture (for advanced usage) -export { useOnboardingStore } from './core/store'; -export { useOnboardingActions } from './core/actions'; - -// Service hooks (for direct service access when needed) -export { useTenantCreation } from './services/useTenantCreation'; -export { useSalesProcessing } from './services/useSalesProcessing'; -export { useInventorySetup } from './services/useInventorySetup'; -export { useTrainingOrchestration } from './services/useTrainingOrchestration'; -export { useProgressTracking } from './services/useProgressTracking'; -export { useResumeLogic } from './services/useResumeLogic'; - -// Main hooks - PRIMARY INTERFACE for components -export { useOnboarding } from './useOnboarding'; -export { useAutoResume } from './core/useAutoResume'; - -// Utility -export { createServiceHook } from './utils/createServiceHook'; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useInventorySetup.ts b/frontend/src/hooks/business/onboarding/services/useInventorySetup.ts deleted file mode 100644 index 2497a8fc..00000000 --- a/frontend/src/hooks/business/onboarding/services/useInventorySetup.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Inventory setup service - Simplified implementation - */ - -import { useCallback, useState } from 'react'; -import { - useCreateIngredient, - useCreateSalesRecord, -} from '../../../../api'; -import { useCurrentTenant } from '../../../../stores'; -import { useOnboardingStore } from '../core/store'; -import type { ProductSuggestionResponse } from '../core/types'; - -export const useInventorySetup = () => { - const createIngredientMutation = useCreateIngredient(); - const createSalesRecordMutation = useCreateSalesRecord(); - const currentTenant = useCurrentTenant(); - const { setStepData } = useOnboardingStore(); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [createdItems, setCreatedItems] = useState([]); - const [inventoryMapping, setInventoryMapping] = useState<{ [productName: string]: string }>({}); - const [salesImportResult, setSalesImportResult] = useState(null); - const [isInventoryConfigured, setIsInventoryConfigured] = useState(false); - - const createInventoryFromSuggestions = useCallback(async ( - suggestions: ProductSuggestionResponse[] - ): Promise<{ - success: boolean; - createdItems?: any[]; - inventoryMapping?: { [productName: string]: string }; - }> => { - console.log('🔄 Creating inventory from suggestions:', suggestions?.length, 'items'); - - if (!suggestions || suggestions.length === 0) { - console.error('❌ No suggestions provided'); - setError('No hay sugerencias para crear el inventario'); - return { success: false }; - } - - if (!currentTenant?.id) { - console.error('❌ No tenant ID available'); - setError('No se pudo obtener información del tenant'); - return { success: false }; - } - - setIsLoading(true); - setError(null); - - try { - const newCreatedItems = []; - const newInventoryMapping: { [key: string]: string } = {}; - - // Create ingredients from approved suggestions - for (const suggestion of suggestions) { - try { - const ingredientData = { - name: suggestion.suggested_name || suggestion.original_name, - category: suggestion.category || 'Sin categoría', - description: suggestion.notes || '', - unit_of_measure: suggestion.unit_of_measure || 'units', - minimum_stock_level: 10, - maximum_stock_level: 100, - low_stock_threshold: 10, - reorder_point: 15, - reorder_quantity: 50, - shelf_life_days: suggestion.estimated_shelf_life_days || 30, - requires_refrigeration: suggestion.requires_refrigeration || false, - requires_freezing: suggestion.requires_freezing || false, - is_seasonal: suggestion.is_seasonal || false, - cost_per_unit: 0, - notes: suggestion.notes || `Producto clasificado automáticamente desde: ${suggestion.original_name}`, - }; - - const createdItem = await createIngredientMutation.mutateAsync({ - tenantId: currentTenant.id, - ingredientData, - }); - - newCreatedItems.push(createdItem); - // Map both original and suggested names to the same ingredient ID for flexibility - newInventoryMapping[suggestion.original_name] = createdItem.id; - if (suggestion.suggested_name && suggestion.suggested_name !== suggestion.original_name) { - newInventoryMapping[suggestion.suggested_name] = createdItem.id; - } - } catch (error) { - console.error(`❌ Error creating ingredient ${suggestion.suggested_name || suggestion.original_name}:`, error); - // Continue with other ingredients even if one fails - } - } - - if (newCreatedItems.length === 0) { - throw new Error('No se pudo crear ningún elemento del inventario'); - } - - console.log('✅ Created', newCreatedItems.length, '/', suggestions.length, 'ingredients'); - - // Update state - setCreatedItems(newCreatedItems); - setInventoryMapping(newInventoryMapping); - setIsInventoryConfigured(true); - - // Update onboarding store - setStepData('smart-inventory-setup', { - inventoryItems: newCreatedItems, - inventoryMapping: newInventoryMapping, - inventoryConfigured: true, - }); - - return { - success: true, - createdItems: newCreatedItems, - inventoryMapping: newInventoryMapping, - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error creating inventory'; - setError(errorMessage); - return { success: false }; - } finally { - setIsLoading(false); - } - }, [createIngredientMutation, currentTenant, setStepData]); - - const importSalesData = useCallback(async ( - salesData: any, - inventoryMapping: { [productName: string]: string } - ): Promise<{ - success: boolean; - recordsCreated: number; - message: string; - }> => { - if (!currentTenant?.id) { - setError('No se pudo obtener información del tenant'); - return { - success: false, - recordsCreated: 0, - message: 'Error: No se pudo obtener información del tenant', - }; - } - - if (!salesData || !salesData.product_list) { - setError('No hay datos de ventas para importar'); - return { - success: false, - recordsCreated: 0, - message: 'Error: No hay datos de ventas para importar', - }; - } - - setIsLoading(true); - setError(null); - - try { - let recordsCreated = 0; - - // Process actual sales data and create sales records - if (salesData.raw_data && Array.isArray(salesData.raw_data)) { - for (const salesRecord of salesData.raw_data) { - try { - // Map product name to inventory product ID - const inventoryProductId = inventoryMapping[salesRecord.product_name]; - - if (inventoryProductId) { - const salesRecordData = { - date: salesRecord.date, - product_name: salesRecord.product_name, - inventory_product_id: inventoryProductId, - quantity_sold: salesRecord.quantity, - unit_price: salesRecord.unit_price, - total_revenue: salesRecord.total_amount || (salesRecord.quantity * salesRecord.unit_price), - channel: salesRecord.channel || 'tienda', - customer_info: salesRecord.customer_info || {}, - notes: salesRecord.notes || '', - }; - - await createSalesRecordMutation.mutateAsync({ - tenantId: currentTenant.id, - salesData: salesRecordData, - }); - - recordsCreated++; - } - } catch (error) { - console.error('Error creating sales record:', error); - // Continue with next record - } - } - } - - const importResult = { - success: recordsCreated > 0, - imported: recordsCreated > 0, - records_created: recordsCreated, - message: recordsCreated > 0 - ? `Se importaron ${recordsCreated} registros de ventas exitosamente` - : 'No se pudieron importar registros de ventas', - }; - - // Update state - setSalesImportResult(importResult); - - // Update onboarding store - setStepData('smart-inventory-setup', { - salesImportResult: importResult, - }); - - return { - success: recordsCreated > 0, - recordsCreated, - message: importResult.message, - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error importando datos de ventas'; - setError(errorMessage); - return { - success: false, - recordsCreated: 0, - message: errorMessage, - }; - } finally { - setIsLoading(false); - } - }, [createSalesRecordMutation, currentTenant, setStepData]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - const reset = useCallback(() => { - setIsLoading(false); - setError(null); - setCreatedItems([]); - setInventoryMapping({}); - setSalesImportResult(null); - setIsInventoryConfigured(false); - }, []); - - return { - // State - isLoading, - error, - createdItems, - inventoryMapping, - salesImportResult, - isInventoryConfigured, - - // Actions - createInventoryFromSuggestions, - importSalesData, - clearError, - reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useProgressTracking.ts b/frontend/src/hooks/business/onboarding/services/useProgressTracking.ts deleted file mode 100644 index 9a4cf9f1..00000000 --- a/frontend/src/hooks/business/onboarding/services/useProgressTracking.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Progress tracking service - Simplified implementation - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { onboardingService } from '../../../../api/services/onboarding'; -import type { UserProgress } from '../../../../api/types/onboarding'; - -export const useProgressTracking = () => { - const initializationAttempted = useRef(false); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [progress, setProgress] = useState(null); - const [isInitialized, setIsInitialized] = useState(false); - const [isCompleted, setIsCompleted] = useState(false); - const [completionPercentage, setCompletionPercentage] = useState(0); - const [currentBackendStep, setCurrentBackendStep] = useState(null); - - // Load initial progress from backend - const loadProgress = useCallback(async (): Promise => { - setIsLoading(true); - setError(null); - - try { - const progressData = await onboardingService.getUserProgress(''); - - // Update state - setProgress(progressData); - setIsInitialized(true); - setIsCompleted(progressData?.fully_completed || false); - setCompletionPercentage(progressData?.completion_percentage || 0); - setCurrentBackendStep(progressData?.current_step || null); - - console.log('📊 Progress loaded:', progressData?.current_step, '(', progressData?.completion_percentage, '%)'); - - return progressData; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error loading progress'; - setError(errorMessage); - return null; - } finally { - setIsLoading(false); - } - }, []); - - // Mark a step as completed and save to backend - const markStepCompleted = useCallback(async ( - stepId: string, - data?: Record - ): Promise => { - console.log(`🔄 Attempting to mark step "${stepId}" as completed with data:`, data); - - setIsLoading(true); - setError(null); - - try { - const updatedProgress = await onboardingService.markStepAsCompleted(stepId, data); - - // Update state - setProgress(updatedProgress); - setIsCompleted(updatedProgress?.fully_completed || false); - setCompletionPercentage(updatedProgress?.completion_percentage || 0); - setCurrentBackendStep(updatedProgress?.current_step || null); - - console.log(`✅ Step "${stepId}" marked as completed in backend`); - return true; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : `Error marking step "${stepId}" as completed`; - setError(errorMessage); - console.error(`❌ Error marking step "${stepId}" as completed:`, error); - console.error(`❌ Attempted to send data:`, data); - return false; - } finally { - setIsLoading(false); - } - }, []); - - // Get the next step the user should work on - const getNextStep = useCallback(async (): Promise => { - try { - return await onboardingService.getNextStepId(); - } catch (error) { - console.error('Error getting next step:', error); - return 'setup'; - } - }, []); - - // Get the step and index where user should resume - const getResumePoint = useCallback(async (): Promise<{ stepId: string; stepIndex: number }> => { - try { - return await onboardingService.getResumeStep(); - } catch (error) { - console.error('Error getting resume point:', error); - return { stepId: 'setup', stepIndex: 0 }; - } - }, []); - - // Complete the entire onboarding process - const completeOnboarding = useCallback(async (): Promise => { - setIsLoading(true); - setError(null); - - try { - const result = await onboardingService.completeOnboarding(); - - if (result.success) { - // Reload progress to get updated status - await loadProgress(); - console.log('🎉 Onboarding completed successfully!'); - return true; - } - - throw new Error('Failed to complete onboarding'); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding'; - setError(errorMessage); - return false; - } finally { - setIsLoading(false); - } - }, [loadProgress]); - - // Check if user can access a specific step - const canAccessStep = useCallback(async (stepId: string): Promise => { - try { - const result = await onboardingService.canAccessStep(stepId); - return result.can_access; - } catch (error) { - console.error(`Error checking access for step "${stepId}":`, error); - return true; // Allow access on error - } - }, []); - - // Auto-load progress on hook initialization - PREVENT multiple attempts - useEffect(() => { - if (!isInitialized && !initializationAttempted.current && !isLoading) { - initializationAttempted.current = true; - loadProgress(); - } - }, [isInitialized, isLoading, loadProgress]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - return { - // State - isLoading, - error, - progress, - isInitialized, - isCompleted, - completionPercentage, - currentBackendStep, - - // Actions - loadProgress, - markStepCompleted, - getNextStep, - getResumePoint, - completeOnboarding, - canAccessStep, - clearError, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useResumeLogic.ts b/frontend/src/hooks/business/onboarding/services/useResumeLogic.ts deleted file mode 100644 index 634d36b6..00000000 --- a/frontend/src/hooks/business/onboarding/services/useResumeLogic.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Resume logic service - Simplified implementation - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useOnboardingStore } from '../core/store'; -import { useProgressTracking } from './useProgressTracking'; - -export const useResumeLogic = () => { - const navigate = useNavigate(); - const progressTracking = useProgressTracking(); - const { setCurrentStep } = useOnboardingStore(); - const resumeAttempted = useRef(false); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [isCheckingResume, setIsCheckingResume] = useState(false); - const [resumePoint, setResumePoint] = useState<{ stepId: string; stepIndex: number } | null>(null); - const [shouldResume, setShouldResume] = useState(false); - - // Check if user should resume onboarding - const checkForResume = useCallback(async (): Promise => { - setIsLoading(true); - setError(null); - setIsCheckingResume(true); - - try { - // Load user's progress from backend - await progressTracking.loadProgress(); - - if (!progressTracking.progress) { - console.log('🔍 No progress found, starting from beginning'); - setIsCheckingResume(false); - setShouldResume(false); - return false; - } - - // If onboarding is already completed, don't resume - if (progressTracking.isCompleted) { - console.log('✅ Onboarding completed, redirecting to dashboard'); - navigate('/app/dashboard'); - setIsCheckingResume(false); - setShouldResume(false); - return false; - } - - // Get the resume point from backend - const resumePointData = await progressTracking.getResumePoint(); - - console.log('🎯 Resuming from step:', resumePointData.stepId); - setIsCheckingResume(false); - setResumePoint(resumePointData); - setShouldResume(true); - - return true; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error checking resume'; - setError(errorMessage); - setIsCheckingResume(false); - setShouldResume(false); - console.error('❌ Resume check failed:', errorMessage); - return false; - } finally { - setIsLoading(false); - } - }, [progressTracking, navigate]); - - // Resume the onboarding flow from the correct step - const resumeFlow = useCallback((): boolean => { - if (!resumePoint) { - console.warn('⚠️ No resume point available'); - return false; - } - - try { - const { stepIndex } = resumePoint; - - // Navigate to the correct step in the flow - setCurrentStep(stepIndex); - - console.log('✅ Resumed onboarding at step', stepIndex); - setShouldResume(false); - - return true; - } catch (error) { - console.error('❌ Error resuming flow:', error); - return false; - } - }, [resumePoint, setCurrentStep]); - - // Handle automatic resume on mount - const handleAutoResume = useCallback(async () => { - try { - // Add a timeout to prevent hanging indefinitely - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Resume check timeout')), 10000) - ); - - const shouldResumeResult = await Promise.race([ - checkForResume(), - timeoutPromise - ]); - - if (shouldResumeResult) { - // Wait a bit for state to update, then resume - setTimeout(() => { - resumeFlow(); - }, 100); - } - } catch (error) { - console.error('❌ Auto-resume failed:', error); - // Reset the checking state in case of error - setIsCheckingResume(false); - setShouldResume(false); - } - }, [checkForResume, resumeFlow]); - - // Auto-check for resume when the hook is first used - PREVENT multiple attempts - useEffect(() => { - if (progressTracking.isInitialized && !isCheckingResume && !resumeAttempted.current) { - resumeAttempted.current = true; - handleAutoResume(); - } - }, [progressTracking.isInitialized, isCheckingResume, handleAutoResume]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - return { - // State - isLoading, - error, - isCheckingResume, - resumePoint, - shouldResume, - - // Actions - checkForResume, - resumeFlow, - handleAutoResume, - clearError, - - // Progress tracking state for convenience - progress: progressTracking.progress, - isCompleted: progressTracking.isCompleted, - completionPercentage: progressTracking.completionPercentage, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useSalesProcessing.ts b/frontend/src/hooks/business/onboarding/services/useSalesProcessing.ts deleted file mode 100644 index 92181602..00000000 --- a/frontend/src/hooks/business/onboarding/services/useSalesProcessing.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * Sales processing service - Simplified implementation - */ - -import { useCallback, useState } from 'react'; -import { useClassifyProductsBatch, useValidateFileOnly } from '../../../../api'; -import { useCurrentTenant } from '../../../../stores'; -import { useOnboardingStore } from '../core/store'; -import type { ProgressCallback, ProductSuggestionResponse } from '../core/types'; - -export const useSalesProcessing = () => { - const classifyProductsMutation = useClassifyProductsBatch(); - const currentTenant = useCurrentTenant(); - const { validateFile } = useValidateFileOnly(); - const { setStepData } = useOnboardingStore(); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [suggestions, setSuggestions] = useState([]); - const [stage, setStage] = useState<'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error'>('idle'); - const [progress, setProgress] = useState(0); - const [currentMessage, setCurrentMessage] = useState(''); - const [validationResults, setValidationResults] = useState(null); - - const updateProgress = useCallback(( - progressValue: number, - stageValue: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error', - message: string, - onProgress?: ProgressCallback - ) => { - setProgress(progressValue); - setStage(stageValue); - setCurrentMessage(message); - onProgress?.(progressValue, stageValue, message); - }, []); - - const extractProductList = useCallback((validationResult: any): string[] => { - // First try to use the direct product_list from backend response - if (validationResult.product_list && Array.isArray(validationResult.product_list)) { - return validationResult.product_list; - } - - // Fallback: Extract unique product names from sample records - if (validationResult.sample_records && Array.isArray(validationResult.sample_records)) { - const productSet = new Set(); - validationResult.sample_records.forEach((record: any) => { - if (record.product_name) { - productSet.add(record.product_name); - } - }); - return Array.from(productSet); - } - - return []; - }, []); - - const generateSuggestions = useCallback(async (productList: string[]): Promise => { - try { - if (!currentTenant?.id) { - console.error('❌ No tenant ID available for classification'); - return []; - } - - console.log('🔄 Generating suggestions for', productList.length, 'products'); - - const requestPayload = { - tenantId: currentTenant.id, - batchData: { - products: productList.map(name => ({ - product_name: name, - description: '' - })) - } - }; - - console.log('📡 Making API request to:', `/tenants/${currentTenant.id}/inventory/classify-products-batch`); - - // Add timeout to the API call with shorter timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('API timeout: The inventory service may be overloaded. Please try again in a few moments.')), 15000); // 15 second timeout - }); - - const response = await Promise.race([ - classifyProductsMutation.mutateAsync(requestPayload), - timeoutPromise - ]); - - console.log('✅ Generated', response?.length || 0, 'suggestions'); - - return response || []; - } catch (error) { - const isTimeout = error instanceof Error && error.message.includes('timeout'); - const isNetworkError = error instanceof Error && (error.message.includes('fetch') || error.message.includes('network')); - - console.error('❌ Error generating suggestions:', { - error: error, - message: error instanceof Error ? error.message : 'Unknown error', - isTimeout, - isNetworkError - }); - - // Re-throw timeout/network errors so they can be handled properly by the UI - if (isTimeout || isNetworkError) { - throw error; - } - - return []; - } - }, [classifyProductsMutation, currentTenant]); - - const processFile = useCallback(async ( - file: File, - onProgress?: ProgressCallback - ): Promise<{ - success: boolean; - validationResults?: any; - suggestions?: ProductSuggestionResponse[]; - }> => { - console.log('🚀 Processing file:', file.name); - setIsLoading(true); - setError(null); - - try { - // Stage 1: Validate file structure - updateProgress(10, 'validating', 'Iniciando validación del archivo...', onProgress); - updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress); - - if (!currentTenant?.id) { - throw new Error('No se pudo obtener información del tenant'); - } - - const result = await validateFile( - currentTenant.id, - file, - { - onProgress: (stage, progress, message) => { - updateProgress(progress, stage as any, message, onProgress); - } - } - ); - - if (!result.success || !result.validationResult) { - throw new Error(result.error || 'Error en la validación del archivo'); - } - - const validationResult = { - ...result.validationResult, - product_list: extractProductList(result.validationResult), - }; - - console.log('📊 File validated:', validationResult.product_list?.length, 'products found'); - - updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress); - - if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) { - console.error('❌ No products found in file'); - throw new Error('No se encontraron productos válidos en el archivo'); - } - - // 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', 'Consultando servicios de IA...', onProgress); - suggestions = await generateSuggestions(productList); - - console.log('🔍 Generated suggestions:', { - suggestionsReceived: suggestions?.length || 0, - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error generando sugerencias'; - suggestionError = errorMessage; - console.error('❌ Suggestions generation failed:', errorMessage); - - // Create basic suggestions from product names as fallback - updateProgress(80, 'analyzing', 'Preparando productos básicos...', onProgress); - - suggestions = productList.map((productName, index) => ({ - suggestion_id: `manual-${index}`, - original_name: productName, - suggested_name: productName, - product_type: 'ingredient', - category: 'Sin categoría', - unit_of_measure: 'units', - confidence_score: 0.5, - estimated_shelf_life_days: 30, - requires_refrigeration: false, - requires_freezing: false, - is_seasonal: false, - notes: 'Clasificación manual - El servicio de IA no está disponible temporalmente' - })); - - console.log('🔧 Created fallback suggestions:', suggestions.length); - } - - updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress); - updateProgress(100, 'completed', 'Sugerencias generadas correctamente', onProgress); - - // Update state with suggestions - setSuggestions(suggestions || []); - - console.log('✅ Suggestions generation completed:', suggestions?.length || 0, 'suggestions'); - - // Update onboarding store with suggestions - setStepData('smart-inventory-setup', (prevData) => ({ - ...prevData, - processingStage: 'completed', - suggestions: suggestions || [], - suggestionError: suggestionError, - })); - - return { - success: true, - suggestions: suggestions || [], - error: suggestionError || undefined, - }; - } catch (error) { - 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, generateSuggestions, setStepData]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - const reset = useCallback(() => { - setIsLoading(false); - setError(null); - setSuggestions([]); - setStage('idle'); - setProgress(0); - setCurrentMessage(''); - setValidationResults(null); - }, []); - - return { - // State - isLoading, - error, - stage, - progress, - currentMessage, - validationResults, - suggestions, - - // Actions - processFile, - generateProductSuggestions, // New separated function - clearError, - reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useTenantCreation.ts b/frontend/src/hooks/business/onboarding/services/useTenantCreation.ts deleted file mode 100644 index 8cc7314b..00000000 --- a/frontend/src/hooks/business/onboarding/services/useTenantCreation.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Tenant creation service - Simplified implementation - */ - -import { useCallback, useState } from 'react'; -import { useRegisterBakery } from '../../../../api'; -import { useTenantStore } from '../../../../stores/tenant.store'; -import { useOnboardingStore } from '../core/store'; -import type { BakeryRegistration, TenantResponse } from '../../../../api'; - -export const useTenantCreation = () => { - const registerBakeryMutation = useRegisterBakery(); - const { setCurrentTenant, loadUserTenants } = useTenantStore(); - const { setStepData } = useOnboardingStore(); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [isSuccess, setIsSuccess] = useState(false); - const [tenantData, setTenantData] = useState(null); - - const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise => { - if (!bakeryData) { - setError('Los datos de la panadería son requeridos'); - return false; - } - - setIsLoading(true); - setError(null); - - try { - // Call API to register bakery - const tenantResponse: TenantResponse = await registerBakeryMutation.mutateAsync(bakeryData); - - // Update tenant store - setCurrentTenant(tenantResponse); - - // Reload user tenants - await loadUserTenants(); - - // Update state - setTenantData(tenantResponse); - setIsSuccess(true); - - // Update onboarding data - setStepData('setup', { - bakery: { - ...bakeryData, - tenantCreated: true, - tenant_id: tenantResponse.id, - } as any, - }); - - console.log('✅ Tenant created successfully'); - return true; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error creating tenant'; - setError(errorMessage); - setIsSuccess(false); - return false; - } finally { - setIsLoading(false); - } - }, [registerBakeryMutation, setCurrentTenant, loadUserTenants, setStepData]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - const reset = useCallback(() => { - setIsLoading(false); - setError(null); - setIsSuccess(false); - setTenantData(null); - }, []); - - return { - // State - isLoading, - error, - isSuccess, - tenantData, - - // Actions - createTenant, - clearError, - reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/services/useTrainingOrchestration.ts b/frontend/src/hooks/business/onboarding/services/useTrainingOrchestration.ts deleted file mode 100644 index 740f1863..00000000 --- a/frontend/src/hooks/business/onboarding/services/useTrainingOrchestration.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * Training orchestration service - Simplified implementation - */ - -import { useCallback, useEffect, useState } from 'react'; -import { - useCreateTrainingJob, - useTrainingJobStatus, - useTrainingWebSocket, -} from '../../../../api'; -import { useCurrentTenant } from '../../../../stores'; -import { useAuthUser } from '../../../../stores/auth.store'; -import { useOnboardingStore } from '../core/store'; -import type { TrainingLog, TrainingMetrics } from '../core/types'; -import type { TrainingJobResponse } from '../../../../api'; - -export const useTrainingOrchestration = () => { - const currentTenant = useCurrentTenant(); - const user = useAuthUser(); - const createTrainingJobMutation = useCreateTrainingJob(); - const { setStepData, getAllStepData } = useOnboardingStore(); - - // Simple, direct state management - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [status, setStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>('idle'); - const [progress, setProgress] = useState(0); - const [currentStep, setCurrentStep] = useState(''); - const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(0); - const [job, setJob] = useState(null); - const [logs, setLogs] = useState([]); - const [metrics, setMetrics] = useState(null); - - const addLog = useCallback((message: string, level: TrainingLog['level'] = 'info') => { - const newLog: TrainingLog = { - timestamp: new Date().toISOString(), - message, - level - }; - - setLogs(prevLogs => [...prevLogs, newLog]); - }, []); - - // Get job status when we have a job ID - const { data: jobStatus } = useTrainingJobStatus( - currentTenant?.id || '', - job?.job_id || '', - { - enabled: !!currentTenant?.id && !!job?.job_id && status === 'training', - refetchInterval: 5000, - } - ); - - // WebSocket for real-time updates - const { data: wsData } = useTrainingWebSocket( - currentTenant?.id || '', - job?.job_id || '', - (user as any)?.token, - { - onProgress: (data: any) => { - setProgress(data.progress?.percentage || progress); - setCurrentStep(data.progress?.current_step || currentStep); - setEstimatedTimeRemaining(data.progress?.estimated_time_remaining || estimatedTimeRemaining); - addLog( - `${data.progress?.current_step} - ${data.progress?.products_completed}/${data.progress?.products_total} productos procesados (${data.progress?.percentage}%)`, - 'info' - ); - }, - onCompleted: (data: any) => { - setStatus('completed'); - setProgress(100); - setMetrics({ - accuracy: data.results.performance_metrics.accuracy, - mape: data.results.performance_metrics.mape, - mae: data.results.performance_metrics.mae, - rmse: data.results.performance_metrics.rmse, - r2_score: data.results.performance_metrics.r2_score || 0, - }); - addLog('¡Entrenamiento ML completado exitosamente!', 'success'); - addLog(`${data.results.successful_trainings} modelos creados exitosamente`, 'success'); - addLog(`Duración total: ${Math.round(data.results.training_duration / 60)} minutos`, 'info'); - }, - onError: (data: any) => { - setError(data.error); - setStatus('failed'); - addLog(`Error en entrenamiento: ${data.error}`, 'error'); - }, - onStarted: (data: any) => { - addLog(`Trabajo de entrenamiento iniciado: ${data.job_id}`, 'success'); - }, - } - ); - - // Update status from polling when WebSocket is not available - useEffect(() => { - if (jobStatus && job?.job_id === jobStatus.job_id) { - setStatus(jobStatus.status as any); - setProgress(jobStatus.progress || progress); - setCurrentStep(jobStatus.current_step || currentStep); - setEstimatedTimeRemaining(jobStatus.estimated_time_remaining || estimatedTimeRemaining); - } - }, [jobStatus, job?.job_id, progress, currentStep, estimatedTimeRemaining]); - - const validateTrainingData = useCallback(async (allStepData?: any): Promise<{ - isValid: boolean; - missingItems: string[]; - }> => { - const missingItems: string[] = []; - const stepData = allStepData || getAllStepData(); - - - // Get data from the smart-inventory-setup step (where sales and inventory are handled) - const smartInventoryData = stepData?.['smart-inventory-setup']; - - // CRITICAL REQUIREMENT 1: Sales file must be uploaded - if (!smartInventoryData?.files?.salesData) { - missingItems.push('Archivo de datos de ventas históricos'); - } - - // CRITICAL REQUIREMENT 2: Sales data must be processed and valid - const hasProcessingResults = smartInventoryData?.processingResults && - smartInventoryData.processingResults.is_valid && - smartInventoryData.processingResults.total_records > 0; - - if (!hasProcessingResults) { - missingItems.push('Datos de ventas procesados y validados'); - } - - // CRITICAL REQUIREMENT 3: Sales data must be imported to backend (MANDATORY for training) - const hasImportResults = smartInventoryData?.salesImportResult && - (smartInventoryData.salesImportResult.records_created > 0 || - smartInventoryData.salesImportResult.success === true || - smartInventoryData.salesImportResult.imported === true); - - if (!hasImportResults) { - missingItems.push('Datos de ventas históricos importados al sistema'); - } - - // CRITICAL REQUIREMENT 4: Products must be approved - const hasApprovedProducts = smartInventoryData?.approvedProducts && - smartInventoryData.approvedProducts.length > 0 && - smartInventoryData.reviewCompleted; - - if (!hasApprovedProducts) { - missingItems.push('Productos revisados y aprobados'); - } - - // CRITICAL REQUIREMENT 5: Inventory must be configured - const hasInventoryConfig = smartInventoryData?.inventoryConfigured && - smartInventoryData?.inventoryItems && - smartInventoryData.inventoryItems.length > 0; - - if (!hasInventoryConfig) { - missingItems.push('Inventario configurado con productos'); - } - - // CRITICAL REQUIREMENT 6: Minimum data volume for training - if (smartInventoryData?.processingResults?.total_records && - smartInventoryData.processingResults.total_records < 10) { - missingItems.push('Suficientes registros de ventas para entrenar (mínimo 10)'); - } - - const isValid = missingItems.length === 0; - - if (!isValid) { - console.log('⚠️ Training validation failed:', missingItems.join(', ')); - } - - return { isValid, missingItems }; - }, [getAllStepData]); - - const startTraining = useCallback(async (options?: { - products?: string[]; - startDate?: string; - endDate?: string; - }): Promise => { - if (!currentTenant?.id) { - setError('No se pudo obtener información del tenant'); - return false; - } - - setIsLoading(true); - setStatus('validating'); - setProgress(0); - setError(null); - - addLog('Validando disponibilidad de datos...', 'info'); - - try { - // Start training job - addLog('Iniciando trabajo de entrenamiento ML...', 'info'); - const trainingJob = await createTrainingJobMutation.mutateAsync({ - tenantId: currentTenant.id, - request: { - products: options?.products, - start_date: options?.startDate, - end_date: options?.endDate, - } - }); - - // Update state - setJob(trainingJob); - setStatus('training'); - - // Update onboarding store - setStepData('ml-training', { - trainingStatus: 'training', - trainingJob: trainingJob, - }); - - addLog(`Trabajo de entrenamiento iniciado: ${trainingJob.job_id}`, 'success'); - return true; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Error starting training'; - setError(errorMessage); - setStatus('failed'); - return false; - } finally { - setIsLoading(false); - } - }, [currentTenant, createTrainingJobMutation, addLog, setStepData]); - - const clearError = useCallback(() => { - setError(null); - }, []); - - const reset = useCallback(() => { - setIsLoading(false); - setError(null); - setStatus('idle'); - setProgress(0); - setCurrentStep(''); - setEstimatedTimeRemaining(0); - setJob(null); - setLogs([]); - setMetrics(null); - }, []); - - return { - // State - isLoading, - error, - status, - progress, - currentStep, - estimatedTimeRemaining, - job, - logs, - metrics, - - // Actions - startTraining, - validateTrainingData, - addLog, - clearError, - reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/useOnboarding.ts b/frontend/src/hooks/business/onboarding/useOnboarding.ts deleted file mode 100644 index 6958bf23..00000000 --- a/frontend/src/hooks/business/onboarding/useOnboarding.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Main onboarding hook - Clean, unified interface for components - * This is the primary hook that all components should use - */ - -import { useAuthUser } from '../../../stores/auth.store'; -import { useCurrentTenant } from '../../../stores'; -import { useOnboardingStore } from './core/store'; -import { useOnboardingActions } from './core/actions'; -import { useTenantCreation } from './services/useTenantCreation'; -import { useSalesProcessing } from './services/useSalesProcessing'; -import { useInventorySetup } from './services/useInventorySetup'; -import { useTrainingOrchestration } from './services/useTrainingOrchestration'; -import { useProgressTracking } from './services/useProgressTracking'; -import { useResumeLogic } from './services/useResumeLogic'; - -export const useOnboarding = () => { - const user = useAuthUser(); - const currentTenant = useCurrentTenant(); - - // Core store and actions - const store = useOnboardingStore(); - const actions = useOnboardingActions(); - - // Service hooks for detailed state access - const tenantCreation = useTenantCreation(); - const salesProcessing = useSalesProcessing(); - const inventorySetup = useInventorySetup(); - const trainingOrchestration = useTrainingOrchestration(); - const progressTracking = useProgressTracking(); - const resumeLogic = useResumeLogic(); - - return { - // Core state from store - currentStep: store.currentStep, // Return the index, not the step object - steps: store.steps, - data: store.data, - progress: store.getProgress(), - isLoading: store.isLoading, - error: store.error, - - // User context - user, - currentTenant, - - // Step data helpers - stepData: { - setup: store.getStepData('setup'), - 'smart-inventory-setup': store.getStepData('smart-inventory-setup'), - suppliers: store.getStepData('suppliers'), - 'ml-training': store.getStepData('ml-training'), - completion: store.getStepData('completion'), - }, - - // Service states (for components that need detailed service info) - tenantCreation: { - isLoading: tenantCreation.isLoading, - isSuccess: tenantCreation.isSuccess, - error: tenantCreation.error, - tenantData: tenantCreation.tenantData, - }, - - salesProcessing: { - isLoading: salesProcessing.isLoading, - error: salesProcessing.error, - stage: salesProcessing.stage, - progress: salesProcessing.progress, - currentMessage: salesProcessing.currentMessage, - validationResults: salesProcessing.validationResults, - suggestions: Array.isArray(salesProcessing.suggestions) ? salesProcessing.suggestions : [], - }, - - inventorySetup: { - isLoading: inventorySetup.isLoading, - error: inventorySetup.error, - createdItems: inventorySetup.createdItems, - inventoryMapping: inventorySetup.inventoryMapping, - salesImportResult: inventorySetup.salesImportResult, - isInventoryConfigured: inventorySetup.isInventoryConfigured, - }, - - trainingOrchestration: { - isLoading: trainingOrchestration.isLoading, - error: trainingOrchestration.error, - status: trainingOrchestration.status, - progress: trainingOrchestration.progress, - currentStep: trainingOrchestration.currentStep, - estimatedTimeRemaining: trainingOrchestration.estimatedTimeRemaining, - job: trainingOrchestration.job, - logs: trainingOrchestration.logs, - metrics: trainingOrchestration.metrics, - }, - - progressTracking: { - isLoading: progressTracking.isLoading, - error: progressTracking.error, - progress: progressTracking.progress, - isCompleted: progressTracking.isCompleted, - completionPercentage: progressTracking.completionPercentage, - isInitialized: progressTracking.isInitialized, - currentBackendStep: progressTracking.currentBackendStep, - }, - - resumeLogic: { - isCheckingResume: resumeLogic.isCheckingResume, - resumePoint: resumeLogic.resumePoint, - shouldResume: resumeLogic.shouldResume, - progress: resumeLogic.progress, - isCompleted: resumeLogic.isCompleted, - completionPercentage: resumeLogic.completionPercentage, - }, - - // Actions from the core actions hook - nextStep: actions.nextStep, - previousStep: actions.previousStep, - goToStep: actions.goToStep, - validateCurrentStep: actions.validateCurrentStep, - - // Step data management - updateStepData: store.setStepData, - clearStepData: store.clearStepData, - - // Step-specific actions - createTenant: actions.createTenant, - processSalesFile: actions.processSalesFile, - generateProductSuggestions: actions.generateProductSuggestions, // New separated function - createInventoryFromSuggestions: actions.createInventoryFromSuggestions, - importSalesData: actions.importSalesData, - startTraining: actions.startTraining, - completeOnboarding: actions.completeOnboarding, - - // Service-specific actions (for components that need direct service access) - generateSuggestions: salesProcessing.generateSuggestions, - addTrainingLog: trainingOrchestration.addLog, - validateTrainingData: trainingOrchestration.validateTrainingData, - - // Resume actions - checkForSavedProgress: resumeLogic.checkForResume, - resumeFromSavedProgress: resumeLogic.resumeFlow, - - // Utility actions - clearError: actions.clearError, - reset: actions.reset, - }; -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/utils/createServiceHook.ts b/frontend/src/hooks/business/onboarding/utils/createServiceHook.ts deleted file mode 100644 index c2d5d5b4..00000000 --- a/frontend/src/hooks/business/onboarding/utils/createServiceHook.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Universal service hook factory - creates standardized service hooks - * This eliminates all the duplicate patterns across service hooks - */ - -import { useState, useCallback } from 'react'; -import type { ServiceState, ServiceActions } from '../core/types'; - -export interface ServiceHookConfig { - initialState?: Partial; - onSuccess?: (data: any, state: T) => T; - onError?: (error: string, state: T) => T; - resetState?: () => T; -} - -export function createServiceHook( - config: ServiceHookConfig = {} -) { - return function useService() { - const defaultState: ServiceState = { - data: null, - isLoading: false, - error: null, - isSuccess: false, - }; - - const [state, setState] = useState({ - ...defaultState, - ...config.initialState, - } as TState); - - const setLoading = useCallback((loading: boolean) => { - setState(prev => ({ ...prev, isLoading: loading })); - }, []); - - const setError = useCallback((error: string | null) => { - setState(prev => ({ - ...prev, - error, - isLoading: false, - isSuccess: false - })); - }, []); - - const setSuccess = useCallback((data: any) => { - setState(prev => { - const newState = { - ...prev, - data, - isLoading: false, - error: null, - isSuccess: true - }; - return config.onSuccess ? config.onSuccess(data, newState) : newState; - }); - }, []); - - const clearError = useCallback(() => { - setState(prev => ({ ...prev, error: null })); - }, []); - - const reset = useCallback(() => { - if (config.resetState) { - setState(config.resetState()); - } else { - setState({ - ...defaultState, - ...config.initialState, - } as TState); - } - }, []); - - const executeAsync = useCallback(async ( - asyncFn: () => Promise - ): Promise<{ success: boolean; data?: T; error?: string }> => { - try { - setLoading(true); - const result = await asyncFn(); - setSuccess(result); - return { success: true, data: result }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - setError(errorMessage); - return { success: false, error: errorMessage }; - } - }, [setLoading, setSuccess, setError]); - - return { - // State - ...state, - - // Core actions - setLoading, - setError, - setSuccess, - clearError, - reset, - - // Async execution helper - executeAsync, - }; - }; -} \ No newline at end of file diff --git a/frontend/src/pages/app/onboarding/OnboardingPage.tsx b/frontend/src/pages/app/onboarding/OnboardingPage.tsx deleted file mode 100644 index 5087c91e..00000000 --- a/frontend/src/pages/app/onboarding/OnboardingPage.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React, { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { OnboardingWizard, OnboardingStep } from '../../../components/domain/onboarding/OnboardingWizard'; -import { useOnboarding, useAutoResume } from '../../../hooks/business/onboarding'; -import { useAuthUser, useIsAuthenticated } from '../../../stores/auth.store'; -import { LoadingSpinner } from '../../../components/shared/LoadingSpinner'; - -// Step Components -import { BakerySetupStep } from '../../../components/domain/onboarding/steps/BakerySetupStep'; -import { SmartInventorySetupStep } from '../../../components/domain/onboarding/steps/SmartInventorySetupStep'; -import { SuppliersStep } from '../../../components/domain/onboarding/steps/SuppliersStep'; -import { MLTrainingStep } from '../../../components/domain/onboarding/steps/MLTrainingStep'; -import { CompletionStep } from '../../../components/domain/onboarding/steps/CompletionStep'; - -const OnboardingPage: React.FC = () => { - const navigate = useNavigate(); - const user = useAuthUser(); - const isAuthenticated = useIsAuthenticated(); - - // Use auto-resume functionality - const autoResume = useAutoResume(); - - // Use the onboarding business hook - const { - currentStep, - steps, - data, - isLoading, - error, - nextStep, - previousStep, - goToStep, - updateStepData, - validateCurrentStep, - createTenant, - processSalesFile, - createInventoryFromSuggestions, - completeOnboarding, - clearError, - reset - } = useOnboarding(); - - // Map steps to components - const stepComponents: { [key: string]: React.ComponentType } = { - 'setup': BakerySetupStep, - 'smart-inventory-setup': SmartInventorySetupStep, - '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 - })); - - // Debug logging - only show if there's an error or important state change - if (error || isLoading || autoResume.isCheckingResume) { - console.log('OnboardingPage Status:', { - stepsLength: steps.length, - currentStep, - isLoading, - error, - isCheckingResume: autoResume.isCheckingResume, - }); - } - - const handleStepChange = (stepIndex: number, stepData: any) => { - const stepId = steps[stepIndex]?.id; - if (stepId) { - updateStepData(stepId, stepData); - } - }; - - const handleNext = async (): Promise => { - try { - const success = await nextStep(); - return success; - } catch (error) { - console.error('Error in handleNext:', error); - return false; - } - }; - - const handlePrevious = (): boolean => { - return previousStep(); - }; - - const handleComplete = async (allData: any): Promise => { - try { - await completeOnboarding(); - } catch (error) { - console.error('Error in handleComplete:', error); - } - }; - - // Redirect if user is not authenticated - useEffect(() => { - if (!isAuthenticated) { - navigate('/auth/login'); - } - }, [isAuthenticated, navigate]); - - // Clear error when user navigates away or when component mounts - useEffect(() => { - // Clear any existing errors when the page loads - if (error) { - console.log('🧹 Clearing existing error on mount:', error); - clearError(); - } - - return () => { - if (error) { - clearError(); - } - }; - }, [clearError]); // Include clearError in dependencies - - // Show loading while processing or checking for saved progress - // Add a safety timeout - if checking resume takes more than 10 seconds, proceed anyway - const [loadingTimeoutReached, setLoadingTimeoutReached] = React.useState(false); - - React.useEffect(() => { - if (isLoading || autoResume.isCheckingResume) { - const timeout = setTimeout(() => { - console.warn('⚠️ Loading timeout reached, proceeding with onboarding'); - setLoadingTimeoutReached(true); - }, 10000); // 10 second timeout - - return () => clearTimeout(timeout); - } else { - setLoadingTimeoutReached(false); - } - }, [isLoading, autoResume.isCheckingResume]); - - if ((isLoading || autoResume.isCheckingResume) && !loadingTimeoutReached) { - const message = autoResume.isCheckingResume - ? "Verificando progreso guardado..." - : "Procesando..."; - - return ( -
-
- -
{message}
-
-
- ); - } - - // Show error state with better recovery options - if (error && !loadingTimeoutReached) { - return ( -
-
-

Error en Onboarding

-

{error}

-
- - -
-

- Si el problema persiste, recarga la página -

-
-
- ); - } - - // Safety check: ensure we have valid steps and current step - if (wizardSteps.length === 0) { - console.error('❌ No wizard steps available, this should not happen'); - return ( -
-
-

Error de Configuración

-

No se pudieron cargar los pasos del onboarding.

- -
-
- ); - } - - return ( -
- -
- ); -}; - -export default OnboardingPage; \ No newline at end of file diff --git a/frontend/src/pages/onboarding/OnboardingPage.tsx b/frontend/src/pages/onboarding/OnboardingPage.tsx new file mode 100644 index 00000000..c754b130 --- /dev/null +++ b/frontend/src/pages/onboarding/OnboardingPage.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { OnboardingWizard } from '../../components/domain/onboarding'; +import { PublicLayout } from '../../components/layout'; + +const OnboardingPage: React.FC = () => { + return ( + +
+
+ +
+
+
+ ); +}; + +export default OnboardingPage; \ No newline at end of file diff --git a/frontend/src/pages/onboarding/index.ts b/frontend/src/pages/onboarding/index.ts new file mode 100644 index 00000000..fde2ba06 --- /dev/null +++ b/frontend/src/pages/onboarding/index.ts @@ -0,0 +1 @@ +export { default as OnboardingPage } from './OnboardingPage'; \ No newline at end of file diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx index 77489de9..5c9f42f0 100644 --- a/frontend/src/router/AppRouter.tsx +++ b/frontend/src/router/AppRouter.tsx @@ -38,7 +38,7 @@ const TrafficPage = React.lazy(() => import('../pages/app/data/traffic/TrafficPa const EventsPage = React.lazy(() => import('../pages/app/data/events/EventsPage')); // Onboarding pages -const OnboardingPage = React.lazy(() => import('../pages/app/onboarding/OnboardingPage')); +const OnboardingPage = React.lazy(() => import('../pages/onboarding/OnboardingPage')); export const AppRouter: React.FC = () => { return ( @@ -262,7 +262,7 @@ export const AppRouter: React.FC = () => { } /> - {/* Onboarding Route - New Complete Flow */} + {/* Onboarding Route - Protected but without AppShell */} ()( get().setCurrentTenant(tenants[0]); } } else { - throw new Error('Failed to load user tenants'); + // No tenants found - this is fine for users who haven't completed onboarding + set({ + availableTenants: [], + isLoading: false, + error: null + }); } } catch (error) { - set({ - isLoading: false, - error: error instanceof Error ? error.message : 'Failed to load tenants', - }); + // Handle 404 gracefully - user might not have created any tenants yet + if (error && typeof error === 'object' && 'status' in error && error.status === 404) { + set({ + availableTenants: [], + isLoading: false, + error: null, + }); + } else { + set({ + isLoading: false, + error: error instanceof Error ? error.message : 'Failed to load tenants', + }); + } } },