Add onboardin steps improvements
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
useTraining,
|
||||
useData,
|
||||
useTrainingWebSocket,
|
||||
useOnboarding,
|
||||
TenantCreate,
|
||||
TrainingJobRequest
|
||||
} from '../../api';
|
||||
|
||||
import { OnboardingRouter } from '../../utils/onboardingRouter';
|
||||
|
||||
interface OnboardingPageProps {
|
||||
user: any;
|
||||
onComplete: () => void;
|
||||
@@ -47,6 +50,15 @@ const MADRID_PRODUCTS = [
|
||||
const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const manualNavigation = useRef(false);
|
||||
|
||||
// Enhanced onboarding with progress tracking
|
||||
const {
|
||||
progress,
|
||||
isLoading: onboardingLoading,
|
||||
completeStep,
|
||||
refreshProgress
|
||||
} = useOnboarding();
|
||||
const [bakeryData, setBakeryData] = useState<BakeryData>({
|
||||
name: '',
|
||||
address: '',
|
||||
@@ -56,9 +68,12 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
});
|
||||
|
||||
// Training progress state
|
||||
const [tenantId, setTenantId] = useState<string>('');
|
||||
const [tenantId, setTenantId] = useState<string>(() => {
|
||||
// Initialize from localStorage
|
||||
return localStorage.getItem('current_tenant_id') || '';
|
||||
});
|
||||
const [trainingJobId, setTrainingJobId] = useState<string>('');
|
||||
const { createTenant, isLoading: tenantLoading } = useTenant();
|
||||
const { createTenant, getUserTenants, isLoading: tenantLoading } = useTenant();
|
||||
const { startTrainingJob } = useTraining({ disablePolling: true });
|
||||
const { uploadSalesHistory, validateSalesData } = useData();
|
||||
|
||||
@@ -78,6 +93,60 @@ 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(() => {
|
||||
const initializeStepFromProgress = async () => {
|
||||
if (progress && !manualNavigation.current) {
|
||||
// Only initialize from progress if we haven't manually navigated
|
||||
try {
|
||||
const { step } = await OnboardingRouter.getOnboardingStepFromProgress();
|
||||
console.log('🎯 Initializing step from progress:', step);
|
||||
setCurrentStep(step);
|
||||
} catch (error) {
|
||||
console.warn('Failed to get step from progress, using default:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeStepFromProgress();
|
||||
}, [progress]);
|
||||
|
||||
// Fetch tenant ID from backend if not available locally
|
||||
useEffect(() => {
|
||||
const fetchTenantIdFromBackend = async () => {
|
||||
if (!tenantId && user) {
|
||||
console.log('🏢 No tenant ID in localStorage, fetching from backend...');
|
||||
try {
|
||||
const userTenants = await getUserTenants();
|
||||
console.log('📋 User tenants:', userTenants);
|
||||
|
||||
if (userTenants.length > 0) {
|
||||
// Use the first (most recent) tenant for onboarding
|
||||
const primaryTenant = userTenants[0];
|
||||
console.log('✅ Found tenant from backend:', primaryTenant.id);
|
||||
setTenantId(primaryTenant.id);
|
||||
storeTenantId(primaryTenant.id);
|
||||
} else {
|
||||
console.log('ℹ️ No tenants found for user - will be created in step 1');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to fetch user tenants:', error);
|
||||
// This is not a critical error - tenant will be created in step 1 if needed
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchTenantIdFromBackend();
|
||||
}, [tenantId, user, getUserTenants]);
|
||||
|
||||
// WebSocket connection for real-time training updates
|
||||
const {
|
||||
@@ -118,9 +187,19 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
currentStep: 'Entrenamiento completado',
|
||||
estimatedTimeRemaining: 0
|
||||
}));
|
||||
|
||||
// Mark training step as completed in onboarding API
|
||||
completeStep('training_completed', {
|
||||
training_completed_at: new Date().toISOString(),
|
||||
user_id: user?.id,
|
||||
tenant_id: tenantId
|
||||
}).catch(error => {
|
||||
console.warn('Failed to mark training as completed in API:', error);
|
||||
});
|
||||
|
||||
// Auto-advance to final step after 2 seconds
|
||||
setTimeout(() => {
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(4);
|
||||
}, 2000);
|
||||
|
||||
@@ -203,32 +282,238 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (validateCurrentStep()) {
|
||||
if (currentStep === 2) {
|
||||
// Always proceed to training step after CSV upload
|
||||
startTraining();
|
||||
} else {
|
||||
setCurrentStep(prev => Math.min(prev + 1, steps.length));
|
||||
const handleNext = async () => {
|
||||
console.log(`🚀 HandleNext called for step ${currentStep}`);
|
||||
|
||||
if (!validateCurrentStep()) {
|
||||
console.log('❌ Validation failed for current step');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (currentStep === 1) {
|
||||
console.log('📝 Processing step 1: Register bakery');
|
||||
await createBakeryAndTenant();
|
||||
console.log('✅ Step 1 completed successfully');
|
||||
} else if (currentStep === 2) {
|
||||
console.log('📂 Processing step 2: Upload sales data');
|
||||
await uploadAndValidateSalesData();
|
||||
console.log('✅ Step 2 completed successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error in step:', error);
|
||||
toast.error('Error al procesar el paso. Por favor, inténtalo de nuevo.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
console.log(`🔄 Loading state set to false, current step: ${currentStep}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(prev => Math.max(prev - 1, 1));
|
||||
};
|
||||
|
||||
// Create bakery and tenant for step 1
|
||||
const createBakeryAndTenant = async () => {
|
||||
try {
|
||||
console.log('🏪 Creating bakery tenant...');
|
||||
|
||||
let currentTenantId = tenantId;
|
||||
|
||||
// Check if user already has a tenant
|
||||
if (currentTenantId) {
|
||||
console.log('ℹ️ User already has tenant ID:', currentTenantId);
|
||||
console.log('✅ Using existing tenant, skipping creation');
|
||||
} else {
|
||||
console.log('📝 Creating new tenant');
|
||||
const tenantData: TenantCreate = {
|
||||
name: bakeryData.name,
|
||||
address: bakeryData.address,
|
||||
business_type: "bakery",
|
||||
postal_code: "28010",
|
||||
phone: "+34655334455",
|
||||
coordinates: bakeryData.coordinates,
|
||||
products: bakeryData.products,
|
||||
has_historical_data: bakeryData.hasHistoricalData,
|
||||
};
|
||||
|
||||
const newTenant = await createTenant(tenantData);
|
||||
console.log('✅ Tenant created:', newTenant.id);
|
||||
|
||||
currentTenantId = newTenant.id;
|
||||
setTenantId(newTenant.id);
|
||||
storeTenantId(newTenant.id);
|
||||
}
|
||||
|
||||
// Mark step as completed in onboarding API (non-blocking)
|
||||
try {
|
||||
await completeStep('bakery_registered', {
|
||||
bakery_name: bakeryData.name,
|
||||
bakery_address: bakeryData.address,
|
||||
business_type: bakeryData.businessType,
|
||||
tenant_id: currentTenantId, // Use the correct tenant ID
|
||||
user_id: user?.id
|
||||
});
|
||||
console.log('✅ Step marked as completed');
|
||||
} catch (stepError) {
|
||||
console.warn('⚠️ Failed to mark step as completed, but continuing:', stepError);
|
||||
// Don't throw here - step completion is not critical for UI flow
|
||||
}
|
||||
|
||||
toast.success('Panadería registrada correctamente');
|
||||
console.log('🎯 Advancing to step 2');
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(2);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating tenant:', error);
|
||||
throw new Error('Error al registrar la panadería');
|
||||
}
|
||||
};
|
||||
|
||||
// Validate sales data without importing
|
||||
const validateSalesFile = async () => {
|
||||
console.log('🔍 validateSalesFile called');
|
||||
console.log('tenantId:', tenantId);
|
||||
console.log('bakeryData.csvFile:', bakeryData.csvFile);
|
||||
|
||||
if (!tenantId || !bakeryData.csvFile) {
|
||||
console.log('❌ Missing tenantId or csvFile');
|
||||
toast.error('Falta información del tenant o archivo');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('⏳ Setting validation status to validating');
|
||||
setValidationStatus({ status: 'validating' });
|
||||
|
||||
try {
|
||||
console.log('📤 Calling validateSalesData API');
|
||||
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
|
||||
console.log('📥 Validation result:', validationResult);
|
||||
|
||||
if (validationResult.is_valid) {
|
||||
console.log('✅ Validation successful');
|
||||
setValidationStatus({
|
||||
status: 'valid',
|
||||
message: validationResult.message,
|
||||
records: validationResult.details?.total_records || 0
|
||||
});
|
||||
toast.success('¡Archivo validado correctamente!');
|
||||
} else {
|
||||
console.log('❌ Validation failed');
|
||||
setValidationStatus({
|
||||
status: 'invalid',
|
||||
message: validationResult.message
|
||||
});
|
||||
toast.error(`Error en validación: ${validationResult.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error validating sales data:', error);
|
||||
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) {
|
||||
console.error('Error uploading sales data:', error);
|
||||
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 () => {
|
||||
const stepMap: Record<number, string> = {
|
||||
1: 'bakery_registered',
|
||||
2: 'sales_data_uploaded',
|
||||
3: 'training_completed',
|
||||
4: 'dashboard_accessible'
|
||||
};
|
||||
|
||||
const stepName = stepMap[currentStep];
|
||||
if (stepName) {
|
||||
const stepData: Record<string, any> = {
|
||||
completed_at: new Date().toISOString(),
|
||||
user_id: user?.id
|
||||
};
|
||||
|
||||
// Add step-specific data
|
||||
if (currentStep === 1) {
|
||||
stepData.bakery_name = bakeryData.name;
|
||||
stepData.bakery_address = bakeryData.address;
|
||||
stepData.business_type = bakeryData.businessType;
|
||||
stepData.products_count = bakeryData.products.length;
|
||||
} else if (currentStep === 2) {
|
||||
stepData.file_name = bakeryData.csvFile?.name;
|
||||
stepData.file_size = bakeryData.csvFile?.size;
|
||||
stepData.file_type = bakeryData.csvFile?.type;
|
||||
stepData.has_historical_data = bakeryData.hasHistoricalData;
|
||||
}
|
||||
|
||||
await completeStep(stepName, stepData);
|
||||
// Note: Not calling refreshProgress() here to avoid step reset
|
||||
|
||||
toast.success(`✅ Paso ${currentStep} completado`);
|
||||
}
|
||||
};
|
||||
|
||||
const validateCurrentStep = (): boolean => {
|
||||
console.log(`🔍 Validating step ${currentStep}`);
|
||||
console.log('Bakery data:', { name: bakeryData.name, address: bakeryData.address });
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
if (!bakeryData.name.trim()) {
|
||||
console.log('❌ Bakery name is empty');
|
||||
toast.error('El nombre de la panadería es obligatorio');
|
||||
return false;
|
||||
}
|
||||
if (!bakeryData.address.trim()) {
|
||||
console.log('❌ Bakery address is empty');
|
||||
toast.error('La dirección es obligatoria');
|
||||
return false;
|
||||
}
|
||||
console.log('✅ Step 1 validation passed');
|
||||
return true;
|
||||
case 2:
|
||||
if (!bakeryData.csvFile) {
|
||||
@@ -252,6 +537,12 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
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;
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
@@ -260,6 +551,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
};
|
||||
|
||||
const startTraining = async () => {
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(3);
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -270,48 +562,9 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
return;
|
||||
}
|
||||
|
||||
// Create tenant first
|
||||
const tenantData: TenantCreate = {
|
||||
name: bakeryData.name,
|
||||
address: bakeryData.address,
|
||||
business_type: "bakery",
|
||||
postal_code: "28010",
|
||||
phone: "+34655334455",
|
||||
coordinates: bakeryData.coordinates,
|
||||
products: bakeryData.products,
|
||||
has_historical_data: bakeryData.hasHistoricalData,
|
||||
};
|
||||
|
||||
const tenant = await createTenant(tenantData);
|
||||
setTenantId(tenant.id);
|
||||
storeTenantId(tenant.id);
|
||||
|
||||
// Step 2: Validate and Upload CSV file if provided
|
||||
if (bakeryData.csvFile) {
|
||||
try {
|
||||
const validationResult = await validateSalesData(tenant.id, bakeryData.csvFile);
|
||||
if (!validationResult.is_valid) {
|
||||
toast.error(`Error en los datos: ${validationResult.message}`);
|
||||
setTrainingProgress(prev => ({
|
||||
...prev,
|
||||
status: 'failed',
|
||||
error: 'Error en la validación de datos históricos'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
await uploadSalesHistory(tenant.id, bakeryData.csvFile);
|
||||
toast.success('Datos históricos validados y subidos correctamente');
|
||||
} catch (error) {
|
||||
console.error('CSV validation/upload error:', error);
|
||||
toast.error('Error al procesar los datos históricos');
|
||||
setTrainingProgress(prev => ({
|
||||
...prev,
|
||||
status: 'failed',
|
||||
error: 'Error al procesar los datos históricos'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!tenantId) {
|
||||
toast.error('Error: No se ha registrado la panadería correctamente.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare training job request - always use uploaded data since CSV is required
|
||||
@@ -323,7 +576,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
};
|
||||
|
||||
// Start training job using the proper API
|
||||
const trainingJob = await startTrainingJob(tenant.id, trainingRequest);
|
||||
const trainingJob = await startTrainingJob(tenantId, trainingRequest);
|
||||
setTrainingJobId(trainingJob.job_id);
|
||||
|
||||
setTrainingProgress({
|
||||
@@ -357,9 +610,24 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
// Start training process
|
||||
await startTraining();
|
||||
} else {
|
||||
// Complete onboarding
|
||||
toast.success('¡Configuración completada exitosamente!');
|
||||
onComplete();
|
||||
try {
|
||||
// Mark final step as completed
|
||||
await completeStep('dashboard_accessible', {
|
||||
completion_time: new Date().toISOString(),
|
||||
user_id: user?.id,
|
||||
tenant_id: tenantId,
|
||||
final_step: true
|
||||
});
|
||||
|
||||
// Complete onboarding
|
||||
toast.success('¡Configuración completada exitosamente!');
|
||||
onComplete();
|
||||
} catch (error) {
|
||||
console.warn('Failed to mark final step as completed:', error);
|
||||
// Continue anyway for better UX
|
||||
toast.success('¡Configuración completada exitosamente!');
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -380,6 +648,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
icon: 'ℹ️',
|
||||
duration: 4000
|
||||
});
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(4);
|
||||
};
|
||||
|
||||
@@ -573,6 +842,8 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
csvFile: file,
|
||||
hasHistoricalData: true
|
||||
}));
|
||||
// Reset validation status when new file is selected
|
||||
setValidationStatus({ status: 'idle' });
|
||||
toast.success(`Archivo ${file.name} seleccionado correctamente`);
|
||||
}
|
||||
}}
|
||||
@@ -582,25 +853,130 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
</div>
|
||||
|
||||
{bakeryData.csvFile ? (
|
||||
<div className="mt-4 p-4 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="h-5 w-5 text-green-500 mr-2" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-700">
|
||||
{bakeryData.csvFile.name}
|
||||
</p>
|
||||
<p className="text-xs text-green-600">
|
||||
{(bakeryData.csvFile.size / 1024).toFixed(1)} KB • {bakeryData.csvFile.type || 'Archivo de datos'}
|
||||
</p>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setBakeryData(prev => ({ ...prev, csvFile: undefined }))}
|
||||
className="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Quitar
|
||||
</button>
|
||||
</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">
|
||||
<p className="text-sm text-gray-500">
|
||||
Debug: validationStatus = "{validationStatus.status}", tenantId = "{tenantId || 'not set'}"
|
||||
</p>
|
||||
{!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>
|
||||
) : (
|
||||
@@ -748,6 +1124,18 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading while onboarding data is being fetched
|
||||
if (onboardingLoading && !progress) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-white to-blue-50">
|
||||
<div className="text-center">
|
||||
<Loader className="h-8 w-8 animate-spin mx-auto mb-4 text-primary-500" />
|
||||
<p className="text-gray-600">Cargando progreso de configuración...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-blue-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -759,6 +1147,20 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
<p className="text-gray-600">
|
||||
Configuremos tu panadería para obtener predicciones precisas de demanda
|
||||
</p>
|
||||
|
||||
{/* Progress Summary */}
|
||||
{progress && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<div className="text-sm text-blue-700">
|
||||
<span className="font-semibold">Progreso:</span> {Math.round(progress.completion_percentage)}%
|
||||
</div>
|
||||
<div className="text-sm text-blue-600">
|
||||
({progress.steps.filter((s: any) => s.completed).length}/{progress.steps.length} pasos completados)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
@@ -790,7 +1192,9 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
<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}%` }}
|
||||
style={{
|
||||
width: `${progress ? progress.completion_percentage : (currentStep / steps.length) * 100}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -857,7 +1261,10 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
</>
|
||||
) : trainingProgress.status === 'completed' ? (
|
||||
<button
|
||||
onClick={() => setCurrentStep(4)}
|
||||
onClick={() => {
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(4);
|
||||
}}
|
||||
className="flex items-center px-6 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all hover:shadow-lg"
|
||||
>
|
||||
Continuar
|
||||
|
||||
Reference in New Issue
Block a user