Improve the sales import

This commit is contained in:
Urtzi Alfaro
2025-10-15 21:09:42 +02:00
parent 8f9e9a7edc
commit dbb48d8e2c
21 changed files with 992 additions and 409 deletions

View File

@@ -20,6 +20,7 @@ interface TrainingProgress {
message: string;
currentStep?: string;
estimatedTimeRemaining?: number;
estimatedCompletionTime?: string;
}
export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
@@ -59,7 +60,8 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
progress: data.data?.progress || 0,
message: data.data?.message || 'Entrenando modelo...',
currentStep: data.data?.current_step,
estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining
estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining,
estimatedCompletionTime: data.data?.estimated_completion_time
});
}, []);
@@ -221,7 +223,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
const formatTime = (seconds?: number) => {
if (!seconds) return '';
if (seconds < 60) {
return `${Math.round(seconds)}s`;
} else if (seconds < 3600) {
@@ -231,6 +233,33 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
}
};
const formatEstimatedCompletionTime = (isoString?: string) => {
if (!isoString) return '';
try {
const completionDate = new Date(isoString);
const now = new Date();
// If completion is today, show time only
if (completionDate.toDateString() === now.toDateString()) {
return completionDate.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
}
// If completion is another day, show date and time
return completionDate.toLocaleString('es-ES', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return '';
}
};
return (
<div className="space-y-6">
<div className="text-center">
@@ -293,24 +322,61 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
</p>
{trainingProgress.stage !== 'completed' && (
<div className="space-y-2">
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className="bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
style={{ width: `${trainingProgress.progress}%` }}
/>
<div className="space-y-3">
{/* Enhanced Progress Bar */}
<div className="relative">
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-3 overflow-hidden">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/80 h-3 rounded-full transition-all duration-500 ease-out relative"
style={{ width: `${trainingProgress.progress}%` }}
>
{/* Animated shimmer effect */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"></div>
</div>
</div>
{/* Progress percentage badge */}
<div className="absolute -top-1 left-1/2 transform -translate-x-1/2 -translate-y-full mb-1">
<span className="text-xs font-semibold text-[var(--color-primary)] bg-[var(--bg-primary)] px-2 py-1 rounded-full shadow-sm border border-[var(--color-primary)]/20">
{trainingProgress.progress}%
</span>
</div>
</div>
<div className="flex justify-between text-xs text-[var(--text-tertiary)]">
<span>{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span>
<div className="flex items-center gap-2">
{/* Training Information */}
<div className="flex flex-col gap-2 text-xs text-[var(--text-tertiary)]">
{/* Current Step */}
<div className="flex justify-between items-center">
<span className="font-medium">{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span>
{jobId && (
<span className={`text-xs ${isConnected ? 'text-green-500' : 'text-red-500'}`}>
{isConnected ? '🟢 Conectado' : '🔴 Desconectado'}
<span className={`text-xs px-2 py-0.5 rounded-full ${isConnected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}>
{isConnected ? '● En vivo' : '● Reconectando...'}
</span>
)}
</div>
{/* Time Information */}
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs">
{trainingProgress.estimatedTimeRemaining && (
<span>{t('onboarding:steps.ml_training.estimated_time_remaining', 'Tiempo restante estimado: {{time}}', { time: formatTime(trainingProgress.estimatedTimeRemaining) })}</span>
<div className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>
{t('onboarding:steps.ml_training.estimated_time_remaining', 'Tiempo restante: {{time}}', {
time: formatTime(trainingProgress.estimatedTimeRemaining)
})}
</span>
</div>
)}
{trainingProgress.estimatedCompletionTime && (
<div className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>
Finalizará: {formatEstimatedCompletionTime(trainingProgress.estimatedCompletionTime)}
</span>
</div>
)}
</div>
</div>

View File

@@ -239,7 +239,7 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
const handleCreateInventory = async () => {
const selectedItems = inventoryItems.filter(item => item.selected);
if (selectedItems.length === 0) {
setError('Por favor selecciona al menos un artículo de inventario para crear');
return;
@@ -254,22 +254,18 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
setError('');
try {
const createdIngredients = [];
// Parallel inventory creation
setProgressState({
stage: 'creating_inventory',
progress: 10,
message: `Creando ${selectedItems.length} artículos de inventario...`
});
for (const item of selectedItems) {
// Ensure reorder_point > minimum_stock_level as required by backend validation
const creationPromises = selectedItems.map(item => {
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,
@@ -285,18 +281,36 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%`
};
const created = await createIngredient.mutateAsync({
return createIngredient.mutateAsync({
tenantId: currentTenant.id,
ingredientData
});
createdIngredients.push({
}).then(created => ({
...created,
initialStock: item.stock_quantity
});
}));
});
const results = await Promise.allSettled(creationPromises);
const createdIngredients = results
.filter(r => r.status === 'fulfilled')
.map(r => (r as PromiseFulfilledResult<any>).value);
const failedCount = results.filter(r => r.status === 'rejected').length;
if (failedCount > 0) {
console.warn(`${failedCount} items failed to create out of ${selectedItems.length}`);
}
console.log(`Successfully created ${createdIngredients.length} inventory items in parallel`);
// After inventory creation, import the sales data
setProgressState({
stage: 'importing_sales',
progress: 50,
message: 'Importando datos de ventas...'
});
console.log('Importing sales data after inventory creation...');
let salesImportResult = null;
try {
@@ -305,29 +319,33 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
tenantId: currentTenant.id,
file: selectedFile
});
salesImportResult = result;
if (result.success) {
console.log('Sales data imported successfully');
setProgressState({
stage: 'completing',
progress: 95,
message: 'Finalizando configuración...'
});
} else {
console.warn('Sales import completed with issues:', result.error);
}
}
} catch (importError) {
console.error('Error importing sales data:', importError);
// Don't fail the entire process if import fails - the inventory has been created successfully
}
setProgressState(null);
onComplete({
createdIngredients,
totalItems: selectedItems.length,
totalItems: createdIngredients.length,
validationResult,
file: selectedFile,
salesImportResult,
inventoryConfigured: true, // Flag for ML training dependency
shouldAutoCompleteSuppliers: true, // Flag to trigger suppliers auto-completion after step completion
userId: user?.id // Pass user ID for suppliers completion
inventoryConfigured: true,
shouldAutoCompleteSuppliers: true,
userId: user?.id
});
} catch (err) {
console.error('Error creating inventory items:', err);