Files
bakery-ia/frontend/src/pages/onboarding/OnboardingPage.tsx
2025-08-03 19:23:20 +02:00

516 lines
19 KiB
TypeScript

import React, { useState } from 'react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check } from 'lucide-react';
import toast from 'react-hot-toast';
interface OnboardingPageProps {
user: any;
onComplete: () => void;
}
interface BakeryData {
name: string;
address: string;
businessType: 'individual' | 'central_workshop';
coordinates?: { lat: number; lng: number };
products: string[];
hasHistoricalData: boolean;
csvFile?: File;
}
const MADRID_PRODUCTS = [
'Croissants', 'Pan de molde', 'Baguettes', 'Panecillos', 'Ensaimadas',
'Napolitanas', 'Magdalenas', 'Donuts', 'Palmeras', 'Café',
'Chocolate caliente', 'Zumos', 'Bocadillos', 'Empanadas', 'Tartas'
];
const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [bakeryData, setBakeryData] = useState<BakeryData>({
name: '',
address: '',
businessType: 'individual',
products: [],
hasHistoricalData: false
});
const steps = [
{ id: 1, title: 'Datos de Panadería', icon: Store },
{ id: 2, title: 'Productos y Servicios', icon: Factory },
{ id: 3, title: 'Datos Históricos', icon: Upload },
{ id: 4, title: 'Configuración Final', icon: Check }
];
const handleNext = () => {
if (validateCurrentStep()) {
setCurrentStep(prev => Math.min(prev + 1, steps.length));
}
};
const handlePrevious = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const validateCurrentStep = (): boolean => {
switch (currentStep) {
case 1:
if (!bakeryData.name.trim()) {
toast.error('El nombre de la panadería es obligatorio');
return false;
}
if (!bakeryData.address.trim()) {
toast.error('La dirección es obligatoria');
return false;
}
return true;
case 2:
if (bakeryData.products.length === 0) {
toast.error('Selecciona al menos un producto');
return false;
}
return true;
case 3:
if (bakeryData.hasHistoricalData && !bakeryData.csvFile) {
toast.error('Por favor, sube tu archivo CSV con datos históricos');
return false;
}
return true;
default:
return true;
}
};
const handleComplete = async () => {
setIsLoading(true);
try {
// Step 1: Register tenant/bakery
const tenantResponse = await fetch('/api/v1/tenants/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
name: bakeryData.name,
address: bakeryData.address,
business_type: bakeryData.businessType,
coordinates: bakeryData.coordinates,
products: bakeryData.products
})
});
if (!tenantResponse.ok) {
throw new Error('Error al registrar la panadería');
}
const tenantData = await tenantResponse.json();
const tenantId = tenantData.tenant.id;
// Step 2: Upload CSV data if provided
if (bakeryData.hasHistoricalData && bakeryData.csvFile) {
const formData = new FormData();
formData.append('file', bakeryData.csvFile);
const uploadResponse = await fetch(`/api/v1/tenants/${tenantId}/data/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: formData
});
if (!uploadResponse.ok) {
throw new Error('Error al subir los datos históricos');
}
// Step 3: Start training process
const trainingResponse = await fetch(`/api/v1/tenants/${tenantId}/training/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
products: bakeryData.products
})
});
if (!trainingResponse.ok) {
throw new Error('Error al iniciar el entrenamiento del modelo');
}
toast.success('¡Datos subidos! El entrenamiento del modelo comenzará pronto.');
}
toast.success('¡Configuración completada! Bienvenido a PanIA');
onComplete();
} catch (error: any) {
console.error('Onboarding completion error:', error);
toast.error(error.message || 'Error al completar la configuración');
} finally {
setIsLoading(false);
}
};
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Información de tu Panadería
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la panadería
</label>
<input
type="text"
value={bakeryData.name}
onChange={(e) => setBakeryData(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Ej: Panadería San Miguel"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dirección completa
</label>
<div className="relative">
<MapPin className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
type="text"
value={bakeryData.address}
onChange={(e) => setBakeryData(prev => ({ ...prev, address: e.target.value }))}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Calle Mayor, 123, Madrid"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Tipo de negocio
</label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'individual' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'individual'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Store className="h-6 w-6 mb-2" />
<div className="font-medium">Panadería Individual</div>
<div className="text-sm text-gray-500">Una sola ubicación</div>
</button>
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'central_workshop' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'central_workshop'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Factory className="h-6 w-6 mb-2" />
<div className="font-medium">Obrador Central</div>
<div className="text-sm text-gray-500">Múltiples ubicaciones</div>
</button>
</div>
</div>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
¿Qué productos vendes?
</h3>
<p className="text-gray-600 mb-6">
Selecciona los productos más comunes en tu panadería
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{MADRID_PRODUCTS.map((product) => (
<button
key={product}
type="button"
onClick={() => {
setBakeryData(prev => ({
...prev,
products: prev.products.includes(product)
? prev.products.filter(p => p !== product)
: [...prev.products, product]
}));
}}
className={`p-3 text-sm border rounded-lg transition-all text-left ${
bakeryData.products.includes(product)
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
{product}
</button>
))}
</div>
<div className="mt-4">
<p className="text-sm text-gray-500">
Productos seleccionados: {bakeryData.products.length}
</p>
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Datos Históricos de Ventas
</h3>
<p className="text-gray-600 mb-6">
Para obtener mejores predicciones, puedes subir tus datos históricos de ventas
</p>
<div className="space-y-4">
<div>
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={bakeryData.hasHistoricalData}
onChange={(e) => setBakeryData(prev => ({
...prev,
hasHistoricalData: e.target.checked,
csvFile: e.target.checked ? prev.csvFile : undefined
}))}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="text-gray-700">
Tengo datos históricos de ventas (recomendado)
</span>
</label>
</div>
{bakeryData.hasHistoricalData && (
<div className="border-2 border-dashed border-gray-300 rounded-xl p-6">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
{bakeryData.csvFile ? (
<div>
<p className="text-sm font-medium text-gray-900 mb-2">
Archivo seleccionado:
</p>
<p className="text-sm text-gray-600 mb-4">
{bakeryData.csvFile.name}
</p>
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, csvFile: undefined }))}
className="text-sm text-red-600 hover:text-red-500"
>
Eliminar archivo
</button>
</div>
) : (
<div>
<p className="text-sm text-gray-600 mb-4">
Sube tu archivo CSV con las ventas históricas
</p>
<input
type="file"
accept=".csv"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setBakeryData(prev => ({ ...prev, csvFile: file }));
}
}}
className="hidden"
id="csv-upload"
/>
<label
htmlFor="csv-upload"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 cursor-pointer"
>
Seleccionar archivo CSV
</label>
</div>
)}
</div>
<div className="mt-4 text-xs text-gray-500">
<p className="font-medium mb-1">Formato esperado del CSV:</p>
<p>Fecha, Producto, Cantidad</p>
<p>2024-01-01, Croissants, 45</p>
<p>2024-01-01, Pan de molde, 12</p>
</div>
</div>
)}
{!bakeryData.hasHistoricalData && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<p className="text-blue-800 text-sm">
No te preocupes, PanIA puede empezar a funcionar sin datos históricos.
Las predicciones mejorarán automáticamente conforme uses el sistema.
</p>
</div>
)}
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-6">
<div className="text-center">
<div className="mx-auto h-16 w-16 bg-success-100 rounded-full flex items-center justify-center mb-4">
<Check className="h-8 w-8 text-success-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
¡Todo listo para comenzar!
</h3>
<p className="text-gray-600 mb-6">
Revisa los datos de tu panadería antes de continuar
</p>
</div>
<div className="bg-gray-50 rounded-xl p-6 space-y-4">
<div>
<span className="text-sm font-medium text-gray-500">Panadería:</span>
<p className="text-gray-900">{bakeryData.name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Dirección:</span>
<p className="text-gray-900">{bakeryData.address}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Tipo de negocio:</span>
<p className="text-gray-900">
{bakeryData.businessType === 'individual' ? 'Panadería Individual' : 'Obrador Central'}
</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Productos:</span>
<p className="text-gray-900">{bakeryData.products.join(', ')}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Datos históricos:</span>
<p className="text-gray-900">
{bakeryData.hasHistoricalData ? `Sí (${bakeryData.csvFile?.name})` : 'No'}
</p>
</div>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-8">
<div className="mx-auto h-12 w-12 bg-primary-500 rounded-xl flex items-center justify-center mb-4">
<span className="text-white text-xl font-bold">🥖</span>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Configuración inicial
</h1>
<p className="text-gray-600">
Vamos a configurar PanIA para tu panadería
</p>
</div>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex justify-between">
{steps.map((step) => (
<div key={step.id} className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all ${
currentStep >= step.id
? 'bg-primary-500 border-primary-500 text-white'
: 'border-gray-300 text-gray-500'
}`}
>
<step.icon className="h-5 w-5" />
</div>
<span className="mt-2 text-xs text-gray-500 text-center max-w-20">
{step.title}
</span>
</div>
))}
</div>
<div className="mt-4 bg-gray-200 rounded-full h-2">
<div
className="bg-primary-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${(currentStep / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Step Content */}
<div className="bg-white rounded-2xl shadow-soft p-6 mb-8">
{renderStep()}
</div>
{/* Navigation */}
<div className="flex justify-between">
<button
onClick={handlePrevious}
disabled={currentStep === 1}
className="flex items-center px-4 py-2 text-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed hover:text-gray-800 transition-colors"
>
<ChevronLeft className="h-5 w-5 mr-1" />
Anterior
</button>
{currentStep < steps.length ? (
<button
onClick={handleNext}
className="flex items-center px-6 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg"
>
Siguiente
<ChevronRight className="h-5 w-5 ml-1" />
</button>
) : (
<button
onClick={handleComplete}
disabled={isLoading}
className="flex items-center px-6 py-2 bg-success-500 text-white rounded-xl hover:bg-success-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Configurando...' : 'Completar configuración'}
</button>
)}
</div>
</div>
</div>
);
};
export default OnboardingPage;