Improve the sales import
This commit is contained in:
@@ -169,6 +169,7 @@ export interface TrainingJobStatus {
|
||||
products_failed: number;
|
||||
error_message?: string | null;
|
||||
estimated_time_remaining_seconds?: number | null; // Estimated time remaining in seconds
|
||||
estimated_completion_time?: string | null; // ISO datetime string of estimated completion
|
||||
message?: string | null; // Optional status message
|
||||
}
|
||||
|
||||
@@ -185,6 +186,8 @@ export interface TrainingJobProgress {
|
||||
products_completed: number;
|
||||
products_total: number;
|
||||
estimated_time_remaining_minutes?: number | null;
|
||||
estimated_time_remaining_seconds?: number | null;
|
||||
estimated_completion_time?: string | null; // ISO datetime string of estimated completion
|
||||
timestamp: string; // ISO datetime string
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Scenario Simulation Page - PROFESSIONAL/ENTERPRISE ONLY
|
||||
* Scenario Simulation Page - PROFESSIONAL+ ONLY
|
||||
*
|
||||
* Interactive "what-if" analysis tool for strategic planning
|
||||
* Allows users to test different scenarios and see potential impacts on demand
|
||||
|
||||
@@ -345,7 +345,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
icon: 'forecasting',
|
||||
requiresAuth: true,
|
||||
requiredRoles: ROLE_COMBINATIONS.MANAGEMENT_ACCESS,
|
||||
requiredAnalyticsLevel: 'predictive',
|
||||
requiredAnalyticsLevel: 'advanced',
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
|
||||
@@ -708,6 +708,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-oven-heat {
|
||||
animation: oven-heat 2s ease-in-out infinite;
|
||||
}
|
||||
@@ -718,4 +727,8 @@
|
||||
|
||||
.animate-rising {
|
||||
animation: rising 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
Reference in New Issue
Block a user