From a55d48e635934e5f4b3531051f92ea4644d077f2 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 3 Sep 2025 14:06:38 +0200 Subject: [PATCH] Add onboarding flow improvements --- .../components/domain/auth/RegisterForm.tsx | 2 +- .../domain/onboarding/CompanyInfoStep.tsx | 402 -------- .../domain/onboarding/OnboardingWizard.tsx | 433 ++++++--- .../domain/onboarding/SystemSetupStep.tsx | 556 ----------- .../src/components/domain/onboarding/index.ts | 11 +- .../onboarding/steps/BakerySetupStep.tsx | 191 ++++ .../onboarding/steps/CompletionStep.tsx | 387 ++++++++ .../onboarding/steps/DataProcessingStep.tsx | 467 +++++++++ .../onboarding/steps/InventorySetupStep.tsx | 565 +++++++++++ .../onboarding/steps/MLTrainingStep.tsx | 780 +++++++++++++++ .../domain/onboarding/steps/ReviewStep.tsx | 285 ++++++ .../domain/onboarding/steps/SuppliersStep.tsx | 585 +++++++++++ .../src/components/shared/LoadingSpinner.tsx | 40 + frontend/src/hooks/useAuth.ts | 18 + .../pages/app/onboarding/OnboardingPage.tsx | 161 ++++ .../analysis/OnboardingAnalysisPage.tsx | 451 --------- .../OnboardingAnalysisPage.tsx.backup | 435 --------- .../pages/app/onboarding/analysis/index.ts | 1 - .../review/OnboardingReviewPage.tsx | 610 ------------ .../review/OnboardingReviewPage.tsx.backup | 579 ----------- .../src/pages/app/onboarding/review/index.ts | 1 - .../onboarding/setup/OnboardingSetupPage.tsx | 906 ------------------ .../setup/OnboardingSetupPage.tsx.backup | 499 ---------- .../src/pages/app/onboarding/setup/index.ts | 1 - .../upload/OnboardingUploadPage.tsx | 454 --------- .../upload/OnboardingUploadPage.tsx.backup | 438 --------- .../src/pages/app/onboarding/upload/index.ts | 1 - .../src/pages/public/LandingPage.tsx.backup | 710 -------------- frontend/src/pages/public/RegisterPage.tsx | 2 +- frontend/src/router/AppRouter.tsx | 43 +- frontend/src/router/routes.config.ts | 50 +- 31 files changed, 3813 insertions(+), 6251 deletions(-) delete mode 100644 frontend/src/components/domain/onboarding/CompanyInfoStep.tsx delete mode 100644 frontend/src/components/domain/onboarding/SystemSetupStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/CompletionStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/ReviewStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx create mode 100644 frontend/src/components/shared/LoadingSpinner.tsx create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/pages/app/onboarding/OnboardingPage.tsx delete mode 100644 frontend/src/pages/app/onboarding/analysis/OnboardingAnalysisPage.tsx delete mode 100644 frontend/src/pages/app/onboarding/analysis/OnboardingAnalysisPage.tsx.backup delete mode 100644 frontend/src/pages/app/onboarding/analysis/index.ts delete mode 100644 frontend/src/pages/app/onboarding/review/OnboardingReviewPage.tsx delete mode 100644 frontend/src/pages/app/onboarding/review/OnboardingReviewPage.tsx.backup delete mode 100644 frontend/src/pages/app/onboarding/review/index.ts delete mode 100644 frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx delete mode 100644 frontend/src/pages/app/onboarding/setup/OnboardingSetupPage.tsx.backup delete mode 100644 frontend/src/pages/app/onboarding/setup/index.ts delete mode 100644 frontend/src/pages/app/onboarding/upload/OnboardingUploadPage.tsx delete mode 100644 frontend/src/pages/app/onboarding/upload/OnboardingUploadPage.tsx.backup delete mode 100644 frontend/src/pages/app/onboarding/upload/index.ts delete mode 100644 frontend/src/pages/public/LandingPage.tsx.backup diff --git a/frontend/src/components/domain/auth/RegisterForm.tsx b/frontend/src/components/domain/auth/RegisterForm.tsx index 74ba20f9..7ca19f0b 100644 --- a/frontend/src/components/domain/auth/RegisterForm.tsx +++ b/frontend/src/components/domain/auth/RegisterForm.tsx @@ -101,7 +101,7 @@ export const RegisterForm: React.FC = ({ } catch (error) { console.error('Error calling onSuccess:', error); // Fallback: direct redirect if callback fails - window.location.href = '/app/onboarding/setup'; + window.location.href = '/app/onboarding'; } return; } diff --git a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx b/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx deleted file mode 100644 index 7d6731ca..00000000 --- a/frontend/src/components/domain/onboarding/CompanyInfoStep.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import React from 'react'; -import { Input, Select, Badge } from '../../ui'; -import type { OnboardingStepProps } from './OnboardingWizard'; - -interface CompanyInfo { - name: string; - type: 'artisan' | 'dependent'; - size: 'small' | 'medium' | 'large'; - locations: number; - specialties: string[]; - address: { - street: string; - city: string; - state: string; - postal_code: string; - country: string; - }; - contact: { - phone: string; - email: string; - website?: string; - }; - established_year?: number; - tax_id?: string; -} - -const BAKERY_TYPES = [ - { value: 'artisan', label: 'Panadería Artesanal Local', description: 'Producción propia y tradicional en el local' }, - { value: 'dependent', label: 'Panadería Dependiente', description: 'Dependiente de un panadero central' }, -]; - -const BAKERY_SIZES = [ - { value: 'small', label: 'Pequeña', description: '1-10 empleados' }, - { value: 'medium', label: 'Mediana', description: '11-50 empleados' }, - { value: 'large', label: 'Grande', description: '50+ empleados' }, -]; - -const COMMON_SPECIALTIES = [ - 'Pan tradicional', - 'Bollería', - 'Repostería', - 'Pan integral', - 'Pasteles', - 'Productos sin gluten', - 'Productos veganos', - 'Pan artesanal', - 'Productos de temporada', - 'Catering', -]; - -export const CompanyInfoStep: React.FC = ({ - data, - onDataChange, -}) => { - const companyData: CompanyInfo = { - name: '', - type: 'artisan', - size: 'small', - locations: 1, - specialties: [], - address: { - street: '', - city: '', - state: '', - postal_code: '', - country: 'España', - }, - contact: { - phone: '', - email: '', - website: '', - }, - ...data, - }; - - const handleInputChange = (field: string, value: any) => { - onDataChange({ - ...companyData, - [field]: value, - }); - }; - - const handleAddressChange = (field: string, value: string) => { - onDataChange({ - ...companyData, - address: { - ...companyData.address, - [field]: value, - }, - }); - }; - - const handleContactChange = (field: string, value: string) => { - onDataChange({ - ...companyData, - contact: { - ...companyData.contact, - [field]: value, - }, - }); - }; - - const handleSpecialtyToggle = (specialty: string) => { - const currentSpecialties = companyData.specialties || []; - const updatedSpecialties = currentSpecialties.includes(specialty) - ? currentSpecialties.filter(s => s !== specialty) - : [...currentSpecialties, specialty]; - - handleInputChange('specialties', updatedSpecialties); - }; - - return ( -
- {/* Basic Information */} -
-

- Información básica -

- -
-
- - handleInputChange('name', e.target.value)} - placeholder="Ej: Panadería San Miguel" - className="w-full" - /> -
- -
- - -
- -
- - -
- -
- - handleInputChange('locations', parseInt(e.target.value) || 1)} - className="w-full" - /> -
- -
- - handleInputChange('established_year', parseInt(e.target.value) || undefined)} - placeholder="Ej: 1995" - className="w-full" - /> -
- -
- - handleInputChange('tax_id', e.target.value)} - placeholder="Ej: B12345678" - className="w-full" - /> -
-
-
- - {/* Specialties */} -
-

- Especialidades -

-

- Selecciona los productos que produces habitualmente -

- -
- {COMMON_SPECIALTIES.map((specialty) => ( - - ))} -
- - {companyData.specialties && companyData.specialties.length > 0 && ( -
-

- Especialidades seleccionadas: -

-
- {companyData.specialties.map((specialty) => ( - - {specialty} - - - ))} -
-
- )} -
- - {/* Address */} -
-

- Dirección principal -

- -
-
- - handleAddressChange('street', e.target.value)} - placeholder="Calle, número, piso, puerta" - className="w-full" - /> -
- -
- - handleAddressChange('city', e.target.value)} - placeholder="Ej: Madrid" - className="w-full" - /> -
- -
- - handleAddressChange('state', e.target.value)} - placeholder="Ej: Madrid" - className="w-full" - /> -
- -
- - handleAddressChange('postal_code', e.target.value)} - placeholder="Ej: 28001" - className="w-full" - /> -
- -
- - -
-
-
- - {/* Contact Information */} -
-

- Información de contacto -

- -
-
- - handleContactChange('phone', e.target.value)} - placeholder="Ej: +34 911 234 567" - className="w-full" - /> -
- -
- - handleContactChange('email', e.target.value)} - placeholder="contacto@panaderia.com" - className="w-full" - /> -
- -
- - handleContactChange('website', e.target.value)} - placeholder="https://www.panaderia.com" - className="w-full" - /> -
-
-
- - {/* Summary */} -
-

Resumen

-
-

Panadería: {companyData.name || 'Sin especificar'}

-

Tipo: {BAKERY_TYPES.find(t => t.value === companyData.type)?.label}

-

Tamaño: {BAKERY_SIZES.find(s => s.value === companyData.size)?.label}

-

