Simplify the onboardinf flow components 2

This commit is contained in:
Urtzi Alfaro
2025-09-08 21:44:04 +02:00
parent 2e1e696cb5
commit c8b1a941f8
6 changed files with 726 additions and 269 deletions

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../../ui/Button';
import { Card, CardHeader, CardBody } from '../../ui/Card';
import { useAuth } from '../../../contexts/AuthContext';
import { useMarkStepCompleted } from '../../../api/hooks/onboarding';
import { useUserProgress, useMarkStepCompleted } from '../../../api/hooks/onboarding';
import { useTenantActions } from '../../../stores/tenant.store';
import { useTenantInitializer } from '../../../stores/useTenantInitializer';
import {
@@ -27,11 +28,13 @@ interface StepProps {
isLastStep: boolean;
}
// Steps must match backend ONBOARDING_STEPS exactly
// Note: "user_registered" is auto-completed and not shown in UI
const STEPS: StepConfig[] = [
{
id: 'setup',
title: 'Registrar Panadería',
description: 'Configura la información de tu panadería',
description: 'Configura la información básica de tu panadería',
component: RegisterTenantStep,
},
{
@@ -43,37 +46,136 @@ const STEPS: StepConfig[] = [
{
id: 'ml-training',
title: 'Entrenamiento IA',
description: 'Entrena tu modelo de inteligencia artificial',
description: 'Entrena tu modelo de inteligencia artificial personalizado',
component: MLTrainingStep,
},
{
id: 'completion',
title: 'Configuración Completa',
description: 'Bienvenido a tu sistema de gestión de panadería',
description: '¡Bienvenido a tu sistema de gestión inteligente!',
component: CompletionStep,
},
];
export const OnboardingWizard: React.FC = () => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [isInitialized, setIsInitialized] = useState(false);
const navigate = useNavigate();
const { user } = useAuth();
// Initialize tenant data for authenticated users
useTenantInitializer();
// Get user progress from backend
const { data: userProgress, isLoading: isLoadingProgress, error: progressError } = useUserProgress(
user?.id || '',
{ enabled: !!user?.id }
);
const markStepCompleted = useMarkStepCompleted();
const { setCurrentTenant } = useTenantActions();
// Auto-complete user_registered step if needed (runs first)
useEffect(() => {
if (userProgress && user?.id) {
const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered');
if (!userRegisteredStep?.completed) {
console.log('🔄 Auto-completing user_registered step for new user...');
markStepCompleted.mutate({
userId: user.id,
stepName: 'user_registered',
data: {
auto_completed: true,
completed_at: new Date().toISOString(),
source: 'onboarding_wizard_auto_completion'
}
}, {
onSuccess: () => {
console.log('✅ user_registered step auto-completed successfully');
// The query will automatically refetch and update userProgress
},
onError: (error) => {
console.error('❌ Failed to auto-complete user_registered step:', error);
}
});
}
}
}, [userProgress, user?.id, markStepCompleted]);
// Initialize step index based on backend progress with validation
useEffect(() => {
if (userProgress && !isInitialized) {
console.log('🔄 Initializing onboarding progress:', userProgress);
// Check if user_registered step is completed
const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered');
if (!userRegisteredStep?.completed) {
console.log('⏳ Waiting for user_registered step to be auto-completed...');
return; // Wait for auto-completion to finish
}
// Find the current step index based on backend progress
const currentStepFromBackend = userProgress.current_step;
let stepIndex = STEPS.findIndex(step => step.id === currentStepFromBackend);
console.log(`🎯 Backend current step: "${currentStepFromBackend}", found at index: ${stepIndex}`);
// If current step is not found (e.g., suppliers step), find the next incomplete step
if (stepIndex === -1) {
console.log('🔍 Current step not found in UI steps, finding first incomplete step...');
// Find the first incomplete step that user can access
for (let i = 0; i < STEPS.length; i++) {
const step = STEPS[i];
const stepProgress = userProgress.steps.find(s => s.step_name === step.id);
if (!stepProgress?.completed) {
stepIndex = i;
console.log(`📍 Found first incomplete step: "${step.id}" at index ${i}`);
break;
}
}
// If all visible steps are completed, go to last step
if (stepIndex === -1) {
stepIndex = STEPS.length - 1;
console.log('✅ All steps completed, going to last step');
}
}
// Ensure user can't skip ahead - find the first incomplete step
const firstIncompleteStepIndex = STEPS.findIndex(step => {
const stepProgress = userProgress.steps.find(s => s.step_name === step.id);
return !stepProgress?.completed;
});
if (firstIncompleteStepIndex !== -1 && stepIndex > firstIncompleteStepIndex) {
console.log(`🚫 User trying to skip ahead. Redirecting to first incomplete step at index ${firstIncompleteStepIndex}`);
stepIndex = firstIncompleteStepIndex;
}
console.log(`🎯 Final step index: ${stepIndex} ("${STEPS[stepIndex]?.id}")`);
if (stepIndex !== currentStepIndex) {
setCurrentStepIndex(stepIndex);
}
setIsInitialized(true);
}
}, [userProgress, isInitialized, currentStepIndex]);
const currentStep = STEPS[currentStepIndex];
const handlePrevious = () => {
if (currentStepIndex > 0) {
setCurrentStepIndex(currentStepIndex - 1);
}
};
const handleStepComplete = async (data?: any) => {
if (!user?.id) {
console.error('User ID not available');
return;
}
console.log(`🎯 Completing step: "${currentStep.id}" with data:`, data);
try {
// Special handling for setup step - set the created tenant in tenant store
if (currentStep.id === 'setup' && data?.tenant) {
@@ -81,11 +183,14 @@ export const OnboardingWizard: React.FC = () => {
}
// Mark step as completed in backend
console.log(`📤 Sending API request to complete step: "${currentStep.id}"`);
await markStepCompleted.mutateAsync({
userId: user?.id || '',
userId: user.id,
stepName: currentStep.id,
data
});
console.log(`✅ Successfully completed step: "${currentStep.id}"`);
if (currentStep.id === 'completion') {
navigate('/app');
@@ -95,91 +200,235 @@ export const OnboardingWizard: React.FC = () => {
setCurrentStepIndex(currentStepIndex + 1);
}
}
} catch (error) {
console.error('Error marking step as completed:', error);
} catch (error: any) {
console.error(`❌ Error completing step "${currentStep.id}":`, error);
// Extract detailed error information
const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error';
const statusCode = error?.response?.status;
console.error(`📊 Error details: Status ${statusCode}, Message: ${errorMessage}`);
// Check if it's a dependency error
if (errorMessage.includes('dependencies not met')) {
console.error('🚫 Dependencies not met for step:', currentStep.id);
// Check what dependencies are missing
if (userProgress) {
console.log('📋 Current progress:', userProgress);
console.log('📋 Completed steps:', userProgress.steps.filter(s => s.completed).map(s => s.step_name));
}
}
// Don't advance automatically on error - user should see the issue
alert(`Error al completar paso "${currentStep.title}": ${errorMessage}`);
}
};
// Show loading state while initializing progress
if (isLoadingProgress || !isInitialized) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6">
<Card padding="lg" shadow="lg">
<CardBody>
<div className="flex items-center justify-center space-x-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
<p className="text-[var(--text-secondary)] text-sm sm:text-base">Cargando tu progreso...</p>
</div>
</CardBody>
</Card>
</div>
);
}
// Show error state if progress fails to load
if (progressError) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6">
<Card padding="lg" shadow="lg">
<CardBody>
<div className="text-center space-y-4">
<div className="w-14 h-14 sm:w-16 sm:h-16 mx-auto bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<svg className="w-7 h-7 sm:w-8 sm:h-8 text-[var(--color-error)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h2 className="text-base sm:text-lg font-semibold text-[var(--text-primary)] mb-2">
Error al cargar progreso
</h2>
<p className="text-sm sm:text-base text-[var(--text-secondary)] mb-4 px-2">
No pudimos cargar tu progreso de configuración. Puedes continuar desde el inicio.
</p>
<Button
onClick={() => setIsInitialized(true)}
variant="primary"
size="lg"
>
Continuar
</Button>
</div>
</div>
</CardBody>
</Card>
</div>
);
}
const StepComponent = currentStep.component;
const progressPercentage = userProgress?.completion_percentage || ((currentStepIndex + 1) / STEPS.length) * 100;
return (
<div className="max-w-4xl mx-auto">
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
Bienvenido a Bakery IA
</h1>
<span className="text-sm text-[var(--text-secondary)]">
Paso {currentStepIndex + 1} de {STEPS.length}
</span>
<div className="max-w-4xl mx-auto px-4 sm:px-6 space-y-4 sm:space-y-6 pb-6">
{/* Enhanced Progress Header */}
<Card shadow="sm" padding="lg">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 space-y-2 sm:space-y-0">
<div className="text-center sm:text-left">
<h1 className="text-xl sm:text-2xl font-bold text-[var(--text-primary)]">
Bienvenido a Bakery IA
</h1>
<p className="text-[var(--text-secondary)] text-xs sm:text-sm mt-1">
Configura tu sistema de gestión inteligente paso a paso
</p>
</div>
<div className="text-center sm:text-right">
<div className="text-sm text-[var(--text-secondary)]">
Paso {currentStepIndex + 1} de {STEPS.length}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
{Math.round(progressPercentage)}% completado
</div>
</div>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2">
{/* Progress Bar */}
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2 sm:h-3 mb-4">
<div
className="bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
style={{ width: `${((currentStepIndex + 1) / STEPS.length) * 100}%` }}
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/80 h-2 sm:h-3 rounded-full transition-all duration-500 ease-out"
style={{ width: `${progressPercentage}%` }}
/>
</div>
<div className="flex justify-between mt-4">
{STEPS.map((step, index) => (
<div
key={step.id}
className={`flex-1 text-center px-2 ${
index <= currentStepIndex
? 'text-[var(--color-primary)]'
: 'text-[var(--text-tertiary)]'
}`}
>
<div className={`text-xs font-medium mb-1`}>
{step.title}
</div>
<div className="text-xs opacity-75">
{step.description}
</div>
</div>
))}
{/* Mobile Step Indicators - Horizontal scroll on small screens */}
<div className="sm:hidden">
<div className="flex space-x-4 overflow-x-auto pb-2 px-1">
{STEPS.map((step, index) => {
const isCompleted = userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex;
const isCurrent = index === currentStepIndex;
return (
<div
key={step.id}
className={`flex-shrink-0 text-center min-w-[80px] ${
isCompleted
? 'text-[var(--color-success)]'
: isCurrent
? 'text-[var(--color-primary)]'
: 'text-[var(--text-tertiary)]'
}`}
>
<div className="flex items-center justify-center mb-1">
{isCompleted ? (
<div className="w-8 h-8 bg-[var(--color-success)] rounded-full flex items-center justify-center shadow-sm">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
) : isCurrent ? (
<div className="w-8 h-8 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white text-sm font-bold shadow-sm ring-2 ring-[var(--color-primary)]/20">
{index + 1}
</div>
) : (
<div className="w-8 h-8 bg-[var(--bg-tertiary)] rounded-full flex items-center justify-center text-[var(--text-tertiary)] text-sm">
{index + 1}
</div>
)}
</div>
<div className="text-xs font-medium leading-tight">
{step.title}
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Desktop Step Indicators */}
<div className="hidden sm:flex sm:justify-between">
{STEPS.map((step, index) => {
const isCompleted = userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex;
const isCurrent = index === currentStepIndex;
return (
<div
key={step.id}
className={`flex-1 text-center px-2 ${
isCompleted
? 'text-[var(--color-success)]'
: isCurrent
? 'text-[var(--color-primary)]'
: 'text-[var(--text-tertiary)]'
}`}
>
<div className="flex items-center justify-center mb-2">
{isCompleted ? (
<div className="w-7 h-7 bg-[var(--color-success)] rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
) : isCurrent ? (
<div className="w-7 h-7 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white text-sm font-bold">
{index + 1}
</div>
) : (
<div className="w-7 h-7 bg-[var(--bg-tertiary)] rounded-full flex items-center justify-center text-[var(--text-tertiary)] text-sm">
{index + 1}
</div>
)}
</div>
<div className="text-xs sm:text-sm font-medium mb-1">
{step.title}
</div>
<div className="text-xs opacity-75">
{step.description}
</div>
</div>
);
})}
</div>
</Card>
{/* Step Content */}
<div className="bg-[var(--bg-primary)] rounded-lg shadow-lg p-8">
<div className="mb-6">
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
{currentStep.title}
</h2>
<p className="text-[var(--text-secondary)]">
{currentStep.description}
</p>
</div>
<StepComponent
onNext={() => {}} // No-op - steps must use onComplete instead
onPrevious={handlePrevious}
onComplete={handleStepComplete}
isFirstStep={currentStepIndex === 0}
isLastStep={currentStepIndex === STEPS.length - 1}
/>
</div>
{/* Navigation */}
<div className="flex justify-between mt-8">
<Button
variant="outline"
onClick={handlePrevious}
disabled={currentStepIndex === 0}
>
Anterior
</Button>
<Card shadow="lg" padding="none">
<CardHeader padding="lg" divider>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<div className="w-5 h-5 sm:w-6 sm:h-6 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white text-xs font-bold">
{currentStepIndex + 1}
</div>
</div>
<div className="flex-1">
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
{currentStep.title}
</h2>
<p className="text-[var(--text-secondary)] text-xs sm:text-sm">
{currentStep.description}
</p>
</div>
</div>
</CardHeader>
<div className="text-sm text-[var(--text-tertiary)] self-center">
Puedes pausar y reanudar este proceso en cualquier momento
</div>
{/* No skip button - all steps are required */}
<div></div>
</div>
<CardBody padding="lg">
<StepComponent
onNext={() => {}} // No-op - steps must use onComplete instead
onPrevious={() => {}} // No-op - users cannot go back once they've moved forward
onComplete={handleStepComplete}
isFirstStep={currentStepIndex === 0}
isLastStep={currentStepIndex === STEPS.length - 1}
/>
</CardBody>
</Card>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useCallback } from 'react';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useCreateTrainingJob, useTrainingWebSocket } from '../../../../api/hooks/training';
@@ -32,50 +32,59 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
const currentTenant = useCurrentTenant();
const createTrainingJob = useCreateTrainingJob();
// WebSocket for real-time training progress
const trainingWebSocket = useTrainingWebSocket(
// Memoized WebSocket callbacks to prevent reconnections
const handleProgress = useCallback((data: any) => {
setTrainingProgress({
stage: 'training',
progress: data.data?.progress || 0,
message: data.data?.message || 'Entrenando modelo...',
currentStep: data.data?.current_step,
estimatedTimeRemaining: data.data?.estimated_time_remaining
});
}, []);
const handleCompleted = useCallback((_data: any) => {
setTrainingProgress({
stage: 'completed',
progress: 100,
message: 'Entrenamiento completado exitosamente'
});
setIsTraining(false);
setTimeout(() => {
onComplete({
jobId: jobId,
success: true,
message: 'Modelo entrenado correctamente'
});
}, 2000);
}, [onComplete, jobId]);
const handleError = useCallback((data: any) => {
setError(data.data?.error || data.error || 'Error durante el entrenamiento');
setIsTraining(false);
setTrainingProgress(null);
}, []);
const handleStarted = useCallback((_data: any) => {
setTrainingProgress({
stage: 'starting',
progress: 5,
message: 'Iniciando entrenamiento del modelo...'
});
}, []);
// WebSocket for real-time training progress - only connect when we have a jobId
const { isConnected, connectionError } = useTrainingWebSocket(
currentTenant?.id || '',
jobId || '',
undefined, // token will be handled by the service
{
onProgress: (data) => {
setTrainingProgress({
stage: 'training',
progress: data.progress?.percentage || 0,
message: data.message || 'Entrenando modelo...',
currentStep: data.progress?.current_step,
estimatedTimeRemaining: data.progress?.estimated_time_remaining
});
},
onCompleted: (data) => {
setTrainingProgress({
stage: 'completed',
progress: 100,
message: 'Entrenamiento completado exitosamente'
});
setIsTraining(false);
setTimeout(() => {
onComplete({
jobId: jobId,
success: true,
message: 'Modelo entrenado correctamente'
});
}, 2000);
},
onError: (data) => {
setError(data.error || 'Error durante el entrenamiento');
setIsTraining(false);
setTrainingProgress(null);
},
onStarted: (data) => {
setTrainingProgress({
stage: 'starting',
progress: 5,
message: 'Iniciando entrenamiento del modelo...'
});
}
}
jobId ? {
onProgress: handleProgress,
onCompleted: handleCompleted,
onError: handleError,
onStarted: handleStarted
} : undefined
);
const handleStartTraining = async () => {
@@ -201,9 +210,16 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<div className="flex justify-between text-xs text-[var(--text-tertiary)]">
<span>{trainingProgress.currentStep || 'Procesando...'}</span>
{trainingProgress.estimatedTimeRemaining && (
<span>Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)}</span>
)}
<div className="flex items-center gap-2">
{jobId && (
<span className={`text-xs ${isConnected ? 'text-green-500' : 'text-red-500'}`}>
{isConnected ? '🟢 Conectado' : '🔴 Desconectado'}
</span>
)}
{trainingProgress.estimatedTimeRemaining && (
<span>Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)}</span>
)}
</div>
</div>
</div>
)}
@@ -224,9 +240,9 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
</ul>
</div>
{error && (
{(error || connectionError) && (
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-4">
<p className="text-[var(--color-error)]">{error}</p>
<p className="text-[var(--color-error)]">{error || connectionError}</p>
</div>
)}

View File

@@ -24,18 +24,28 @@ interface ProgressState {
interface InventoryItem {
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: string;
category: string;
unit_of_measure: string;
selected: boolean;
stock_quantity: number;
expiration_days: number;
cost_per_unit: number;
confidence_score: number;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
notes?: string;
sales_data?: {
total_quantity: number;
average_daily_sales: number;
peak_day: string;
frequency: number;
};
// UI-specific fields
selected: boolean;
stock_quantity: number;
cost_per_unit: number;
}
export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
@@ -131,15 +141,9 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
setProgressState({ stage: 'analyzing', progress: 25, message: 'Analizando productos de ventas...' });
try {
// Extract product data from validation result
const products = validationResult.product_summary?.map((product: any) => ({
product_name: product.name,
sales_volume: product.total_quantity,
sales_data: {
total_quantity: product.total_quantity,
average_daily_sales: product.average_daily_sales,
frequency: product.frequency
}
// Extract product data from validation result - use the exact backend structure
const products = validationResult.product_list?.map((productName: string) => ({
product_name: productName
})) || [];
if (products.length === 0) {
@@ -158,7 +162,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
setProgressState({ stage: 'preparing', progress: 75, message: 'Preparando sugerencias de inventario...' });
// Convert API response to InventoryItem format
// Convert API response to InventoryItem format - use exact backend structure plus UI fields
const items: InventoryItem[] = suggestions.map(suggestion => {
// Calculate default stock quantity based on sales data
const defaultStock = Math.max(
@@ -172,19 +176,25 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
3.0;
return {
// Exact backend fields
suggestion_id: suggestion.suggestion_id,
original_name: suggestion.original_name,
suggested_name: suggestion.suggested_name,
product_type: suggestion.product_type,
category: suggestion.category,
unit_of_measure: suggestion.unit_of_measure,
selected: suggestion.confidence_score > 0.7, // Auto-select high confidence items
stock_quantity: defaultStock,
expiration_days: suggestion.estimated_shelf_life_days || 30,
cost_per_unit: estimatedCost,
confidence_score: suggestion.confidence_score,
estimated_shelf_life_days: suggestion.estimated_shelf_life_days,
requires_refrigeration: suggestion.requires_refrigeration,
requires_freezing: suggestion.requires_freezing,
is_seasonal: suggestion.is_seasonal,
notes: suggestion.notes
suggested_supplier: suggestion.suggested_supplier,
notes: suggestion.notes,
sales_data: suggestion.sales_data,
// UI-specific fields
selected: suggestion.confidence_score > 0.7, // Auto-select high confidence items
stock_quantity: defaultStock,
cost_per_unit: estimatedCost
};
});
@@ -241,18 +251,31 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
const createdIngredients = [];
for (const item of selectedItems) {
// Ensure reorder_point > minimum_stock_level as required by backend validation
const minimumStock = Math.max(1, Math.ceil(item.stock_quantity * 0.2));
const calculatedReorderPoint = Math.ceil(item.stock_quantity * 0.3);
const reorderPoint = Math.max(minimumStock + 2, calculatedReorderPoint, minimumStock + 1);
console.log(`📊 Inventory validation for "${item.suggested_name}":`, {
stockQuantity: item.stock_quantity,
minimumStock,
calculatedReorderPoint,
finalReorderPoint: reorderPoint,
isValid: reorderPoint > minimumStock
});
const ingredientData = {
name: item.suggested_name,
category: item.category,
unit_of_measure: item.unit_of_measure,
minimum_stock_level: Math.ceil(item.stock_quantity * 0.2),
maximum_stock_level: item.stock_quantity * 2,
reorder_point: Math.ceil(item.stock_quantity * 0.3),
shelf_life_days: item.expiration_days,
low_stock_threshold: minimumStock,
max_stock_level: item.stock_quantity * 2,
reorder_point: reorderPoint,
shelf_life_days: item.estimated_shelf_life_days || 30,
requires_refrigeration: item.requires_refrigeration,
requires_freezing: item.requires_freezing,
is_seasonal: item.is_seasonal,
cost_per_unit: item.cost_per_unit,
average_cost: item.cost_per_unit,
notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%`
};
@@ -338,12 +361,12 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
{/* Summary */}
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-3 sm:space-y-0">
<div className="text-center sm:text-left">
<p className="font-medium text-sm sm:text-base">
{selectedCount} de {inventoryItems.length} artículos seleccionados
</p>
<p className="text-sm text-[var(--text-secondary)]">
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">
Los artículos con alta confianza están preseleccionados
</p>
</div>
@@ -351,6 +374,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
variant="outline"
size="sm"
onClick={handleSelectAll}
className="w-full sm:w-auto"
>
{allSelected ? 'Deseleccionar Todos' : 'Seleccionar Todos'}
</Button>
@@ -361,7 +385,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<div className="space-y-4 max-h-96 overflow-y-auto">
{inventoryItems.map((item) => (
<div
key={item.id}
key={item.suggestion_id}
className={`border rounded-lg p-4 transition-colors ${
item.selected
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
@@ -373,7 +397,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<input
type="checkbox"
checked={item.selected}
onChange={() => handleToggleSelection(item.id)}
onChange={() => handleToggleSelection(item.suggestion_id)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-[var(--color-primary)]"
/>
</div>
@@ -381,7 +405,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<div className="flex-1 space-y-3">
<div>
<h3 className="font-medium text-[var(--text-primary)]">
{item.name}
{item.suggested_name}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{item.category} Unidad: {item.unit_of_measure}
@@ -395,18 +419,28 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
Requiere refrigeración
</span>
)}
{item.requires_freezing && (
<span className="text-xs bg-cyan-100 text-cyan-800 px-2 py-1 rounded">
Requiere congelación
</span>
)}
{item.is_seasonal && (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
Producto estacional
</span>
)}
</div>
</div>
{item.selected && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-3 border-t border-[var(--border-secondary)]">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 pt-3 border-t border-[var(--border-secondary)]">
<Input
label="Stock Inicial"
type="number"
min="0"
value={item.stock_quantity.toString()}
onChange={(e) => handleUpdateItem(
item.id,
item.suggestion_id,
'stock_quantity',
Number(e.target.value)
)}
@@ -419,7 +453,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
step="0.01"
value={item.cost_per_unit.toString()}
onChange={(e) => handleUpdateItem(
item.id,
item.suggestion_id,
'cost_per_unit',
Number(e.target.value)
)}
@@ -429,13 +463,14 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
label="Días de Caducidad"
type="number"
min="1"
value={item.expiration_days.toString()}
value={(item.estimated_shelf_life_days || 30).toString()}
onChange={(e) => handleUpdateItem(
item.id,
'expiration_days',
item.suggestion_id,
'estimated_shelf_life_days',
Number(e.target.value)
)}
size="sm"
className="sm:col-span-2 lg:col-span-1"
/>
</div>
)}
@@ -452,10 +487,11 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
)}
{/* Actions */}
<div className="flex justify-between">
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-0">
<Button
variant="outline"
onClick={() => setShowInventoryStep(false)}
className="order-2 sm:order-1 w-full sm:w-auto"
>
Volver
</Button>
@@ -466,8 +502,14 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
loadingText="Creando Inventario..."
size="lg"
disabled={selectedCount === 0}
className="order-1 sm:order-2 w-full sm:w-auto"
>
Crear {selectedCount} Artículo{selectedCount !== 1 ? 's' : ''} de Inventario
<span className="hidden sm:inline">
Crear {selectedCount} Artículo{selectedCount !== 1 ? 's' : ''} de Inventario
</span>
<span className="sm:hidden">
Crear {selectedCount} Artículo{selectedCount !== 1 ? 's' : ''}
</span>
</Button>
</div>
</div>
@@ -574,7 +616,9 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<p className="font-medium text-[var(--color-warning)]">Warnings:</p>
<ul className="list-disc list-inside">
{validationResult.warnings.map((warning, index) => (
<li key={index} className="text-[var(--color-warning)]">{warning}</li>
<li key={index} className="text-[var(--color-warning)]">
{typeof warning === 'string' ? warning : JSON.stringify(warning)}
</li>
))}
</ul>
</div>
@@ -591,23 +635,26 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
)}
{/* Actions */}
<div className="flex justify-between">
<div className="flex flex-col sm:flex-row justify-between gap-3 sm:gap-0">
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
className="order-2 sm:order-1 w-full sm:w-auto"
>
Previous
Anterior
</Button>
<div className="space-x-3">
<div className="flex flex-col sm:flex-row gap-3 order-1 sm:order-2">
{selectedFile && !validationResult && (
<Button
onClick={handleValidateFile}
isLoading={isValidating}
loadingText="Validating..."
loadingText="Validando..."
size="lg"
className="w-full sm:w-auto"
>
Validate File
Validar Archivo
</Button>
)}
@@ -615,8 +662,9 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
<Button
onClick={handleContinue}
size="lg"
className="w-full sm:w-auto"
>
Continue with This Data
Continuar con estos Datos
</Button>
)}
</div>