Start integrating the onboarding flow with backend 12

This commit is contained in:
Urtzi Alfaro
2025-09-07 17:25:30 +02:00
parent 9005286ada
commit b73f3b4993
32 changed files with 3172 additions and 3087 deletions

View File

@@ -3,8 +3,7 @@ export { default as OnboardingWizard } from './OnboardingWizard';
// Individual step components
export { BakerySetupStep } from './steps/BakerySetupStep';
export { HistoricalSalesValidationStep } from './steps/HistoricalSalesValidationStep';
export { InventorySetupStep } from './steps/InventorySetupStep';
export { SmartInventorySetupStep } from './steps/SmartInventorySetupStep';
export { SuppliersStep } from './steps/SuppliersStep';
export { MLTrainingStep } from './steps/MLTrainingStep';
export { CompletionStep } from './steps/CompletionStep';

View File

@@ -1,854 +0,0 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores';
import type { ProductSuggestionResponse } from '../../../../api';
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'review' | 'completed' | 'error';
interface ProcessingResult {
// Validation data
is_valid: boolean;
total_records: number;
unique_products: number;
product_list: string[];
validation_errors: string[];
validation_warnings: string[];
summary: {
date_range: string;
total_sales: number;
average_daily_sales: number;
};
// Analysis data
productsIdentified: number;
categoriesDetected: number;
businessModel: string;
confidenceScore: number;
recommendations: string[];
}
interface Product {
id: string;
name: string;
category: string;
confidence: number;
sales_count?: number;
estimated_price?: number;
status: 'approved' | 'rejected' | 'pending';
notes?: string;
// Fields from API suggestion
suggestion_id?: string;
original_name: string;
suggested_name: string;
product_type: 'ingredient' | 'finished_product';
unit_of_measure: string;
estimated_shelf_life_days: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
sales_data?: {
total_quantity: number;
average_daily_sales: number;
peak_day: string;
frequency: number;
};
}
// Convert API suggestions to Product interface
const convertSuggestionsToProducts = (suggestions: ProductSuggestionResponse[]): Product[] => {
const products = suggestions.map((suggestion, index) => ({
id: suggestion.suggestion_id || `product-${index}`,
name: suggestion.suggested_name,
category: suggestion.category,
confidence: Math.round(suggestion.confidence_score * 100),
status: 'pending' as const,
// Store original API data
suggestion_id: suggestion.suggestion_id,
original_name: suggestion.original_name,
suggested_name: suggestion.suggested_name,
product_type: suggestion.product_type,
unit_of_measure: suggestion.unit_of_measure,
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,
// Legacy fields for display
sales_count: suggestion.sales_data?.total_quantity || 0,
estimated_price: 0 // Price estimation not provided by current API
}));
return products;
};
export const HistoricalSalesValidationStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
_onNext,
_onPrevious,
_isFirstStep,
_isLastStep
}) => {
const user = useAuthUser();
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
const tenantLoading = useTenantLoading();
// Use the new onboarding hooks
const {
processSalesFile,
salesProcessing: {
stage: onboardingStage,
progress: onboardingProgress,
currentMessage: onboardingMessage,
validationResults,
suggestions
},
tenantCreation,
error,
clearError
} = useOnboarding();
const toast = useToast();
// Get tenant ID from multiple sources with fallback
const getTenantId = (): string | null => {
const onboardingTenantId = data.bakery?.tenant_id;
const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null;
return tenantId;
};
// Check if tenant data is available
const isTenantAvailable = (): boolean => {
const hasAuth = !authLoading && user;
const hasTenantId = getTenantId() !== null;
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
const isAvailable = hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding);
return isAvailable;
};
// State management
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
const [products, setProducts] = useState<Product[]>(() => {
if (data.detectedProducts) {
return data.detectedProducts;
}
// Generate from existing suggestions if available
if (suggestions && suggestions.length > 0) {
return convertSuggestionsToProducts(suggestions);
}
return [];
});
const [selectedCategory, setSelectedCategory] = useState<string>('all');
// Derive current state from onboarding hooks or local state
const stage = (localStage === 'completed' || localStage === 'error')
? localStage
: (onboardingStage || localStage);
const progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || '';
const results = useMemo(() => (validationResults && suggestions) ? {
...validationResults,
aiSuggestions: suggestions,
productsIdentified: validationResults.unique_products || validationResults.product_list?.length || 0,
categoriesDetected: suggestions ? new Set(suggestions.map((s: ProductSuggestionResponse) => s.category)).size : 0,
businessModel: 'production',
confidenceScore: 85,
recommendations: validationResults.summary?.suggestions || [],
totalRecords: validationResults.total_records || 0,
validRecords: validationResults.valid_records || 0,
invalidRecords: validationResults.invalid_records || 0,
fileFormat: validationResults.summary?.file_format || 'csv',
fileSizeMb: validationResults.summary?.file_size_mb || 0,
estimatedProcessingTime: validationResults.summary?.estimated_processing_time_seconds || 0,
detectedColumns: validationResults.summary?.detected_columns || [],
validationMessage: validationResults.message || 'Validación completada'
} : localResults, [validationResults, suggestions, localResults]);
// Update products when suggestions change
useEffect(() => {
if (suggestions && suggestions.length > 0 && products.length === 0) {
const newProducts = convertSuggestionsToProducts(suggestions);
setProducts(newProducts);
}
}, [suggestions, products.length]);
// Auto-progress to review stage when validation completes and suggestions are available
useEffect(() => {
if (stage === 'analyzing' && results && suggestions && suggestions.length > 0) {
setLocalStage('review');
}
}, [stage, results, suggestions]);
const [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile, products });
// Memoized computed values
const approvedProducts = useMemo(() =>
products.filter(p => p.status === 'approved'),
[products]
);
const reviewCompleted = useMemo(() =>
products.length > 0 && products.every(p => p.status !== 'pending'),
[products]
);
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
const getFilteredProducts = () => {
if (selectedCategory === 'all') {
return products;
}
return products.filter(p => p.category === selectedCategory);
};
const stats = {
total: products.length,
approved: products.filter(p => p.status === 'approved').length,
rejected: products.filter(p => p.status === 'rejected').length,
pending: products.filter(p => p.status === 'pending').length
};
// Update parent data when state changes
useEffect(() => {
const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile, products };
if (JSON.stringify(currentState) !== JSON.stringify(lastStateRef.current)) {
lastStateRef.current = currentState;
onDataChange({
processingStage: stage === 'review' ? 'completed' : stage,
processingProgress: progress,
currentMessage: currentMessage,
processingResults: results,
suggestions: suggestions,
files: {
...data.files,
salesData: uploadedFile
},
detectedProducts: products,
approvedProducts,
reviewCompleted
});
}
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, products, approvedProducts, reviewCompleted, onDataChange, data.files]);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
handleFileUpload(files[0]);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
};
const handleFileUpload = async (file: File) => {
// Validate file type
const validExtensions = ['.csv', '.xlsx', '.xls', '.json'];
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', {
title: 'Formato inválido',
type: 'error'
});
return;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', {
title: 'Archivo muy grande',
type: 'error'
});
return;
}
setUploadedFile(file);
setLocalStage('validating');
try {
// Wait for user data to load if still loading
if (!isTenantAvailable()) {
setUploadedFile(null);
setLocalStage('upload');
toast.addToast('Por favor espere mientras cargamos su información...', {
title: 'Esperando datos de usuario',
type: 'info'
});
return;
}
// Use the onboarding hook for file processing
const success = await processSalesFile(file, (progress, stage, message) => {
console.log(`Processing: ${progress}% - ${stage} - ${message}`);
});
if (success) {
setLocalStage('review');
// Update products from suggestions
if (validationResults && suggestions) {
const newProducts = convertSuggestionsToProducts(suggestions);
setProducts(newProducts);
const processedResults = {
...validationResults,
aiSuggestions: suggestions,
productsIdentified: validationResults.product_list?.length || 0,
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
businessModel: 'production',
confidenceScore: 85,
recommendations: []
};
setLocalResults(processedResults);
}
toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', {
title: 'Procesamiento completado',
type: 'success'
});
} else {
throw new Error('Error procesando el archivo');
}
} catch (error) {
console.error('HistoricalSalesValidationStep - Processing error:', error);
setLocalStage('error');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
toast.addToast(errorMessage, {
title: 'Error en el procesamiento',
type: 'error'
});
}
};
const downloadTemplate = () => {
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.addToast('La plantilla se descargó correctamente.', {
title: 'Plantilla descargada',
type: 'success'
});
};
const resetProcess = () => {
setLocalStage('upload');
setUploadedFile(null);
setLocalResults(null);
setProducts([]);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
if (error) {
clearError();
}
};
const handleProductAction = (productId: string, action: 'approve' | 'reject') => {
setProducts(prev => prev.map(product =>
product.id === productId
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
: product
));
};
const handleBulkAction = (action: 'approve' | 'reject') => {
const filteredProducts = getFilteredProducts();
setProducts(prev => prev.map(product =>
filteredProducts.some(fp => fp.id === product.id)
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
: product
));
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 90) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
if (confidence >= 75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20';
return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
};
const getStatusColor = (status: string) => {
switch (status) {
case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]';
}
};
return (
<div className="space-y-8">
{/* Loading state when tenant data is not available */}
{!isTenantAvailable() && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
Cargando datos de usuario...
</h3>
<p className="text-[var(--text-secondary)]">
Por favor espere mientras cargamos su información de tenant
</p>
</Card>
)}
{/* Upload Stage */}
{(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && (
<>
<div
className={`
border-2 border-dashed rounded-2xl p-16 text-center transition-all duration-300 cursor-pointer group
${
dragActive
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 scale-[1.02] shadow-lg'
: uploadedFile
? 'border-[var(--color-success)] bg-[var(--color-success)]/10 shadow-md'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)]/30 hover:shadow-lg'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept=".csv,.xlsx,.xls"
onChange={handleFileSelect}
className="hidden"
/>
<div className="space-y-8">
{uploadedFile ? (
<>
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
</div>
<div>
<h3 className="text-3xl font-bold text-[var(--color-success)] mb-3">
¡Perfecto! Archivo listo
</h3>
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 inline-block">
<p className="text-[var(--text-primary)] font-medium text-lg">
📄 {uploadedFile.name}
</p>
<p className="text-[var(--text-secondary)] text-sm mt-1">
{(uploadedFile.size / 1024 / 1024).toFixed(2)} MB Listo para procesar
</p>
</div>
</div>
</>
) : (
<>
<div className="w-20 h-20 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto group-hover:scale-110 transition-transform duration-300">
<Upload className="w-10 h-10 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
Sube tu historial de ventas
</h3>
<p className="text-[var(--text-secondary)] text-xl leading-relaxed max-w-md mx-auto">
Arrastra y suelta tu archivo aquí, o <span className="text-[var(--color-primary)] font-semibold">haz clic para seleccionar</span>
</p>
</div>
<div className="flex justify-center space-x-8 mt-8">
<div className="text-center">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<span className="text-2xl">📊</span>
</div>
<span className="text-sm text-[var(--text-secondary)]">CSV</span>
</div>
<div className="text-center">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<span className="text-2xl">📈</span>
</div>
<span className="text-sm text-[var(--text-secondary)]">Excel</span>
</div>
<div className="text-center">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
<span className="text-2xl"></span>
</div>
<span className="text-sm text-[var(--text-secondary)]">Hasta 10MB</span>
</div>
</div>
</>
)}
</div>
<div className="mt-10 px-4 py-2 bg-[var(--bg-secondary)]/50 rounded-lg text-sm text-[var(--text-tertiary)] inline-block">
💡 Formatos aceptados: CSV, Excel (XLSX, XLS) Tamaño máximo: 10MB
</div>
</div>
{/* Template Download Section */}
<div className="bg-gradient-to-r from-[var(--color-info)]/5 to-[var(--color-primary)]/5 rounded-xl p-6 border border-[var(--color-info)]/20">
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-6">
<div className="w-16 h-16 rounded-full bg-[var(--color-info)]/10 flex items-center justify-center flex-shrink-0">
<Download className="w-8 h-8 text-[var(--color-info)]" />
</div>
<div className="flex-1 text-center md:text-left">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
¿Necesitas ayuda con el formato?
</h4>
<p className="text-[var(--text-secondary)] mb-4">
Descarga nuestra plantilla Excel con ejemplos y formato correcto para tus datos de ventas
</p>
<Button
onClick={downloadTemplate}
className="bg-[var(--color-info)] hover:bg-[var(--color-info)]/90 text-white shadow-lg"
>
<Download className="w-4 h-4 mr-2" />
Descargar Plantilla Gratuita
</Button>
</div>
</div>
</div>
</>
)}
{/* Processing Stages */}
{(stage === 'validating' || stage === 'analyzing') && (
<Card className="p-8">
<div className="text-center">
<div className="relative mb-8">
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 ${
stage === 'validating'
? 'bg-[var(--color-info)]/10 animate-pulse'
: 'bg-[var(--color-primary)]/10 animate-pulse'
}`}>
{stage === 'validating' ? (
<FileText className={`w-8 h-8 ${stage === 'validating' ? 'text-[var(--color-info)]' : 'text-[var(--color-primary)]'}`} />
) : (
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
)}
</div>
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
{stage === 'validating' ? 'Validando datos...' : 'Analizando con IA...'}
</h3>
<p className="text-[var(--text-secondary)] mb-8">
{currentMessage}
</p>
</div>
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-medium text-[var(--text-primary)]">
Progreso
</span>
<span className="text-sm text-[var(--text-secondary)]">{progress}%</span>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3">
<div
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-3 rounded-full transition-all duration-500 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Processing Steps */}
<div className="grid grid-cols-3 gap-4">
<div className={`p-4 rounded-lg text-center ${
progress >= 40 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
}`}>
<FileText className="w-6 h-6 mx-auto mb-2" />
<span className="text-sm font-medium">Validación</span>
</div>
<div className={`p-4 rounded-lg text-center ${
progress >= 70 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
}`}>
<Brain className="w-6 h-6 mx-auto mb-2" />
<span className="text-sm font-medium">Análisis IA</span>
</div>
<div className={`p-4 rounded-lg text-center ${
progress >= 100 ? 'bg-[var(--color-success)]/10 text-[var(--color-success)]' : 'bg-[var(--bg-secondary)]'
}`}>
<CheckCircle className="w-6 h-6 mx-auto mb-2" />
<span className="text-sm font-medium">Completo</span>
</div>
</div>
</div>
</Card>
)}
{/* Review Stage - Combined validation results and product approval */}
{(stage === 'review' || (stage === 'completed' && products.length > 0)) && results && (
<div className="space-y-8">
{/* Success Header with Validation Results */}
<div className="text-center">
<div className="w-16 h-16 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-3">
¡Validación Completada!
</h3>
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
{results.validationMessage || 'Tus datos han sido procesados exitosamente. Revisa y aprueba los productos detectados.'}
</p>
</div>
{/* Enhanced Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">{results.totalRecords || results.total_records}</p>
<p className="text-sm text-[var(--text-secondary)]">Total Registros</p>
</div>
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{results.validRecords || results.valid_records}</p>
<p className="text-sm text-[var(--text-secondary)]">Válidos</p>
</div>
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-error)]">{results.invalidRecords || results.invalid_records || 0}</p>
<p className="text-sm text-[var(--text-secondary)]">Inválidos</p>
</div>
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-primary)]">{results.productsIdentified}</p>
<p className="text-sm text-[var(--text-secondary)]">Productos</p>
</div>
</div>
{/* Product Review Section */}
{products.length > 0 && (
<>
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{stats.total}</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Productos detectados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">{stats.approved}</div>
<div className="text-sm font-medium text-[var(--color-success)]">Aprobados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">{stats.rejected}</div>
<div className="text-sm font-medium text-[var(--color-error)]">Rechazados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="text-3xl font-bold text-[var(--color-warning)] mb-2">{stats.pending}</div>
<div className="text-sm font-medium text-[var(--color-warning)]">Pendientes</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'Todas las categorías' : category}
</option>
))}
</select>
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{getFilteredProducts().length} productos
</Badge>
</div>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5"
>
Aprobar todos
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
Rechazar todos
</Button>
</div>
</div>
{/* Products List */}
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{product.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? '✓ Aprobado' :
product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{product.category}
</Badge>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getConfidenceColor(product.confidence)}`}>
{product.confidence}% confianza
</Badge>
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
{product.original_name && product.original_name !== product.name && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[120px]">Nombre original:</span>
<span className="font-medium text-[var(--text-primary)]">{product.original_name}</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Tipo:</span>
<span className="font-medium text-[var(--text-primary)]">
{product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Unidad:</span>
<span className="font-medium text-[var(--text-primary)]">{product.unit_of_measure}</span>
</div>
{product.sales_data && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Ventas:</span>
<span className="font-medium text-[var(--text-primary)]">{product.sales_data.total_quantity}</span>
</div>
)}
</div>
{product.notes && (
<div className="text-xs italic bg-[var(--bg-secondary)] p-2 rounded border-l-4 border-[var(--color-primary)]/30">
<span className="font-medium text-[var(--text-tertiary)]">Nota:</span> {product.notes}
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant={product.status === 'approved' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'approve')}
className={product.status === 'approved'
? 'bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white shadow-sm'
: 'text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5'
}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant={product.status === 'rejected' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'reject')}
className={product.status === 'rejected'
? 'bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white shadow-sm'
: 'text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5'
}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
</Button>
</div>
</div>
</Card>
))}
</div>
</>
)}
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📋 Revisión de Datos de Ventas:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Datos validados</strong> - Tu archivo ha sido procesado y validado correctamente</li>
<li> <strong>Productos detectados</strong> - La IA ha identificado automáticamente productos desde tus ventas</li>
<li> <strong>Revisa cuidadosamente</strong> - Aprueba o rechaza cada producto según sea correcto para tu negocio</li>
<li> <strong>Verifica nombres</strong> - Compara el nombre original vs. el nombre sugerido</li>
<li> <strong>Revisa clasificaciones</strong> - Confirma si son ingredientes o productos terminados</li>
<li> <strong>Usa filtros</strong> - Filtra por categoría para revisar productos similares</li>
<li> <strong>Acciones masivas</strong> - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso</li>
</ul>
</Card>
</div>
)}
{/* Error State */}
{stage === 'error' && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-6">
<AlertCircle className="w-8 h-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-[var(--color-error)] mb-3">
Error en el procesamiento
</h3>
<p className="text-[var(--text-secondary)] mb-6">
{currentMessage}
</p>
<Button onClick={resetProcess} variant="outline">
Intentar nuevamente
</Button>
</Card>
)}
</div>
);
};

View File

@@ -1,769 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores';
interface InventoryItem {
id: string;
name: string;
category: 'ingredient' | 'finished_product';
current_stock: number;
min_stock: number;
max_stock: number;
unit: string;
expiry_date?: string;
supplier?: string;
cost_per_unit?: number;
requires_refrigeration: boolean;
// API fields
suggestion_id?: string;
original_name?: string;
estimated_shelf_life_days?: number;
is_seasonal?: boolean;
}
// Convert approved products to inventory items
const convertProductsToInventory = (approvedProducts: any[]): InventoryItem[] => {
return approvedProducts.map((product, index) => ({
id: `inventory-${index}`,
name: product.suggested_name || product.original_name,
category: product.product_type === 'ingredient' ? 'ingredient' : 'finished_product',
current_stock: 0, // To be configured by user
min_stock: 1, // Default minimum
max_stock: 100, // Default maximum
unit: product.unit_of_measure || 'units',
requires_refrigeration: product.requires_refrigeration || false,
// Store API data
suggestion_id: product.suggestion_id,
original_name: product.original_name,
estimated_shelf_life_days: product.estimated_shelf_life_days,
is_seasonal: product.is_seasonal,
// Optional fields to be filled by user
expiry_date: undefined,
supplier: product.suggested_supplier,
cost_per_unit: 0
}));
};
export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { showToast } = useToast();
// Use the business onboarding hooks
const {
updateStepData,
createInventoryFromSuggestions,
importSalesData,
inventorySetup: {
createdItems,
inventoryMapping,
salesImportResult,
isInventoryConfigured
},
allStepData,
isLoading,
error,
clearError
} = useOnboarding();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
if (showToast && typeof showToast === 'function') {
showToast({
title: alert.title,
message: alert.message,
type: alert.type
});
} else {
// Fallback to console if showToast is not available
console.warn(`Toast would show: ${alert.title} - ${alert.message}`);
}
};
// Use modal for confirmations and editing
const editModal = useModal();
const confirmModal = useModal();
const [isCreating, setIsCreating] = useState(false);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [inventoryCreationAttempted, setInventoryCreationAttempted] = useState(false);
// Generate inventory items from approved products
const generateInventoryFromProducts = (approvedProducts: any[]): InventoryItem[] => {
if (!approvedProducts || approvedProducts.length === 0) {
createAlert({
type: 'warning',
category: 'system',
priority: 'medium',
title: 'Sin productos aprobados',
message: 'No hay productos aprobados para crear inventario. Regrese al paso anterior.',
source: 'onboarding'
});
return [];
}
return convertProductsToInventory(approvedProducts);
};
const [items, setItems] = useState<InventoryItem[]>(() => {
if (data.inventoryItems) {
return data.inventoryItems;
}
// Try to get approved products from business hooks data first, then from component props
const approvedProducts = data.approvedProducts ||
allStepData?.['sales-validation']?.approvedProducts ||
data.allStepData?.['sales-validation']?.approvedProducts;
return generateInventoryFromProducts(approvedProducts || []);
});
// Update items when approved products become available (for when component is already mounted)
useEffect(() => {
const approvedProducts = data.approvedProducts ||
allStepData?.['sales-validation']?.approvedProducts ||
data.allStepData?.['sales-validation']?.approvedProducts;
if (approvedProducts && approvedProducts.length > 0 && items.length === 0) {
const newItems = generateInventoryFromProducts(approvedProducts);
setItems(newItems);
}
}, [data.approvedProducts, allStepData, data.allStepData]);
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
const filteredItems = filterCategory === 'all'
? items
: items.filter(item => item.category === filterCategory);
// Create inventory items via API using business hooks
const handleCreateInventory = async () => {
console.log('InventorySetup - Starting handleCreateInventory');
const approvedProducts = data.approvedProducts ||
allStepData?.['sales-validation']?.approvedProducts ||
data.allStepData?.['sales-validation']?.approvedProducts;
console.log('InventorySetup - approvedProducts:', {
fromDataProp: data.approvedProducts,
fromAllStepData: allStepData?.['sales-validation']?.approvedProducts,
fromDataAllStepData: data.allStepData?.['sales-validation']?.approvedProducts,
finalProducts: approvedProducts,
allStepDataKeys: Object.keys(allStepData || {}),
dataKeys: Object.keys(data || {})
});
// Get tenant ID from current tenant context or user
const tenantId = currentTenant?.id || user?.tenant_id;
console.log('InventorySetup - tenantId:', tenantId);
if (!tenantId || !approvedProducts || approvedProducts.length === 0) {
console.log('InventorySetup - Missing requirements: tenantId =', tenantId, 'approvedProducts length =', approvedProducts?.length);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: !tenantId ? 'No se pudo obtener información del tenant' : 'No se pueden crear elementos de inventario sin productos aprobados.',
source: 'onboarding'
});
return;
}
setIsCreating(true);
try {
// Approved products should already be in ProductSuggestionResponse format
// Just ensure they have all required fields
const suggestions = approvedProducts.map((product: any, index: number) => ({
suggestion_id: product.suggestion_id || `suggestion-${Date.now()}-${index}`,
original_name: product.original_name,
suggested_name: product.suggested_name,
product_type: product.product_type,
category: product.category,
unit_of_measure: product.unit_of_measure,
confidence_score: product.confidence_score || 0.8,
estimated_shelf_life_days: product.estimated_shelf_life_days,
requires_refrigeration: product.requires_refrigeration,
requires_freezing: product.requires_freezing,
is_seasonal: product.is_seasonal,
suggested_supplier: product.suggested_supplier,
notes: product.notes
}));
// Use business onboarding hook to create inventory
const inventorySuccess = await createInventoryFromSuggestions(suggestions);
if (inventorySuccess) {
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Inventario creado',
message: `Se crearon ${suggestions.length} elementos de inventario exitosamente.`,
source: 'onboarding'
});
// Now try to import sales data if available
const salesDataFile = data.allStepData?.['sales-validation']?.salesDataFile ||
allStepData?.['sales-validation']?.salesDataFile;
const processingResults = data.allStepData?.['sales-validation']?.processingResults ||
allStepData?.['sales-validation']?.processingResults;
if (salesDataFile && processingResults?.is_valid && inventoryMapping) {
try {
createAlert({
type: 'info',
category: 'system',
priority: 'medium',
title: 'Subiendo datos de ventas',
message: 'Subiendo historial de ventas al sistema para entrenamiento de IA...',
source: 'onboarding'
});
const salesSuccess = await importSalesData(processingResults, inventoryMapping);
if (salesSuccess) {
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Datos de ventas subidos',
message: `Se subieron ${processingResults.total_records} registros de ventas al sistema exitosamente.`,
source: 'onboarding'
});
}
} catch (salesError) {
console.error('Error uploading sales data:', salesError);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al subir datos de ventas',
message: 'El inventario se creó correctamente, pero hubo un problema al subir los datos de ventas.',
source: 'onboarding'
});
}
}
// Update component data and business hook data
const updatedData = {
...data,
inventoryItems: items,
inventoryConfigured: true,
inventoryCreated: true,
inventoryMapping,
createdInventoryItems: createdItems,
salesImportResult
};
onDataChange(updatedData);
// Also update step data in business hooks
updateStepData('inventory', {
inventoryItems: items,
inventoryConfigured: true,
inventoryCreated: true,
inventoryMapping,
createdInventoryItems: createdItems,
salesImportResult
});
} else {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear inventario',
message: 'No se pudieron crear los elementos de inventario.',
source: 'onboarding'
});
}
} catch (error) {
console.error('Error creating inventory:', error);
const errorMessage = error instanceof Error ? error.message : 'Error al crear inventario';
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear inventario',
message: errorMessage,
source: 'onboarding'
});
} finally {
setIsCreating(false);
}
};
const lastItemsRef = useRef(items);
const lastIsCreatingRef = useRef(isCreating);
useEffect(() => {
// Only update if items or isCreating actually changed
if (JSON.stringify(items) !== JSON.stringify(lastItemsRef.current) || isCreating !== lastIsCreatingRef.current) {
lastItemsRef.current = items;
lastIsCreatingRef.current = isCreating;
const hasValidStock = items.length > 0 && items.every(item =>
item.min_stock >= 0 && item.max_stock > item.min_stock
);
const stepData = {
inventoryItems: items,
inventoryConfigured: hasValidStock && !isCreating
};
// Update component props
onDataChange({
...data,
...stepData
});
// Update business hooks data
updateStepData('inventory', stepData);
}
}, [items, isCreating]); // Only depend on items and isCreating
// Auto-create inventory when step is completed (when user clicks Next)
useEffect(() => {
const hasValidStock = items.length > 0 && items.every(item =>
item.min_stock >= 0 && item.max_stock > item.min_stock
);
// If inventory is configured but not yet created in backend, create it automatically
if (hasValidStock && !data.inventoryCreated && !isCreating && !inventoryCreationAttempted) {
console.log('InventorySetup - Auto-creating inventory on step completion');
setInventoryCreationAttempted(true);
handleCreateInventory();
}
}, [data.inventoryCreated, items, isCreating, inventoryCreationAttempted]);
const handleAddItem = () => {
const newItem: InventoryItem = {
id: Date.now().toString(),
name: '',
category: 'ingredient',
current_stock: 0,
min_stock: 1,
max_stock: 10,
unit: 'unidad',
requires_refrigeration: false
};
setItems([...items, newItem]);
setEditingItem(newItem);
setIsAddingNew(true);
};
const handleSaveItem = (updatedItem: InventoryItem) => {
setItems(items.map(item =>
item.id === updatedItem.id ? updatedItem : item
));
setEditingItem(null);
setIsAddingNew(false);
};
const handleDeleteItem = (id: string) => {
setItems(items.filter(item => item.id !== id));
if (editingItem?.id === id) {
setEditingItem(null);
setIsAddingNew(false);
}
};
const handleCancelEdit = () => {
if (isAddingNew && editingItem) {
setItems(items.filter(item => item.id !== editingItem.id));
}
setEditingItem(null);
setIsAddingNew(false);
};
const getStockStatus = (item: InventoryItem) => {
if (item.current_stock <= item.min_stock) return 'critical';
if (item.current_stock <= item.min_stock * 1.5) return 'warning';
return 'good';
};
const getStockStatusColor = (status: string) => {
switch (status) {
case 'critical': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
case 'warning': return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20';
default: return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
}
};
if (items.length === 0) {
return (
<div className="space-y-8">
<div className="text-center py-16">
<Package className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-gray-600 mb-2">
Sin productos para inventario
</h3>
<p className="text-gray-500 mb-6 max-w-md mx-auto">
No hay productos aprobados para crear inventario.
Regrese al paso anterior para aprobar productos.
</p>
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Volver al paso anterior
</Button>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Header */}
<div className="text-center">
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
Configuración de Inventario
</h2>
<p className="text-[var(--text-secondary)] text-lg max-w-2xl mx-auto">
Configure los niveles de stock, fechas de vencimiento y otros detalles para sus productos.
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{items.length}</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Elementos totales</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<div className="text-3xl font-bold text-[var(--color-info)] mb-2">
{items.filter(item => item.category === 'ingredient').length}
</div>
<div className="text-sm font-medium text-[var(--color-info)]">Ingredientes</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">
{items.filter(item => item.category === 'finished_product').length}
</div>
<div className="text-sm font-medium text-[var(--color-success)]">Productos terminados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">
{items.filter(item => getStockStatus(item) === 'critical').length}
</div>
<div className="text-sm font-medium text-[var(--color-error)]">Stock crítico</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value as any)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
<option value="all">Todos los elementos</option>
<option value="ingredient">Ingredientes</option>
<option value="finished_product">Productos terminados</option>
</select>
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{filteredItems.length} elementos
</Badge>
</div>
<div className="flex space-x-2">
<Button
onClick={handleAddItem}
size="sm"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white shadow-sm"
>
<Plus className="w-4 h-4 mr-1" />
Agregar elemento
</Button>
<Button
onClick={handleCreateInventory}
disabled={isCreating || items.length === 0 || data.inventoryCreated}
size="sm"
variant="outline"
className="border-[var(--color-success)]/30 text-[var(--color-success)] hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5 disabled:opacity-50 disabled:cursor-not-allowed"
>
<CheckCircle className="w-4 h-4 mr-1" />
{isCreating ? 'Creando...' : data.inventoryCreated ? 'Inventario creado automáticamente' : 'Crear inventario manualmente'}
</Button>
</div>
</div>
{/* Items List */}
<div className="space-y-4">
{filteredItems.map((item) => (
<Card key={item.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
{editingItem?.id === item.id ? (
<InventoryItemEditor
item={item}
onSave={handleSaveItem}
onCancel={handleCancelEdit}
showToast={showToast}
/>
) : (
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{item.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStockStatus(item) === 'critical' ? getStockStatusColor('critical') : getStockStatus(item) === 'warning' ? getStockStatusColor('warning') : getStockStatusColor('good')}`}>
Stock: {getStockStatus(item) === 'critical' ? '🔴 Crítico' : getStockStatus(item) === 'warning' ? '🟡 Bajo' : '🟢 Bueno'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</Badge>
{item.requires_refrigeration && (
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-info)]/10 text-[var(--color-info)] border border-[var(--color-info)]/20">
Refrigeración
</Badge>
)}
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[100px]">Stock actual:</span>
<span className="font-bold text-[var(--text-primary)]">{item.current_stock} {item.unit}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[70px]">Mínimo:</span>
<span className="font-medium text-[var(--text-primary)]">{item.min_stock}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[70px]">Máximo:</span>
<span className="font-medium text-[var(--text-primary)]">{item.max_stock}</span>
</div>
</div>
{(item.expiry_date || item.supplier) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{item.expiry_date && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Vence:</span>
<span className="font-medium text-[var(--text-primary)]">{item.expiry_date}</span>
</div>
)}
{item.supplier && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[80px]">Proveedor:</span>
<span className="font-medium text-[var(--text-primary)]">{item.supplier}</span>
</div>
)}
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant="outline"
onClick={() => setEditingItem(item)}
className="text-[var(--color-primary)] border-[var(--color-primary)]/30 hover:bg-[var(--color-primary)]/10 bg-[var(--color-primary)]/5"
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteItem(item.id)}
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
<Trash2 className="w-4 h-4 mr-1" />
Eliminar
</Button>
</div>
</div>
)}
</Card>
))}
</div>
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📦 Configuración de Inventario:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Configure stock inicial</strong> - Establezca los niveles de stock actuales para cada producto</li>
<li> <strong>Defina límites</strong> - Establezca stock mínimo y máximo para recibir alertas automáticas</li>
<li> <strong>Agregue detalles</strong> - Incluya fechas de vencimiento, proveedores y unidades de medida</li>
<li> <strong>Marque refrigeración</strong> - Indique qué productos requieren condiciones especiales de almacenamiento</li>
<li> <strong>Edite elementos</strong> - Haga clic en "Editar" para modificar cualquier producto</li>
<li> <strong>Creación automática</strong> - El inventario se creará automáticamente al hacer clic en "Siguiente"</li>
</ul>
</Card>
</div>
);
};
// Inventory Item Editor Component
const InventoryItemEditor: React.FC<{
item: InventoryItem;
onSave: (item: InventoryItem) => void;
onCancel: () => void;
showToast: (toast: any) => void;
}> = ({ item, onSave, onCancel, showToast }) => {
const [editedItem, setEditedItem] = useState<InventoryItem>(item);
const handleSave = () => {
if (!editedItem.name.trim()) {
showToast({
title: 'Error de validación',
message: 'El nombre es requerido',
type: 'error'
});
return;
}
if (editedItem.min_stock < 0 || editedItem.max_stock <= editedItem.min_stock) {
showToast({
title: 'Error de validación',
message: 'Los niveles de stock deben ser válidos (máximo > mínimo >= 0)',
type: 'error'
});
return;
}
onSave(editedItem);
};
return (
<div className="space-y-4 p-6 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Nombre</label>
<Input
value={editedItem.name}
onChange={(e) => setEditedItem({ ...editedItem, name: e.target.value })}
placeholder="Nombre del producto"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Categoría</label>
<select
value={editedItem.category}
onChange={(e) => setEditedItem({ ...editedItem, category: e.target.value as any })}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
<option value="ingredient">Ingrediente</option>
<option value="finished_product">Producto terminado</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock actual</label>
<Input
type="number"
value={editedItem.current_stock}
onChange={(e) => setEditedItem({ ...editedItem, current_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Unidad</label>
<Input
value={editedItem.unit}
onChange={(e) => setEditedItem({ ...editedItem, unit: e.target.value })}
placeholder="kg, litros, unidades..."
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock mínimo</label>
<Input
type="number"
value={editedItem.min_stock}
onChange={(e) => setEditedItem({ ...editedItem, min_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock máximo</label>
<Input
type="number"
value={editedItem.max_stock}
onChange={(e) => setEditedItem({ ...editedItem, max_stock: Number(e.target.value) })}
min="1"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Fecha de vencimiento</label>
<Input
type="date"
value={editedItem.expiry_date || ''}
onChange={(e) => setEditedItem({ ...editedItem, expiry_date: e.target.value })}
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Proveedor</label>
<Input
value={editedItem.supplier || ''}
onChange={(e) => setEditedItem({ ...editedItem, supplier: e.target.value })}
placeholder="Nombre del proveedor"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editedItem.requires_refrigeration}
onChange={(e) => setEditedItem({ ...editedItem, requires_refrigeration: e.target.checked })}
className="rounded border-[var(--border-primary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/>
<span className="text-sm font-medium text-[var(--text-primary)]">Requiere refrigeración</span>
</label>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onCancel}
className="text-[var(--text-secondary)] border-[var(--border-primary)] hover:bg-[var(--bg-tertiary)]"
>
Cancelar
</Button>
<Button
onClick={handleSave}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white"
>
Guardar
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,922 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, Package, Edit } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useToast } from '../../../../hooks/ui/useToast';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores';
import type { ProductSuggestionResponse } from '../../../../api';
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'review' | 'inventory-config' | 'creating' | 'completed' | 'error';
interface EnhancedProductCard {
// From AI suggestion
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: 'ingredient' | 'finished_product';
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;
};
// Inventory configuration (user editable)
current_stock: number;
min_stock: number;
max_stock: number;
cost_per_unit?: number;
custom_expiry_date?: string;
// UI state
status: 'pending' | 'approved' | 'rejected';
}
// Convert API suggestions to enhanced product cards
const convertSuggestionsToCards = (suggestions: ProductSuggestionResponse[]): EnhancedProductCard[] => {
return suggestions.map(suggestion => ({
// From API
suggestion_id: suggestion.suggestion_id,
original_name: suggestion.original_name,
suggested_name: suggestion.suggested_name,
product_type: suggestion.product_type as 'ingredient' | 'finished_product',
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,
// Smart defaults based on sales data
current_stock: 0,
min_stock: Math.max(1, Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7)), // Week buffer
max_stock: Math.max(10, Math.ceil((suggestion.sales_data?.total_quantity || 10) * 2)), // 2x peak quantity
cost_per_unit: 0,
custom_expiry_date: undefined,
// Initial state
status: suggestion.confidence_score >= 0.8 ? 'approved' : 'pending'
}));
};
export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange
}) => {
const user = useAuthUser();
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
// Use onboarding hooks
const {
processSalesFile,
createInventoryFromSuggestions,
importSalesData,
salesProcessing: {
stage: onboardingStage,
progress: onboardingProgress,
currentMessage: onboardingMessage,
validationResults,
suggestions
},
inventorySetup: {
createdItems,
inventoryMapping,
salesImportResult
},
tenantCreation,
error,
clearError
} = useOnboarding();
const toast = useToast();
// Get tenant ID
const getTenantId = (): string | null => {
const onboardingTenantId = data.bakery?.tenant_id;
const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null;
return tenantId;
};
// Check if tenant data is available
const isTenantAvailable = (): boolean => {
const hasAuth = !authLoading && user;
const hasTenantId = getTenantId() !== null;
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
return Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding));
};
// Local state
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [products, setProducts] = useState<EnhancedProductCard[]>(() => {
if (data.detectedProducts) {
return data.detectedProducts;
}
if (suggestions && suggestions.length > 0) {
return convertSuggestionsToCards(suggestions);
}
return [];
});
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [editingProduct, setEditingProduct] = useState<EnhancedProductCard | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Update products when suggestions change
useEffect(() => {
if (suggestions && suggestions.length > 0 && products.length === 0) {
const newProducts = convertSuggestionsToCards(suggestions);
setProducts(newProducts);
setLocalStage('review');
}
}, [suggestions, products.length]);
// Derive current stage
const stage = (localStage === 'completed' || localStage === 'error')
? localStage
: (onboardingStage === 'completed' ? 'review' : onboardingStage || localStage);
const progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || '';
// Product stats
const stats = useMemo(() => ({
total: products.length,
approved: products.filter(p => p.status === 'approved').length,
rejected: products.filter(p => p.status === 'rejected').length,
pending: products.filter(p => p.status === 'pending').length
}), [products]);
const approvedProducts = useMemo(() =>
products.filter(p => p.status === 'approved'),
[products]
);
const reviewCompleted = useMemo(() =>
products.length > 0 && products.every(p => p.status !== 'pending'),
[products]
);
const inventoryConfigured = useMemo(() =>
approvedProducts.length > 0 &&
approvedProducts.every(p => p.min_stock >= 0 && p.max_stock > p.min_stock),
[approvedProducts]
);
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
const getFilteredProducts = () => {
if (selectedCategory === 'all') return products;
return products.filter(p => p.category === selectedCategory);
};
// File handling
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
handleFileUpload(files[0]);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
};
const handleFileUpload = async (file: File) => {
const validExtensions = ['.csv', '.xlsx', '.xls', '.json'];
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', {
title: 'Formato inválido',
type: 'error'
});
return;
}
if (file.size > 10 * 1024 * 1024) {
toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', {
title: 'Archivo muy grande',
type: 'error'
});
return;
}
if (!isTenantAvailable()) {
toast.addToast('Por favor espere mientras cargamos su información...', {
title: 'Esperando datos de usuario',
type: 'info'
});
return;
}
setUploadedFile(file);
setLocalStage('validating');
try {
const success = await processSalesFile(file);
if (success) {
setLocalStage('review');
toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', {
title: 'Procesamiento completado',
type: 'success'
});
} else {
throw new Error('Error procesando el archivo');
}
} catch (error) {
console.error('Error processing file:', error);
setLocalStage('error');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
toast.addToast(errorMessage, {
title: 'Error en el procesamiento',
type: 'error'
});
}
};
const downloadTemplate = () => {
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.addToast('La plantilla se descargó correctamente.', {
title: 'Plantilla descargada',
type: 'success'
});
};
// Product actions
const handleProductAction = (productId: string, action: 'approve' | 'reject') => {
setProducts(prev => prev.map(product =>
product.suggestion_id === productId
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
: product
));
};
const handleBulkAction = (action: 'approve' | 'reject') => {
const filteredProducts = getFilteredProducts();
setProducts(prev => prev.map(product =>
filteredProducts.some(fp => fp.suggestion_id === product.suggestion_id)
? { ...product, status: action === 'approve' ? 'approved' : 'rejected' }
: product
));
};
const handleEditProduct = (product: EnhancedProductCard) => {
setEditingProduct({ ...product });
};
const handleSaveProduct = (updatedProduct: EnhancedProductCard) => {
setProducts(prev => prev.map(product =>
product.suggestion_id === updatedProduct.suggestion_id ? updatedProduct : product
));
setEditingProduct(null);
};
const handleCancelEdit = () => {
setEditingProduct(null);
};
// Update parent data
useEffect(() => {
const updatedData = {
...data,
files: { ...data.files, salesData: uploadedFile },
processingStage: stage === 'review' ? 'completed' : (stage === 'completed' ? 'completed' : stage),
processingResults: validationResults,
suggestions: suggestions,
detectedProducts: products,
approvedProducts: approvedProducts,
reviewCompleted: reviewCompleted,
inventoryConfigured: inventoryConfigured,
inventoryItems: createdItems,
inventoryMapping: inventoryMapping,
salesImportResult: salesImportResult
};
// Debug logging
console.log('SmartInventorySetupStep - Updating parent data:', {
stage,
reviewCompleted,
inventoryConfigured,
hasSalesImportResult: !!salesImportResult,
salesImportResult,
approvedProductsCount: approvedProducts.length
});
// Only update if data actually changed to prevent infinite loops
const dataString = JSON.stringify(updatedData);
const currentDataString = JSON.stringify(data);
if (dataString !== currentDataString) {
onDataChange(updatedData);
}
}, [stage, uploadedFile, validationResults, suggestions, products, approvedProducts, reviewCompleted, inventoryConfigured, createdItems, inventoryMapping, salesImportResult, data, onDataChange]);
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.9) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
if (confidence >= 0.75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20';
return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
};
const getStatusColor = (status: string) => {
switch (status) {
case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]';
}
};
return (
<div className="space-y-8">
{/* Loading state when tenant data is not available */}
{!isTenantAvailable() && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
Cargando datos de usuario...
</h3>
<p className="text-[var(--text-secondary)]">
Por favor espere mientras cargamos su información de tenant
</p>
</Card>
)}
{/* Upload Stage */}
{(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && (
<>
<div className="text-center">
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
📦 Configuración Inteligente de Inventario
</h2>
<p className="text-[var(--text-secondary)] text-lg max-w-2xl mx-auto">
Sube tu historial de ventas y crearemos automáticamente tu inventario con configuraciones inteligentes
</p>
</div>
<div
className={`
border-2 border-dashed rounded-2xl p-16 text-center transition-all duration-300 cursor-pointer group
${dragActive
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 scale-[1.02] shadow-lg'
: uploadedFile
? 'border-[var(--color-success)] bg-[var(--color-success)]/10 shadow-md'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)]/30 hover:shadow-lg'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept=".csv,.xlsx,.xls"
onChange={handleFileSelect}
className="hidden"
/>
<div className="space-y-8">
{uploadedFile ? (
<>
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
</div>
<div>
<h3 className="text-3xl font-bold text-[var(--color-success)] mb-3">
¡Perfecto! Archivo listo
</h3>
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 inline-block">
<p className="text-[var(--text-primary)] font-medium text-lg">
📄 {uploadedFile.name}
</p>
<p className="text-[var(--text-secondary)] text-sm mt-1">
{(uploadedFile.size / 1024 / 1024).toFixed(2)} MB Listo para procesar
</p>
</div>
</div>
</>
) : (
<>
<div className="w-20 h-20 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto group-hover:scale-110 transition-transform duration-300">
<Upload className="w-10 h-10 text-[var(--color-primary)]" />
</div>
<div>
<h3 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
Sube tu historial de ventas
</h3>
<p className="text-[var(--text-secondary)] text-xl leading-relaxed max-w-md mx-auto">
Arrastra y suelta tu archivo aquí, o <span className="text-[var(--color-primary)] font-semibold">haz clic para seleccionar</span>
</p>
</div>
</>
)}
</div>
</div>
{/* Template Download Section */}
<div className="bg-gradient-to-r from-[var(--color-info)]/5 to-[var(--color-primary)]/5 rounded-xl p-6 border border-[var(--color-info)]/20">
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-6">
<div className="w-16 h-16 rounded-full bg-[var(--color-info)]/10 flex items-center justify-center flex-shrink-0">
<Download className="w-8 h-8 text-[var(--color-info)]" />
</div>
<div className="flex-1 text-center md:text-left">
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
¿Necesitas ayuda con el formato?
</h4>
<p className="text-[var(--text-secondary)] mb-4">
Descarga nuestra plantilla con ejemplos para tus datos de ventas
</p>
<Button
onClick={downloadTemplate}
className="bg-[var(--color-info)] hover:bg-[var(--color-info)]/90 text-white shadow-lg"
>
<Download className="w-4 h-4 mr-2" />
Descargar Plantilla
</Button>
</div>
</div>
</div>
</>
)}
{/* Processing Stages */}
{(stage === 'validating' || stage === 'analyzing') && (
<Card className="p-8">
<div className="text-center">
<div className="relative mb-8">
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 ${
stage === 'validating'
? 'bg-[var(--color-info)]/10 animate-pulse'
: 'bg-[var(--color-primary)]/10 animate-pulse'
}`}>
{stage === 'validating' ? (
<FileText className="w-8 h-8 text-[var(--color-info)]" />
) : (
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
)}
</div>
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
{stage === 'validating' ? 'Validando datos...' : 'Analizando con IA...'}
</h3>
<p className="text-[var(--text-secondary)] mb-8">
{currentMessage}
</p>
</div>
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-medium text-[var(--text-primary)]">
Progreso
</span>
<span className="text-sm text-[var(--text-secondary)]">{progress}%</span>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3">
<div
className="bg-gradient-to-r from-[var(--color-info)] to-[var(--color-primary)] h-3 rounded-full transition-all duration-500 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</Card>
)}
{/* Creating Stage */}
{isCreating && (
<Card className="p-8">
<div className="text-center">
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
<Package className="w-8 h-8 text-[var(--color-success)]" />
</div>
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
Creando tu inventario...
</h3>
<p className="text-[var(--text-secondary)]">
Configurando productos e importando datos de ventas
</p>
</div>
</Card>
)}
{/* Review & Configure Stage */}
{(stage === 'review') && products.length > 0 && (
<div className="space-y-8">
<div className="text-center">
<div className="w-16 h-16 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-3">
¡Productos Detectados!
</h3>
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
Revisa, aprueba y configura los niveles de inventario para tus productos
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{stats.total}</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Productos detectados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">{stats.approved}</div>
<div className="text-sm font-medium text-[var(--color-success)]">Aprobados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">{stats.rejected}</div>
<div className="text-sm font-medium text-[var(--color-error)]">Rechazados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="text-3xl font-bold text-[var(--color-warning)] mb-2">{stats.pending}</div>
<div className="text-sm font-medium text-[var(--color-warning)]">Pendientes</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'Todas las categorías' : category}
</option>
))}
</select>
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{getFilteredProducts().length} productos
</Badge>
</div>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5"
>
Aprobar todos
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
Rechazar todos
</Button>
</div>
</div>
{/* Products List */}
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.suggestion_id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">
{product.suggested_name}
</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? '✓ Aprobado' :
product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'}
</Badge>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getConfidenceColor(product.confidence_score)}`}>
{Math.round(product.confidence_score * 100)}% confianza
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{product.category}
</Badge>
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-info)]/10 text-[var(--color-info)] border border-[var(--color-info)]/20">
{product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</Badge>
{product.requires_refrigeration && (
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-blue-50 text-blue-600 border border-blue-200">
Refrigeración
</Badge>
)}
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
{product.original_name !== product.suggested_name && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[100px]">Original:</span>
<span className="text-[var(--text-primary)]">{product.original_name}</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[80px]">Stock min:</span>
<span className="font-bold text-[var(--text-primary)]">{product.min_stock}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[80px]">Stock max:</span>
<span className="font-bold text-[var(--text-primary)]">{product.max_stock}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Unidad:</span>
<span className="text-[var(--text-primary)]">{product.unit_of_measure}</span>
</div>
</div>
{product.sales_data && (
<div className="bg-[var(--color-info)]/5 p-3 rounded border-l-4 border-[var(--color-info)]/30">
<div className="text-xs font-medium text-[var(--color-info)] mb-1">Datos de Ventas:</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<span>Total vendido: {product.sales_data.total_quantity}</span>
<span>Promedio diario: {product.sales_data.average_daily_sales.toFixed(1)}</span>
</div>
</div>
)}
{product.notes && (
<div className="text-xs italic bg-[var(--bg-secondary)] p-2 rounded border-l-4 border-[var(--color-primary)]/30">
<span className="font-medium text-[var(--text-tertiary)]">Nota:</span> {product.notes}
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2 flex-shrink-0">
<Button
size="sm"
variant={product.status === 'approved' ? 'primary' : 'outline'}
onClick={() => handleProductAction(product.suggestion_id, 'approve')}
className={product.status === 'approved'
? 'bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white'
: 'text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10'
}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant={product.status === 'rejected' ? 'danger' : 'outline'}
onClick={() => handleProductAction(product.suggestion_id, 'reject')}
className={product.status === 'rejected'
? 'bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white'
: 'text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10'
}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleEditProduct(product)}
className="text-[var(--color-primary)] border-[var(--color-primary)]/30 hover:bg-[var(--color-primary)]/10"
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Ready to Proceed Status */}
{approvedProducts.length > 0 && reviewCompleted && (
<div className="flex justify-center">
<Card className="p-6 bg-gradient-to-r from-[var(--color-success)]/10 to-[var(--color-primary)]/10 border-[var(--color-success)]/20">
<div className="text-center">
<div className="flex justify-center items-center mb-3">
<div className="w-12 h-12 bg-[var(--color-success)]/20 rounded-full flex items-center justify-center">
<Package className="w-6 h-6 text-[var(--color-success)]" />
</div>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Listo para crear inventario
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{approvedProducts.length} productos aprobados y listos para crear el inventario.
</p>
<div className="text-sm text-[var(--color-info)]">
Haz clic en "Siguiente" para crear automáticamente el inventario e importar los datos de ventas.
</div>
</div>
</Card>
</div>
)}
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📦 Inventario Inteligente:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Configuración automática</strong> - Los niveles de stock se calculan basándose en tus datos de ventas</li>
<li> <strong>Revisa y aprueba</strong> - Confirma que cada producto sea correcto para tu negocio</li>
<li> <strong>Edita configuración</strong> - Ajusta niveles de stock, proveedores y otros detalles</li>
<li> <strong>Creación integral</strong> - Se creará el inventario e importarán los datos de ventas automáticamente</li>
</ul>
</Card>
</div>
)}
{/* Completed Stage */}
{stage === 'completed' && (
<div className="text-center py-16">
<div className="w-20 h-20 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-10 h-10 text-white" />
</div>
<h3 className="text-3xl font-bold text-[var(--color-success)] mb-4">
¡Inventario Creado!
</h3>
<p className="text-[var(--text-secondary)] text-lg max-w-md mx-auto mb-6">
Tu inventario inteligente ha sido configurado exitosamente con {approvedProducts.length} productos.
</p>
{salesImportResult?.success && (
<p className="text-[var(--color-info)] font-medium">
Se importaron {salesImportResult.records_created} registros de ventas para entrenar tu IA.
</p>
)}
</div>
)}
{/* Error State */}
{stage === 'error' && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-6">
<AlertCircle className="w-8 h-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-[var(--color-error)] mb-3">
Error en el procesamiento
</h3>
<p className="text-[var(--text-secondary)] mb-6">
{currentMessage || error}
</p>
<Button
onClick={() => {
setLocalStage('upload');
setUploadedFile(null);
setProducts([]);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
clearError();
}}
variant="outline"
>
Intentar nuevamente
</Button>
</Card>
)}
{/* Product Editor Modal */}
{editingProduct && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-y-auto m-4">
<div className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Editar Producto: {editingProduct.suggested_name}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock actual</label>
<Input
type="number"
value={editingProduct.current_stock}
onChange={(e) => setEditingProduct({ ...editingProduct, current_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock mínimo</label>
<Input
type="number"
value={editingProduct.min_stock}
onChange={(e) => setEditingProduct({ ...editingProduct, min_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock máximo</label>
<Input
type="number"
value={editingProduct.max_stock}
onChange={(e) => setEditingProduct({ ...editingProduct, max_stock: Number(e.target.value) })}
min="1"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Costo por unidad</label>
<Input
type="number"
value={editingProduct.cost_per_unit || 0}
onChange={(e) => setEditingProduct({ ...editingProduct, cost_per_unit: Number(e.target.value) })}
min="0"
step="0.01"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Fecha de vencimiento personalizada</label>
<Input
type="date"
value={editingProduct.custom_expiry_date || ''}
onChange={(e) => setEditingProduct({ ...editingProduct, custom_expiry_date: e.target.value })}
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button
variant="outline"
onClick={handleCancelEdit}
className="text-[var(--text-secondary)] border-[var(--border-primary)] hover:bg-[var(--bg-tertiary)]"
>
Cancelar
</Button>
<Button
onClick={() => handleSaveProduct(editingProduct)}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white"
>
Guardar
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};