Especialidades: {companyData.specialties?.length || 0} seleccionadas

-

Ubicaciones: {companyData.locations}

-
-
-
- ); -}; - -export default CompanyInfoStep; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx index d5e6a047..0d4bc999 100644 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx @@ -74,7 +74,67 @@ export const OnboardingWizard: React.FC = ({ return true; }, [currentStep, stepData]); - const goToNextStep = useCallback(() => { + const goToNextStep = useCallback(async () => { + // Special handling for setup step - create tenant if needed + if (currentStep.id === 'setup') { + const data = stepData[currentStep.id] || {}; + + if (!data.bakery?.tenant_id) { + // Create tenant inline + updateStepData(currentStep.id, { + ...data, + bakery: { ...data.bakery, isCreating: true } + }); + + try { + const mockTenantService = { + createTenant: async (formData: any) => { + await new Promise(resolve => setTimeout(resolve, 1000)); + return { + tenant_id: `tenant_${Date.now()}`, + name: formData.name, + ...formData + }; + } + }; + + const tenantData = await mockTenantService.createTenant(data.bakery); + + updateStepData(currentStep.id, { + ...data, + bakery: { + ...data.bakery, + tenant_id: tenantData.tenant_id, + created_at: new Date().toISOString(), + isCreating: false + } + }); + + } catch (error) { + console.error('Error creating tenant:', error); + updateStepData(currentStep.id, { + ...data, + bakery: { ...data.bakery, isCreating: false } + }); + return; + } + } + } + + // Special handling for suppliers step - trigger ML training + if (currentStep.id === 'suppliers') { + const nextStepIndex = currentStepIndex + 1; + const nextStep = steps[nextStepIndex]; + + if (nextStep && nextStep.id === 'ml-training') { + // Set autoStartTraining flag for the ML training step + updateStepData(nextStep.id, { + ...stepData[nextStep.id], + autoStartTraining: true + }); + } + } + if (validateCurrentStep()) { if (currentStepIndex < steps.length - 1) { setCurrentStepIndex(currentStepIndex + 1); @@ -83,7 +143,7 @@ export const OnboardingWizard: React.FC = ({ onComplete(stepData); } } - }, [currentStepIndex, steps.length, validateCurrentStep, onComplete, stepData]); + }, [currentStep.id, currentStepIndex, steps, validateCurrentStep, onComplete, stepData, updateStepData]); const goToPreviousStep = useCallback(() => { if (currentStepIndex > 0) { @@ -100,72 +160,186 @@ export const OnboardingWizard: React.FC = ({ }; const renderStepIndicator = () => ( -
- {steps.map((step, index) => { - const isCompleted = completedSteps.has(step.id); - const isCurrent = index === currentStepIndex; - const hasError = validationErrors[step.id]; +
+
+ + {/* Progress summary */} +
+
+ Paso + + {currentStepIndex + 1} + + de + + {steps.length} + +
+ + {Math.round(calculateProgress())}% completado + +
+
- return ( -
- - -
-

- {step.title} - {step.isRequired && *} -

- {hasError && ( -

{hasError}

- )} + {/* 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); + + return ( + +
+ + + {/* Step label */} +
+
+ {step.title.replace(/🏢|📊|🤖|📋|⚙️|🏪|🧠|🎉/g, '').trim()} +
+
+ + {/* Error indicator */} + {hasError && ( +
+ ! +
+ )} +
+ + {/* Connection line to next step */} + {index < steps.length - 1 && ( +
+
+
+
+
+ )} + + ); + })}
- {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 && ( +
+
+
+ )} + + ); + })} +
+
- ); - })} +
+ +
); - const renderProgressBar = () => ( -
-
- - Progreso del onboarding - - - {completedSteps.size} de {steps.length} completados - -
-
-
-
-
- ); if (!currentStep) { return ( @@ -178,75 +352,92 @@ export const OnboardingWizard: React.FC = ({ const StepComponent = currentStep.component; return ( -
- {/* Header */} -
-
-

+
+
+ {/* Header with clean design */} +
+

Configuración inicial

-

+

Completa estos pasos para comenzar a usar la plataforma

+ + {onExit && ( + + )}
- {onExit && ( - - )} -
+ {renderStepIndicator()} - {renderProgressBar()} - {renderStepIndicator()} + {/* Main Content - Single clean card */} +
+ {/* Step header */} +
+

+ {currentStep.title} +

+

+ {currentStep.description} +

+
- {/* Current Step Content */} - -
-

- {currentStep.title} -

-

- {currentStep.description} -

+ {/* Step content */} +
+ updateStepData(currentStep.id, data)} + onNext={goToNextStep} + onPrevious={goToPreviousStep} + isFirstStep={currentStepIndex === 0} + isLastStep={currentStepIndex === steps.length - 1} + /> +
+ + {/* Navigation footer */} +
+
+ + + +
+
- - updateStepData(currentStep.id, data)} - onNext={goToNextStep} - onPrevious={goToPreviousStep} - isFirstStep={currentStepIndex === 0} - isLastStep={currentStepIndex === steps.length - 1} - /> -
- - {/* Navigation */} -
- - -
- - Paso {currentStepIndex + 1} de {steps.length} - -
- -
); diff --git a/frontend/src/components/domain/onboarding/SystemSetupStep.tsx b/frontend/src/components/domain/onboarding/SystemSetupStep.tsx deleted file mode 100644 index 766e0f56..00000000 --- a/frontend/src/components/domain/onboarding/SystemSetupStep.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import React from 'react'; -import { Card, Button, Input, Select, Badge } from '../../ui'; -import type { OnboardingStepProps } from './OnboardingWizard'; - -interface SystemConfig { - timezone: string; - currency: string; - language: string; - date_format: string; - number_format: string; - working_hours: { - start: string; - end: string; - days: number[]; - }; - notifications: { - email_enabled: boolean; - sms_enabled: boolean; - push_enabled: boolean; - alert_preferences: string[]; - }; - integrations: { - pos_system?: string; - accounting_software?: string; - payment_provider?: string; - }; - features: { - inventory_management: boolean; - production_planning: boolean; - sales_analytics: boolean; - customer_management: boolean; - financial_reporting: boolean; - quality_control: boolean; - }; -} - -const TIMEZONES = [ - { value: 'Europe/Madrid', label: 'Madrid (GMT+1)' }, - { value: 'Europe/London', label: 'London (GMT+0)' }, - { value: 'Europe/Paris', label: 'Paris (GMT+1)' }, - { value: 'America/New_York', label: 'New York (GMT-5)' }, - { value: 'America/Los_Angeles', label: 'Los Angeles (GMT-8)' }, - { value: 'America/Mexico_City', label: 'Mexico City (GMT-6)' }, - { value: 'America/Buenos_Aires', label: 'Buenos Aires (GMT-3)' }, -]; - -const CURRENCIES = [ - { value: 'EUR', label: 'Euro (€)', symbol: '€' }, - { value: 'USD', label: 'US Dollar ($)', symbol: '$' }, - { value: 'GBP', label: 'British Pound (£)', symbol: '£' }, - { value: 'MXN', label: 'Mexican Peso ($)', symbol: '$' }, - { value: 'ARS', label: 'Argentine Peso ($)', symbol: '$' }, - { value: 'COP', label: 'Colombian Peso ($)', symbol: '$' }, -]; - -const LANGUAGES = [ - { value: 'es', label: 'Español' }, - { value: 'en', label: 'English' }, - { value: 'fr', label: 'Français' }, - { value: 'pt', label: 'Português' }, - { value: 'it', label: 'Italiano' }, -]; - -const DAYS_OF_WEEK = [ - { value: 1, label: 'Lunes', short: 'L' }, - { value: 2, label: 'Martes', short: 'M' }, - { value: 3, label: 'Miércoles', short: 'X' }, - { value: 4, label: 'Jueves', short: 'J' }, - { value: 5, label: 'Viernes', short: 'V' }, - { value: 6, label: 'Sábado', short: 'S' }, - { value: 0, label: 'Domingo', short: 'D' }, -]; - -const ALERT_TYPES = [ - { value: 'low_stock', label: 'Stock bajo', description: 'Cuando los ingredientes están por debajo del mínimo' }, - { value: 'production_delays', label: 'Retrasos de producción', description: 'Cuando los lotes se retrasan' }, - { value: 'quality_issues', label: 'Problemas de calidad', description: 'Cuando se detectan problemas de calidad' }, - { value: 'financial_targets', label: 'Objetivos financieros', description: 'Cuando se alcanzan o no se cumplen objetivos' }, - { value: 'equipment_maintenance', label: 'Mantenimiento de equipos', description: 'Recordatorios de mantenimiento' }, - { value: 'food_safety', label: 'Seguridad alimentaria', description: 'Alertas relacionadas con seguridad alimentaria' }, -]; - -const FEATURES = [ - { - key: 'inventory_management', - title: 'Gestión de inventario', - description: 'Control de stock, ingredientes y materias primas', - recommended: true, - }, - { - key: 'production_planning', - title: 'Planificación de producción', - description: 'Programación de lotes y gestión de recetas', - recommended: true, - }, - { - key: 'sales_analytics', - title: 'Analytics de ventas', - description: 'Reportes y análisis de ventas y tendencias', - recommended: true, - }, - { - key: 'customer_management', - title: 'Gestión de clientes', - description: 'Base de datos de clientes y programa de fidelización', - recommended: false, - }, - { - key: 'financial_reporting', - title: 'Reportes financieros', - description: 'Análisis de costos, márgenes y rentabilidad', - recommended: true, - }, - { - key: 'quality_control', - title: 'Control de calidad', - description: 'Seguimiento de calidad y estándares', - recommended: true, - }, -]; - -export const SystemSetupStep: React.FC = ({ - data, - onDataChange, -}) => { - const systemData: SystemConfig = { - timezone: 'Europe/Madrid', - currency: 'EUR', - language: 'es', - date_format: 'DD/MM/YYYY', - number_format: 'European', - working_hours: { - start: '06:00', - end: '20:00', - days: [1, 2, 3, 4, 5, 6], // Monday to Saturday - }, - notifications: { - email_enabled: true, - sms_enabled: false, - push_enabled: true, - alert_preferences: ['low_stock', 'production_delays', 'quality_issues'], - }, - integrations: {}, - features: { - inventory_management: true, - production_planning: true, - sales_analytics: true, - customer_management: false, - financial_reporting: true, - quality_control: true, - }, - ...data, - }; - - const handleInputChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - [field]: value, - }); - }; - - const handleWorkingHoursChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - working_hours: { - ...systemData.working_hours, - [field]: value, - }, - }); - }; - - const handleNotificationChange = (field: string, value: any) => { - onDataChange({ - ...systemData, - notifications: { - ...systemData.notifications, - [field]: value, - }, - }); - }; - - const handleIntegrationChange = (field: string, value: string) => { - onDataChange({ - ...systemData, - integrations: { - ...systemData.integrations, - [field]: value, - }, - }); - }; - - const handleFeatureToggle = (feature: string) => { - onDataChange({ - ...systemData, - features: { - ...systemData.features, - [feature]: !systemData.features[feature as keyof typeof systemData.features], - }, - }); - }; - - const toggleWorkingDay = (day: number) => { - const currentDays = systemData.working_hours.days; - const updatedDays = currentDays.includes(day) - ? currentDays.filter(d => d !== day) - : [...currentDays, day].sort(); - - handleWorkingHoursChange('days', updatedDays); - }; - - const toggleAlertPreference = (alert: string) => { - const current = systemData.notifications.alert_preferences; - const updated = current.includes(alert) - ? current.filter(a => a !== alert) - : [...current, alert]; - - handleNotificationChange('alert_preferences', updated); - }; - - return ( -
- {/* Regional Settings */} -
-

- Configuración regional -

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - {/* Working Hours */} -
-

- Horario de trabajo -

- -
-
- - handleWorkingHoursChange('start', e.target.value)} - className="w-full" - /> -
- -
- - handleWorkingHoursChange('end', e.target.value)} - className="w-full" - /> -
-
- -
- -
- {DAYS_OF_WEEK.map((day) => ( - - ))} -
-
-
- - {/* Features */} -
-

- Módulos y características -

-

- Selecciona las características que quieres activar. Podrás cambiar esto más tarde. -

- -
- {FEATURES.map((feature) => ( - handleFeatureToggle(feature.key)} - > -
-
-
-

{feature.title}

- {feature.recommended && ( - - Recomendado - - )} -
-

{feature.description}

-
- -
- {systemData.features[feature.key as keyof typeof systemData.features] && ( - - )} -
-
-
- ))} -
-
- - {/* Notifications */} -
-

- Notificaciones -

- -
-
- - - - - -
- -
- -
- {ALERT_TYPES.map((alert) => ( -
toggleAlertPreference(alert.value)} - > -
-
-
{alert.label}
-

{alert.description}

-
-
- {systemData.notifications.alert_preferences.includes(alert.value) && ( - - )} -
-
-
- ))} -
-
-
-
- - {/* Integrations */} -
-

- Integraciones (opcional) -

-

- Estas integraciones se pueden configurar más tarde desde el panel de administración. -

- -
-
- - -
- -
- - -
- -
- - -
-
-
- - {/* Summary */} -
-

Configuración seleccionada

-
-

Zona horaria: {TIMEZONES.find(tz => tz.value === systemData.timezone)?.label}

-

Moneda: {CURRENCIES.find(c => c.value === systemData.currency)?.label}

-

Horario: {systemData.working_hours.start} - {systemData.working_hours.end}

-

Días operativos: {systemData.working_hours.days.length} días por semana

-

Módulos activados: {Object.values(systemData.features).filter(Boolean).length} de {Object.keys(systemData.features).length}

-
-
-
- ); -}; - -export default SystemSetupStep; \ 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 d4fa0975..29708272 100644 --- a/frontend/src/components/domain/onboarding/index.ts +++ b/frontend/src/components/domain/onboarding/index.ts @@ -1,7 +1,14 @@ // Onboarding domain components export { default as OnboardingWizard } from './OnboardingWizard'; -export { default as CompanyInfoStep } from './CompanyInfoStep'; -export { default as SystemSetupStep } from './SystemSetupStep'; + +// Individual step components +export { BakerySetupStep } from './steps/BakerySetupStep'; +export { DataProcessingStep } from './steps/DataProcessingStep'; +export { ReviewStep } from './steps/ReviewStep'; +export { InventorySetupStep } from './steps/InventorySetupStep'; +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 diff --git a/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx new file mode 100644 index 00000000..6a922577 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/BakerySetupStep.tsx @@ -0,0 +1,191 @@ +import React, { useState, useEffect } from 'react'; +import { Store, MapPin, Phone, Mail } from 'lucide-react'; +import { Button, Card, Input } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +export const BakerySetupStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [formData, setFormData] = useState({ + name: data.bakery?.name || '', + type: data.bakery?.type || '', + location: data.bakery?.location || '', + phone: data.bakery?.phone || '', + email: data.bakery?.email || '', + description: data.bakery?.description || '' + }); + + const bakeryTypes = [ + { + value: 'artisan', + label: 'Panadería Artesanal', + description: 'Producción propia tradicional con recetas artesanales', + icon: '🥖' + }, + { + value: 'industrial', + label: 'Panadería Industrial', + description: 'Producción a gran escala con procesos automatizados', + icon: '🏭' + }, + { + value: 'retail', + label: 'Panadería Retail', + description: 'Punto de venta que compra productos terminados', + icon: '🏪' + }, + { + value: 'hybrid', + label: 'Modelo Híbrido', + description: 'Combina producción propia con productos externos', + icon: '🔄' + } + ]; + + useEffect(() => { + // Update parent data when form changes + onDataChange({ + bakery: { + ...formData, + tenant_id: data.bakery?.tenant_id + } + }); + }, [formData]); + + const handleInputChange = (field: string, value: string) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + + return ( +
+ + {/* Form */} +
+ {/* Basic Info */} +
+
+ + handleInputChange('name', e.target.value)} + placeholder="Ej: Panadería Artesanal El Buen Pan" + className="w-full text-lg py-3" + /> +
+ + {/* Bakery Type - Simplified */} +
+ +
+ {bakeryTypes.map((type) => ( + + ))} +
+
+ + {/* Location and Contact - Simplified */} +
+
+ +
+ + handleInputChange('location', e.target.value)} + placeholder="Dirección completa de tu panadería" + className="w-full pl-12 py-3" + /> +
+
+ +
+
+ +
+ + handleInputChange('phone', e.target.value)} + placeholder="+1 234 567 8900" + className="w-full pl-10" + /> +
+
+ +
+ +
+ + handleInputChange('email', e.target.value)} + placeholder="contacto@panaderia.com" + className="w-full pl-10" + /> +
+
+
+
+
+
+ + {/* Optional: Show loading state when creating tenant */} + {data.bakery?.isCreating && ( +
+
+

+ Creando tu espacio de trabajo... +

+
+ )} +
+ ); +}; \ 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 new file mode 100644 index 00000000..3f26dda7 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/CompletionStep.tsx @@ -0,0 +1,387 @@ +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'; + +interface CompletionStats { + totalProducts: number; + inventoryItems: number; + suppliersConfigured: number; + mlModelAccuracy: number; + estimatedTimeSaved: string; + completionScore: number; +} + +export const CompletionStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [showConfetti, setShowConfetti] = useState(false); + const [completionStats, setCompletionStats] = useState(null); + + 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() + }; + + setCompletionStats(stats); + + // Update parent data + onDataChange({ + ...data, + completionStats: stats, + onboardingCompleted: true, + completedAt: new Date().toISOString() + }); + + 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); + alert(`🎓 Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`); + }; + + const scheduleDemo = () => { + // Mock demo scheduling + alert('📅 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); + alert('✅ 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 */} +
+ )} + + {/* Celebration Header */} +
+
+ +
+

