2025-08-03 19:23:20 +02:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check } from 'lucide-react';
|
|
|
|
|
import toast from 'react-hot-toast';
|
|
|
|
|
|
2025-08-04 07:37:19 +02:00
|
|
|
import { useTenant, useTraining, useData, useAuth } from '../../api/hooks';
|
|
|
|
|
import { TenantCreate } from '../../api/types';
|
|
|
|
|
import { UserResponse } from '../../api/types';
|
|
|
|
|
|
2025-08-03 19:23:20 +02:00
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-04 07:37:19 +02:00
|
|
|
const { createTenant, isLoading: tenantLoading } = useTenant();
|
|
|
|
|
const { startTrainingJob } = useTraining();
|
|
|
|
|
const { uploadSalesHistory, validateSalesData, uploadProgress } = useData();
|
|
|
|
|
const [ setUploadProgress ] = useState<number>(0);
|
|
|
|
|
|
2025-08-03 19:23:20 +02:00
|
|
|
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 () => {
|
2025-08-04 07:37:19 +02:00
|
|
|
if (!validateCurrentStep()) return;
|
|
|
|
|
|
2025-08-03 19:23:20 +02:00
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-04 07:37:19 +02:00
|
|
|
// Step 1: Create tenant using the API service
|
|
|
|
|
const tenantData: TenantCreate = {
|
|
|
|
|
name: bakeryData.name,
|
|
|
|
|
address: bakeryData.address,
|
|
|
|
|
business_type: bakeryData.businessType,
|
|
|
|
|
coordinates: bakeryData.coordinates,
|
|
|
|
|
products: bakeryData.products,
|
|
|
|
|
has_historical_data: bakeryData.hasHistoricalData,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const newTenant = await createTenant(tenantData);
|
|
|
|
|
const tenantId = newTenant.id;
|
|
|
|
|
|
|
|
|
|
// Step 2: Validate and Upload CSV file if provided
|
2025-08-03 19:23:20 +02:00
|
|
|
if (bakeryData.hasHistoricalData && bakeryData.csvFile) {
|
2025-08-04 07:37:19 +02:00
|
|
|
try {
|
|
|
|
|
// Step 2.1: First validate the CSV data
|
|
|
|
|
toast.loading('Validando datos del archivo CSV...', { id: 'csv-validation' });
|
|
|
|
|
|
|
|
|
|
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
|
|
|
|
|
|
|
|
|
|
toast.dismiss('csv-validation');
|
|
|
|
|
|
|
|
|
|
// Check validation result
|
|
|
|
|
if (!validationResult.success) {
|
|
|
|
|
// Validation failed - show errors but let user decide
|
|
|
|
|
const errorMessages = validationResult.errors?.slice(0, 3).join(', ') || 'Errores de validación';
|
|
|
|
|
const hasMoreErrors = validationResult.errors && validationResult.errors.length > 3;
|
|
|
|
|
|
|
|
|
|
toast.error(
|
|
|
|
|
`Errores en el archivo CSV: ${errorMessages}${hasMoreErrors ? '...' : ''}. Revisa la consola para más detalles.`
|
|
|
|
|
);
|
|
|
|
|
console.error('CSV validation errors:', validationResult.errors);
|
|
|
|
|
console.warn('CSV validation warnings:', validationResult.warnings);
|
|
|
|
|
|
|
|
|
|
// Don't proceed with upload if validation fails
|
|
|
|
|
throw new Error('Validación del CSV falló');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validation passed - show summary and proceed
|
|
|
|
|
if (validationResult.warnings && validationResult.warnings.length > 0) {
|
|
|
|
|
toast.warn(`CSV validado con ${validationResult.warnings.length} advertencias. Continuando con la subida...`);
|
|
|
|
|
console.warn('CSV validation warnings:', validationResult.warnings);
|
|
|
|
|
} else {
|
|
|
|
|
toast.success('CSV validado correctamente. Procediendo con la subida...');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2.2: Now upload the validated CSV
|
|
|
|
|
toast.loading('Subiendo datos históricos...', { id: 'csv-upload' });
|
|
|
|
|
|
|
|
|
|
const uploadResult = await uploadSalesHistory(tenantId, bakeryData.csvFile, {
|
|
|
|
|
source: 'onboarding_upload',
|
|
|
|
|
validate_only: false,
|
|
|
|
|
onProgress: (progress) => {
|
|
|
|
|
setUploadProgress(progress.percentage);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toast.dismiss('csv-upload');
|
|
|
|
|
|
|
|
|
|
// Check upload result
|
|
|
|
|
if (uploadResult.success) {
|
|
|
|
|
toast.success(
|
|
|
|
|
`¡Datos históricos subidos exitosamente! ${uploadResult.records_created} de ${uploadResult.records_processed} registros procesados.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Show additional info if some records failed
|
|
|
|
|
if (uploadResult.records_failed > 0) {
|
|
|
|
|
toast.warn(
|
|
|
|
|
`${uploadResult.records_failed} registros fallaron durante la subida. Success rate: ${uploadResult.success_rate?.toFixed(1)}%`
|
|
|
|
|
);
|
|
|
|
|
console.warn('Upload errors:', uploadResult.errors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log warnings for debugging
|
|
|
|
|
if (uploadResult.warnings && uploadResult.warnings.length > 0) {
|
|
|
|
|
console.info('Upload warnings:', uploadResult.warnings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Start training process (if you have a training service)
|
|
|
|
|
await startTrainingJob(tenantId);
|
|
|
|
|
toast.success('¡Entrenamiento del modelo iniciado!');
|
|
|
|
|
} catch (trainingError) {
|
|
|
|
|
console.warn('Training start failed:', trainingError);
|
|
|
|
|
// Don't fail onboarding if training fails to start
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// Upload failed
|
|
|
|
|
throw new Error(`Upload failed: ${uploadResult.errors?.join(', ') || 'Unknown error'}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset progress when done
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
|
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
// Handle validation or upload error gracefully
|
|
|
|
|
console.error('CSV validation/upload error:', uploadError);
|
|
|
|
|
|
|
|
|
|
const errorMessage = uploadError instanceof Error ? uploadError.message : 'Error desconocido';
|
|
|
|
|
|
|
|
|
|
if (errorMessage.includes('Validación')) {
|
|
|
|
|
toast.error('No se pudieron subir los datos históricos debido a errores de validación. La configuración se completó sin datos históricos.');
|
|
|
|
|
} else {
|
|
|
|
|
toast.warn(`Error al procesar archivo CSV: ${errorMessage}. La configuración se completó sin datos históricos.`);
|
|
|
|
|
}
|
2025-08-03 19:23:20 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 07:37:19 +02:00
|
|
|
|
|
|
|
|
toast.success('¡Configuración completada exitosamente!');
|
2025-08-03 19:23:20 +02:00
|
|
|
onComplete();
|
2025-08-04 07:37:19 +02:00
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-08-03 19:23:20 +02:00
|
|
|
console.error('Onboarding completion error:', error);
|
2025-08-04 07:37:19 +02:00
|
|
|
const errorMessage = error instanceof Error ? error.message : 'Error al completar la configuración';
|
|
|
|
|
toast.error(errorMessage);
|
2025-08-03 19:23:20 +02:00
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
2025-08-04 07:37:19 +02:00
|
|
|
setUploadProgress(0); // Reset progress in case of error
|
2025-08-03 19:23:20 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|