// 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, ArrowLeftIcon, CloudArrowUpIcon, BuildingStorefrontIcon, UserCircleIcon, CpuChipIcon } 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'; import { TenantCreate } 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; // 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 [currentStep, setCurrentStep] = useState(1); 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({ full_name: '', email: '', password: '', confirm_password: '', bakery_name: '', address: '', city: 'Madrid', // Default to Madrid postal_code: '', has_nearby_schools: false, has_nearby_offices: false, selected_products: defaultProducts.slice(0, 3), // Default selected products salesFile: null, trainingStatus: 'pending', trainingTaskId: null, }); const [errors, setErrors] = useState>({}); 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); } 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 useEffect(() => { // If user is already authenticated and on onboarding, redirect to dashboard if (user && currentStep === 1) { router.push('/dashboard'); } }, [user, router, currentStep]); const showNotification = useCallback((type: 'success' | 'error' | 'warning' | 'info', title: string, message: string) => { setNotification({ id: Date.now().toString(), type, title, message }); setTimeout(() => setNotification(null), 5000); }, []); 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; } 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); // ✅ 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; } } } 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; } 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 = () => { if (currentStep > 1) { setCurrentStep(currentStep - 1); } }; 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); } }; // 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}

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

{errors.email}

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

{errors.password}

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

{errors.confirm_password}

}
), }, { 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 && ( )}
); }; export default OnboardingPage;