+ ¡Felicidades! 🎉 +

+

+ {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. +

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

+ Puntuación de Configuración +

+

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

+
+ +
+
+

{completionStats.totalProducts}

+

Productos

+
+
+

{completionStats.inventoryItems}

+

Inventario

+
+
+

{completionStats.suppliersConfigured}

+

Proveedores

+
+
+

{completionStats.mlModelAccuracy.toFixed(1)}%

+

Precisión IA

+
+
+

{completionStats.estimatedTimeSaved}

+

Tiempo Ahorrado

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

+ 🏆 Logros Desbloqueados +

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

{badge.title}

+

{badge.description}

+ {badge.earned && ( +
+ Conseguido +
+ )} +
+ ))} +
+
+ + {/* Quick Actions */} + +

+ 🚀 Próximos Pasos +

+
+ {quickStartActions.map((action) => ( + + ))} +
+
+ + {/* 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 +
+
+
+
+ + Alertas automáticas de restock +
+
+ + Análisis de tendencias de venta +
+
+ + Control multi-tenant seguro +
+
+
+ +
+

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

+
+
+
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx b/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx new file mode 100644 index 00000000..ac970844 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx @@ -0,0 +1,467 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react'; +import { Button, Card, Badge } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error'; + +interface ProcessingResult { + // Validation data + 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; + }; + // Analysis data + productsIdentified: number; + categoriesDetected: number; + businessModel: string; + confidenceScore: number; + recommendations: string[]; +} + +// Unified mock service that handles both validation and analysis +const mockDataProcessingService = { + processFile: async (file: File, onProgress: (progress: number, stage: string, message: string) => void) => { + return new Promise((resolve, reject) => { + let progress = 0; + + const stages = [ + { threshold: 20, stage: 'validating', message: 'Validando estructura del archivo...' }, + { threshold: 40, stage: 'validating', message: 'Verificando integridad de datos...' }, + { threshold: 60, stage: 'analyzing', message: 'Identificando productos únicos...' }, + { threshold: 80, stage: 'analyzing', message: 'Analizando patrones de venta...' }, + { threshold: 90, stage: 'analyzing', message: 'Generando recomendaciones con IA...' }, + { threshold: 100, stage: 'completed', message: 'Procesamiento completado' } + ]; + + const interval = setInterval(() => { + if (progress < 100) { + progress += 10; + const currentStage = stages.find(s => progress <= s.threshold); + if (currentStage) { + onProgress(progress, currentStage.stage, currentStage.message); + } + } + + if (progress >= 100) { + clearInterval(interval); + // Return combined validation + analysis results + resolve({ + // Validation results + is_valid: true, + total_records: Math.floor(Math.random() * 1000) + 100, + unique_products: Math.floor(Math.random() * 50) + 10, + product_list: ['Pan Integral', 'Croissant', 'Baguette', 'Empanadas', 'Pan de Centeno', 'Medialunas'], + validation_errors: [], + validation_warnings: [ + 'Algunas fechas podrían tener formato inconsistente', + '3 productos sin categoría definida' + ], + summary: { + date_range: '2024-01-01 to 2024-12-31', + total_sales: 15420.50, + average_daily_sales: 42.25 + }, + // Analysis results + productsIdentified: 15, + categoriesDetected: 4, + businessModel: 'artisan', + confidenceScore: 94, + recommendations: [ + 'Se detectó un modelo de panadería artesanal con producción propia', + 'Los productos más vendidos son panes tradicionales y bollería', + 'Recomendamos categorizar el inventario por tipo de producto', + 'Considera ampliar la línea de productos de repostería' + ] + }); + } + }, 400); + }); + } +}; + +export const DataProcessingStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [stage, setStage] = useState(data.processingStage || 'upload'); + const [uploadedFile, setUploadedFile] = useState(data.files?.salesData || null); + const [progress, setProgress] = useState(data.processingProgress || 0); + const [currentMessage, setCurrentMessage] = useState(data.currentMessage || ''); + const [results, setResults] = useState(data.processingResults || null); + const [dragActive, setDragActive] = useState(false); + + const fileInputRef = useRef(null); + + useEffect(() => { + // Update parent data when state changes + onDataChange({ + ...data, + processingStage: stage, + processingProgress: progress, + currentMessage: currentMessage, + processingResults: results, + files: { + ...data.files, + salesData: uploadedFile + } + }); + }, [stage, progress, currentMessage, results, uploadedFile]); + + 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) => { + // Validate file type + const validExtensions = ['.csv', '.xlsx', '.xls']; + const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); + + if (!validExtensions.includes(fileExtension)) { + alert('Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)'); + return; + } + + // Check file size (max 10MB) + if (file.size > 10 * 1024 * 1024) { + alert('El archivo es demasiado grande. Máximo 10MB permitido.'); + return; + } + + setUploadedFile(file); + setStage('validating'); + setProgress(0); + + try { + const result = await mockDataProcessingService.processFile( + file, + (newProgress, newStage, message) => { + setProgress(newProgress); + setStage(newStage as ProcessingStage); + setCurrentMessage(message); + } + ); + + setResults(result); + setStage('completed'); + } catch (error) { + console.error('Processing error:', error); + setStage('error'); + setCurrentMessage('Error en el procesamiento de datos'); + } + }; + + 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 +2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda +2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`; + + 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); + }; + + const resetProcess = () => { + setStage('upload'); + setUploadedFile(null); + setProgress(0); + setCurrentMessage(''); + setResults(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ {/* Improved Upload Stage */} + {stage === 'upload' && ( + <> +
fileInputRef.current?.click()} + > + + +
+ {uploadedFile ? ( + <> +
+ +
+
+

+ ¡Perfecto! Archivo listo +

+
+

+ 📄 {uploadedFile.name} +

+

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

+
+
+ + ) : ( + <> +
+ +
+
+

+ Sube tu historial de ventas +

+

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

+
+ + {/* Visual indicators */} +
+
+
+ 📊 +
+ CSV +
+
+
+ 📈 +
+ Excel +
+
+
+ +
+ Hasta 10MB +
+
+ + )} +
+ +
+ 💡 Formatos aceptados: CSV, Excel (XLSX, XLS) • Tamaño máximo: 10MB +
+
+ + {/* Improved Template Download Section */} +
+
+
+ +
+
+

+ ¿Necesitas ayuda con el formato? +

+

+ Descarga nuestra plantilla Excel con ejemplos y formato correcto para tus datos de ventas +

+ +
+
+
+ + )} + + {/* Processing Stages */} + {(stage === 'validating' || stage === 'analyzing') && ( + +
+
+
+ {stage === 'validating' ? ( + + ) : ( + + )} +
+ +

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

+

+ {currentMessage} +

+
+ + {/* Progress Bar */} +
+
+ + Progreso + + {progress}% +
+
+
+
+
+ + {/* Processing Steps */} +
+
= 40 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]' + }`}> + + Validación +
+
= 70 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]' + }`}> + + Análisis IA +
+
= 100 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]' + }`}> + + Completo +
+
+
+ + )} + + {/* Simplified Results Stage */} + {stage === 'completed' && results && ( +
+ {/* Success Header */} +
+
+ +
+

+ ¡Procesamiento Completado! +

+

+ Tus datos han sido procesados exitosamente +

+
+ + {/* Simple Stats Cards */} +
+
+

{results.total_records}

+

Registros

+
+ +
+

{results.productsIdentified}

+

Productos

+
+ +
+

{results.confidenceScore}%

+

Confianza

+
+ +
+

+ {results.businessModel === 'artisan' ? 'Artesanal' : + results.businessModel === 'retail' ? 'Retail' : 'Híbrido'} +

+

Modelo

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

+ Error en el procesamiento +

+

+ {currentMessage} +

+ +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx new file mode 100644 index 00000000..020a39dd --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx @@ -0,0 +1,565 @@ +import React, { useState, useEffect } from 'react'; +import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2 } from 'lucide-react'; +import { Button, Card, Input, Badge } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +interface InventoryItem { + id: string; + name: string; + category: 'ingredient' | 'finished_product'; + current_stock: number; + min_stock: number; + max_stock: number; + unit: string; + expiry_date?: string; + supplier?: string; + cost_per_unit?: number; + requires_refrigeration: boolean; +} + +// Mock inventory items based on approved products +const mockInventoryItems: InventoryItem[] = [ + { + id: '1', name: 'Harina de Trigo', category: 'ingredient', + current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg', + expiry_date: '2024-12-31', supplier: 'Molinos del Sur', + cost_per_unit: 1.20, requires_refrigeration: false + }, + { + id: '2', name: 'Levadura Fresca', category: 'ingredient', + current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg', + expiry_date: '2024-03-15', supplier: 'Levaduras Pro', + cost_per_unit: 3.50, requires_refrigeration: true + }, + { + id: '3', name: 'Pan Integral', category: 'finished_product', + current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades', + expiry_date: '2024-01-25', requires_refrigeration: false + }, + { + id: '4', name: 'Mantequilla', category: 'ingredient', + current_stock: 15, min_stock: 5, max_stock: 30, unit: 'kg', + expiry_date: '2024-02-28', supplier: 'Lácteos Premium', + cost_per_unit: 4.20, requires_refrigeration: true + } +]; + +export const InventorySetupStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [items, setItems] = useState( + data.inventoryItems || mockInventoryItems + ); + const [editingItem, setEditingItem] = useState(null); + const [isAddingNew, setIsAddingNew] = useState(false); + const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all'); + + useEffect(() => { + onDataChange({ + ...data, + inventoryItems: items, + inventoryConfigured: items.length > 0 && items.every(item => + item.min_stock > 0 && item.max_stock > item.min_stock + ) + }); + }, [items]); + + const handleAddItem = () => { + const newItem: InventoryItem = { + id: Date.now().toString(), + name: '', + category: 'ingredient', + current_stock: 0, + min_stock: 0, + max_stock: 0, + unit: 'kg', + requires_refrigeration: false + }; + setEditingItem(newItem); + setIsAddingNew(true); + }; + + const handleSaveItem = (item: InventoryItem) => { + if (isAddingNew) { + setItems(prev => [...prev, item]); + } else { + setItems(prev => prev.map(i => i.id === item.id ? item : i)); + } + setEditingItem(null); + setIsAddingNew(false); + }; + + const handleDeleteItem = (id: string) => { + if (window.confirm('¿Estás seguro de eliminar este elemento del inventario?')) { + setItems(prev => prev.filter(item => item.id !== id)); + } + }; + + const handleQuickSetup = () => { + // Auto-configure basic inventory based on approved products + const autoItems = data.detectedProducts + ?.filter((p: any) => p.status === 'approved') + .map((product: any, index: number) => ({ + id: `auto_${index}`, + name: product.name, + category: 'finished_product' as const, + current_stock: Math.floor(Math.random() * 20) + 5, + min_stock: 5, + max_stock: 50, + unit: 'unidades', + requires_refrigeration: product.category === 'Repostería' || product.category === 'Salados' + })) || []; + + setItems(prev => [...prev, ...autoItems]); + }; + + const getFilteredItems = () => { + return filterCategory === 'all' + ? items + : items.filter(item => item.category === filterCategory); + }; + + const getStockStatus = (item: InventoryItem) => { + if (item.current_stock <= item.min_stock) return { status: 'low', color: 'red', text: 'Stock Bajo' }; + if (item.current_stock >= item.max_stock) return { status: 'high', color: 'blue', text: 'Stock Alto' }; + return { status: 'normal', color: 'green', text: 'Normal' }; + }; + + const isNearExpiry = (expiryDate?: string) => { + if (!expiryDate) return false; + const expiry = new Date(expiryDate); + const today = new Date(); + const diffDays = (expiry.getTime() - today.getTime()) / (1000 * 3600 * 24); + return diffDays <= 7; + }; + + const stats = { + total: items.length, + ingredients: items.filter(i => i.category === 'ingredient').length, + products: items.filter(i => i.category === 'finished_product').length, + lowStock: items.filter(i => i.current_stock <= i.min_stock).length, + nearExpiry: items.filter(i => isNearExpiry(i.expiry_date)).length, + refrigerated: items.filter(i => i.requires_refrigeration).length + }; + + return ( +
+ {/* Quick Actions */} + +
+
+

+ {stats.total} elementos configurados +

+
+ +
+ + +
+
+
+ + {/* Stats */} +
+ +

{stats.total}

+

Total

+
+ +

{stats.ingredients}

+

Ingredientes

+
+ +

{stats.products}

+

Productos

+
+ +

{stats.lowStock}

+

Stock Bajo

+
+ +

{stats.nearExpiry}

+

Por Vencer

+
+ +

{stats.refrigerated}

+

Refrigerado

+
+
+ + {/* Filters */} + +
+ +
+ {[ + { value: 'all', label: 'Todos' }, + { value: 'ingredient', label: 'Ingredientes' }, + { value: 'finished_product', label: 'Productos' } + ].map(filter => ( + + ))} +
+
+
+ + {/* Inventory Items */} +
+ {getFilteredItems().map((item) => { + const stockStatus = getStockStatus(item); + const nearExpiry = isNearExpiry(item.expiry_date); + + return ( + +
+
+ {/* Category Icon */} +
+ +
+ + {/* Item Info */} +
+

{item.name}

+ +
+ + {item.category === 'ingredient' ? 'Ingrediente' : 'Producto'} + + {item.requires_refrigeration && ( + ❄️ Refrigeración + )} + + {stockStatus.text} + + {nearExpiry && ( + Vence Pronto + )} +
+ +
+
+ Stock Actual: + + {item.current_stock} {item.unit} + +
+ +
+ Rango: + + {item.min_stock} - {item.max_stock} {item.unit} + +
+ + {item.expiry_date && ( +
+ Vencimiento: + + {new Date(item.expiry_date).toLocaleDateString()} + +
+ )} + + {item.cost_per_unit && ( +
+ Costo/Unidad: + + ${item.cost_per_unit.toFixed(2)} + +
+ )} +
+
+
+ + {/* Actions */} +
+ + +
+
+
+ ); + })} + + {getFilteredItems().length === 0 && ( + + +

No hay elementos en esta categoría

+
+ )} +
+ + {/* Warnings */} + {(stats.lowStock > 0 || stats.nearExpiry > 0) && ( + +
+ +
+

Advertencias de Inventario

+ {stats.lowStock > 0 && ( +

+ • {stats.lowStock} elemento(s) con stock bajo +

+ )} + {stats.nearExpiry > 0 && ( +

+ • {stats.nearExpiry} elemento(s) próximos a vencer +

+ )} +
+
+
+ )} + + {/* Edit Modal */} + {editingItem && ( +
+ +

+ {isAddingNew ? 'Agregar Elemento' : 'Editar Elemento'} +

+ + { + setEditingItem(null); + setIsAddingNew(false); + }} + /> +
+
+ )} + + {/* Information */} + +

