Fix new services implementation 2

This commit is contained in:
Urtzi Alfaro
2025-08-14 13:26:59 +02:00
parent 262b3dc9c4
commit 0951547e92
39 changed files with 1203 additions and 917 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader, TrendingUp } from 'lucide-react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader, TrendingUp } from 'lucide-react';
import toast from 'react-hot-toast';
import SimplifiedTrainingProgress from '../../components/SimplifiedTrainingProgress';
@@ -25,7 +25,7 @@ interface OnboardingPageProps {
interface BakeryData {
name: string;
address: string;
businessType: 'individual' | 'central_workshop';
businessType?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant'; // Will be auto-detected from sales data
coordinates?: { lat: number; lng: number };
products: string[];
hasHistoricalData: boolean;
@@ -51,7 +51,6 @@ const MADRID_PRODUCTS = [
const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [useSmartImport, setUseSmartImport] = useState(true); // New state for smart import
const manualNavigation = useRef(false);
// Enhanced onboarding with progress tracking
@@ -64,7 +63,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
const [bakeryData, setBakeryData] = useState<BakeryData>({
name: '',
address: '',
businessType: 'individual',
// businessType will be auto-detected during smart data import
products: MADRID_PRODUCTS, // Automatically assign all products
hasHistoricalData: false
});
@@ -95,14 +94,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
estimatedTimeRemaining: 0
});
// Sales data validation state
const [validationStatus, setValidationStatus] = useState<{
status: 'idle' | 'validating' | 'valid' | 'invalid';
message?: string;
records?: number;
}>({
status: 'idle'
});
// Initialize current step from onboarding progress (only when not manually navigating)
useEffect(() => {
@@ -292,8 +283,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
try {
if (currentStep === 1) {
await createBakeryAndTenant();
} else if (currentStep === 2) {
await uploadAndValidateSalesData();
// Smart import step handles its own processing
}
} catch (error) {
toast.error('Error al procesar el paso. Por favor, inténtalo de nuevo.');
@@ -317,7 +307,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
const tenantData: TenantCreate = {
name: bakeryData.name,
address: bakeryData.address,
business_type: "individual",
business_type: 'bakery', // Default value - will be automatically updated after AI analyzes sales data
postal_code: "28010",
phone: "+34655334455",
coordinates: bakeryData.coordinates,
@@ -337,7 +327,8 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
await completeStep('bakery_registered', {
bakery_name: bakeryData.name,
bakery_address: bakeryData.address,
business_type: bakeryData.businessType,
business_type: 'bakery', // Default - will be auto-detected from sales data
auto_detect_business_type: true, // Flag indicating business type will be auto-detected
tenant_id: currentTenantId, // Use the correct tenant ID
user_id: user?.id
});
@@ -354,81 +345,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
}
};
// Validate sales data without importing
const validateSalesFile = async () => {
if (!tenantId || !bakeryData.csvFile) {
toast.error('Falta información del tenant o archivo');
return;
}
setValidationStatus({ status: 'validating' });
try {
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
if (validationResult.is_valid) {
setValidationStatus({
status: 'valid',
message: validationResult.message,
records: validationResult.details?.total_records || 0
});
toast.success('¡Archivo validado correctamente!');
} else {
setValidationStatus({
status: 'invalid',
message: validationResult.message
});
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
});
toast.error('Error al validar el archivo');
}
};
// Upload and validate sales data for step 2
const uploadAndValidateSalesData = async () => {
if (!tenantId || !bakeryData.csvFile) {
throw new Error('Falta información del tenant o archivo');
}
try {
// If not already validated, validate first
if (validationStatus.status !== 'valid') {
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
if (!validationResult.is_valid) {
toast.error(`Error en validación: ${validationResult.message}`);
return;
}
}
// If validation passes, upload the data
const uploadResult = await uploadSalesHistory(tenantId, bakeryData.csvFile);
// Mark step as completed
await completeStep('sales_data_uploaded', {
file_name: bakeryData.csvFile.name,
file_size: bakeryData.csvFile.size,
records_imported: uploadResult.records_imported || 0,
validation_status: 'success',
tenant_id: tenantId,
user_id: user?.id
});
toast.success(`Datos importados correctamente (${uploadResult.records_imported} registros)`);
// Automatically start training after successful upload
await startTraining();
} catch (error: any) {
// Error uploading sales data
const message = error?.response?.data?.detail || error.message || 'Error al subir datos de ventas';
throw new Error(message);
}
};
// Helper function to mark current step as completed
const markStepCompleted = async () => {
@@ -450,7 +367,8 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
if (currentStep === 1) {
stepData.bakery_name = bakeryData.name;
stepData.bakery_address = bakeryData.address;
stepData.business_type = bakeryData.businessType;
stepData.business_type = 'bakery'; // Default - will be auto-detected
stepData.auto_detect_business_type = true;
stepData.products_count = bakeryData.products.length;
} else if (currentStep === 2) {
stepData.file_name = bakeryData.csvFile?.name;
@@ -479,39 +397,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
}
return true;
case 2:
// Skip validation if using smart import (it handles its own validation)
if (useSmartImport) {
return true;
}
if (!bakeryData.csvFile) {
toast.error('Por favor, selecciona un archivo con tus datos históricos');
return false;
}
// Validate file format
const fileName = bakeryData.csvFile.name.toLowerCase();
const supportedFormats = ['.csv', '.xlsx', '.xls', '.json'];
const isValidFormat = supportedFormats.some(format => fileName.endsWith(format));
if (!isValidFormat) {
toast.error('Formato de archivo no soportado. Usa CSV, Excel (.xlsx, .xls) o JSON');
return false;
}
// Validate file size (10MB limit as per backend)
const maxSize = 10 * 1024 * 1024;
if (bakeryData.csvFile.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB');
return false;
}
// Check if validation was successful
if (validationStatus.status !== 'valid') {
toast.error('Por favor, valida el archivo antes de continuar');
return false;
}
// Smart import handles its own validation
return true;
default:
return true;
@@ -672,37 +558,20 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
</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-5 w-5 mb-2" />
<div className="font-medium">Panadería Individual</div>
<div className="text-sm text-gray-500">Un solo punto de venta</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-5 w-5 mb-2" />
<div className="font-medium">Obrador Central</div>
<div className="text-sm text-gray-500">Múltiples puntos de venta</div>
</button>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg">
<div className="flex items-start">
<div className="flex-shrink-0">
<Brain className="w-5 h-5 text-blue-500 mt-0.5" />
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
🤖 Detección automática del tipo de negocio
</h4>
<p className="text-sm text-blue-700 mt-1">
Nuestro sistema de IA analizará automáticamente tus datos de ventas para identificar
si eres una panadería, cafetería, pastelería o restaurante. No necesitas seleccionar nada manualmente.
</p>
</div>
</div>
</div>
</div>
@@ -724,360 +593,30 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
);
}
// Use Smart Import by default, with option to switch to traditional
if (useSmartImport) {
return (
<div className="space-y-6">
{/* Header with import mode toggle */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold text-gray-900">
Importación Inteligente de Datos 🧠
</h3>
<p className="text-gray-600 mt-1">
Nuestra IA creará automáticamente tu inventario desde tus datos históricos
</p>
</div>
<button
onClick={() => setUseSmartImport(false)}
className="flex items-center space-x-2 px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<Upload className="w-4 h-4" />
<span>Importación tradicional</span>
</button>
</div>
{/* Smart Import Component */}
<SmartHistoricalDataImport
tenantId={tenantId}
onComplete={(result) => {
// Mark sales data as uploaded and proceed to training
completeStep('sales_data_uploaded', {
smart_import: true,
records_imported: result.successful_imports,
import_job_id: result.import_job_id,
tenant_id: tenantId,
user_id: user?.id
}).then(() => {
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
}).catch(() => {
// Continue even if step completion fails
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
});
}}
onBack={() => setUseSmartImport(false)}
/>
</div>
);
}
// Traditional import fallback
// Smart Import only
return (
<div className="space-y-6">
{/* Header with import mode toggle */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900">
Datos Históricos (Modo Tradicional)
</h3>
<p className="text-gray-600 mt-1">
Sube tus datos y configura tu inventario manualmente
</p>
</div>
<button
onClick={() => setUseSmartImport(true)}
className="flex items-center space-x-2 px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg hover:from-blue-600 hover:to-purple-600 transition-colors"
>
<Brain className="w-4 h-4" />
<span>Activar IA</span>
</button>
</div>
<p className="text-gray-600 mb-6">
Para obtener predicciones precisas, necesitamos tus datos históricos de ventas.
Puedes subir archivos en varios formatos.
</p>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">!</span>
</div>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
Formatos soportados y estructura de datos
</h4>
<div className="mt-2 text-sm text-blue-700">
<p className="mb-3"><strong>Formatos aceptados:</strong></p>
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<p className="font-medium">📊 Hojas de cálculo:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.xlsx (Excel moderno)</li>
<li>.xls (Excel clásico)</li>
</ul>
</div>
<div>
<p className="font-medium">📄 Datos estructurados:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.csv (Valores separados por comas)</li>
<li>.json (Formato JSON)</li>
</ul>
</div>
</div>
<p className="mb-2"><strong>Columnas requeridas (en cualquier idioma):</strong></p>
<ul className="list-disc list-inside text-xs space-y-1">
<li><strong>Fecha</strong>: fecha, date, datum (formato: YYYY-MM-DD, DD/MM/YYYY, etc.)</li>
<li><strong>Producto</strong>: producto, product, item, articulo, nombre</li>
<li><strong>Cantidad</strong>: cantidad, quantity, cantidad_vendida, qty</li>
</ul>
</div>
</div>
</div>
</div>
<div className="mt-6 p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-primary-300 transition-colors">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<label htmlFor="sales-file-upload" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Subir archivo de datos históricos
</span>
<span className="mt-1 block text-sm text-gray-500">
Arrastra y suelta tu archivo aquí, o haz clic para seleccionar
</span>
<span className="mt-1 block text-xs text-gray-400">
Máximo 10MB - CSV, Excel (.xlsx, .xls), JSON
</span>
</label>
<input
id="sales-file-upload"
type="file"
accept=".csv,.xlsx,.xls,.json"
required
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB.');
return;
}
// Update bakery data with the selected file
setBakeryData(prev => ({
...prev,
csvFile: file,
hasHistoricalData: true
}));
toast.success(`Archivo ${file.name} seleccionado correctamente`);
// Auto-validate the file after upload if tenantId exists
if (tenantId) {
setValidationStatus({ status: 'validating' });
try {
const validationResult = await validateSalesData(tenantId, file);
if (validationResult.is_valid) {
setValidationStatus({
status: 'valid',
message: validationResult.message,
records: validationResult.details?.total_records || 0
});
toast.success('¡Archivo validado correctamente!');
} else {
setValidationStatus({
status: 'invalid',
message: validationResult.message
});
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
});
toast.error('Error al validar el archivo');
}
} else {
// If no tenantId yet, set to idle and wait for manual validation
setValidationStatus({ status: 'idle' });
}
}
}}
className="hidden"
/>
</div>
</div>
{bakeryData.csvFile ? (
<div className="mt-4 space-y-3">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-gray-500 mr-2" />
<div>
<p className="text-sm font-medium text-gray-700">
{bakeryData.csvFile.name}
</p>
<p className="text-xs text-gray-600">
{(bakeryData.csvFile.size / 1024).toFixed(1)} KB {bakeryData.csvFile.type || 'Archivo de datos'}
</p>
</div>
</div>
<button
onClick={() => {
setBakeryData(prev => ({ ...prev, csvFile: undefined }));
setValidationStatus({ status: 'idle' });
}}
className="text-red-600 hover:text-red-800 text-sm"
>
Quitar
</button>
</div>
</div>
{/* Validation Section */}
<div className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-gray-900">
Validación de datos
</h4>
{validationStatus.status === 'validating' ? (
<Loader className="h-4 w-4 animate-spin text-blue-500" />
) : validationStatus.status === 'valid' ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : validationStatus.status === 'invalid' ? (
<AlertTriangle className="h-4 w-4 text-red-500" />
) : (
<Clock className="h-4 w-4 text-gray-400" />
)}
</div>
{validationStatus.status === 'idle' && tenantId ? (
<div className="space-y-2">
<p className="text-sm text-gray-600">
Valida tu archivo para verificar que tiene el formato correcto.
</p>
<button
onClick={validateSalesFile}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Validar archivo
</button>
</div>
) : (
<div className="space-y-2">
{!tenantId ? (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No se ha encontrado la panadería registrada.
</p>
<p className="text-xs text-yellow-600 mt-1">
Ve al paso anterior para registrar tu panadería o espera mientras se carga desde el servidor.
</p>
</div>
) : validationStatus.status !== 'idle' ? (
<button
onClick={() => setValidationStatus({ status: 'idle' })}
className="px-4 py-2 bg-gray-500 text-white text-sm rounded-lg hover:bg-gray-600"
>
Resetear validación
</button>
) : null}
</div>
)}
{validationStatus.status === 'validating' && (
<p className="text-sm text-blue-600">
Validando archivo... Por favor espera.
</p>
)}
{validationStatus.status === 'valid' && (
<div className="space-y-2">
<p className="text-sm text-green-700">
Archivo validado correctamente
</p>
{validationStatus.records && (
<p className="text-xs text-green-600">
{validationStatus.records} registros encontrados
</p>
)}
{validationStatus.message && (
<p className="text-xs text-green-600">
{validationStatus.message}
</p>
)}
</div>
)}
{validationStatus.status === 'invalid' && (
<div className="space-y-2">
<p className="text-sm text-red-700">
Error en validación
</p>
{validationStatus.message && (
<p className="text-xs text-red-600">
{validationStatus.message}
</p>
)}
<button
onClick={validateSalesFile}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Validar de nuevo
</button>
</div>
)}
</div>
</div>
) : (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<p className="text-sm text-yellow-800">
<strong>Archivo requerido:</strong> Selecciona un archivo con tus datos históricos de ventas
</p>
</div>
</div>
)}
</div>
{/* Show switch to smart import suggestion if traditional validation fails */}
{validationStatus.status === 'invalid' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start space-x-3">
<Brain className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-blue-900 mb-2">
💡 ¿Problemas con la validación?
</h4>
<p className="text-sm text-blue-700 mb-3">
Nuestra IA puede manejar archivos con formatos más flexibles y ayudarte a solucionar problemas automáticamente.
</p>
<button
onClick={() => setUseSmartImport(true)}
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
>
Probar importación inteligente
</button>
</div>
</div>
</div>
)}
{/* Smart Import Component */}
<SmartHistoricalDataImport
tenantId={tenantId}
onComplete={(result) => {
// Mark sales data as uploaded and proceed to training
completeStep('sales_data_uploaded', {
smart_import: true,
records_imported: result.successful_imports,
import_job_id: result.import_job_id,
tenant_id: tenantId,
user_id: user?.id
}).then(() => {
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
}).catch(() => {
// Continue even if step completion fails
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
});
}}
/>
</div>
);
@@ -1369,7 +908,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
</main>
{/* Navigation with Enhanced Accessibility - Hidden during training (step 3), completion (step 4), and smart import */}
{currentStep < 3 && !(currentStep === 2 && useSmartImport) && (
{currentStep < 3 && currentStep !== 2 && (
<nav
className="flex justify-between items-center bg-white rounded-3xl shadow-sm p-6"
role="navigation"
@@ -1391,16 +930,15 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
<div className="text-sm text-gray-500">Paso {currentStep} de 4</div>
<div className="text-xs text-gray-400 mt-1">
{currentStep === 1 && "Configura tu panadería"}
{currentStep === 2 && "Sube datos de ventas"}
</div>
</div>
{/* Dynamic Next Button with contextual text */}
<button
onClick={handleNext}
disabled={isLoading || (currentStep === 2 && validationStatus?.status !== 'valid')}
disabled={isLoading}
className="flex items-center px-8 py-3 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-xl hover:from-primary-600 hover:to-primary-700 transition-all hover:shadow-lg hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none group focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
aria-label={`${currentStep === 1 ? 'Save bakery information and continue' : currentStep === 2 && validationStatus?.status === 'valid' ? 'Start AI training' : 'Continue to next step'}`}
aria-label={`${currentStep === 1 ? 'Save bakery information and continue' : 'Continue to next step'}`}
>
{isLoading ? (
<>
@@ -1412,11 +950,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
) : (
<>
{currentStep === 1 && "Guardar y Continuar"}
{currentStep === 2 && (
<>
{validationStatus?.status === 'valid' ? "Iniciar Entrenamiento" : "Continuar"}
</>
)}
<ChevronRight className="h-5 w-5 ml-2 group-hover:translate-x-0.5 transition-transform" aria-hidden="true" />
</>
)}