2025-09-08 17:19:00 +02:00
|
|
|
import React, { useState, useRef } from 'react';
|
2025-10-19 19:22:37 +02:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2025-09-08 17:19:00 +02:00
|
|
|
import { Button } from '../../../ui/Button';
|
|
|
|
|
import { Input } from '../../../ui/Input';
|
|
|
|
|
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
2025-10-06 15:27:01 +02:00
|
|
|
import { useCreateIngredient, useClassifyBatch } from '../../../../api/hooks/inventory';
|
|
|
|
|
import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales';
|
2025-10-19 19:22:37 +02:00
|
|
|
import type { ImportValidationResponse } from '../../../../api/types/dataImport';
|
|
|
|
|
import type { ProductSuggestionResponse } from '../../../../api/types/inventory';
|
2025-09-08 22:28:26 +02:00
|
|
|
import { useAuth } from '../../../../contexts/AuthContext';
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
interface UploadSalesDataStepProps {
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
onPrevious: () => void;
|
|
|
|
|
onComplete: (data?: any) => void;
|
|
|
|
|
isFirstStep: boolean;
|
|
|
|
|
isLastStep: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ProgressState {
|
|
|
|
|
stage: string;
|
|
|
|
|
progress: number;
|
|
|
|
|
message: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface InventoryItem {
|
|
|
|
|
suggestion_id: string;
|
2025-09-08 21:44:04 +02:00
|
|
|
original_name: string;
|
2025-09-08 17:19:00 +02:00
|
|
|
suggested_name: string;
|
2025-09-08 21:44:04 +02:00
|
|
|
product_type: string;
|
2025-09-08 17:19:00 +02:00
|
|
|
category: string;
|
|
|
|
|
unit_of_measure: string;
|
|
|
|
|
confidence_score: number;
|
2025-09-08 21:44:04 +02:00
|
|
|
estimated_shelf_life_days?: number;
|
2025-09-08 17:19:00 +02:00
|
|
|
requires_refrigeration: boolean;
|
|
|
|
|
requires_freezing: boolean;
|
|
|
|
|
is_seasonal: boolean;
|
2025-09-08 21:44:04 +02:00
|
|
|
suggested_supplier?: string;
|
2025-09-08 17:19:00 +02:00
|
|
|
notes?: string;
|
2025-09-08 21:44:04 +02:00
|
|
|
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;
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
|
|
|
|
onPrevious,
|
|
|
|
|
onComplete,
|
|
|
|
|
isFirstStep
|
|
|
|
|
}) => {
|
2025-10-19 19:22:37 +02:00
|
|
|
const { t } = useTranslation();
|
2025-09-08 17:19:00 +02:00
|
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
|
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
|
|
|
const [validationResult, setValidationResult] = useState<ImportValidationResponse | null>(null);
|
|
|
|
|
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
|
|
|
|
|
const [showInventoryStep, setShowInventoryStep] = useState(false);
|
|
|
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string>('');
|
|
|
|
|
const [progressState, setProgressState] = useState<ProgressState | null>(null);
|
2025-10-19 19:22:37 +02:00
|
|
|
const [showGuide, setShowGuide] = useState(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
const currentTenant = useCurrentTenant();
|
2025-09-08 22:28:26 +02:00
|
|
|
const { user } = useAuth();
|
2025-10-06 15:27:01 +02:00
|
|
|
const validateFileMutation = useValidateImportFile();
|
2025-09-08 17:19:00 +02:00
|
|
|
const createIngredient = useCreateIngredient();
|
2025-10-06 15:27:01 +02:00
|
|
|
const importMutation = useImportSalesData();
|
|
|
|
|
const classifyBatchMutation = useClassifyBatch();
|
2025-09-08 17:19:00 +02:00
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
2025-09-08 17:19:00 +02:00
|
|
|
const file = event.target.files?.[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
setSelectedFile(file);
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
setError('');
|
2025-09-08 22:28:26 +02:00
|
|
|
|
|
|
|
|
// Automatically trigger validation and classification
|
|
|
|
|
await handleAutoValidateAndClassify(file);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
2025-09-08 17:19:00 +02:00
|
|
|
event.preventDefault();
|
|
|
|
|
const file = event.dataTransfer.files[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
setSelectedFile(file);
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
setError('');
|
2025-09-08 22:28:26 +02:00
|
|
|
|
|
|
|
|
// Automatically trigger validation and classification
|
|
|
|
|
await handleAutoValidateAndClassify(file);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
const handleAutoValidateAndClassify = async (file: File) => {
|
|
|
|
|
if (!currentTenant?.id) return;
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
setIsValidating(true);
|
|
|
|
|
setError('');
|
2025-09-08 22:28:26 +02:00
|
|
|
setProgressState({ stage: 'preparing', progress: 0, message: 'Preparando validación automática del archivo...' });
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
try {
|
2025-09-08 22:28:26 +02:00
|
|
|
// Step 1: Validate the file
|
2025-10-06 15:27:01 +02:00
|
|
|
const validationResult = await validateFileMutation.mutateAsync({
|
|
|
|
|
tenantId: currentTenant.id,
|
|
|
|
|
file
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The API returns the validation result directly (not wrapped)
|
|
|
|
|
if (validationResult && validationResult.is_valid !== undefined) {
|
|
|
|
|
setValidationResult(validationResult);
|
2025-09-08 22:28:26 +02:00
|
|
|
setProgressState({ stage: 'analyzing', progress: 60, message: 'Validación exitosa. Generando sugerencias automáticamente...' });
|
2025-10-06 15:27:01 +02:00
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
// Step 2: Automatically trigger classification
|
2025-10-06 15:27:01 +02:00
|
|
|
await generateInventorySuggestionsAuto(validationResult);
|
2025-09-08 17:19:00 +02:00
|
|
|
} else {
|
2025-10-06 15:27:01 +02:00
|
|
|
setError('Respuesta de validación inválida del servidor');
|
2025-09-08 17:19:00 +02:00
|
|
|
setProgressState(null);
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setError('Error validando archivo: ' + (error instanceof Error ? error.message : 'Error desconocido'));
|
|
|
|
|
setProgressState(null);
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2025-10-19 19:22:37 +02:00
|
|
|
const generateInventorySuggestionsAuto = async (validationData: ImportValidationResponse) => {
|
2025-09-08 22:28:26 +02:00
|
|
|
if (!currentTenant?.id) {
|
2025-09-08 17:19:00 +02:00
|
|
|
setError('No hay datos de validación disponibles para generar sugerencias');
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
|
|
|
|
setProgressState(null);
|
2025-09-08 17:19:00 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-08 22:28:26 +02:00
|
|
|
setProgressState({ stage: 'analyzing', progress: 65, message: 'Analizando productos de ventas...' });
|
|
|
|
|
|
2025-09-08 21:44:04 +02:00
|
|
|
// Extract product data from validation result - use the exact backend structure
|
2025-09-08 22:28:26 +02:00
|
|
|
const products = validationData.product_list?.map((productName: string) => ({
|
2025-09-08 21:44:04 +02:00
|
|
|
product_name: productName
|
2025-09-08 17:19:00 +02:00
|
|
|
})) || [];
|
|
|
|
|
|
|
|
|
|
if (products.length === 0) {
|
|
|
|
|
setError('No se encontraron productos en los datos de ventas');
|
|
|
|
|
setProgressState(null);
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
setProgressState({ stage: 'classifying', progress: 75, message: 'Clasificando productos con IA...' });
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
// Call the classification API
|
2025-10-06 15:27:01 +02:00
|
|
|
const classificationResponse = await classifyBatchMutation.mutateAsync({
|
2025-09-08 17:19:00 +02:00
|
|
|
tenantId: currentTenant.id,
|
2025-10-06 15:27:01 +02:00
|
|
|
products
|
2025-09-08 17:19:00 +02:00
|
|
|
});
|
|
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
setProgressState({ stage: 'preparing', progress: 90, message: 'Preparando sugerencias de inventario...' });
|
2025-09-08 17:19:00 +02:00
|
|
|
|
2025-09-08 21:44:04 +02:00
|
|
|
// Convert API response to InventoryItem format - use exact backend structure plus UI fields
|
2025-10-19 19:22:37 +02:00
|
|
|
const items: InventoryItem[] = classificationResponse.suggestions.map((suggestion: ProductSuggestionResponse) => {
|
2025-09-08 17:19:00 +02:00
|
|
|
// Calculate default stock quantity based on sales data
|
|
|
|
|
const defaultStock = Math.max(
|
|
|
|
|
Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7), // 1 week supply
|
|
|
|
|
1
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Estimate cost per unit based on category
|
|
|
|
|
const estimatedCost = suggestion.category === 'Dairy' ? 5.0 :
|
|
|
|
|
suggestion.category === 'Baking Ingredients' ? 2.0 :
|
|
|
|
|
3.0;
|
|
|
|
|
|
|
|
|
|
return {
|
2025-09-08 21:44:04 +02:00
|
|
|
// Exact backend fields
|
2025-09-08 17:19:00 +02:00
|
|
|
suggestion_id: suggestion.suggestion_id,
|
2025-09-08 21:44:04 +02:00
|
|
|
original_name: suggestion.original_name,
|
2025-09-08 17:19:00 +02:00
|
|
|
suggested_name: suggestion.suggested_name,
|
2025-09-08 21:44:04 +02:00
|
|
|
product_type: suggestion.product_type,
|
2025-09-08 17:19:00 +02:00
|
|
|
category: suggestion.category,
|
|
|
|
|
unit_of_measure: suggestion.unit_of_measure,
|
|
|
|
|
confidence_score: suggestion.confidence_score,
|
2025-09-08 21:44:04 +02:00
|
|
|
estimated_shelf_life_days: suggestion.estimated_shelf_life_days,
|
2025-09-08 17:19:00 +02:00
|
|
|
requires_refrigeration: suggestion.requires_refrigeration,
|
|
|
|
|
requires_freezing: suggestion.requires_freezing,
|
|
|
|
|
is_seasonal: suggestion.is_seasonal,
|
2025-09-08 21:44:04 +02:00
|
|
|
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
|
2025-09-08 17:19:00 +02:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setInventoryItems(items);
|
|
|
|
|
setShowInventoryStep(true);
|
|
|
|
|
setProgressState(null);
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error generating inventory suggestions:', err);
|
|
|
|
|
setError('Error al generar sugerencias de inventario. Por favor, inténtalo de nuevo.');
|
|
|
|
|
setProgressState(null);
|
2025-09-08 22:28:26 +02:00
|
|
|
setIsValidating(false);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-08 22:28:26 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
const handleToggleSelection = (id: string) => {
|
|
|
|
|
setInventoryItems(items =>
|
|
|
|
|
items.map(item =>
|
|
|
|
|
item.suggestion_id === id ? { ...item, selected: !item.selected } : item
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpdateItem = (id: string, field: keyof InventoryItem, value: number) => {
|
|
|
|
|
setInventoryItems(items =>
|
|
|
|
|
items.map(item =>
|
|
|
|
|
item.suggestion_id === id ? { ...item, [field]: value } : item
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectAll = () => {
|
|
|
|
|
const allSelected = inventoryItems.every(item => item.selected);
|
|
|
|
|
setInventoryItems(items =>
|
|
|
|
|
items.map(item => ({ ...item, selected: !allSelected }))
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateInventory = async () => {
|
|
|
|
|
const selectedItems = inventoryItems.filter(item => item.selected);
|
2025-10-15 21:09:42 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
if (selectedItems.length === 0) {
|
|
|
|
|
setError('Por favor selecciona al menos un artículo de inventario para crear');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentTenant?.id) {
|
|
|
|
|
setError('No se encontró información del tenant');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsCreating(true);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-15 21:09:42 +02:00
|
|
|
// Parallel inventory creation
|
|
|
|
|
setProgressState({
|
|
|
|
|
stage: 'creating_inventory',
|
|
|
|
|
progress: 10,
|
|
|
|
|
message: `Creando ${selectedItems.length} artículos de inventario...`
|
|
|
|
|
});
|
2025-09-08 17:19:00 +02:00
|
|
|
|
2025-10-15 21:09:42 +02:00
|
|
|
const creationPromises = selectedItems.map(item => {
|
2025-09-08 21:44:04 +02:00
|
|
|
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);
|
2025-10-15 21:09:42 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
const ingredientData = {
|
|
|
|
|
name: item.suggested_name,
|
2025-11-05 13:34:56 +01:00
|
|
|
product_type: item.product_type,
|
2025-09-08 17:19:00 +02:00
|
|
|
category: item.category,
|
|
|
|
|
unit_of_measure: item.unit_of_measure,
|
2025-09-08 21:44:04 +02:00
|
|
|
low_stock_threshold: minimumStock,
|
|
|
|
|
max_stock_level: item.stock_quantity * 2,
|
|
|
|
|
reorder_point: reorderPoint,
|
|
|
|
|
shelf_life_days: item.estimated_shelf_life_days || 30,
|
2025-09-08 17:19:00 +02:00
|
|
|
requires_refrigeration: item.requires_refrigeration,
|
|
|
|
|
requires_freezing: item.requires_freezing,
|
|
|
|
|
is_seasonal: item.is_seasonal,
|
2025-09-08 21:44:04 +02:00
|
|
|
average_cost: item.cost_per_unit,
|
2025-09-08 17:19:00 +02:00
|
|
|
notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%`
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-15 21:09:42 +02:00
|
|
|
return createIngredient.mutateAsync({
|
2025-09-08 17:19:00 +02:00
|
|
|
tenantId: currentTenant.id,
|
|
|
|
|
ingredientData
|
2025-10-15 21:09:42 +02:00
|
|
|
}).then(created => ({
|
2025-09-08 17:19:00 +02:00
|
|
|
...created,
|
|
|
|
|
initialStock: item.stock_quantity
|
2025-10-15 21:09:42 +02:00
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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}`);
|
2025-09-08 17:19:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-15 21:09:42 +02:00
|
|
|
console.log(`Successfully created ${createdIngredients.length} inventory items in parallel`);
|
|
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
// After inventory creation, import the sales data
|
2025-10-15 21:09:42 +02:00
|
|
|
setProgressState({
|
|
|
|
|
stage: 'importing_sales',
|
|
|
|
|
progress: 50,
|
|
|
|
|
message: 'Importando datos de ventas...'
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
console.log('Importing sales data after inventory creation...');
|
|
|
|
|
let salesImportResult = null;
|
|
|
|
|
try {
|
|
|
|
|
if (selectedFile) {
|
2025-10-06 15:27:01 +02:00
|
|
|
const result = await importMutation.mutateAsync({
|
|
|
|
|
tenantId: currentTenant.id,
|
|
|
|
|
file: selectedFile
|
|
|
|
|
});
|
2025-10-15 21:09:42 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
salesImportResult = result;
|
|
|
|
|
if (result.success) {
|
|
|
|
|
console.log('Sales data imported successfully');
|
2025-10-15 21:09:42 +02:00
|
|
|
setProgressState({
|
|
|
|
|
stage: 'completing',
|
|
|
|
|
progress: 95,
|
|
|
|
|
message: 'Finalizando configuración...'
|
|
|
|
|
});
|
2025-09-08 17:19:00 +02:00
|
|
|
} else {
|
|
|
|
|
console.warn('Sales import completed with issues:', result.error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (importError) {
|
|
|
|
|
console.error('Error importing sales data:', importError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setProgressState(null);
|
|
|
|
|
onComplete({
|
|
|
|
|
createdIngredients,
|
2025-10-15 21:09:42 +02:00
|
|
|
totalItems: createdIngredients.length,
|
2025-09-08 17:19:00 +02:00
|
|
|
validationResult,
|
|
|
|
|
file: selectedFile,
|
2025-09-08 22:28:26 +02:00
|
|
|
salesImportResult,
|
2025-10-15 21:09:42 +02:00
|
|
|
inventoryConfigured: true,
|
|
|
|
|
shouldAutoCompleteSuppliers: true,
|
|
|
|
|
userId: user?.id
|
2025-09-08 17:19:00 +02:00
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error creating inventory items:', err);
|
|
|
|
|
setError('Error al crear artículos de inventario. Por favor, inténtalo de nuevo.');
|
|
|
|
|
setIsCreating(false);
|
|
|
|
|
setProgressState(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatFileSize = (bytes: number) => {
|
|
|
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectedCount = inventoryItems.filter(item => item.selected).length;
|
|
|
|
|
const allSelected = inventoryItems.length > 0 && inventoryItems.every(item => item.selected);
|
|
|
|
|
|
|
|
|
|
if (showInventoryStep) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-6">
|
2025-09-08 22:28:26 +02:00
|
|
|
¡Perfecto! Hemos analizado automáticamente tus datos de ventas y generado estas sugerencias de inventario inteligentes.
|
2025-09-08 17:19:00 +02:00
|
|
|
Revisa y selecciona los artículos que te gustaría agregar a tu inventario.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Summary */}
|
|
|
|
|
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
2025-09-08 21:44:04 +02:00
|
|
|
<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">
|
2025-09-08 17:19:00 +02:00
|
|
|
{selectedCount} de {inventoryItems.length} artículos seleccionados
|
|
|
|
|
</p>
|
2025-09-08 21:44:04 +02:00
|
|
|
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">
|
2025-09-08 17:19:00 +02:00
|
|
|
Los artículos con alta confianza están preseleccionados
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleSelectAll}
|
2025-09-08 21:44:04 +02:00
|
|
|
className="w-full sm:w-auto"
|
2025-09-08 17:19:00 +02:00
|
|
|
>
|
|
|
|
|
{allSelected ? 'Deseleccionar Todos' : 'Seleccionar Todos'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Inventory Items */}
|
|
|
|
|
<div className="space-y-4 max-h-96 overflow-y-auto">
|
|
|
|
|
{inventoryItems.map((item) => (
|
|
|
|
|
<div
|
2025-09-08 21:44:04 +02:00
|
|
|
key={item.suggestion_id}
|
2025-09-08 17:19:00 +02:00
|
|
|
className={`border rounded-lg p-4 transition-colors ${
|
|
|
|
|
item.selected
|
|
|
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
|
|
|
: 'border-[var(--border-secondary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start gap-4">
|
|
|
|
|
<div className="flex-shrink-0 pt-1">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={item.selected}
|
2025-09-08 21:44:04 +02:00
|
|
|
onChange={() => handleToggleSelection(item.suggestion_id)}
|
2025-09-08 17:19:00 +02:00
|
|
|
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-[var(--color-primary)]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="font-medium text-[var(--text-primary)]">
|
2025-09-08 21:44:04 +02:00
|
|
|
{item.suggested_name}
|
2025-09-08 17:19:00 +02:00
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
|
|
|
{item.category} • Unidad: {item.unit_of_measure}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
|
<span className="text-xs bg-[var(--bg-tertiary)] px-2 py-1 rounded">
|
|
|
|
|
Confianza: {Math.round(item.confidence_score * 100)}%
|
|
|
|
|
</span>
|
|
|
|
|
{item.requires_refrigeration && (
|
|
|
|
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
|
|
|
Requiere refrigeración
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2025-09-08 21:44:04 +02:00
|
|
|
{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>
|
|
|
|
|
)}
|
2025-09-08 17:19:00 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{item.selected && (
|
2025-09-08 21:44:04 +02:00
|
|
|
<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)]">
|
2025-09-08 17:19:00 +02:00
|
|
|
<Input
|
|
|
|
|
label="Stock Inicial"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
value={item.stock_quantity.toString()}
|
|
|
|
|
onChange={(e) => handleUpdateItem(
|
2025-09-08 21:44:04 +02:00
|
|
|
item.suggestion_id,
|
2025-09-08 17:19:00 +02:00
|
|
|
'stock_quantity',
|
|
|
|
|
Number(e.target.value)
|
|
|
|
|
)}
|
|
|
|
|
size="sm"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Costo por Unidad (€)"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="0.01"
|
|
|
|
|
value={item.cost_per_unit.toString()}
|
|
|
|
|
onChange={(e) => handleUpdateItem(
|
2025-09-08 21:44:04 +02:00
|
|
|
item.suggestion_id,
|
2025-09-08 17:19:00 +02:00
|
|
|
'cost_per_unit',
|
|
|
|
|
Number(e.target.value)
|
|
|
|
|
)}
|
|
|
|
|
size="sm"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Días de Caducidad"
|
|
|
|
|
type="number"
|
|
|
|
|
min="1"
|
2025-09-08 21:44:04 +02:00
|
|
|
value={(item.estimated_shelf_life_days || 30).toString()}
|
2025-09-08 17:19:00 +02:00
|
|
|
onChange={(e) => handleUpdateItem(
|
2025-09-08 21:44:04 +02:00
|
|
|
item.suggestion_id,
|
|
|
|
|
'estimated_shelf_life_days',
|
2025-09-08 17:19:00 +02:00
|
|
|
Number(e.target.value)
|
|
|
|
|
)}
|
|
|
|
|
size="sm"
|
2025-09-08 21:44:04 +02:00
|
|
|
className="sm:col-span-2 lg:col-span-1"
|
2025-09-08 17:19:00 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
2025-09-08 22:28:26 +02:00
|
|
|
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0">{/* Removed back button */}
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleCreateInventory}
|
|
|
|
|
isLoading={isCreating}
|
|
|
|
|
loadingText="Creando Inventario..."
|
|
|
|
|
size="lg"
|
|
|
|
|
disabled={selectedCount === 0}
|
2025-09-08 22:28:26 +02:00
|
|
|
className="w-full sm:w-auto"
|
2025-09-08 17:19:00 +02:00
|
|
|
>
|
2025-09-08 21:44:04 +02:00
|
|
|
<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>
|
2025-09-08 17:19:00 +02:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-6">
|
2025-09-08 22:28:26 +02:00
|
|
|
Sube tus datos de ventas (formato CSV o JSON) y automáticamente validaremos y generaremos sugerencias de inventario inteligentes.
|
2025-09-08 17:19:00 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-19 19:22:37 +02:00
|
|
|
{/* File Format Guide */}
|
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<h3 className="font-semibold text-blue-900">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.title', 'Guía de Formato de Archivo')}
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowGuide(!showGuide)}
|
|
|
|
|
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{showGuide
|
|
|
|
|
? t('onboarding:steps.inventory_setup.file_format_guide.collapse_guide', 'Ocultar Guía')
|
|
|
|
|
: t('onboarding:steps.inventory_setup.file_format_guide.toggle_guide', 'Ver Guía Completa')
|
|
|
|
|
}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Quick Summary - Always Visible */}
|
|
|
|
|
<div className="text-sm text-blue-800 space-y-1">
|
|
|
|
|
<p>
|
|
|
|
|
<strong>{t('onboarding:steps.inventory_setup.file_format_guide.supported_formats.title', 'Formatos Soportados')}:</strong>{' '}
|
|
|
|
|
CSV, JSON, Excel (XLSX) • {t('onboarding:steps.inventory_setup.file_format_guide.supported_formats.max_size', 'Tamaño máximo: 10MB')}
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<strong>{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.title', 'Columnas Requeridas')}:</strong>{' '}
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.date', 'Fecha')},{' '}
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.product', 'Nombre del Producto')},{' '}
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity', 'Cantidad Vendida')}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Detailed Guide - Collapsible */}
|
|
|
|
|
{showGuide && (
|
|
|
|
|
<div className="mt-4 pt-4 border-t border-blue-200 space-y-4">
|
|
|
|
|
{/* Required Columns Detail */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-blue-900 mb-2">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.title', 'Columnas Requeridas')}
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="text-sm text-blue-800 space-y-1 pl-4">
|
|
|
|
|
<p>
|
|
|
|
|
• <strong>{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.date', 'Fecha')}:</strong>{' '}
|
|
|
|
|
<span className="font-mono text-xs bg-blue-100 px-1 rounded">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.date_examples', 'date, fecha, data')}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
• <strong>{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.product', 'Nombre del Producto')}:</strong>{' '}
|
|
|
|
|
<span className="font-mono text-xs bg-blue-100 px-1 rounded">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.product_examples', 'product, producto, product_name')}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
• <strong>{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity', 'Cantidad Vendida')}:</strong>{' '}
|
|
|
|
|
<span className="font-mono text-xs bg-blue-100 px-1 rounded">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.required_columns.quantity_examples', 'quantity, cantidad, quantity_sold')}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Optional Columns */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-blue-900 mb-2">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.title', 'Columnas Opcionales')}
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="text-sm text-blue-800 space-y-1 pl-4">
|
|
|
|
|
<p>• {t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.revenue', 'Ingresos (revenue, ingresos, ventas)')}</p>
|
|
|
|
|
<p>• {t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.unit_price', 'Precio Unitario (unit_price, precio, price)')}</p>
|
|
|
|
|
<p>• {t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.category', 'Categoría (category, categoria)')}</p>
|
|
|
|
|
<p>• {t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.sku', 'SKU del Producto')}</p>
|
|
|
|
|
<p>• {t('onboarding:steps.inventory_setup.file_format_guide.optional_columns.location', 'Ubicación/Tienda')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Date Formats */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-blue-900 mb-2">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.date_formats.title', 'Formatos de Fecha Soportados')}
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="text-sm text-blue-800 pl-4">
|
|
|
|
|
<p>{t('onboarding:steps.inventory_setup.file_format_guide.date_formats.formats', 'YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY, DD-MM-YYYY, y más')}</p>
|
|
|
|
|
<p className="text-xs mt-1">{t('onboarding:steps.inventory_setup.file_format_guide.date_formats.with_time', 'También se admiten formatos con hora')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Automatic Features */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-blue-900 mb-2">
|
|
|
|
|
{t('onboarding:steps.inventory_setup.file_format_guide.features.title', 'Características Automáticas')}
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="text-sm text-blue-800 space-y-1 pl-4">
|
|
|
|
|
<p>✓ {t('onboarding:steps.inventory_setup.file_format_guide.features.multilingual', 'Detección multiidioma de columnas')}</p>
|
|
|
|
|
<p>✓ {t('onboarding:steps.inventory_setup.file_format_guide.features.validation', 'Validación automática con reporte detallado')}</p>
|
|
|
|
|
<p>✓ {t('onboarding:steps.inventory_setup.file_format_guide.features.ai_classification', 'Clasificación de productos con IA')}</p>
|
|
|
|
|
<p>✓ {t('onboarding:steps.inventory_setup.file_format_guide.features.inventory_suggestions', 'Sugerencias inteligentes de inventario')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
{/* File Upload Area */}
|
|
|
|
|
<div
|
|
|
|
|
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
|
|
|
selectedFile
|
|
|
|
|
? 'border-[var(--color-success)] bg-[var(--color-success)]/5'
|
|
|
|
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)]/5'
|
|
|
|
|
}`}
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept=".csv,.json"
|
|
|
|
|
onChange={handleFileSelect}
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{selectedFile ? (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="text-[var(--color-success)]">
|
|
|
|
|
<svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-lg font-medium">Archivo Seleccionado</p>
|
|
|
|
|
<p className="text-[var(--text-secondary)]">{selectedFile.name}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-tertiary)]">
|
|
|
|
|
{formatFileSize(selectedFile.size)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
>
|
|
|
|
|
Choose Different File
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<svg className="mx-auto h-12 w-12 text-[var(--text-tertiary)]" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
|
|
|
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-lg font-medium">Drop your sales data here</p>
|
|
|
|
|
<p className="text-[var(--text-secondary)]">or click to browse files</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-tertiary)] mt-2">
|
2025-09-08 22:28:26 +02:00
|
|
|
Supported formats: CSV, JSON (max 100MB)<br/>
|
|
|
|
|
<span className="text-[var(--color-primary)]">Auto-validates and generates suggestions</span>
|
2025-09-08 17:19:00 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
>
|
|
|
|
|
Choose File
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Progress */}
|
|
|
|
|
{progressState && (
|
|
|
|
|
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
|
|
|
|
<div className="flex justify-between text-sm mb-2">
|
|
|
|
|
<span className="font-medium">{progressState.message}</span>
|
|
|
|
|
<span>{progressState.progress}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<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: `${progressState.progress}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Validation Results */}
|
|
|
|
|
{validationResult && (
|
|
|
|
|
<div className="bg-[var(--color-success)]/10 border border-[var(--color-success)]/20 rounded-lg p-4">
|
|
|
|
|
<h3 className="font-semibold text-[var(--color-success)] mb-2">Validation Successful!</h3>
|
|
|
|
|
<div className="space-y-2 text-sm">
|
|
|
|
|
<p>Total records: {validationResult.total_records}</p>
|
|
|
|
|
<p>Valid records: {validationResult.valid_records}</p>
|
|
|
|
|
{validationResult.invalid_records > 0 && (
|
|
|
|
|
<p className="text-[var(--color-warning)]">
|
|
|
|
|
Invalid records: {validationResult.invalid_records}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{validationResult.warnings && validationResult.warnings.length > 0 && (
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<p className="font-medium text-[var(--color-warning)]">Warnings:</p>
|
|
|
|
|
<ul className="list-disc list-inside">
|
2025-10-19 19:22:37 +02:00
|
|
|
{validationResult.warnings.map((warning: any, index: number) => (
|
2025-09-08 21:44:04 +02:00
|
|
|
<li key={index} className="text-[var(--color-warning)]">
|
|
|
|
|
{typeof warning === 'string' ? warning : JSON.stringify(warning)}
|
|
|
|
|
</li>
|
2025-09-08 17:19:00 +02:00
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error */}
|
|
|
|
|
{error && (
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
2025-09-08 22:28:26 +02:00
|
|
|
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0">{/* Removed back button */}
|
|
|
|
|
|
|
|
|
|
{selectedFile && !showInventoryStep && (
|
|
|
|
|
<div className="flex items-center justify-center px-4 py-2 bg-[var(--bg-secondary)] rounded-lg">
|
|
|
|
|
{isValidating ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[var(--color-primary)] mr-2"></div>
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Procesando automáticamente...</span>
|
|
|
|
|
</>
|
|
|
|
|
) : validationResult ? (
|
|
|
|
|
<>
|
|
|
|
|
<svg className="w-4 h-4 text-[var(--color-success)] mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="text-sm text-[var(--color-success)]">Archivo procesado exitosamente</span>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-08 17:19:00 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|