+ 📦 Configuración de Inventario: +

+
    +
  • Stock Mínimo: Nivel que dispara alertas de reabastecimiento
  • +
  • Stock Máximo: Capacidad máxima de almacenamiento
  • +
  • Fechas de Vencimiento: Control automático de productos perecederos
  • +
  • Refrigeración: Identifica productos que requieren frío
  • +
+
+ +
+ ); +}; + +// Component for editing inventory items +interface InventoryItemFormProps { + item: InventoryItem; + onSave: (item: InventoryItem) => void; + onCancel: () => void; +} + +const InventoryItemForm: React.FC = ({ item, onSave, onCancel }) => { + const [formData, setFormData] = useState(item); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name.trim()) { + alert('El nombre es requerido'); + return; + } + if (formData.min_stock >= formData.max_stock) { + alert('El stock máximo debe ser mayor al mínimo'); + return; + } + onSave(formData); + }; + + return ( +
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Nombre del producto/ingrediente" + /> +
+ +
+
+ + +
+ +
+ + setFormData(prev => ({ ...prev, unit: e.target.value }))} + placeholder="kg, unidades, litros..." + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, current_stock: Number(e.target.value) }))} + min="0" + /> +
+ +
+ + setFormData(prev => ({ ...prev, min_stock: Number(e.target.value) }))} + min="0" + /> +
+ +
+ + setFormData(prev => ({ ...prev, max_stock: Number(e.target.value) }))} + min="1" + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, expiry_date: e.target.value }))} + /> +
+ +
+ + setFormData(prev => ({ ...prev, cost_per_unit: Number(e.target.value) }))} + min="0" + /> +
+
+ +
+ + setFormData(prev => ({ ...prev, supplier: e.target.value }))} + placeholder="Nombre del proveedor" + /> +
+ +
+ setFormData(prev => ({ ...prev, requires_refrigeration: e.target.checked }))} + className="rounded" + /> + +
+ +
+ + +
+
+ ); +}; \ 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 new file mode 100644 index 00000000..10c4d283 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx @@ -0,0 +1,780 @@ +import React, { useState, useEffect } from 'react'; +import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp } from 'lucide-react'; +import { Button, Card, Badge } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +interface TrainingMetrics { + accuracy: number; + precision: number; + recall: number; + f1_score: number; + training_loss: number; + validation_loss: number; + epochs_completed: number; + total_epochs: number; +} + +interface TrainingLog { + timestamp: string; + message: string; + level: 'info' | 'warning' | 'error' | 'success'; +} + +// Enhanced Mock ML training service that matches backend behavior +const mockMLService = { + startTraining: async (trainingData: any) => { + return new Promise((resolve) => { + let progress = 0; + let step_count = 0; + const total_steps = 12; + + // Backend-matching training steps and messages + const trainingSteps = [ + { step: 'data_validation', message: 'Validando datos de entrenamiento...', progress: 10 }, + { step: 'data_preparation_start', message: 'Preparando conjunto de datos...', progress: 20 }, + { step: 'feature_engineering', message: 'Creando características para el modelo...', progress: 30 }, + { step: 'data_preparation_complete', message: 'Preparación de datos completada', progress: 35 }, + { step: 'ml_training_start', message: 'Iniciando entrenamiento del modelo Prophet...', progress: 40 }, + { step: 'model_fitting', message: 'Ajustando modelo a los datos históricos...', progress: 55 }, + { step: 'pattern_detection', message: 'Detectando patrones estacionales y tendencias...', progress: 70 }, + { step: 'validation', message: 'Validando precisión del modelo...', progress: 80 }, + { step: 'training_complete', message: 'Entrenamiento ML completado', progress: 85 }, + { step: 'storing_models', message: 'Guardando modelo entrenado...', progress: 90 }, + { step: 'performance_metrics', message: 'Calculando métricas de rendimiento...', progress: 95 }, + { step: 'completed', message: 'Tu Asistente Inteligente está listo!', progress: 100 } + ]; + + const interval = setInterval(() => { + if (step_count >= trainingSteps.length) { + clearInterval(interval); + return; + } + + const currentStep = trainingSteps[step_count]; + progress = currentStep.progress; + + // Generate realistic metrics that improve over time + const stepProgress = step_count / trainingSteps.length; + const baseAccuracy = 0.65; + const maxAccuracy = 0.93; + const currentAccuracy = baseAccuracy + (maxAccuracy - baseAccuracy) * Math.pow(stepProgress, 0.8); + + const metrics: TrainingMetrics = { + accuracy: Math.min(maxAccuracy, currentAccuracy + (Math.random() * 0.02 - 0.01)), + precision: Math.min(0.95, 0.70 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)), + recall: Math.min(0.94, 0.68 + stepProgress * 0.24 + (Math.random() * 0.02 - 0.01)), + f1_score: Math.min(0.94, 0.69 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)), + training_loss: Math.max(0.08, 1.2 - stepProgress * 1.0 + (Math.random() * 0.05 - 0.025)), + validation_loss: Math.max(0.10, 1.3 - stepProgress * 1.1 + (Math.random() * 0.06 - 0.03)), + epochs_completed: Math.floor(stepProgress * 15) + 1, + total_epochs: 15 + }; + + // Generate step-specific logs that match backend behavior + const logs: TrainingLog[] = []; + + // Add the current step message + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: currentStep.message, + level: currentStep.step === 'completed' ? 'success' : 'info' + }); + + // Add specific logs based on the training step + if (currentStep.step === 'data_validation') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: `Productos analizados: ${trainingData.products?.length || 3}`, + level: 'info' + }); + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: `Registros de inventario: ${trainingData.inventory?.length || 3}`, + level: 'info' + }); + } else if (currentStep.step === 'ml_training_start') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: 'Usando modelo Prophet optimizado para panadería', + level: 'info' + }); + } else if (currentStep.step === 'model_fitting') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: `Precisión actual: ${(metrics.accuracy * 100).toFixed(1)}%`, + level: 'info' + }); + } else if (currentStep.step === 'pattern_detection') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: 'Patrones estacionales detectados: días de la semana, horas pico', + level: 'success' + }); + } else if (currentStep.step === 'validation') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: `MAE: ${(Math.random() * 2 + 1.5).toFixed(2)}, MAPE: ${((1 - metrics.accuracy) * 100).toFixed(1)}%`, + level: 'info' + }); + } else if (currentStep.step === 'storing_models') { + logs.push({ + timestamp: new Date().toLocaleTimeString(), + message: `Modelo guardado: bakery_prophet_${Date.now()}`, + level: 'success' + }); + } + + // Emit progress event that matches backend WebSocket message structure + window.dispatchEvent(new CustomEvent('mlTrainingProgress', { + detail: { + type: 'progress', + job_id: `training_${Date.now()}`, + data: { + progress, + current_step: currentStep.step, + step_details: currentStep.message, + products_completed: Math.floor(step_count / 3), + products_total: Math.max(3, trainingData.products?.length || 3), + estimated_time_remaining_minutes: Math.max(0, Math.floor((total_steps - step_count) * 0.75)) + }, + timestamp: new Date().toISOString(), + // Keep old format for backward compatibility + status: progress >= 100 ? 'completed' : 'training', + currentPhase: currentStep.message, + metrics, + logs + } + })); + + step_count++; + + if (progress >= 100) { + clearInterval(interval); + + // Generate final results matching backend response structure + const finalAccuracy = 0.89 + Math.random() * 0.05; + const finalResult = { + success: true, + job_id: `training_${Date.now()}`, + tenant_id: 'demo_tenant', + status: 'completed', + training_results: { + total_products: Math.max(3, trainingData.products?.length || 3), + successful_trainings: Math.max(3, trainingData.products?.length || 3), + failed_trainings: 0, + products: Array.from({length: Math.max(3, trainingData.products?.length || 3)}, (_, i) => ({ + inventory_product_id: `product_${i + 1}`, + status: 'completed', + model_id: `model_${i + 1}_${Date.now()}`, + data_points: 45 + Math.floor(Math.random() * 30), + metrics: { + mape: (1 - finalAccuracy) * 100 + (Math.random() * 2 - 1), + mae: 1.5 + Math.random() * 1.0, + rmse: 2.1 + Math.random() * 1.2, + r2_score: finalAccuracy + (Math.random() * 0.02 - 0.01) + } + })), + overall_training_time_seconds: 45 + Math.random() * 15 + }, + data_summary: { + sales_records: trainingData.inventory?.length * 30 || 1500, + weather_records: 90, + traffic_records: 85, + date_range: { + start: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(), + end: new Date().toISOString() + }, + data_sources_used: ['bakery_sales', 'weather_forecast', 'madrid_traffic'], + constraints_applied: {} + }, + finalMetrics: { + accuracy: finalAccuracy, + precision: finalAccuracy * 0.98, + recall: finalAccuracy * 0.96, + f1_score: finalAccuracy * 0.97, + training_loss: 0.12 + Math.random() * 0.05, + validation_loss: 0.15 + Math.random() * 0.05, + total_epochs: 15 + }, + modelId: `bakery_prophet_${Date.now()}`, + deploymentUrl: '/api/v1/training/models/predict', + trainingDuration: `${(45 + Math.random() * 15).toFixed(1)} segundos`, + datasetSize: `${trainingData.inventory?.length * 30 || 1500} registros`, + modelVersion: '2.1.0', + completed_at: new Date().toISOString() + }; + + resolve(finalResult); + } + }, 1800); // Matches backend timing + }); + } +}; + +const getTrainingPhase = (epoch: number, total: number) => { + const progress = epoch / total; + if (progress <= 0.3) return 'Inicializando modelo de IA...'; + if (progress <= 0.6) return 'Entrenando patrones de venta...'; + if (progress <= 0.8) return 'Optimizando predicciones...'; + if (progress <= 0.95) return 'Validando rendimiento...'; + return 'Finalizando entrenamiento...'; +}; + +export const MLTrainingStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [trainingStatus, setTrainingStatus] = useState(data.trainingStatus || 'pending'); + const [progress, setProgress] = useState(data.trainingProgress || 0); + const [currentPhase, setCurrentPhase] = useState(data.currentPhase || ''); + const [metrics, setMetrics] = useState(data.trainingMetrics || null); + const [logs, setLogs] = useState(data.trainingLogs || []); + const [finalResults, setFinalResults] = useState(data.finalResults || null); + + // New state variables for backend-compatible data + const [productsCompleted, setProductsCompleted] = useState(0); + const [productsTotal, setProductsTotal] = useState(0); + const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null); + + useEffect(() => { + // Listen for ML training progress updates + const handleProgress = (event: CustomEvent) => { + const detail = event.detail; + + // Handle new backend-compatible WebSocket message format + if (detail.type === 'progress' && detail.data) { + const progressData = detail.data; + setProgress(progressData.progress || 0); + setCurrentPhase(progressData.step_details || progressData.current_step || 'En progreso...'); + + // Update products progress if available + if (progressData.products_completed !== undefined) { + setProductsCompleted(progressData.products_completed); + } + if (progressData.products_total !== undefined) { + setProductsTotal(progressData.products_total); + } + + // Update time estimate if available + if (progressData.estimated_time_remaining_minutes !== undefined) { + setEstimatedTimeRemaining(progressData.estimated_time_remaining_minutes); + } + + // Handle metrics (from legacy format for backward compatibility) + if (detail.metrics) { + setMetrics(detail.metrics); + } + + // Handle logs (from legacy format for backward compatibility) + if (detail.logs && detail.logs.length > 0) { + setLogs(prev => [...prev, ...detail.logs]); + } + + // Check completion status + if (progressData.progress >= 100) { + setTrainingStatus('completed'); + } else { + setTrainingStatus('training'); + } + } else { + // Handle legacy format for backward compatibility + const { progress: newProgress, metrics: newMetrics, logs: newLogs, status, currentPhase: newPhase } = detail; + + setProgress(newProgress || 0); + setMetrics(newMetrics); + setCurrentPhase(newPhase || 'En progreso...'); + setTrainingStatus(status || 'training'); + + if (newLogs && newLogs.length > 0) { + setLogs(prev => [...prev, ...newLogs]); + } + } + }; + + window.addEventListener('mlTrainingProgress', handleProgress as EventListener); + return () => window.removeEventListener('mlTrainingProgress', handleProgress as EventListener); + }, []); + + useEffect(() => { + // Update parent data when state changes + onDataChange({ + ...data, + trainingStatus, + trainingProgress: progress, + currentPhase, + trainingMetrics: metrics, + trainingLogs: logs, + finalResults + }); + }, [trainingStatus, progress, currentPhase, metrics, logs, finalResults]); + + // Auto-start training when coming from suppliers step + useEffect(() => { + if (data.autoStartTraining && trainingStatus === 'pending') { + const timer = setTimeout(() => { + startTraining(); + // Remove the auto-start flag so it doesn't trigger again + onDataChange({ ...data, autoStartTraining: false }); + }, 1000); + + return () => clearTimeout(timer); + } + }, [data.autoStartTraining, trainingStatus]); + + const startTraining = async () => { + // Access data from previous steps through allStepData + const inventoryData = data.allStepData?.inventory?.inventoryItems || + data.allStepData?.['inventory-setup']?.inventoryItems || + data.inventoryItems || []; + + const detectedProducts = data.allStepData?.review?.detectedProducts?.filter((p: any) => p.status === 'approved') || + data.allStepData?.['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') || + data.detectedProducts?.filter((p: any) => p.status === 'approved') || []; + + const salesValidation = data.allStepData?.['data-processing']?.validation || + data.allStepData?.review?.validation || + data.validation || {}; + + // If no data is available, create mock data for demo purposes + let finalInventoryData = inventoryData; + let finalDetectedProducts = detectedProducts; + let finalValidation = salesValidation; + + if (inventoryData.length === 0 && detectedProducts.length === 0) { + console.log('No data found from previous steps, using mock data for demo'); + + // Create mock data for demonstration + finalInventoryData = [ + { id: '1', name: 'Harina de Trigo', category: 'ingredient', current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg' }, + { id: '2', name: 'Levadura Fresca', category: 'ingredient', current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg' }, + { id: '3', name: 'Pan Integral', category: 'finished_product', current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades' } + ]; + + finalDetectedProducts = [ + { name: 'Pan Francés', status: 'approved', category: 'Panadería' }, + { name: 'Croissants', status: 'approved', category: 'Repostería' }, + { name: 'Pan Integral', status: 'approved', category: 'Panadería' } + ]; + + finalValidation = { + total_records: 1500, + summary: { + date_range: '2024-01-01 to 2024-12-31' + } + }; + } + + setTrainingStatus('training'); + setProgress(0); + setLogs([{ + timestamp: new Date().toLocaleTimeString(), + message: 'Iniciando entrenamiento del modelo de Machine Learning...', + level: 'info' + }]); + + try { + const result = await mockMLService.startTraining({ + products: finalDetectedProducts, + inventory: finalInventoryData, + salesData: finalValidation + }); + + setFinalResults(result); + setTrainingStatus('completed'); + + } catch (error) { + console.error('ML Training error:', error); + setTrainingStatus('error'); + setLogs(prev => [...prev, { + timestamp: new Date().toLocaleTimeString(), + message: 'Error durante el entrenamiento del modelo', + level: 'error' + }]); + } + }; + + const retryTraining = () => { + setTrainingStatus('pending'); + setProgress(0); + setMetrics(null); + setLogs([]); + setFinalResults(null); + }; + + const getLogIcon = (level: string) => { + switch (level) { + case 'success': return '✅'; + case 'warning': return '⚠️'; + case 'error': return '❌'; + default: return 'ℹ️'; + } + }; + + const getLogColor = (level: string) => { + switch (level) { + case 'success': return 'text-[var(--color-success)]'; + case 'warning': return 'text-[var(--color-warning)]'; + case 'error': return 'text-[var(--color-error)]'; + default: return 'text-[var(--text-secondary)]'; + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+

