Files
bakery-ia/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx

783 lines
32 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef } from 'react';
2025-10-19 19:22:37 +02:00
import { useTranslation } from 'react-i18next';
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';
import { useAuth } from '../../../../contexts/AuthContext';
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;
original_name: string;
suggested_name: string;
product_type: string;
category: string;
unit_of_measure: string;
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> = ({
onPrevious,
onComplete,
isFirstStep
}) => {
2025-10-19 19:22:37 +02:00
const { t } = useTranslation();
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);
const fileInputRef = useRef<HTMLInputElement>(null);
const currentTenant = useCurrentTenant();
const { user } = useAuth();
2025-10-06 15:27:01 +02:00
const validateFileMutation = useValidateImportFile();
const createIngredient = useCreateIngredient();
2025-10-06 15:27:01 +02:00
const importMutation = useImportSalesData();
const classifyBatchMutation = useClassifyBatch();
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setSelectedFile(file);
setValidationResult(null);
setError('');
// Automatically trigger validation and classification
await handleAutoValidateAndClassify(file);
}
};
const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const file = event.dataTransfer.files[0];
if (file) {
setSelectedFile(file);
setValidationResult(null);
setError('');
// Automatically trigger validation and classification
await handleAutoValidateAndClassify(file);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
const handleAutoValidateAndClassify = async (file: File) => {
if (!currentTenant?.id) return;
setIsValidating(true);
setError('');
setProgressState({ stage: 'preparing', progress: 0, message: 'Preparando validación automática del archivo...' });
try {
// 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);
setProgressState({ stage: 'analyzing', progress: 60, message: 'Validación exitosa. Generando sugerencias automáticamente...' });
2025-10-06 15:27:01 +02:00
// Step 2: Automatically trigger classification
2025-10-06 15:27:01 +02:00
await generateInventorySuggestionsAuto(validationResult);
} else {
2025-10-06 15:27:01 +02:00
setError('Respuesta de validación inválida del servidor');
setProgressState(null);
setIsValidating(false);
}
} catch (error) {
setError('Error validando archivo: ' + (error instanceof Error ? error.message : 'Error desconocido'));
setProgressState(null);
setIsValidating(false);
}
};
2025-10-19 19:22:37 +02:00
const generateInventorySuggestionsAuto = async (validationData: ImportValidationResponse) => {
if (!currentTenant?.id) {
setError('No hay datos de validación disponibles para generar sugerencias');
setIsValidating(false);
setProgressState(null);
return;
}
try {
setProgressState({ stage: 'analyzing', progress: 65, message: 'Analizando productos de ventas...' });
// Extract product data from validation result - use the exact backend structure
const products = validationData.product_list?.map((productName: string) => ({
product_name: productName
})) || [];
if (products.length === 0) {
setError('No se encontraron productos en los datos de ventas');
setProgressState(null);
setIsValidating(false);
return;
}
setProgressState({ stage: 'classifying', progress: 75, message: 'Clasificando productos con IA...' });
// Call the classification API
2025-10-06 15:27:01 +02:00
const classificationResponse = await classifyBatchMutation.mutateAsync({
tenantId: currentTenant.id,
2025-10-06 15:27:01 +02:00
products
});
setProgressState({ stage: 'preparing', progress: 90, message: 'Preparando sugerencias de inventario...' });
// 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) => {
// 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 {
// 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,
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,
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
};
});
setInventoryItems(items);
setShowInventoryStep(true);
setProgressState(null);
setIsValidating(false);
} catch (err) {
console.error('Error generating inventory suggestions:', err);
setError('Error al generar sugerencias de inventario. Por favor, inténtalo de nuevo.');
setProgressState(null);
setIsValidating(false);
}
};
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
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-10-15 21:09:42 +02:00
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);
2025-10-15 21:09:42 +02:00
const ingredientData = {
name: item.suggested_name,
2025-11-05 13:34:56 +01:00
product_type: item.product_type,
category: item.category,
unit_of_measure: item.unit_of_measure,
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,
average_cost: item.cost_per_unit,
notes: item.notes || `Creado durante onboarding - Confianza: ${Math.round(item.confidence_score * 100)}%`
};
2025-10-15 21:09:42 +02:00
return createIngredient.mutateAsync({
tenantId: currentTenant.id,
ingredientData
2025-10-15 21:09:42 +02:00
}).then(created => ({
...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-10-15 21:09:42 +02:00
console.log(`Successfully created ${createdIngredients.length} inventory items in parallel`);
// 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...'
});
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
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...'
});
} 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,
validationResult,
file: selectedFile,
salesImportResult,
2025-10-15 21:09:42 +02:00
inventoryConfigured: true,
shouldAutoCompleteSuppliers: true,
userId: user?.id
});
} 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">
¡Perfecto! Hemos analizado automáticamente tus datos de ventas y generado estas sugerencias de inventario inteligentes.
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">
<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-xs sm:text-sm text-[var(--text-secondary)]">
Los artículos con alta confianza están preseleccionados
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
className="w-full sm:w-auto"
>
{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
key={item.suggestion_id}
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}
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>
<div className="flex-1 space-y-3">
<div>
<h3 className="font-medium text-[var(--text-primary)]">
{item.suggested_name}
</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>
)}
{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 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.suggestion_id,
'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(
item.suggestion_id,
'cost_per_unit',
Number(e.target.value)
)}
size="sm"
/>
<Input
label="Días de Caducidad"
type="number"
min="1"
value={(item.estimated_shelf_life_days || 30).toString()}
onChange={(e) => handleUpdateItem(
item.suggestion_id,
'estimated_shelf_life_days',
Number(e.target.value)
)}
size="sm"
className="sm:col-span-2 lg:col-span-1"
/>
</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 */}
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0">{/* Removed back button */}
<Button
onClick={handleCreateInventory}
isLoading={isCreating}
loadingText="Creando Inventario..."
size="lg"
disabled={selectedCount === 0}
className="w-full sm:w-auto"
>
<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>
);
}
return (
<div className="space-y-6">
<div className="text-center">
<p className="text-[var(--text-secondary)] mb-6">
Sube tus datos de ventas (formato CSV o JSON) y automáticamente validaremos y generaremos sugerencias de inventario inteligentes.
</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>
{/* 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">
Supported formats: CSV, JSON (max 100MB)<br/>
<span className="text-[var(--color-primary)]">Auto-validates and generates suggestions</span>
</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) => (
<li key={index} className="text-[var(--color-warning)]">
{typeof warning === 'string' ? warning : JSON.stringify(warning)}
</li>
))}
</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 */}
<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>
)}
</div>
</div>
);
};