diff --git a/frontend/src/api/services/notificationService.ts b/frontend/src/api/services/notificationService.ts index ca53c973..048d85cf 100644 --- a/frontend/src/api/services/notificationService.ts +++ b/frontend/src/api/services/notificationService.ts @@ -107,7 +107,7 @@ export class NotificationService { */ async sendNotification(notification: NotificationCreate): Promise { const response = await apiClient.post>( - '/notifications/send', + '/api/v1/notifications/send', notification ); return response.data!; @@ -118,7 +118,7 @@ export class NotificationService { */ async sendBulkNotifications(request: BulkNotificationRequest): Promise { const response = await apiClient.post>( - '/notifications/bulk', + '/api/v1/notifications/bulk', request ); return response.data!; @@ -140,7 +140,7 @@ export class NotificationService { page: number; pages: number; }> { - const response = await apiClient.get>('/notifications/history', { params }); + const response = await apiClient.get>('/api/v1/notifications/history', { params }); return response.data!; } @@ -149,7 +149,7 @@ export class NotificationService { */ async getNotification(notificationId: string): Promise { const response = await apiClient.get>( - `/notifications/${notificationId}` + `/api/v1/notifications/${notificationId}` ); return response.data!; } @@ -159,7 +159,7 @@ export class NotificationService { */ async retryNotification(notificationId: string): Promise { const response = await apiClient.post>( - `/notifications/${notificationId}/retry` + `/api/v1/notifications/${notificationId}/retry` ); return response.data!; } @@ -168,7 +168,7 @@ export class NotificationService { * Cancel scheduled notification */ async cancelNotification(notificationId: string): Promise { - await apiClient.post(`/notifications/${notificationId}/cancel`); + await apiClient.post(`/api/v1/notifications/${notificationId}/cancel`); } /** @@ -180,7 +180,7 @@ export class NotificationService { type?: string; }): Promise { const response = await apiClient.get>( - '/notifications/stats', + '/api/v1/notifications/stats', { params } ); return response.data!; @@ -191,7 +191,7 @@ export class NotificationService { */ async getBulkStatus(batchId: string): Promise { const response = await apiClient.get>( - `/notifications/bulk/${batchId}/status` + `/api/v1/notifications/bulk/${batchId}/status` ); return response.data!; } @@ -210,7 +210,7 @@ export class NotificationService { page: number; pages: number; }> { - const response = await apiClient.get>('/notifications/templates', { params }); + const response = await apiClient.get>('/api/v1/notifications/templates', { params }); return response.data!; } @@ -219,7 +219,7 @@ export class NotificationService { */ async getTemplate(templateId: string): Promise { const response = await apiClient.get>( - `/notifications/templates/${templateId}` + `/api/v1/notifications/templates/${templateId}` ); return response.data!; } @@ -236,7 +236,7 @@ export class NotificationService { variables?: string[]; }): Promise { const response = await apiClient.post>( - '/notifications/templates', + '/api/v1/notifications/templates', template ); return response.data!; @@ -250,7 +250,7 @@ export class NotificationService { updates: Partial ): Promise { const response = await apiClient.put>( - `/notifications/templates/${templateId}`, + `/api/v1/notifications/templates/${templateId}`, updates ); return response.data!; @@ -260,7 +260,7 @@ export class NotificationService { * Delete notification template */ async deleteTemplate(templateId: string): Promise { - await apiClient.delete(`/notifications/templates/${templateId}`); + await apiClient.delete(`/api/v1/notifications/templates/${templateId}`); } /** @@ -268,7 +268,7 @@ export class NotificationService { */ async getPreferences(): Promise { const response = await apiClient.get>( - '/notifications/preferences' + '/api/v1/notifications/preferences' ); return response.data!; } @@ -278,7 +278,7 @@ export class NotificationService { */ async updatePreferences(preferences: Partial): Promise { const response = await apiClient.put>( - '/notifications/preferences', + '/api/v1/notifications/preferences', preferences ); return response.data!; @@ -293,7 +293,7 @@ export class NotificationService { delivery_time_ms?: number; }> { const response = await apiClient.post>( - '/notifications/test', + '/api/v1/notifications/test', { type, recipient } ); return response.data!; @@ -320,7 +320,7 @@ export class NotificationService { page: number; pages: number; }> { - const response = await apiClient.get>('/notifications/webhooks', { params }); + const response = await apiClient.get>('/api/v1/notifications/webhooks', { params }); return response.data!; } @@ -333,7 +333,7 @@ export class NotificationService { webhook_url: string; created_at: string; }> { - const response = await apiClient.post>('/notifications/subscribe', { + const response = await apiClient.post>('/api/v1/notifications/subscribe', { events, webhook_url: webhookUrl, }); @@ -344,7 +344,7 @@ export class NotificationService { * Unsubscribe from notification events */ async unsubscribeFromEvents(subscriptionId: string): Promise { - await apiClient.delete(`/notifications/subscribe/${subscriptionId}`); + await apiClient.delete(`/api/v1/notifications/subscribe/${subscriptionId}`); } } diff --git a/frontend/src/api/services/tenantService.ts b/frontend/src/api/services/tenantService.ts index ec4519cb..048efc39 100644 --- a/frontend/src/api/services/tenantService.ts +++ b/frontend/src/api/services/tenantService.ts @@ -98,7 +98,7 @@ export class TenantService { * Corresponds to POST /tenants/register */ async registerBakery(bakeryData: TenantCreate): Promise { - const response = await apiClient.post>('/api/v1/tenants/register', bakeryData); + const response = await apiClient.post('/api/v1/tenants/register', bakeryData); return response.data!; } diff --git a/frontend/src/pages/onboarding.tsx b/frontend/src/pages/onboarding.tsx index 140bbabd..bcea6b00 100644 --- a/frontend/src/pages/onboarding.tsx +++ b/frontend/src/pages/onboarding.tsx @@ -1,311 +1,260 @@ -// frontend/src/pages/onboarding.tsx - ORIGINAL DESIGN WITH AUTH FIXES ONLY import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; -import { - CheckIcon, - ArrowRightIcon, +import { + CheckIcon, + ArrowRightIcon, ArrowLeftIcon, CloudArrowUpIcon, BuildingStorefrontIcon, UserCircleIcon, - CpuChipIcon + CpuChipIcon, + SparklesIcon, + ChartBarIcon, + ExclamationTriangleIcon, + XMarkIcon } from '@heroicons/react/24/outline'; -import { SalesUploader } from '../components/data/SalesUploader'; -import { TrainingProgressCard } from '../components/training/TrainingProgressCard'; -import { useAuth, RegisterData } from '../contexts/AuthContext'; -import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api'; -import { NotificationToast } from '../components/common/NotificationToast'; -import { Product, defaultProducts } from '../components/common/ProductSelector'; +// Real API services imports import { - TenantCreate + TenantUser, } from '@/api/services'; import api from '@/api/services'; -// Define the shape of the form data -interface OnboardingFormData { - // Step 1: User Registration - full_name: string; - email: string; - password: string; - confirm_password: string; +import { useAuth } from '../contexts/AuthContext'; - // Step 2: Bakery Information - bakery_name: string; - address: string; - city: string; - postal_code: string; - has_nearby_schools: boolean; - has_nearby_offices: boolean; - selected_products: Product[]; - - // Step 3: Sales History File - salesFile: File | null; - - // Step 4: Model Training status - trainingStatus: 'pending' | 'running' | 'completed' | 'failed'; - trainingTaskId: string | null; -} - -const OnboardingPage: React.FC = () => { - const router = useRouter(); - const { user, register } = useAuth(); // FIXED: Removed login - not needed anymore +const OnboardingPage = () => { const [currentStep, setCurrentStep] = useState(1); - const [completedSteps, setCompletedSteps] = useState([]); + const [completedSteps, setCompletedSteps] = useState([]); const [loading, setLoading] = useState(false); - const [notification, setNotification] = useState<{ - id: string; - type: 'success' | 'error' | 'warning' | 'info'; - title: string; - message: string; - } | null>(null); - - const [formData, setFormData] = useState({ + const [notification, setNotification] = useState(null); + const [currentTenantId, setCurrentTenantId] = useState(null); + const [formData, setFormData] = useState({ full_name: '', email: '', password: '', confirm_password: '', bakery_name: '', address: '', - city: 'Madrid', // Default to Madrid + city: 'Madrid', postal_code: '', has_nearby_schools: false, has_nearby_offices: false, - selected_products: defaultProducts.slice(0, 3), // Default selected products + selected_products: ['Pan de molde', 'Croissants', 'Magdalenas'], salesFile: null, trainingStatus: 'pending', trainingTaskId: null, + trainingProgress: 0, + business_type: 'bakery' }); + const [errors, setErrors] = useState({}); + const [uploadValidation, setUploadValidation] = useState(null); + const [trainingProgress, setTrainingProgress] = useState(null); - const [errors, setErrors] = useState>({}); + const fileInputRef = useRef(null); - const addressInputRef = useRef(null); // Ref for the address input - let autocompleteTimeout: NodeJS.Timeout | null = null; // For debouncing API calls - - const handleAddressInputChange = useCallback((e: React.ChangeEvent) => { - const query = e.target.value; - setFormData(prevData => ({ ...prevData, address: query })); // Update address immediately - - if (autocompleteTimeout) { - clearTimeout(autocompleteTimeout); + // Steps configuration + const steps = [ + { + id: 1, + title: 'Crear Cuenta', + subtitle: 'Información personal', + icon: UserCircleIcon, + color: 'bg-blue-500' + }, + { + id: 2, title: 'Datos de Panadería', + subtitle: 'Información del negocio', + icon: BuildingStorefrontIcon, + color: 'bg-purple-500' + }, + { + id: 3, + title: 'Historial de Ventas', + subtitle: 'Subir datos históricos', + icon: CloudArrowUpIcon, + color: 'bg-green-500' + }, + { + id: 4, + title: 'Entrenar Modelos', + subtitle: 'IA analizando datos', + icon: CpuChipIcon, + color: 'bg-orange-500' + }, + { + id: 5, + title: '¡Listo!', + subtitle: 'Sistema configurado', + icon: SparklesIcon, + color: 'bg-pink-500' } + ]; - if (query.length < 3) { // Only search if at least 3 characters are typed - return; - } - - autocompleteTimeout = setTimeout(async () => { - try { - // Construct the Nominatim API URL - // Make sure NOMINATIM_PORT matches your .env file, default is 8080 - const gatewayNominatimApiUrl = `/api/v1/nominatim/search`; // Relative path if frontend serves from gateway's domain/port - - const params = new URLSearchParams({ - q: query, - format: 'json', - addressdetails: '1', // Request detailed address components - limit: '5', // Number of results to return - 'accept-language': 'es', // Request results in Spanish - countrycodes: 'es' // Restrict search to Spain - }); - - const response = await fetch(`${gatewayNominatimApiUrl}?${params.toString()}`); - const data = await response.json(); - - // Process Nominatim results and update form data - if (data && data.length > 0) { - // Take the first result or let the user choose from suggestions if you implement a dropdown - const place = data[0]; // For simplicity, take the first result - - let address = ''; - let city = ''; - let postal_code = ''; - - // Nominatim's 'address' object contains components - if (place.address) { - const addr = place.address; - - // Reconstruct the address in a common format - const street = addr.road || ''; - const houseNumber = addr.house_number || ''; - address = `${street} ${houseNumber}`.trim(); - - city = addr.city || addr.town || addr.village || ''; - postal_code = addr.postcode || ''; - } - - setFormData(prevData => ({ - ...prevData, - address: address || query, // Use parsed address or fall back to user input - city: city || prevData.city, - postal_code: postal_code || prevData.postal_code, - })); - } - } catch (error) { - console.error('Error fetching Nominatim suggestions:', error); - // Optionally show an error notification - // showNotification('error', 'Error de Autocompletado', 'No se pudieron cargar las sugerencias de dirección.'); - } - }, 500); // Debounce time: 500ms - }, []); // Re-create if dependencies change, none for now - + // Mock WebSocket for training progress useEffect(() => { - // If user is already authenticated and on onboarding, redirect to dashboard - if (user && currentStep === 1) { - router.push('/dashboard'); - } - }, [user, router, currentStep]); + if (formData.trainingTaskId && currentStep === 4) { + const interval = setInterval(async () => { + try { + const status = await api.training.getTrainingStatus(formData.trainingTaskId); + setTrainingProgress(status); + + if (status.status === 'completed') { + setCompletedSteps([...completedSteps, 4]); + clearInterval(interval); + showNotification('success', '¡Entrenamiento completado!', 'Los modelos están listos para usar.'); + } else if (status.status === 'failed') { + clearInterval(interval); + showNotification('error', 'Error en entrenamiento', 'Hubo un problema durante el entrenamiento.'); + } + } catch (error) { + console.error('Error fetching training status:', error); + } + }, 2000); - const showNotification = useCallback((type: 'success' | 'error' | 'warning' | 'info', title: string, message: string) => { - setNotification({ id: Date.now().toString(), type, title, message }); + return () => clearInterval(interval); + } + }, [formData.trainingTaskId, currentStep]); + + const showNotification = (type, title, message) => { + const id = Date.now().toString(); + setNotification({ id, type, title, message }); setTimeout(() => setNotification(null), 5000); - }, []); + }; + + const validateStep = (step) => { + const newErrors = {}; + + switch (step) { + case 1: + if (!formData.full_name.trim()) newErrors.full_name = 'Nombre requerido'; + if (!formData.email.trim()) newErrors.email = 'Email requerido'; + if (!formData.password) newErrors.password = 'Contraseña requerida'; + if (formData.password !== formData.confirm_password) { + newErrors.confirm_password = 'Las contraseñas no coinciden'; + } + break; + case 2: + if (!formData.bakery_name.trim()) newErrors.bakery_name = 'Nombre de panadería requerido'; + if (!formData.address.trim()) newErrors.address = 'Dirección requerida'; + if (!formData.postal_code.trim()) newErrors.postal_code = 'Código postal requerido'; + break; + case 3: + if (!formData.salesFile) newErrors.salesFile = 'Archivo de ventas requerido'; + break; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; const handleNext = async () => { - setErrors({}); // Clear previous errors - let newErrors: Partial = {}; - - // Validate current step before proceeding - // In your handleNext function, for step 1 registration: - if (currentStep === 1) { - if (!formData.full_name) newErrors.full_name = 'Nombre completo es requerido.'; - if (!formData.email || !/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = 'Email inválido.'; - if (!formData.password || formData.password.length < 6) newErrors.password = 'La contraseña debe tener al menos 6 caracteres.'; - if (formData.password !== formData.confirm_password) newErrors.confirm_password = 'Las contraseñas no coinciden.'; - - if (Object.keys(newErrors).length > 0) { - setErrors(newErrors); - return; - } + if (!validateStep(currentStep)) return; setLoading(true); + try { - const registerData: RegisterData = { - full_name: formData.full_name, - email: formData.email, - password: formData.password, - }; - - console.log('📤 Sending registration request:', registerData); - - // ✅ FIX: Better error handling for registration - try { - await register(registerData); - showNotification('success', 'Registro exitoso', 'Tu cuenta ha sido creada.'); - setCompletedSteps([...completedSteps, 1]); - } catch (registrationError: any) { - console.error('❌ Registration failed:', registrationError); + if (currentStep === 1) { + // Register user using real auth service + const userData = { + email: formData.email, + password: formData.password, + full_name: formData.full_name + }; - // ✅ FIX: Handle specific error cases - if (registrationError.message?.includes('already exists')) { - // If user already exists, show a more helpful message - showNotification('info', 'Usuario existente', 'Ya tienes una cuenta. Has sido conectado automáticamente.'); - setCompletedSteps([...completedSteps, 1]); - } else if (registrationError.message?.includes('Network error')) { - newErrors.email = 'Error de conexión. Verifica tu internet.'; - showNotification('error', 'Error de conexión', newErrors.email); - setErrors(newErrors); - return; - } else { - // Other registration errors - newErrors.email = registrationError.message || 'Error al registrar usuario.'; - showNotification('error', 'Error de registro', newErrors.email); - setErrors(newErrors); - return; + const user = await api.auth.register(userData); + showNotification('success', 'Usuario creado', 'Cuenta registrada exitosamente.'); + + } else if (currentStep === 2) { + // Register bakery using real tenant service + const bakeryData = { + name: formData.bakery_name, + business_type: formData.business_type, + address: formData.address, + city: formData.city, + postal_code: formData.postal_code, + phone: formData.phone || '+34600000000' // Default phone if not provided + }; + + const tenant = await api.tenant.registerBakery(bakeryData); + setCurrentTenantId(tenant.id); + showNotification('success', 'Panadería registrada', 'Información guardada correctamente.'); + + } else if (currentStep === 3) { + // Upload and validate sales data using real data service + if (formData.salesFile) { + // First validate the data + const validation = await api.data.validateSalesData(formData.salesFile); + setUploadValidation(validation); + + if (validation.valid || validation.warnings.length === 0) { + // Upload the file + const uploadResult = await api.data.uploadSalesHistory( + formData.salesFile, + { tenant_id: currentTenantId } + ); + showNotification('success', 'Archivo subido', `${uploadResult.records_processed} registros procesados.`); + } else { + showNotification('warning', 'Datos con advertencias', 'Se encontraron algunas advertencias pero los datos son válidos.'); + } } + + } else if (currentStep === 4) { + // Start training using real training service + const trainingConfig = { + include_weather: true, + include_traffic: true, + products: formData.selected_products, + min_data_points: 30, + forecast_horizon_days: 7, + cross_validation_folds: 3, + hyperparameter_tuning: true + }; + + const trainingJob = await api.training.startTraining(trainingConfig); + setFormData(prev => ({ + ...prev, + trainingTaskId: trainingJob.id, + trainingStatus: 'running' + })); + showNotification('info', 'Entrenamiento iniciado', 'Los modelos se están entrenando...'); + + // Don't move to next step automatically, wait for training completion + setLoading(false); + return; } - } catch (err: any) { - // This catch block should rarely be reached now - console.error('❌ Unexpected error:', err); - newErrors.email = 'Error inesperado. Inténtalo de nuevo.'; - showNotification('error', 'Error inesperado', newErrors.email); - setErrors(newErrors); - return; + + // Mark step as completed and move to next + setCompletedSteps([...completedSteps, currentStep]); + if (currentStep < 5) { + setCurrentStep(currentStep + 1); + } + + } catch (error) { + console.error('Error in step:', error); + + // Handle specific API errors + let errorMessage = 'Hubo un problema. Inténtalo de nuevo.'; + let errorTitle = 'Error'; + + if (error.response?.status === 400) { + errorMessage = error.response.data?.detail || 'Datos inválidos.'; + errorTitle = 'Error de validación'; + } else if (error.response?.status === 409) { + errorMessage = 'El email ya está registrado.'; + errorTitle = 'Usuario existente'; + } else if (error.response?.status === 422) { + errorMessage = 'Formato de datos incorrecto.'; + errorTitle = 'Error de formato'; + } else if (error.response?.status >= 500) { + errorMessage = 'Error del servidor. Inténtalo más tarde.'; + errorTitle = 'Error del servidor'; + } + + showNotification('error', errorTitle, errorMessage); } finally { setLoading(false); } -} else if (currentStep === 2) { - if (!formData.bakery_name) newErrors.bakery_name = 'Nombre de la panadería es requerido.'; - if (!formData.address) newErrors.address = 'Dirección es requerida.'; - if (!formData.city) newErrors.city = 'Ciudad es requerida.'; - if (!formData.postal_code) newErrors.postal_code = 'Código postal es requerido.'; - if (formData.selected_products.length === 0) newErrors.selected_products = 'Debes seleccionar al menos un producto.' as any; - - if (Object.keys(newErrors).length > 0) { - setErrors(newErrors); - return; - } - - setLoading(true); - try { - // Prepare data for tenant (bakery) registration - const tenantData: TenantCreate = { - name: formData.bakery_name, - email: formData.email, // Assuming tenant email is same as user email - phone: '', // Placeholder, add a field in UI if needed - address: `${formData.address}, ${formData.city}, ${formData.postal_code}`, - latitude: 0, // Placeholder, integrate with Nominatim if needed - longitude: 0, // Placeholder, integrate with Nominatim if needed - business_type: 'individual_bakery', // Default to individual_bakery - settings: { - has_nearby_schools: formData.has_nearby_schools, - has_nearby_offices: formData.has_nearby_offices, - city: formData.city, - selected_products: formData.selected_products.map(p => p.name), - } - }; - - console.log('📤 Sending tenant registration request:', tenantData); - // Call the tenant registration API - await api.tenant.registerBakery(tenantData); - showNotification('success', 'Panadería registrada', 'La información de tu panadería ha sido guardada.'); - setCompletedSteps([...completedSteps, 2]); - } catch (err: any) { - console.error('❌ Tenant registration failed:', err); - showNotification('error', 'Error al registrar panadería', err.message || 'No se pudo registrar la información de la panadería.'); - setLoading(false); // Stop loading on error - return; // Prevent moving to next step if registration fails - } finally { - setLoading(false); // Stop loading after attempt - } - - } else if (currentStep === 3) { - if (!formData.salesFile) { - showNotification('warning', 'Archivo requerido', 'Debes subir un archivo de historial de ventas.'); - return; - } - - setCompletedSteps([...completedSteps, 3]); - } else if (currentStep === 4) { - setLoading(true); - try { - // Start model training logic here - const trainingRequest: TrainingRequest = { - products: formData.selected_products.map(p => p.name), - location_factors: { - has_nearby_schools: formData.has_nearby_schools, - has_nearby_offices: formData.has_nearby_offices, - city: formData.city - } - }; - - showNotification('success', 'Entrenamiento iniciado', 'El modelo de predicción está siendo entrenado.'); - setCompletedSteps([...completedSteps, 4]); - } catch (err: any) { - showNotification('error', 'Error de entrenamiento', 'No se pudo iniciar el entrenamiento del modelo.'); - return; - } finally { - setLoading(false); - } - } - - // Move to next step - if (currentStep < 5) { - setCurrentStep(currentStep + 1); - } }; const handleBack = () => { @@ -314,384 +263,726 @@ const OnboardingPage: React.FC = () => { } }; - const handleSubmitFinal = async () => { - setLoading(true); - try { - showNotification('success', '¡Configuración completa!', 'Tu sistema está listo para usar.'); - setTimeout(() => { - router.push('/dashboard'); - }, 2000); - } catch (err: any) { - showNotification('error', 'Error final', 'Hubo un problema al completar la configuración.'); - } finally { - setLoading(false); + const handleFileUpload = async (event) => { + const file = event.target.files?.[0]; + if (file) { + setFormData(prev => ({ ...prev, salesFile: file })); + setUploadValidation(null); + + // Auto-validate file on selection + try { + setLoading(true); + const validation = await api.data.validateSalesData(file); + setUploadValidation(validation); + + if (validation.valid) { + showNotification('success', 'Archivo válido', `${validation.recordCount} registros detectados.`); + } else if (validation.warnings.length > 0 && validation.errors.length === 0) { + showNotification('warning', 'Archivo con advertencias', 'El archivo es válido pero tiene algunas advertencias.'); + } else { + showNotification('error', 'Archivo inválido', 'El archivo tiene errores que deben corregirse.'); + } + } catch (error) { + console.error('Error validating file:', error); + showNotification('error', 'Error de validación', 'No se pudo validar el archivo.'); + } finally { + setLoading(false); + } } }; - // ORIGINAL DESIGN: Step configuration with icons and content - const steps = [ - { - id: 1, - name: 'Cuenta de Usuario', - icon: UserCircleIcon, - content: ( -
-
- - setFormData({ ...formData, full_name: e.target.value })} - required - /> - {errors.full_name &&

{errors.full_name}

} + const handleFinalSubmit = () => { + showNotification('success', '¡Configuración completa!', 'Tu sistema está listo para usar.'); + setTimeout(() => { + // Navigate to dashboard + window.location.href = '/dashboard'; + }, 2000); + }; + + const renderStepIndicator = () => ( +
+
+ {steps.map((step, index) => ( +
+
+
+ {completedSteps.includes(step.id) ? ( + + ) : ( + + )} +
+
+
{step.title}
+
{step.subtitle}
+
+
+ {index < steps.length - 1 && ( +
+ )}
+ ))} +
+
+ ); + + const renderStep1 = () => ( +
+
+

Crear tu cuenta

+

Empecemos con tu información personal

+
+ +
+
+ + setFormData(prev => ({ ...prev, full_name: e.target.value }))} + className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all ${ + errors.full_name ? 'border-red-500' : 'border-gray-300' + }`} + placeholder="Tu nombre completo" + /> + {errors.full_name &&

{errors.full_name}

} +
+ +
+ + setFormData(prev => ({ ...prev, email: e.target.value }))} + className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all ${ + errors.email ? 'border-red-500' : 'border-gray-300' + }`} + placeholder="tu@email.com" + /> + {errors.email &&

{errors.email}

} +
+ +
- - setFormData({ ...formData, email: e.target.value })} - required - /> - {errors.email &&

{errors.email}

} -
-
-
+
-
-
- ), - }, - { - id: 2, - name: 'Detalles de la Panadería', - icon: BuildingStorefrontIcon, - content: ( -
-
- - setFormData({ ...formData, bakery_name: e.target.value })} - required - /> - {errors.bakery_name &&

{errors.bakery_name}

} -
-
- - - {errors.address &&

{errors.address}

} -
-
-
- - setFormData({ ...formData, city: e.target.value })} - required - /> - {errors.city &&

{errors.city}

} -
-
- - setFormData({ ...formData, postal_code: e.target.value })} - required - /> - {errors.postal_code &&

{errors.postal_code}

} -
-
- -
-

Factores de Ubicación

-
- - -
-
-
- ), - }, - { - id: 3, - name: 'Historial de Ventas', - icon: CloudArrowUpIcon, - content: ( -
-
- -

- Sube tu historial de ventas -

-

- Para crear predicciones precisas, necesitamos tus datos históricos de ventas -

-
- { - // Store the file in form data - setFormData({ ...formData, salesFile: file }); - // Show success notification - showNotification('success', 'Archivo seleccionado', `Archivo "${file.name}" listo para procesar.`); - }} - /> -
- ), - }, - { - id: 4, - name: 'Entrenar Modelo', - icon: CpuChipIcon, - content: ( -
-
- -

- Entrenar tu modelo de predicción -

-

- Crearemos un modelo personalizado basado en tus datos de ventas -

-
- setFormData({ ...formData, trainingStatus: 'running' })} - /> -
- ), - }, - { - id: 5, - name: 'Completado', - icon: CheckIcon, - content: ( -
-
- -
-

- ¡Configuración Completada! -

-

- Tu sistema de predicción de demanda está listo para usar. -

-
-
-
    -
  • Cuenta de usuario creada
  • -
  • Información de panadería guardada
  • -
  • Historial de ventas procesado
  • -
  • Modelo de predicción entrenado
  • -
-
-
-
- ), - }, - ]; - - const currentStepData = steps.find(step => step.id === currentStep) || steps[0]; - - return ( -
- - Configuración - Bakery Forecast - - - {notification && ( - setNotification(null)} - /> - )} - -
-
-
-

Configuración Inicial

-

Configura tu cuenta y comienza a predecir la demanda

-
- - {/* ORIGINAL DESIGN: Step Progress Indicator */} - - - {/* ORIGINAL DESIGN: Step Content */} -
-

- Paso {currentStep}: {currentStepData.name} -

- {currentStepData.content} -
- - {/* ORIGINAL DESIGN: Navigation Buttons */} -
- {currentStep > 1 && currentStep < 5 && ( - - )} - - {currentStep < 5 && ( - - )} - - {currentStep === 5 && ( - - )} + {errors.confirm_password &&

{errors.confirm_password}

}
); + + const renderStep2 = () => ( +
+
+

Datos de tu panadería

+

Información sobre tu negocio

+
+ +
+
+ + setFormData(prev => ({ ...prev, bakery_name: e.target.value }))} + className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all ${ + errors.bakery_name ? 'border-red-500' : 'border-gray-300' + }`} + placeholder="Panadería El Buen Pan" + /> + {errors.bakery_name &&

{errors.bakery_name}

} +
+ +
+ + setFormData(prev => ({ ...prev, address: e.target.value }))} + className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all ${ + errors.address ? 'border-red-500' : 'border-gray-300' + }`} + placeholder="Calle Mayor 123" + /> + {errors.address &&

{errors.address}

} +
+ +
+
+ + setFormData(prev => ({ ...prev, city: e.target.value }))} + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all" + placeholder="Madrid" + /> +
+ +
+ + setFormData(prev => ({ ...prev, postal_code: e.target.value }))} + className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all ${ + errors.postal_code ? 'border-red-500' : 'border-gray-300' + }`} + placeholder="28001" + /> + {errors.postal_code &&

{errors.postal_code}

} +
+
+ +
+

Ubicación del negocio

+
+ + +
+
+ +
+

Productos principales

+
+ {['Pan de molde', 'Baguettes', 'Croissants', 'Magdalenas', 'Donuts', 'Empanadas'].map((product) => ( + + ))} +
+
+
+
+ ); + + const renderStep3 = () => ( +
+
+

Historial de ventas

+

Sube tus datos históricos para entrenar la IA

+
+ +
+
+ + + {formData.salesFile ? ( +
+ +
+

{formData.salesFile.name}

+

+ {(formData.salesFile.size / 1024 / 1024).toFixed(2)} MB +

+
+ +
+ ) : ( +
+ +
+

Arrastra tu archivo aquí

+

o haz clic para seleccionar

+
+ +
+ )} +
+ + {errors.salesFile && ( +
+

{errors.salesFile}

+
+ )} + + {uploadValidation && ( +
0 + ? 'bg-red-50 border-red-200' + : 'bg-yellow-50 border-yellow-200' + }`}> +
+ {uploadValidation.valid ? ( + + ) : uploadValidation.errors.length > 0 ? ( + + ) : ( + + )} +
+

0 + ? 'text-red-800' + : 'text-yellow-800' + }`}> + {uploadValidation.valid + ? 'Datos válidos' + : uploadValidation.errors.length > 0 + ? 'Errores encontrados' + : 'Advertencias encontradas' + } +

+

0 + ? 'text-red-700' + : 'text-yellow-700' + }`}> + {uploadValidation.recordCount} registros encontrados + {uploadValidation.duplicates > 0 && `, ${uploadValidation.duplicates} duplicados`} +

+ + {uploadValidation.errors.length > 0 && ( +
+

Errores:

+
    + {uploadValidation.errors.map((error, idx) => ( +
  • • {error}
  • + ))} +
+
+ )} + + {uploadValidation.warnings.length > 0 && ( +
+

Advertencias:

+
    + {uploadValidation.warnings.map((warning, idx) => ( +
  • • {warning}
  • + ))} +
+
+ )} +
+
+
+ )} + +
+

Formato requerido

+
+

• Archivo CSV o Excel (.csv, .xlsx, .xls)

+

• Columnas: Fecha, Producto, Cantidad, Precio

+

• Mínimo 30 días de datos históricos

+

• Fechas en formato DD/MM/YYYY

+
+
+
+
+ ); + + const renderStep4 = () => ( +
+
+

Entrenando modelos IA

+

Nuestros algoritmos están analizando tus datos

+
+ +
+ {/* Main Progress Card */} +
+
+
+ +
+

Entrenamiento en progreso

+

+ {trainingProgress ? trainingProgress.current_step : 'Iniciando...'} +

+
+
+
+

+ {trainingProgress ? trainingProgress.progress : 0}% +

+

Completado

+
+
+ +
+
+
+
+ + {/* Training Steps */} +
+ {[ + { title: 'Validación de datos', icon: CheckIcon, completed: trainingProgress?.progress > 10 }, + { title: 'Procesamiento', icon: CpuChipIcon, completed: trainingProgress?.progress > 30 }, + { title: 'Entrenamiento', icon: ChartBarIcon, completed: trainingProgress?.progress > 60 }, + { title: 'Validación final', icon: SparklesIcon, completed: trainingProgress?.progress > 90 } + ].map((step, idx) => ( +
(idx * 25) + ? 'bg-blue-50 border-blue-200' + : 'bg-gray-50 border-gray-200' + } + `}> +
+ + + {step.title} + +
+
+ ))} +
+ + {/* Products Progress */} + {trainingProgress && ( +
+

Productos entrenados

+
+ {formData.selected_products.map((product, idx) => ( +
+ {product} +
+ {idx < (trainingProgress.products_completed || 0) ? ( + + ) : idx === (trainingProgress.products_completed || 0) ? ( +
+ ) : ( +
+ )} +
+
+ ))} +
+
+ {trainingProgress.products_completed || 0} de {trainingProgress.products_total || formData.selected_products.length} productos completados +
+
+ )} + + {/* Estimated Time */} + {trainingProgress && trainingProgress.progress < 100 && ( +
+
+
+ + Tiempo estimado restante: ~{Math.max(1, Math.round((100 - trainingProgress.progress) / 10))} minutos + +
+
+ )} +
+
+ ); + + const renderStep5 = () => ( +
+
+
+ +
+

¡Todo listo!

+

Tu sistema de predicción está configurado y funcionando

+
+ +
+
+ +

Modelos Entrenados

+

{formData.selected_products.length} productos configurados

+
+ +
+ +

IA Activa

+

Predicciones en tiempo real

+
+ +
+ +

Panadería Lista

+

Sistema completamente configurado

+
+
+ +
+

¿Qué puedes hacer ahora?

+
+
+ + Ver predicciones de demanda para los próximos 7 días +
+
+ + Configurar alertas automáticas +
+
+ + Analizar tendencias históricas +
+
+ + Optimizar tu producción diaria +
+
+
+ +
+ +
+
+ ); + + const renderCurrentStep = () => { + switch (currentStep) { + case 1: return renderStep1(); + case 2: return renderStep2(); + case 3: return renderStep3(); + case 4: return renderStep4(); + case 5: return renderStep5(); + default: return renderStep1(); + } + }; + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

BakeryForecast

+

Configuración inicial

+
+
+
+
+ + {/* Main Content */} +
+ {renderStepIndicator()} + +
+
+ {renderCurrentStep()} +
+ + {/* Navigation */} +
+ {currentStep > 1 && currentStep < 5 ? ( + + ) : ( +
+ )} + + {currentStep < 4 && ( + + )} + + {currentStep === 4 && trainingProgress?.status !== 'completed' && ( + + )} + + {currentStep === 4 && trainingProgress?.status === 'completed' && ( + + )} +
+
+
+ + {/* Notification Toast */} + {notification && ( +
+
+
+
+
+ {notification.type === 'success' && } + {notification.type === 'error' && } + {notification.type === 'warning' && } + {notification.type === 'info' && } +
+
+

{notification.title}

+

{notification.message}

+
+
+ +
+
+
+ )} +
+ ); }; export default OnboardingPage; \ No newline at end of file diff --git a/gateway/app/middleware/auth.py b/gateway/app/middleware/auth.py index 2d13035d..255ce08f 100644 --- a/gateway/app/middleware/auth.py +++ b/gateway/app/middleware/auth.py @@ -32,6 +32,7 @@ PUBLIC_ROUTES = [ "/api/v1/auth/register", "/api/v1/auth/refresh", "/api/v1/auth/verify", + "/api/v1/tenant/register", "/api/v1/nominatim/search" ]