+ Tu asistente inteligente analizará tus datos históricos para ayudarte a tomar mejores decisiones de negocio +

+
+ + {/* Auto-start notification */} + {data.autoStartTraining && trainingStatus === 'pending' && ( + +
+
+

+ Creando tu asistente inteligente automáticamente... +

+
+
+ )} + + {/* Training Controls */} + {trainingStatus === 'pending' && !data.autoStartTraining && ( + +

+ Crear tu Asistente Inteligente +

+

+ Analizaremos tus datos de ventas y productos para crear un asistente que te ayude a predecir demanda y optimizar tu inventario. +

+ +
+
+ +

Predicción de Ventas

+

Anticipa cuánto vas a vender

+
+ +
+ +

Recomendaciones

+

Cuánto producir y comprar

+
+ +
+ +

Alertas Automáticas

+

Te avisa cuando reordenar

+
+
+ +
+

Información que analizaremos:

+
+
+ Productos: +
+ {(() => { + const allStepData = data.allStepData || {}; + const detectedProducts = allStepData.review?.detectedProducts?.filter((p: any) => p.status === 'approved') || + allStepData['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') || + data.detectedProducts?.filter((p: any) => p.status === 'approved') || []; + return detectedProducts.length || 3; // fallback to mock data count + })()} +
+
+ Inventario: +
+ {(() => { + const allStepData = data.allStepData || {}; + const inventoryData = allStepData.inventory?.inventoryItems || + allStepData['inventory-setup']?.inventoryItems || + allStepData['inventory']?.inventoryItems || + data.inventoryItems || []; + return inventoryData.length || 3; // fallback to mock data count + })()} elementos +
+
+ Registros: +
+ {(() => { + const allStepData = data.allStepData || {}; + const validation = allStepData['data-processing']?.validation || + allStepData.review?.validation || + data.validation || {}; + return validation.total_records || 1500; // fallback to mock data + })()} +
+
+ Período: +
+ {(() => { + const allStepData = data.allStepData || {}; + const validation = allStepData['data-processing']?.validation || + allStepData.review?.validation || + data.validation || {}; + const dateRange = validation.summary?.date_range || '2024-01-01 to 2024-12-31'; + return dateRange.split(' to ').map((date: string) => + new Date(date).toLocaleDateString() + ).join(' - '); + })()} +
+
+
+ + +
+ )} + + {/* Training Progress */} + {trainingStatus === 'training' && ( +
+ +
+
+ +
+

+ Creando tu Asistente Inteligente... +

+

{currentPhase}

+
+ + {/* Progress Bar */} +
+
+ + Progreso de Creación + +
+ + {progress.toFixed(0)}% + + {productsTotal > 0 && ( +
+ {productsCompleted}/{productsTotal} productos +
+ )} + {estimatedTimeRemaining !== null && estimatedTimeRemaining > 0 && ( +
+ ~{estimatedTimeRemaining} min restantes +
+ )} +
+
+
+
+
+
+
+
+ + {/* Current Metrics */} + {metrics && ( +
+
+

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

+

Precisión

+
+
+

+ {metrics.training_loss.toFixed(3)} +

+

Loss

+
+
+

+ {metrics.epochs_completed}/{metrics.total_epochs} +

+

Epochs

+
+
+

+ {(metrics.f1_score * 100).toFixed(1)}% +

+

F1-Score

+
+
+ )} + + + {/* Training Logs */} + +

Progreso del Asistente

+
+ {logs.slice(-10).map((log, index) => ( +
+ [{log.timestamp}] + {getLogIcon(log.level)} {log.message} +
+ ))} +
+
+
+ )} + + {/* Training Completed */} + {trainingStatus === 'completed' && finalResults && ( +
+ +
+
+ +
+

+ ¡Tu Asistente Inteligente está Listo! +

+

+ Tu asistente personalizado está listo para ayudarte con predicciones y recomendaciones +

+
+ + {/* Final Metrics */} +
+
+

+ {(finalResults.finalMetrics.accuracy * 100).toFixed(1)}% +

+

Exactitud

+
+
+

+ {(finalResults.finalMetrics.precision * 100).toFixed(1)}% +

+

Confiabilidad

+
+
+

+ {(finalResults.finalMetrics.recall * 100).toFixed(1)}% +

+

Cobertura

+
+
+

+ {(finalResults.finalMetrics.f1_score * 100).toFixed(1)}% +

+

Rendimiento

+
+
+

+ {finalResults.finalMetrics.total_epochs} +

+

Iteraciones

+
+
+ + {/* Model Info */} +
+

Información del Modelo:

+
+
+ ID del Modelo: +

{finalResults.modelId}

+
+
+ Versión: +

{finalResults.modelVersion}

+
+
+ Duración: +

{finalResults.trainingDuration}

+
+
+ Dataset: +

{finalResults.datasetSize}

+
+
+ Endpoint: +

{finalResults.deploymentUrl}

+
+
+ Estado: +

✓ Activo

+
+
+
+ + {/* Capabilities */} +
+
+ +

Predicción de Ventas

+

+ Te dice cuánto vas a vender cada día +

+
+ +
+ +

Recomendaciones

+

+ Te sugiere cuánto producir y comprar +

+
+ +
+ +

Alertas Automáticas

+

+ Te avisa cuando necesitas reordenar +

+
+
+
+
+ )} + + {/* Training Error */} + {trainingStatus === 'error' && ( + +
+
+ +
+

+ Error al Crear el Asistente +

+

+ Ocurrió un problema durante la creación de tu asistente inteligente. Por favor, intenta nuevamente. +

+ +
+
+ )} + + {/* Information */} + +

+ 🎯 ¿Cómo funciona tu Asistente Inteligente? +

+
    +
  • Analiza tus ventas: Estudia tus datos históricos para encontrar patrones
  • +
  • Entiende tu negocio: Aprende sobre temporadas altas y bajas
  • +
  • Hace predicciones: Te dice cuánto vas a vender cada día
  • +
  • Da recomendaciones: Te sugiere cuánto producir y comprar
  • +
  • Mejora con el tiempo: Se hace más inteligente con cada venta nueva
  • +
+
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx b/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx new file mode 100644 index 00000000..77834d86 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from 'react'; +import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react'; +import { Button, Card, Badge } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +interface Product { + id: string; + name: string; + category: string; + confidence: number; + sales_count: number; + estimated_price: number; + status: 'approved' | 'rejected' | 'pending'; + notes?: string; +} + +// Mock detected products +const mockDetectedProducts: Product[] = [ + { id: '1', name: 'Pan Integral', category: 'Panadería', confidence: 95, sales_count: 45, estimated_price: 2.50, status: 'pending' }, + { id: '2', name: 'Croissant', category: 'Bollería', confidence: 92, sales_count: 38, estimated_price: 1.80, status: 'pending' }, + { id: '3', name: 'Baguette', category: 'Panadería', confidence: 88, sales_count: 22, estimated_price: 3.00, status: 'pending' }, + { id: '4', name: 'Empanada de Pollo', category: 'Salados', confidence: 85, sales_count: 31, estimated_price: 4.50, status: 'pending' }, + { id: '5', name: 'Tarta de Manzana', category: 'Repostería', confidence: 78, sales_count: 12, estimated_price: 15.00, status: 'pending' }, + { id: '6', name: 'Pan de Centeno', category: 'Panadería', confidence: 91, sales_count: 18, estimated_price: 2.80, status: 'pending' }, + { id: '7', name: 'Medialunas', category: 'Bollería', confidence: 87, sales_count: 29, estimated_price: 1.20, status: 'pending' }, + { id: '8', name: 'Sandwich Mixto', category: 'Salados', confidence: 82, sales_count: 25, estimated_price: 5.50, status: 'pending' } +]; + +export const ReviewStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + // Generate products from processing results or use mock data + const generateProductsFromResults = (results: any) => { + if (!results?.product_list) return mockDetectedProducts; + + return results.product_list.map((name: string, index: number) => ({ + id: (index + 1).toString(), + name, + category: index < 3 ? 'Panadería' : index < 5 ? 'Bollería' : 'Salados', + confidence: Math.max(75, results.confidenceScore - Math.random() * 15), + sales_count: Math.floor(Math.random() * 50) + 10, + estimated_price: Math.random() * 5 + 1.5, + status: 'pending' as const + })); + }; + + const [products, setProducts] = useState( + data.detectedProducts || generateProductsFromResults(data.processingResults) + ); + const [selectedCategory, setSelectedCategory] = useState('all'); + + const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))]; + + useEffect(() => { + onDataChange({ + ...data, + detectedProducts: products, + reviewCompleted: products.every(p => p.status !== 'pending') + }); + }, [products]); + + const handleProductAction = (productId: string, action: 'approve' | 'reject') => { + setProducts(prev => prev.map(product => + product.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.id === product.id) + ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } + : product + )); + }; + + const getFilteredProducts = () => { + if (selectedCategory === 'all') { + return products; + } + return products.filter(p => p.category === selectedCategory); + }; + + const stats = { + 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 + }; + + return ( +
+ {/* Summary Stats */} +
+
+

{stats.total}

+

Total

+
+
+

{stats.approved}

+

Aprobados

+
+
+

{stats.rejected}

+

Rechazados

+
+
+

{stats.pending}

+

Pendientes

+
+
+ + {/* Filters and Actions */} + +
+
+ + +
+ +
+ + +
+
+
+ + {/* Products List */} +
+ {getFilteredProducts().map((product) => ( + +
+
+ {/* Status Icon */} +
+ {product.status === 'approved' ? ( + + ) : product.status === 'rejected' ? ( + + ) : ( + + )} +
+ + {/* Product Info */} +
+

{product.name}

+ +
+ {product.category} + {product.status !== 'pending' && ( + + {product.status === 'approved' ? 'Aprobado' : 'Rechazado'} + + )} +
+ +
+ = 90 + ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' + : product.confidence >= 75 + ? 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]' + : 'bg-[var(--color-error)]/10 text-[var(--color-error)]' + }`}> + {product.confidence}% confianza + + {product.sales_count} ventas + ${product.estimated_price.toFixed(2)} +
+
+
+ + {/* Actions */} +
+ {product.status === 'pending' ? ( + <> + + + + ) : ( + + )} +
+
+
+ ))} +
+ + {/* Progress Indicator */} + {stats.pending > 0 && ( + +
+ +
+

+ {stats.pending} productos pendientes de revisión +

+

+ Revisa todos los productos antes de continuar al siguiente paso +

+
+
+
+ )} + + {/* Help Information */} + +

+ 💡 Consejos para la revisión: +

+
    +
  • Confianza alta (90%+): Productos identificados con alta precisión
  • +
  • Confianza media (75-89%): Revisar nombres y categorías
  • +
  • Confianza baja (<75%): Verificar que corresponden a tu catálogo
  • +
  • • Usa las acciones masivas para aprobar/rechazar por categoría completa
  • +
+
+
+ ); +}; \ 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 new file mode 100644 index 00000000..772cdc89 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/SuppliersStep.tsx @@ -0,0 +1,585 @@ +import React, { useState, useEffect } from 'react'; +import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin } from 'lucide-react'; +import { Button, Card, Input, Badge } from '../../../ui'; +import { OnboardingStepProps } from '../OnboardingWizard'; + +interface Supplier { + id: string; + name: string; + contact_person: string; + phone: string; + email: string; + address: string; + categories: string[]; + payment_terms: string; + delivery_days: string[]; + min_order_amount?: number; + notes?: string; + status: 'active' | 'inactive'; + created_at: string; +} + +// Mock suppliers +const mockSuppliers: Supplier[] = [ + { + id: '1', + name: 'Molinos del Sur', + contact_person: 'Juan Pérez', + phone: '+1 555-0123', + email: 'ventas@molinosdelsur.com', + address: 'Av. Industrial 123, Zona Sur', + categories: ['Harinas', 'Granos'], + payment_terms: '30 días', + delivery_days: ['Lunes', 'Miércoles', 'Viernes'], + min_order_amount: 200, + notes: 'Proveedor principal de harinas, muy confiable', + status: 'active', + created_at: '2024-01-15' + }, + { + id: '2', + name: 'Lácteos Premium', + contact_person: 'María González', + phone: '+1 555-0456', + email: 'pedidos@lacteospremium.com', + address: 'Calle Central 456, Centro', + categories: ['Lácteos', 'Mantequillas', 'Quesos'], + payment_terms: '15 días', + delivery_days: ['Martes', 'Jueves', 'Sábado'], + min_order_amount: 150, + status: 'active', + created_at: '2024-01-20' + } +]; + +const daysOfWeek = [ + 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo' +]; + +const commonCategories = [ + 'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos', + 'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes' +]; + +export const SuppliersStep: React.FC = ({ + data, + onDataChange, + onNext, + onPrevious, + isFirstStep, + isLastStep +}) => { + const [suppliers, setSuppliers] = useState( + data.suppliers || mockSuppliers + ); + const [editingSupplier, setEditingSupplier] = useState(null); + const [isAddingNew, setIsAddingNew] = useState(false); + const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all'); + + useEffect(() => { + onDataChange({ + ...data, + suppliers: suppliers, + suppliersConfigured: true // This step is optional + }); + }, [suppliers]); + + const handleAddSupplier = () => { + const newSupplier: Supplier = { + id: Date.now().toString(), + name: '', + contact_person: '', + phone: '', + email: '', + address: '', + categories: [], + payment_terms: '30 días', + delivery_days: [], + status: 'active', + created_at: new Date().toISOString().split('T')[0] + }; + setEditingSupplier(newSupplier); + setIsAddingNew(true); + }; + + const handleSaveSupplier = (supplier: Supplier) => { + if (isAddingNew) { + setSuppliers(prev => [...prev, supplier]); + } else { + setSuppliers(prev => prev.map(s => s.id === supplier.id ? supplier : s)); + } + setEditingSupplier(null); + setIsAddingNew(false); + }; + + const handleDeleteSupplier = (id: string) => { + if (window.confirm('¿Estás seguro de eliminar este proveedor?')) { + setSuppliers(prev => prev.filter(s => s.id !== id)); + } + }; + + const toggleSupplierStatus = (id: string) => { + setSuppliers(prev => prev.map(s => + s.id === id + ? { ...s, status: s.status === 'active' ? 'inactive' : 'active' } + : s + )); + }; + + const getFilteredSuppliers = () => { + return filterStatus === 'all' + ? suppliers + : suppliers.filter(s => s.status === filterStatus); + }; + + const stats = { + total: suppliers.length, + active: suppliers.filter(s => s.status === 'active').length, + inactive: suppliers.filter(s => s.status === 'inactive').length, + categories: Array.from(new Set(suppliers.flatMap(s => s.categories))).length + }; + + 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.categories}

+

Categorías

+
+
+ + {/* 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.status === 'active' ? 'Activo' : 'Inactivo'} + + {supplier.categories.slice(0, 2).map((cat, idx) => ( + {cat} + ))} + {supplier.categories.length > 2 && ( + +{supplier.categories.length - 2} + )} +
+ +
+
+ Contacto: + {supplier.contact_person} +
+ +
+ Entrega: + + {supplier.delivery_days.slice(0, 2).join(', ')} + {supplier.delivery_days.length > 2 && ` +${supplier.delivery_days.length - 2}`} + +
+ +
+ Pago: + {supplier.payment_terms} +
+ + {supplier.min_order_amount && ( +
+ Mín: + ${supplier.min_order_amount} +
+ )} +
+ +
+
+ + {supplier.phone} +
+
+ + {supplier.email} +
+
+ + {supplier.address} +
+
+ + {supplier.notes && ( +
+ Notas: + {supplier.notes} +
+ )} +
+
+ + {/* Actions */} +
+ + + + + +
+
+
+ ))} + + {getFilteredSuppliers().length === 0 && ( + + +

No hay proveedores en esta categoría

+ +
+ )} +
+ + {/* Edit Modal */} + {editingSupplier && ( +
+ +

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

+ + { + setEditingSupplier(null); + setIsAddingNew(false); + }} + /> +
+
+ )} + + {/* Information */} + +

+ 🚚 Gestión de Proveedores: +

+
    +
  • Este paso es opcional - puedes configurar proveedores más tarde
  • +
  • • Define categorías de productos para facilitar la búsqueda
  • +
  • • Establece días de entrega y términos de pago
  • +
  • • Configura montos mínimos de pedido para optimizar compras
  • +
+
+ +
+ ); +}; + +// Component for editing suppliers +interface SupplierFormProps { + supplier: Supplier; + onSave: (supplier: Supplier) => void; + onCancel: () => void; +} + +const SupplierForm: React.FC = ({ supplier, onSave, onCancel }) => { + const [formData, setFormData] = useState(supplier); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name.trim()) { + alert('El nombre es requerido'); + return; + } + if (!formData.contact_person.trim()) { + alert('El contacto es requerido'); + return; + } + onSave(formData); + }; + + const toggleCategory = (category: string) => { + setFormData(prev => ({ + ...prev, + categories: prev.categories.includes(category) + ? prev.categories.filter(c => c !== category) + : [...prev.categories, category] + })); + }; + + const toggleDeliveryDay = (day: string) => { + setFormData(prev => ({ + ...prev, + delivery_days: prev.delivery_days.includes(day) + ? prev.delivery_days.filter(d => d !== day) + : [...prev.delivery_days, day] + })); + }; + + return ( +
+
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Molinos del Sur" + /> +
+ +
+ + setFormData(prev => ({ ...prev, contact_person: e.target.value }))} + placeholder="Juan Pérez" + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, phone: e.target.value }))} + placeholder="+1 555-0123" + /> +
+ +
+ + setFormData(prev => ({ ...prev, email: e.target.value }))} + placeholder="ventas@proveedor.com" + /> +
+
+ +
+ + setFormData(prev => ({ ...prev, address: e.target.value }))} + placeholder="Av. Industrial 123, Zona Sur" + /> +
+ +
+ +
+ {commonCategories.map(category => ( + + ))} +
+
+ +
+
+ + +
+ +
+ + setFormData(prev => ({ ...prev, min_order_amount: Number(e.target.value) }))} + placeholder="200" + min="0" + /> +
+
+ +
+ +
+ {daysOfWeek.map(day => ( + + ))} +
+
+ +
+ +