Simplify the onboardinf flow components 2
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user