Add onboarding flow improvements

This commit is contained in:
Urtzi Alfaro
2025-09-03 14:06:38 +02:00
parent 0fb9f9d0f0
commit a55d48e635
31 changed files with 3813 additions and 6251 deletions

View File

@@ -0,0 +1,191 @@
import React, { useState, useEffect } from 'react';
import { Store, MapPin, Phone, Mail } from 'lucide-react';
import { Button, Card, Input } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
export const BakerySetupStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [formData, setFormData] = useState({
name: data.bakery?.name || '',
type: data.bakery?.type || '',
location: data.bakery?.location || '',
phone: data.bakery?.phone || '',
email: data.bakery?.email || '',
description: data.bakery?.description || ''
});
const bakeryTypes = [
{
value: 'artisan',
label: 'Panadería Artesanal',
description: 'Producción propia tradicional con recetas artesanales',
icon: '🥖'
},
{
value: 'industrial',
label: 'Panadería Industrial',
description: 'Producción a gran escala con procesos automatizados',
icon: '🏭'
},
{
value: 'retail',
label: 'Panadería Retail',
description: 'Punto de venta que compra productos terminados',
icon: '🏪'
},
{
value: 'hybrid',
label: 'Modelo Híbrido',
description: 'Combina producción propia con productos externos',
icon: '🔄'
}
];
useEffect(() => {
// Update parent data when form changes
onDataChange({
bakery: {
...formData,
tenant_id: data.bakery?.tenant_id
}
});
}, [formData]);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<div className="space-y-8">
{/* Form */}
<div className="space-y-8">
{/* Basic Info */}
<div className="space-y-6">
<div>
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
Nombre de la Panadería *
</label>
<Input
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="Ej: Panadería Artesanal El Buen Pan"
className="w-full text-lg py-3"
/>
</div>
{/* Bakery Type - Simplified */}
<div>
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-4">
Tipo de Panadería *
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{bakeryTypes.map((type) => (
<label
key={type.value}
className={`
flex items-center p-4 border-2 rounded-lg cursor-pointer transition-all duration-200 hover:shadow-sm
${formData.type === type.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
}
`}
>
<input
type="radio"
name="bakeryType"
value={type.value}
checked={formData.type === type.value}
onChange={(e) => handleInputChange('type', e.target.value)}
className="sr-only"
/>
<div className="flex items-center space-x-3 w-full">
<span className="text-2xl">{type.icon}</span>
<div>
<h4 className="font-medium text-[var(--text-primary)]">
{type.label}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
{type.description}
</p>
</div>
</div>
</label>
))}
</div>
</div>
{/* Location and Contact - Simplified */}
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-3">
Ubicación *
</label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
<Input
value={formData.location}
onChange={(e) => handleInputChange('location', e.target.value)}
placeholder="Dirección completa de tu panadería"
className="w-full pl-12 py-3"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Teléfono (opcional)
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
<Input
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="+1 234 567 8900"
className="w-full pl-10"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Email (opcional)
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
<Input
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="contacto@panaderia.com"
className="w-full pl-10"
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Optional: Show loading state when creating tenant */}
{data.bakery?.isCreating && (
<div className="text-center p-6 bg-[var(--color-primary)]/5 rounded-lg">
<div className="animate-spin w-8 h-8 border-3 border-[var(--color-primary)] border-t-transparent rounded-full mx-auto mb-4" />
<p className="text-[var(--color-primary)] font-medium">
Creando tu espacio de trabajo...
</p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,387 @@
import React, { useState, useEffect } from 'react';
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
interface CompletionStats {
totalProducts: number;
inventoryItems: number;
suppliersConfigured: number;
mlModelAccuracy: number;
estimatedTimeSaved: string;
completionScore: number;
}
export const CompletionStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [showConfetti, setShowConfetti] = useState(false);
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
useEffect(() => {
// Show confetti animation
setShowConfetti(true);
const timer = setTimeout(() => setShowConfetti(false), 3000);
// Calculate completion stats
const stats: CompletionStats = {
totalProducts: data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0,
inventoryItems: data.inventoryItems?.length || 0,
suppliersConfigured: data.suppliers?.length || 0,
mlModelAccuracy: data.trainingMetrics?.accuracy * 100 || 0,
estimatedTimeSaved: '15-20 horas',
completionScore: calculateCompletionScore()
};
setCompletionStats(stats);
// Update parent data
onDataChange({
...data,
completionStats: stats,
onboardingCompleted: true,
completedAt: new Date().toISOString()
});
return () => clearTimeout(timer);
}, []);
const calculateCompletionScore = () => {
let score = 0;
// Base score for completing setup
if (data.bakery?.tenant_id) score += 20;
// Data upload and analysis
if (data.validation?.is_valid) score += 15;
if (data.analysisStatus === 'completed') score += 15;
// Product review
const approvedProducts = data.detectedProducts?.filter((p: any) => p.status === 'approved').length || 0;
if (approvedProducts > 0) score += 20;
// Inventory setup
if (data.inventoryItems?.length > 0) score += 15;
// ML training
if (data.trainingStatus === 'completed') score += 15;
return Math.min(score, 100);
};
const generateCertificate = () => {
// Mock certificate generation
const certificateData = {
bakeryName: data.bakery?.name || 'Tu Panadería',
completionDate: new Date().toLocaleDateString('es-ES'),
score: completionStats?.completionScore || 0,
features: [
'Configuración de Tenant Multi-inquilino',
'Análisis de Datos con IA',
'Gestión de Inventario Inteligente',
'Entrenamiento de Modelo ML',
'Integración de Proveedores'
]
};
console.log('Generating certificate:', certificateData);
alert(`🎓 Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`);
};
const scheduleDemo = () => {
// Mock demo scheduling
alert('📅 Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.');
};
const shareSuccess = () => {
// Mock social sharing
const shareText = `¡Acabo de completar la configuración de mi panadería inteligente con IA! 🥖🤖 Puntuación: ${completionStats?.completionScore}/100`;
navigator.clipboard.writeText(shareText);
alert('✅ Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!');
};
const quickStartActions = [
{
id: 'dashboard',
title: 'Ir al Dashboard',
description: 'Explora tu panel de control personalizado',
icon: <ArrowRight className="w-5 h-5" />,
action: () => onNext(),
primary: true
},
{
id: 'inventory',
title: 'Gestionar Inventario',
description: 'Revisa y actualiza tu stock actual',
icon: <Gift className="w-5 h-5" />,
action: () => console.log('Navigate to inventory'),
primary: false
},
{
id: 'predictions',
title: 'Ver Predicciones IA',
description: 'Consulta las predicciones de demanda',
icon: <Rocket className="w-5 h-5" />,
action: () => console.log('Navigate to predictions'),
primary: false
}
];
const achievementBadges = [
{
title: 'Pionero IA',
description: 'Primera configuración con Machine Learning',
icon: '🤖',
earned: data.trainingStatus === 'completed'
},
{
title: 'Organizador',
description: 'Inventario completamente configurado',
icon: '📦',
earned: (data.inventoryItems?.length || 0) >= 5
},
{
title: 'Analista',
description: 'Análisis de datos exitoso',
icon: '📊',
earned: data.analysisStatus === 'completed'
},
{
title: 'Perfeccionista',
description: 'Puntuación de configuración 90+',
icon: '⭐',
earned: (completionStats?.completionScore || 0) >= 90
}
];
return (
<div className="space-y-6">
{/* Confetti Effect */}
{showConfetti && (
<div className="fixed inset-0 pointer-events-none z-50 overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-6xl animate-bounce">🎉</div>
</div>
{/* Additional confetti elements could be added here */}
</div>
)}
{/* Celebration Header */}
<div className="text-center mb-8">
<div className="w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
<CheckCircle className="w-12 h-12 text-white" />
</div>
<h1 className="text-4xl font-bold text-[var(--text-primary)] mb-4">
¡Felicidades! 🎉
</h1>
<h2 className="text-xl font-semibold text-[var(--color-success)] mb-4">
{data.bakery?.name} está listo para funcionar
</h2>
<p className="text-[var(--text-secondary)] max-w-3xl mx-auto text-lg leading-relaxed">
Has completado exitosamente la configuración inicial de tu panadería inteligente.
Tu sistema está optimizado con IA y listo para transformar la gestión de tu negocio.
</p>
</div>
{/* Completion Stats */}
{completionStats && (
<Card className="p-6">
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-[var(--color-success)] to-[var(--color-primary)] rounded-full mb-4">
<span className="text-2xl font-bold text-white">{completionStats.completionScore}</span>
<span className="text-sm text-white/80 ml-1">/100</span>
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Puntuación de Configuración
</h3>
<p className="text-sm text-[var(--text-secondary)]">
¡Excelente trabajo! Has superado el promedio de la industria (78/100)
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">{completionStats.totalProducts}</p>
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-primary)]">{completionStats.inventoryItems}</p>
<p className="text-xs text-[var(--text-secondary)]">Inventario</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{completionStats.suppliersConfigured}</p>
<p className="text-xs text-[var(--text-secondary)]">Proveedores</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-secondary)]">{completionStats.mlModelAccuracy.toFixed(1)}%</p>
<p className="text-xs text-[var(--text-secondary)]">Precisión IA</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--color-warning)]">{completionStats.estimatedTimeSaved}</p>
<p className="text-xs text-[var(--text-secondary)]">Tiempo Ahorrado</p>
</div>
</div>
</Card>
)}
{/* Achievement Badges */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 text-center">
🏆 Logros Desbloqueados
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{achievementBadges.map((badge, index) => (
<div
key={index}
className={`text-center p-4 rounded-lg border-2 transition-all duration-200 ${
badge.earned
? 'border-[var(--color-success)]/50 bg-[var(--color-success)]/10'
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)] opacity-50'
}`}
>
<div className="text-2xl mb-2">{badge.icon}</div>
<h4 className="font-medium text-[var(--text-primary)] mb-1 text-sm">{badge.title}</h4>
<p className="text-xs text-[var(--text-secondary)]">{badge.description}</p>
{badge.earned && (
<div className="mt-2">
<Badge variant="green" className="text-xs">Conseguido</Badge>
</div>
)}
</div>
))}
</div>
</Card>
{/* Quick Actions */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
🚀 Próximos Pasos
</h3>
<div className="space-y-3">
{quickStartActions.map((action) => (
<button
key={action.id}
onClick={action.action}
className={`w-full text-left p-4 rounded-lg border transition-all duration-200 hover:shadow-md ${
action.primary
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)] bg-[var(--bg-secondary)]'
}`}
>
<div className="flex items-center space-x-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
action.primary ? 'bg-[var(--color-primary)]' : 'bg-[var(--bg-tertiary)]'
}`}>
<span className={action.primary ? 'text-white' : 'text-[var(--text-secondary)]'}>
{action.icon}
</span>
</div>
<div className="flex-1">
<h4 className={`font-medium ${
action.primary ? 'text-[var(--color-primary)]' : 'text-[var(--text-primary)]'
}`}>
{action.title}
</h4>
<p className="text-sm text-[var(--text-secondary)]">{action.description}</p>
</div>
<ArrowRight className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
</button>
))}
</div>
</Card>
{/* Additional Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="p-4 text-center">
<Download className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-2">Certificado</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
Descarga tu certificado de configuración
</p>
<Button size="sm" variant="outline" onClick={generateCertificate}>
Descargar
</Button>
</Card>
<Card className="p-4 text-center">
<Calendar className="w-6 h-6 text-[var(--color-primary)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-2">Demo Personal</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
Agenda una demostración 1-a-1
</p>
<Button size="sm" variant="outline" onClick={scheduleDemo}>
Agendar
</Button>
</Card>
<Card className="p-4 text-center">
<Share2 className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-2">Compartir Éxito</h4>
<p className="text-sm text-[var(--text-secondary)] mb-3">
Comparte tu logro en redes sociales
</p>
<Button size="sm" variant="outline" onClick={shareSuccess}>
Compartir
</Button>
</Card>
</div>
{/* Summary & Thanks */}
<Card className="p-6 bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-success)]/5 border-[var(--color-primary)]/20">
<div className="text-center">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-3">
🙏 ¡Gracias por confiar en nuestra plataforma!
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Tu panadería ahora cuenta con tecnología de vanguardia para:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left mb-6">
<div className="space-y-2">
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Predicciones de demanda con IA</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Gestión inteligente de inventario</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Optimización de compras</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Alertas automáticas de restock</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Análisis de tendencias de venta</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<CheckCircle className="w-4 h-4 text-[var(--color-success)]" />
<span>Control multi-tenant seguro</span>
</div>
</div>
</div>
<div className="bg-[var(--color-info)]/10 rounded-lg p-4">
<p className="text-sm text-[var(--color-info)]">
💡 <strong>Consejo:</strong> Explora el dashboard para descubrir todas las funcionalidades disponibles.
El sistema aprenderá de tus patrones y mejorará sus recomendaciones con el tiempo.
</p>
</div>
</div>
</Card>
</div>
);
};

View File

@@ -0,0 +1,467 @@
import React, { useState, useRef, useEffect } from 'react';
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | '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[];
}
// Unified mock service that handles both validation and analysis
const mockDataProcessingService = {
processFile: async (file: File, onProgress: (progress: number, stage: string, message: string) => void) => {
return new Promise<ProcessingResult>((resolve, reject) => {
let progress = 0;
const stages = [
{ threshold: 20, stage: 'validating', message: 'Validando estructura del archivo...' },
{ threshold: 40, stage: 'validating', message: 'Verificando integridad de datos...' },
{ threshold: 60, stage: 'analyzing', message: 'Identificando productos únicos...' },
{ threshold: 80, stage: 'analyzing', message: 'Analizando patrones de venta...' },
{ threshold: 90, stage: 'analyzing', message: 'Generando recomendaciones con IA...' },
{ threshold: 100, stage: 'completed', message: 'Procesamiento completado' }
];
const interval = setInterval(() => {
if (progress < 100) {
progress += 10;
const currentStage = stages.find(s => progress <= s.threshold);
if (currentStage) {
onProgress(progress, currentStage.stage, currentStage.message);
}
}
if (progress >= 100) {
clearInterval(interval);
// Return combined validation + analysis results
resolve({
// Validation results
is_valid: true,
total_records: Math.floor(Math.random() * 1000) + 100,
unique_products: Math.floor(Math.random() * 50) + 10,
product_list: ['Pan Integral', 'Croissant', 'Baguette', 'Empanadas', 'Pan de Centeno', 'Medialunas'],
validation_errors: [],
validation_warnings: [
'Algunas fechas podrían tener formato inconsistente',
'3 productos sin categoría definida'
],
summary: {
date_range: '2024-01-01 to 2024-12-31',
total_sales: 15420.50,
average_daily_sales: 42.25
},
// Analysis results
productsIdentified: 15,
categoriesDetected: 4,
businessModel: 'artisan',
confidenceScore: 94,
recommendations: [
'Se detectó un modelo de panadería artesanal con producción propia',
'Los productos más vendidos son panes tradicionales y bollería',
'Recomendamos categorizar el inventario por tipo de producto',
'Considera ampliar la línea de productos de repostería'
]
});
}
}, 400);
});
}
};
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [progress, setProgress] = useState(data.processingProgress || 0);
const [currentMessage, setCurrentMessage] = useState(data.currentMessage || '');
const [results, setResults] = useState<ProcessingResult | null>(data.processingResults || null);
const [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Update parent data when state changes
onDataChange({
...data,
processingStage: stage,
processingProgress: progress,
currentMessage: currentMessage,
processingResults: results,
files: {
...data.files,
salesData: uploadedFile
}
});
}, [stage, progress, currentMessage, results, uploadedFile]);
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'];
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
alert('Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)');
return;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert('El archivo es demasiado grande. Máximo 10MB permitido.');
return;
}
setUploadedFile(file);
setStage('validating');
setProgress(0);
try {
const result = await mockDataProcessingService.processFile(
file,
(newProgress, newStage, message) => {
setProgress(newProgress);
setStage(newStage as ProcessingStage);
setCurrentMessage(message);
}
);
setResults(result);
setStage('completed');
} catch (error) {
console.error('Processing error:', error);
setStage('error');
setCurrentMessage('Error en el procesamiento de datos');
}
};
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);
};
const resetProcess = () => {
setStage('upload');
setUploadedFile(null);
setProgress(0);
setCurrentMessage('');
setResults(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="space-y-8">
{/* Improved Upload Stage */}
{stage === 'upload' && (
<>
<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>
{/* Visual indicators */}
<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>
{/* Improved 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>
)}
{/* Simplified Results Stage */}
{stage === 'completed' && results && (
<div className="space-y-8">
{/* Success Header */}
<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">
¡Procesamiento Completado!
</h3>
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
Tus datos han sido procesados exitosamente
</p>
</div>
{/* Simple 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.total_records}</p>
<p className="text-sm text-[var(--text-secondary)]">Registros</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 className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{results.confidenceScore}%</p>
<p className="text-sm text-[var(--text-secondary)]">Confianza</p>
</div>
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--text-primary)]">
{results.businessModel === 'artisan' ? 'Artesanal' :
results.businessModel === 'retail' ? 'Retail' : 'Híbrido'}
</p>
<p className="text-sm text-[var(--text-secondary)]">Modelo</p>
</div>
</div>
</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

@@ -0,0 +1,565 @@
import React, { useState, useEffect } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2 } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
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;
}
// Mock inventory items based on approved products
const mockInventoryItems: InventoryItem[] = [
{
id: '1', name: 'Harina de Trigo', category: 'ingredient',
current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg',
expiry_date: '2024-12-31', supplier: 'Molinos del Sur',
cost_per_unit: 1.20, requires_refrigeration: false
},
{
id: '2', name: 'Levadura Fresca', category: 'ingredient',
current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg',
expiry_date: '2024-03-15', supplier: 'Levaduras Pro',
cost_per_unit: 3.50, requires_refrigeration: true
},
{
id: '3', name: 'Pan Integral', category: 'finished_product',
current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades',
expiry_date: '2024-01-25', requires_refrigeration: false
},
{
id: '4', name: 'Mantequilla', category: 'ingredient',
current_stock: 15, min_stock: 5, max_stock: 30, unit: 'kg',
expiry_date: '2024-02-28', supplier: 'Lácteos Premium',
cost_per_unit: 4.20, requires_refrigeration: true
}
];
export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [items, setItems] = useState<InventoryItem[]>(
data.inventoryItems || mockInventoryItems
);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
useEffect(() => {
onDataChange({
...data,
inventoryItems: items,
inventoryConfigured: items.length > 0 && items.every(item =>
item.min_stock > 0 && item.max_stock > item.min_stock
)
});
}, [items]);
const handleAddItem = () => {
const newItem: InventoryItem = {
id: Date.now().toString(),
name: '',
category: 'ingredient',
current_stock: 0,
min_stock: 0,
max_stock: 0,
unit: 'kg',
requires_refrigeration: false
};
setEditingItem(newItem);
setIsAddingNew(true);
};
const handleSaveItem = (item: InventoryItem) => {
if (isAddingNew) {
setItems(prev => [...prev, item]);
} else {
setItems(prev => prev.map(i => i.id === item.id ? item : i));
}
setEditingItem(null);
setIsAddingNew(false);
};
const handleDeleteItem = (id: string) => {
if (window.confirm('¿Estás seguro de eliminar este elemento del inventario?')) {
setItems(prev => prev.filter(item => item.id !== id));
}
};
const handleQuickSetup = () => {
// Auto-configure basic inventory based on approved products
const autoItems = data.detectedProducts
?.filter((p: any) => p.status === 'approved')
.map((product: any, index: number) => ({
id: `auto_${index}`,
name: product.name,
category: 'finished_product' as const,
current_stock: Math.floor(Math.random() * 20) + 5,
min_stock: 5,
max_stock: 50,
unit: 'unidades',
requires_refrigeration: product.category === 'Repostería' || product.category === 'Salados'
})) || [];
setItems(prev => [...prev, ...autoItems]);
};
const getFilteredItems = () => {
return filterCategory === 'all'
? items
: items.filter(item => item.category === filterCategory);
};
const getStockStatus = (item: InventoryItem) => {
if (item.current_stock <= item.min_stock) return { status: 'low', color: 'red', text: 'Stock Bajo' };
if (item.current_stock >= item.max_stock) return { status: 'high', color: 'blue', text: 'Stock Alto' };
return { status: 'normal', color: 'green', text: 'Normal' };
};
const isNearExpiry = (expiryDate?: string) => {
if (!expiryDate) return false;
const expiry = new Date(expiryDate);
const today = new Date();
const diffDays = (expiry.getTime() - today.getTime()) / (1000 * 3600 * 24);
return diffDays <= 7;
};
const stats = {
total: items.length,
ingredients: items.filter(i => i.category === 'ingredient').length,
products: items.filter(i => i.category === 'finished_product').length,
lowStock: items.filter(i => i.current_stock <= i.min_stock).length,
nearExpiry: items.filter(i => isNearExpiry(i.expiry_date)).length,
refrigerated: items.filter(i => i.requires_refrigeration).length
};
return (
<div className="space-y-6">
{/* Quick Actions */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<p className="text-sm text-[var(--text-secondary)]">
{stats.total} elementos configurados
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleQuickSetup}
className="text-[var(--color-info)]"
>
Auto-configurar Productos
</Button>
<Button
size="sm"
onClick={handleAddItem}
>
<Plus className="w-4 h-4 mr-2" />
Agregar Elemento
</Button>
</div>
</div>
</Card>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
<p className="text-xs text-[var(--text-secondary)]">Total</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.ingredients}</p>
<p className="text-xs text-[var(--text-secondary)]">Ingredientes</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.products}</p>
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.lowStock}</p>
<p className="text-xs text-[var(--text-secondary)]">Stock Bajo</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.nearExpiry}</p>
<p className="text-xs text-[var(--text-secondary)]">Por Vencer</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.refrigerated}</p>
<p className="text-xs text-[var(--text-secondary)]">Refrigerado</p>
</Card>
</div>
{/* Filters */}
<Card className="p-4">
<div className="flex items-center space-x-4">
<label className="text-sm font-medium text-[var(--text-secondary)]">Filtrar:</label>
<div className="flex space-x-2">
{[
{ value: 'all', label: 'Todos' },
{ value: 'ingredient', label: 'Ingredientes' },
{ value: 'finished_product', label: 'Productos' }
].map(filter => (
<button
key={filter.value}
onClick={() => setFilterCategory(filter.value as any)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filterCategory === filter.value
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
{filter.label}
</button>
))}
</div>
</div>
</Card>
{/* Inventory Items */}
<div className="space-y-4">
{getFilteredItems().map((item) => {
const stockStatus = getStockStatus(item);
const nearExpiry = isNearExpiry(item.expiry_date);
return (
<Card key={item.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Category Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
item.category === 'ingredient'
? 'bg-[var(--color-primary)]/10'
: 'bg-[var(--color-success)]/10'
}`}>
<Package className={`w-4 h-4 ${
item.category === 'ingredient'
? 'text-[var(--color-primary)]'
: 'text-[var(--color-success)]'
}`} />
</div>
{/* Item Info */}
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{item.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant={item.category === 'ingredient' ? 'blue' : 'green'}>
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto'}
</Badge>
{item.requires_refrigeration && (
<Badge variant="gray"> Refrigeración</Badge>
)}
<Badge variant={stockStatus.color}>
{stockStatus.text}
</Badge>
{nearExpiry && (
<Badge variant="red">Vence Pronto</Badge>
)}
</div>
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)]">
<div>
<span className="text-[var(--text-tertiary)]">Stock Actual: </span>
<span className={`font-medium ${stockStatus.status === 'low' ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
{item.current_stock} {item.unit}
</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Rango: </span>
<span className="font-medium text-[var(--text-primary)]">
{item.min_stock} - {item.max_stock} {item.unit}
</span>
</div>
{item.expiry_date && (
<div>
<span className="text-[var(--text-tertiary)]">Vencimiento: </span>
<span className={`font-medium ${nearExpiry ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
{new Date(item.expiry_date).toLocaleDateString()}
</span>
</div>
)}
{item.cost_per_unit && (
<div>
<span className="text-[var(--text-tertiary)]">Costo/Unidad: </span>
<span className="font-medium text-[var(--text-primary)]">
${item.cost_per_unit.toFixed(2)}
</span>
</div>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 ml-4 mt-1">
<Button
size="sm"
variant="outline"
onClick={() => setEditingItem(item)}
>
<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)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
);
})}
{getFilteredItems().length === 0 && (
<Card className="p-8 text-center">
<Package className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">No hay elementos en esta categoría</p>
</Card>
)}
</div>
{/* Warnings */}
{(stats.lowStock > 0 || stats.nearExpiry > 0) && (
<Card className="p-4 bg-[var(--color-warning-50)] border-[var(--color-warning-200)]">
<div className="flex items-start space-x-3">
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-medium text-[var(--color-warning-800)] mb-1">Advertencias de Inventario</h4>
{stats.lowStock > 0 && (
<p className="text-sm text-[var(--color-warning-700)] mb-1">
{stats.lowStock} elemento(s) con stock bajo
</p>
)}
{stats.nearExpiry > 0 && (
<p className="text-sm text-[var(--color-warning-700)]">
{stats.nearExpiry} elemento(s) próximos a vencer
</p>
)}
</div>
</div>
</Card>
)}
{/* Edit Modal */}
{editingItem && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
{isAddingNew ? 'Agregar Elemento' : 'Editar Elemento'}
</h3>
<InventoryItemForm
item={editingItem}
onSave={handleSaveItem}
onCancel={() => {
setEditingItem(null);
setIsAddingNew(false);
}}
/>
</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>Stock Mínimo:</strong> Nivel que dispara alertas de reabastecimiento</li>
<li> <strong>Stock Máximo:</strong> Capacidad máxima de almacenamiento</li>
<li> <strong>Fechas de Vencimiento:</strong> Control automático de productos perecederos</li>
<li> <strong>Refrigeración:</strong> Identifica productos que requieren frío</li>
</ul>
</Card>
</div>
);
};
// Component for editing inventory items
interface InventoryItemFormProps {
item: InventoryItem;
onSave: (item: InventoryItem) => void;
onCancel: () => void;
}
const InventoryItemForm: React.FC<InventoryItemFormProps> = ({ item, onSave, onCancel }) => {
const [formData, setFormData] = useState(item);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
alert('El nombre es requerido');
return;
}
if (formData.min_stock >= formData.max_stock) {
alert('El stock máximo debe ser mayor al mínimo');
return;
}
onSave(formData);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Nombre *
</label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nombre del producto/ingrediente"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Categoría *
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as any }))}
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[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 text-[var(--text-primary)] mb-1">
Unidad *
</label>
<Input
value={formData.unit}
onChange={(e) => setFormData(prev => ({ ...prev, unit: e.target.value }))}
placeholder="kg, unidades, litros..."
/>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Actual
</label>
<Input
type="number"
value={formData.current_stock}
onChange={(e) => setFormData(prev => ({ ...prev, current_stock: Number(e.target.value) }))}
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Mín. *
</label>
<Input
type="number"
value={formData.min_stock}
onChange={(e) => setFormData(prev => ({ ...prev, min_stock: Number(e.target.value) }))}
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Máx. *
</label>
<Input
type="number"
value={formData.max_stock}
onChange={(e) => setFormData(prev => ({ ...prev, max_stock: Number(e.target.value) }))}
min="1"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Fecha Vencimiento
</label>
<Input
type="date"
value={formData.expiry_date || ''}
onChange={(e) => setFormData(prev => ({ ...prev, expiry_date: e.target.value }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Costo por Unidad
</label>
<Input
type="number"
step="0.01"
value={formData.cost_per_unit || ''}
onChange={(e) => setFormData(prev => ({ ...prev, cost_per_unit: Number(e.target.value) }))}
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Proveedor
</label>
<Input
value={formData.supplier || ''}
onChange={(e) => setFormData(prev => ({ ...prev, supplier: e.target.value }))}
placeholder="Nombre del proveedor"
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="refrigeration"
checked={formData.requires_refrigeration}
onChange={(e) => setFormData(prev => ({ ...prev, requires_refrigeration: e.target.checked }))}
className="rounded"
/>
<label htmlFor="refrigeration" className="text-sm text-[var(--text-primary)]">
Requiere refrigeración
</label>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
Guardar
</Button>
</div>
</form>
);
};

View File

@@ -0,0 +1,780 @@
import React, { useState, useEffect } from 'react';
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
interface TrainingMetrics {
accuracy: number;
precision: number;
recall: number;
f1_score: number;
training_loss: number;
validation_loss: number;
epochs_completed: number;
total_epochs: number;
}
interface TrainingLog {
timestamp: string;
message: string;
level: 'info' | 'warning' | 'error' | 'success';
}
// Enhanced Mock ML training service that matches backend behavior
const mockMLService = {
startTraining: async (trainingData: any) => {
return new Promise((resolve) => {
let progress = 0;
let step_count = 0;
const total_steps = 12;
// Backend-matching training steps and messages
const trainingSteps = [
{ step: 'data_validation', message: 'Validando datos de entrenamiento...', progress: 10 },
{ step: 'data_preparation_start', message: 'Preparando conjunto de datos...', progress: 20 },
{ step: 'feature_engineering', message: 'Creando características para el modelo...', progress: 30 },
{ step: 'data_preparation_complete', message: 'Preparación de datos completada', progress: 35 },
{ step: 'ml_training_start', message: 'Iniciando entrenamiento del modelo Prophet...', progress: 40 },
{ step: 'model_fitting', message: 'Ajustando modelo a los datos históricos...', progress: 55 },
{ step: 'pattern_detection', message: 'Detectando patrones estacionales y tendencias...', progress: 70 },
{ step: 'validation', message: 'Validando precisión del modelo...', progress: 80 },
{ step: 'training_complete', message: 'Entrenamiento ML completado', progress: 85 },
{ step: 'storing_models', message: 'Guardando modelo entrenado...', progress: 90 },
{ step: 'performance_metrics', message: 'Calculando métricas de rendimiento...', progress: 95 },
{ step: 'completed', message: 'Tu Asistente Inteligente está listo!', progress: 100 }
];
const interval = setInterval(() => {
if (step_count >= trainingSteps.length) {
clearInterval(interval);
return;
}
const currentStep = trainingSteps[step_count];
progress = currentStep.progress;
// Generate realistic metrics that improve over time
const stepProgress = step_count / trainingSteps.length;
const baseAccuracy = 0.65;
const maxAccuracy = 0.93;
const currentAccuracy = baseAccuracy + (maxAccuracy - baseAccuracy) * Math.pow(stepProgress, 0.8);
const metrics: TrainingMetrics = {
accuracy: Math.min(maxAccuracy, currentAccuracy + (Math.random() * 0.02 - 0.01)),
precision: Math.min(0.95, 0.70 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
recall: Math.min(0.94, 0.68 + stepProgress * 0.24 + (Math.random() * 0.02 - 0.01)),
f1_score: Math.min(0.94, 0.69 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
training_loss: Math.max(0.08, 1.2 - stepProgress * 1.0 + (Math.random() * 0.05 - 0.025)),
validation_loss: Math.max(0.10, 1.3 - stepProgress * 1.1 + (Math.random() * 0.06 - 0.03)),
epochs_completed: Math.floor(stepProgress * 15) + 1,
total_epochs: 15
};
// Generate step-specific logs that match backend behavior
const logs: TrainingLog[] = [];
// Add the current step message
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: currentStep.message,
level: currentStep.step === 'completed' ? 'success' : 'info'
});
// Add specific logs based on the training step
if (currentStep.step === 'data_validation') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: `Productos analizados: ${trainingData.products?.length || 3}`,
level: 'info'
});
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: `Registros de inventario: ${trainingData.inventory?.length || 3}`,
level: 'info'
});
} else if (currentStep.step === 'ml_training_start') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: 'Usando modelo Prophet optimizado para panadería',
level: 'info'
});
} else if (currentStep.step === 'model_fitting') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: `Precisión actual: ${(metrics.accuracy * 100).toFixed(1)}%`,
level: 'info'
});
} else if (currentStep.step === 'pattern_detection') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: 'Patrones estacionales detectados: días de la semana, horas pico',
level: 'success'
});
} else if (currentStep.step === 'validation') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: `MAE: ${(Math.random() * 2 + 1.5).toFixed(2)}, MAPE: ${((1 - metrics.accuracy) * 100).toFixed(1)}%`,
level: 'info'
});
} else if (currentStep.step === 'storing_models') {
logs.push({
timestamp: new Date().toLocaleTimeString(),
message: `Modelo guardado: bakery_prophet_${Date.now()}`,
level: 'success'
});
}
// Emit progress event that matches backend WebSocket message structure
window.dispatchEvent(new CustomEvent('mlTrainingProgress', {
detail: {
type: 'progress',
job_id: `training_${Date.now()}`,
data: {
progress,
current_step: currentStep.step,
step_details: currentStep.message,
products_completed: Math.floor(step_count / 3),
products_total: Math.max(3, trainingData.products?.length || 3),
estimated_time_remaining_minutes: Math.max(0, Math.floor((total_steps - step_count) * 0.75))
},
timestamp: new Date().toISOString(),
// Keep old format for backward compatibility
status: progress >= 100 ? 'completed' : 'training',
currentPhase: currentStep.message,
metrics,
logs
}
}));
step_count++;
if (progress >= 100) {
clearInterval(interval);
// Generate final results matching backend response structure
const finalAccuracy = 0.89 + Math.random() * 0.05;
const finalResult = {
success: true,
job_id: `training_${Date.now()}`,
tenant_id: 'demo_tenant',
status: 'completed',
training_results: {
total_products: Math.max(3, trainingData.products?.length || 3),
successful_trainings: Math.max(3, trainingData.products?.length || 3),
failed_trainings: 0,
products: Array.from({length: Math.max(3, trainingData.products?.length || 3)}, (_, i) => ({
inventory_product_id: `product_${i + 1}`,
status: 'completed',
model_id: `model_${i + 1}_${Date.now()}`,
data_points: 45 + Math.floor(Math.random() * 30),
metrics: {
mape: (1 - finalAccuracy) * 100 + (Math.random() * 2 - 1),
mae: 1.5 + Math.random() * 1.0,
rmse: 2.1 + Math.random() * 1.2,
r2_score: finalAccuracy + (Math.random() * 0.02 - 0.01)
}
})),
overall_training_time_seconds: 45 + Math.random() * 15
},
data_summary: {
sales_records: trainingData.inventory?.length * 30 || 1500,
weather_records: 90,
traffic_records: 85,
date_range: {
start: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
end: new Date().toISOString()
},
data_sources_used: ['bakery_sales', 'weather_forecast', 'madrid_traffic'],
constraints_applied: {}
},
finalMetrics: {
accuracy: finalAccuracy,
precision: finalAccuracy * 0.98,
recall: finalAccuracy * 0.96,
f1_score: finalAccuracy * 0.97,
training_loss: 0.12 + Math.random() * 0.05,
validation_loss: 0.15 + Math.random() * 0.05,
total_epochs: 15
},
modelId: `bakery_prophet_${Date.now()}`,
deploymentUrl: '/api/v1/training/models/predict',
trainingDuration: `${(45 + Math.random() * 15).toFixed(1)} segundos`,
datasetSize: `${trainingData.inventory?.length * 30 || 1500} registros`,
modelVersion: '2.1.0',
completed_at: new Date().toISOString()
};
resolve(finalResult);
}
}, 1800); // Matches backend timing
});
}
};
const getTrainingPhase = (epoch: number, total: number) => {
const progress = epoch / total;
if (progress <= 0.3) return 'Inicializando modelo de IA...';
if (progress <= 0.6) return 'Entrenando patrones de venta...';
if (progress <= 0.8) return 'Optimizando predicciones...';
if (progress <= 0.95) return 'Validando rendimiento...';
return 'Finalizando entrenamiento...';
};
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [trainingStatus, setTrainingStatus] = useState(data.trainingStatus || 'pending');
const [progress, setProgress] = useState(data.trainingProgress || 0);
const [currentPhase, setCurrentPhase] = useState(data.currentPhase || '');
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
const [logs, setLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
const [finalResults, setFinalResults] = useState<any>(data.finalResults || null);
// New state variables for backend-compatible data
const [productsCompleted, setProductsCompleted] = useState(0);
const [productsTotal, setProductsTotal] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number | null>(null);
useEffect(() => {
// Listen for ML training progress updates
const handleProgress = (event: CustomEvent) => {
const detail = event.detail;
// Handle new backend-compatible WebSocket message format
if (detail.type === 'progress' && detail.data) {
const progressData = detail.data;
setProgress(progressData.progress || 0);
setCurrentPhase(progressData.step_details || progressData.current_step || 'En progreso...');
// Update products progress if available
if (progressData.products_completed !== undefined) {
setProductsCompleted(progressData.products_completed);
}
if (progressData.products_total !== undefined) {
setProductsTotal(progressData.products_total);
}
// Update time estimate if available
if (progressData.estimated_time_remaining_minutes !== undefined) {
setEstimatedTimeRemaining(progressData.estimated_time_remaining_minutes);
}
// Handle metrics (from legacy format for backward compatibility)
if (detail.metrics) {
setMetrics(detail.metrics);
}
// Handle logs (from legacy format for backward compatibility)
if (detail.logs && detail.logs.length > 0) {
setLogs(prev => [...prev, ...detail.logs]);
}
// Check completion status
if (progressData.progress >= 100) {
setTrainingStatus('completed');
} else {
setTrainingStatus('training');
}
} else {
// Handle legacy format for backward compatibility
const { progress: newProgress, metrics: newMetrics, logs: newLogs, status, currentPhase: newPhase } = detail;
setProgress(newProgress || 0);
setMetrics(newMetrics);
setCurrentPhase(newPhase || 'En progreso...');
setTrainingStatus(status || 'training');
if (newLogs && newLogs.length > 0) {
setLogs(prev => [...prev, ...newLogs]);
}
}
};
window.addEventListener('mlTrainingProgress', handleProgress as EventListener);
return () => window.removeEventListener('mlTrainingProgress', handleProgress as EventListener);
}, []);
useEffect(() => {
// Update parent data when state changes
onDataChange({
...data,
trainingStatus,
trainingProgress: progress,
currentPhase,
trainingMetrics: metrics,
trainingLogs: logs,
finalResults
});
}, [trainingStatus, progress, currentPhase, metrics, logs, finalResults]);
// Auto-start training when coming from suppliers step
useEffect(() => {
if (data.autoStartTraining && trainingStatus === 'pending') {
const timer = setTimeout(() => {
startTraining();
// Remove the auto-start flag so it doesn't trigger again
onDataChange({ ...data, autoStartTraining: false });
}, 1000);
return () => clearTimeout(timer);
}
}, [data.autoStartTraining, trainingStatus]);
const startTraining = async () => {
// Access data from previous steps through allStepData
const inventoryData = data.allStepData?.inventory?.inventoryItems ||
data.allStepData?.['inventory-setup']?.inventoryItems ||
data.inventoryItems || [];
const detectedProducts = data.allStepData?.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
data.allStepData?.['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
const salesValidation = data.allStepData?.['data-processing']?.validation ||
data.allStepData?.review?.validation ||
data.validation || {};
// If no data is available, create mock data for demo purposes
let finalInventoryData = inventoryData;
let finalDetectedProducts = detectedProducts;
let finalValidation = salesValidation;
if (inventoryData.length === 0 && detectedProducts.length === 0) {
console.log('No data found from previous steps, using mock data for demo');
// Create mock data for demonstration
finalInventoryData = [
{ id: '1', name: 'Harina de Trigo', category: 'ingredient', current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg' },
{ id: '2', name: 'Levadura Fresca', category: 'ingredient', current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg' },
{ id: '3', name: 'Pan Integral', category: 'finished_product', current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades' }
];
finalDetectedProducts = [
{ name: 'Pan Francés', status: 'approved', category: 'Panadería' },
{ name: 'Croissants', status: 'approved', category: 'Repostería' },
{ name: 'Pan Integral', status: 'approved', category: 'Panadería' }
];
finalValidation = {
total_records: 1500,
summary: {
date_range: '2024-01-01 to 2024-12-31'
}
};
}
setTrainingStatus('training');
setProgress(0);
setLogs([{
timestamp: new Date().toLocaleTimeString(),
message: 'Iniciando entrenamiento del modelo de Machine Learning...',
level: 'info'
}]);
try {
const result = await mockMLService.startTraining({
products: finalDetectedProducts,
inventory: finalInventoryData,
salesData: finalValidation
});
setFinalResults(result);
setTrainingStatus('completed');
} catch (error) {
console.error('ML Training error:', error);
setTrainingStatus('error');
setLogs(prev => [...prev, {
timestamp: new Date().toLocaleTimeString(),
message: 'Error durante el entrenamiento del modelo',
level: 'error'
}]);
}
};
const retryTraining = () => {
setTrainingStatus('pending');
setProgress(0);
setMetrics(null);
setLogs([]);
setFinalResults(null);
};
const getLogIcon = (level: string) => {
switch (level) {
case 'success': return '✅';
case 'warning': return '⚠️';
case 'error': return '❌';
default: return '';
}
};
const getLogColor = (level: string) => {
switch (level) {
case 'success': return 'text-[var(--color-success)]';
case 'warning': return 'text-[var(--color-warning)]';
case 'error': return 'text-[var(--color-error)]';
default: return 'text-[var(--text-secondary)]';
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="text-center mb-6">
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
</div>
<p className="text-[var(--text-secondary)]">
Tu asistente inteligente analizará tus datos históricos para ayudarte a tomar mejores decisiones de negocio
</p>
</div>
{/* Auto-start notification */}
{data.autoStartTraining && trainingStatus === 'pending' && (
<Card className="p-4 bg-[var(--color-info)]/10 border-[var(--color-info)]/20 mb-4">
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-info)] border-t-transparent rounded-full"></div>
<p className="text-[var(--color-info)] font-medium">
Creando tu asistente inteligente automáticamente...
</p>
</div>
</Card>
)}
{/* Training Controls */}
{trainingStatus === 'pending' && !data.autoStartTraining && (
<Card className="p-6 text-center">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
Crear tu Asistente Inteligente
</h3>
<p className="text-[var(--text-secondary)] mb-6">
Analizaremos tus datos de ventas y productos para crear un asistente que te ayude a predecir demanda y optimizar tu inventario.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<Activity className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)]">Predicción de Ventas</h4>
<p className="text-sm text-[var(--text-secondary)]">Anticipa cuánto vas a vender</p>
</div>
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)]">Recomendaciones</h4>
<p className="text-sm text-[var(--text-secondary)]">Cuánto producir y comprar</p>
</div>
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
<Zap className="w-6 h-6 text-[var(--color-warning)] mx-auto mb-2" />
<h4 className="font-medium text-[var(--text-primary)]">Alertas Automáticas</h4>
<p className="text-sm text-[var(--text-secondary)]">Te avisa cuando reordenar</p>
</div>
</div>
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4 mb-6">
<h4 className="font-medium text-[var(--color-info)] mb-2">Información que analizaremos:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-[var(--color-info)]">
<div>
<span className="font-medium">Productos:</span>
<br />
{(() => {
const allStepData = data.allStepData || {};
const detectedProducts = allStepData.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
allStepData['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
return detectedProducts.length || 3; // fallback to mock data count
})()}
</div>
<div>
<span className="font-medium">Inventario:</span>
<br />
{(() => {
const allStepData = data.allStepData || {};
const inventoryData = allStepData.inventory?.inventoryItems ||
allStepData['inventory-setup']?.inventoryItems ||
allStepData['inventory']?.inventoryItems ||
data.inventoryItems || [];
return inventoryData.length || 3; // fallback to mock data count
})()} elementos
</div>
<div>
<span className="font-medium">Registros:</span>
<br />
{(() => {
const allStepData = data.allStepData || {};
const validation = allStepData['data-processing']?.validation ||
allStepData.review?.validation ||
data.validation || {};
return validation.total_records || 1500; // fallback to mock data
})()}
</div>
<div>
<span className="font-medium">Período:</span>
<br />
{(() => {
const allStepData = data.allStepData || {};
const validation = allStepData['data-processing']?.validation ||
allStepData.review?.validation ||
data.validation || {};
const dateRange = validation.summary?.date_range || '2024-01-01 to 2024-12-31';
return dateRange.split(' to ').map((date: string) =>
new Date(date).toLocaleDateString()
).join(' - ');
})()}
</div>
</div>
</div>
<Button onClick={startTraining} className="px-8 py-2">
<Brain className="w-4 h-4 mr-2" />
Crear mi Asistente Inteligente
</Button>
</Card>
)}
{/* Training Progress */}
{trainingStatus === 'training' && (
<div className="space-y-6">
<Card className="p-6">
<div className="text-center mb-6">
<div className="animate-pulse w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-4">
<Brain className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
Creando tu Asistente Inteligente...
</h3>
<p className="text-[var(--text-secondary)]">{currentPhase}</p>
</div>
{/* Progress Bar */}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-primary)]">
Progreso de Creación
</span>
<div className="text-right">
<span className="text-sm text-[var(--text-secondary)]">
{progress.toFixed(0)}%
</span>
{productsTotal > 0 && (
<div className="text-xs text-[var(--text-tertiary)]">
{productsCompleted}/{productsTotal} productos
</div>
)}
{estimatedTimeRemaining !== null && estimatedTimeRemaining > 0 && (
<div className="text-xs text-[var(--text-tertiary)]">
~{estimatedTimeRemaining} min restantes
</div>
)}
</div>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-4">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-info)] h-4 rounded-full transition-all duration-1000 relative overflow-hidden"
style={{ width: `${progress}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse" />
</div>
</div>
</div>
{/* Current Metrics */}
{metrics && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-xl font-bold text-[var(--color-success)]">
{(metrics.accuracy * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">Precisión</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-xl font-bold text-[var(--color-info)]">
{metrics.training_loss.toFixed(3)}
</p>
<p className="text-xs text-[var(--text-secondary)]">Loss</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-xl font-bold text-[var(--color-primary)]">
{metrics.epochs_completed}/{metrics.total_epochs}
</p>
<p className="text-xs text-[var(--text-secondary)]">Epochs</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-xl font-bold text-[var(--color-secondary)]">
{(metrics.f1_score * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">F1-Score</p>
</div>
</div>
)}
</Card>
{/* Training Logs */}
<Card className="p-6">
<h4 className="font-medium text-[var(--text-primary)] mb-4">Progreso del Asistente</h4>
<div className="bg-black rounded-lg p-4 max-h-48 overflow-y-auto font-mono text-sm">
{logs.slice(-10).map((log, index) => (
<div key={index} className={`mb-1 ${getLogColor(log.level)}`}>
<span className="text-[var(--text-quaternary)]">[{log.timestamp}]</span>
<span className="ml-2">{getLogIcon(log.level)} {log.message}</span>
</div>
))}
</div>
</Card>
</div>
)}
{/* Training Completed */}
{trainingStatus === 'completed' && finalResults && (
<div className="space-y-6">
<Card className="p-6">
<div className="text-center mb-6">
<div className="w-12 h-12 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-semibold text-[var(--color-success)] mb-2">
¡Tu Asistente Inteligente está Listo!
</h3>
<p className="text-[var(--text-secondary)]">
Tu asistente personalizado está listo para ayudarte con predicciones y recomendaciones
</p>
</div>
{/* Final Metrics */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">
{(finalResults.finalMetrics.accuracy * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">Exactitud</p>
</div>
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">
{(finalResults.finalMetrics.precision * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">Confiabilidad</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)]">
{(finalResults.finalMetrics.recall * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">Cobertura</p>
</div>
<div className="text-center p-4 bg-[var(--color-secondary-100)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-secondary)]">
{(finalResults.finalMetrics.f1_score * 100).toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">Rendimiento</p>
</div>
<div className="text-center p-4 bg-[var(--color-warning-100)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-warning)]">
{finalResults.finalMetrics.total_epochs}
</p>
<p className="text-xs text-[var(--text-secondary)]">Iteraciones</p>
</div>
</div>
{/* Model Info */}
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 mb-6">
<h4 className="font-medium text-[var(--text-primary)] mb-3">Información del Modelo:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium text-[var(--text-tertiary)]">ID del Modelo:</span>
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelId}</p>
</div>
<div>
<span className="font-medium text-[var(--text-tertiary)]">Versión:</span>
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelVersion}</p>
</div>
<div>
<span className="font-medium text-[var(--text-tertiary)]">Duración:</span>
<p className="text-[var(--text-primary)]">{finalResults.trainingDuration}</p>
</div>
<div>
<span className="font-medium text-[var(--text-tertiary)]">Dataset:</span>
<p className="text-[var(--text-primary)]">{finalResults.datasetSize}</p>
</div>
<div>
<span className="font-medium text-[var(--text-tertiary)]">Endpoint:</span>
<p className="font-mono text-[var(--text-primary)]">{finalResults.deploymentUrl}</p>
</div>
<div>
<span className="font-medium text-[var(--text-tertiary)]">Estado:</span>
<p className="text-[var(--color-success)] font-medium"> Activo</p>
</div>
</div>
</div>
{/* Capabilities */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 border border-[var(--color-success)]/20 rounded-lg">
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-1">Predicción de Ventas</h4>
<p className="text-sm text-[var(--text-secondary)]">
Te dice cuánto vas a vender cada día
</p>
</div>
<div className="p-4 border border-[var(--color-info)]/20 rounded-lg">
<Activity className="w-6 h-6 text-[var(--color-info)] mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-1">Recomendaciones</h4>
<p className="text-sm text-[var(--text-secondary)]">
Te sugiere cuánto producir y comprar
</p>
</div>
<div className="p-4 border border-[var(--color-warning)]/20 rounded-lg">
<Zap className="w-6 h-6 text-[var(--color-warning)] mb-2" />
<h4 className="font-medium text-[var(--text-primary)] mb-1">Alertas Automáticas</h4>
<p className="text-sm text-[var(--text-secondary)]">
Te avisa cuando necesitas reordenar
</p>
</div>
</div>
</Card>
</div>
)}
{/* Training Error */}
{trainingStatus === 'error' && (
<Card className="p-6">
<div className="text-center">
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
<AlertCircle className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-semibold text-[var(--color-error)] mb-2">
Error al Crear el Asistente
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Ocurrió un problema durante la creación de tu asistente inteligente. Por favor, intenta nuevamente.
</p>
<Button onClick={retryTraining} variant="outline">
Intentar Nuevamente
</Button>
</div>
</Card>
)}
{/* 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">
🎯 ¿Cómo funciona tu Asistente Inteligente?
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Analiza tus ventas:</strong> Estudia tus datos históricos para encontrar patrones</li>
<li> <strong>Entiende tu negocio:</strong> Aprende sobre temporadas altas y bajas</li>
<li> <strong>Hace predicciones:</strong> Te dice cuánto vas a vender cada día</li>
<li> <strong>Da recomendaciones:</strong> Te sugiere cuánto producir y comprar</li>
<li> <strong>Mejora con el tiempo:</strong> Se hace más inteligente con cada venta nueva</li>
</ul>
</Card>
</div>
);
};

View File

@@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
interface Product {
id: string;
name: string;
category: string;
confidence: number;
sales_count: number;
estimated_price: number;
status: 'approved' | 'rejected' | 'pending';
notes?: string;
}
// Mock detected products
const mockDetectedProducts: Product[] = [
{ id: '1', name: 'Pan Integral', category: 'Panadería', confidence: 95, sales_count: 45, estimated_price: 2.50, status: 'pending' },
{ id: '2', name: 'Croissant', category: 'Bollería', confidence: 92, sales_count: 38, estimated_price: 1.80, status: 'pending' },
{ id: '3', name: 'Baguette', category: 'Panadería', confidence: 88, sales_count: 22, estimated_price: 3.00, status: 'pending' },
{ id: '4', name: 'Empanada de Pollo', category: 'Salados', confidence: 85, sales_count: 31, estimated_price: 4.50, status: 'pending' },
{ id: '5', name: 'Tarta de Manzana', category: 'Repostería', confidence: 78, sales_count: 12, estimated_price: 15.00, status: 'pending' },
{ id: '6', name: 'Pan de Centeno', category: 'Panadería', confidence: 91, sales_count: 18, estimated_price: 2.80, status: 'pending' },
{ id: '7', name: 'Medialunas', category: 'Bollería', confidence: 87, sales_count: 29, estimated_price: 1.20, status: 'pending' },
{ id: '8', name: 'Sandwich Mixto', category: 'Salados', confidence: 82, sales_count: 25, estimated_price: 5.50, status: 'pending' }
];
export const ReviewStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
// Generate products from processing results or use mock data
const generateProductsFromResults = (results: any) => {
if (!results?.product_list) return mockDetectedProducts;
return results.product_list.map((name: string, index: number) => ({
id: (index + 1).toString(),
name,
category: index < 3 ? 'Panadería' : index < 5 ? 'Bollería' : 'Salados',
confidence: Math.max(75, results.confidenceScore - Math.random() * 15),
sales_count: Math.floor(Math.random() * 50) + 10,
estimated_price: Math.random() * 5 + 1.5,
status: 'pending' as const
}));
};
const [products, setProducts] = useState<Product[]>(
data.detectedProducts || generateProductsFromResults(data.processingResults)
);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
useEffect(() => {
onDataChange({
...data,
detectedProducts: products,
reviewCompleted: products.every(p => p.status !== 'pending')
});
}, [products]);
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 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
};
return (
<div className="space-y-8">
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
<p className="text-sm text-[var(--text-secondary)]">Total</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)]">{stats.approved}</p>
<p className="text-sm text-[var(--text-secondary)]">Aprobados</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)]">{stats.rejected}</p>
<p className="text-sm text-[var(--text-secondary)]">Rechazados</p>
</div>
<div className="text-center p-4 bg-[var(--color-warning)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.pending}</p>
<p className="text-sm text-[var(--text-secondary)]">Pendientes</p>
</div>
</div>
{/* Filters and Actions */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2">
Filtrar por categoría:
</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="border border-[var(--border-secondary)] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(cat => (
<option key={cat} value={cat}>
{cat === 'all' ? 'Todas las categorías' : cat}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar Visibles
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4 mr-1" />
Rechazar Visibles
</Button>
</div>
</div>
</Card>
{/* Products List */}
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Status Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
product.status === 'approved'
? 'bg-[var(--color-success)]'
: product.status === 'rejected'
? 'bg-[var(--color-error)]'
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
}`}>
{product.status === 'approved' ? (
<CheckCircle className="w-4 h-4 text-white" />
) : product.status === 'rejected' ? (
<Trash2 className="w-4 h-4 text-white" />
) : (
<Eye className="w-4 h-4 text-[var(--text-tertiary)]" />
)}
</div>
{/* Product Info */}
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{product.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant="gray">{product.category}</Badge>
{product.status !== 'pending' && (
<Badge variant={product.status === 'approved' ? 'green' : 'red'}>
{product.status === 'approved' ? 'Aprobado' : 'Rechazado'}
</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<span className={`px-2 py-1 rounded text-xs ${
product.confidence >= 90
? 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
: product.confidence >= 75
? 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
: 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
}`}>
{product.confidence}% confianza
</span>
<span>{product.sales_count} ventas</span>
<span>${product.estimated_price.toFixed(2)}</span>
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 ml-4 mt-1">
{product.status === 'pending' ? (
<>
<Button
size="sm"
onClick={() => handleProductAction(product.id, 'approve')}
className="bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleProductAction(product.id, 'reject')}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4 mr-1" />
Rechazar
</Button>
</>
) : (
<Button
size="sm"
variant="outline"
onClick={() => setProducts(prev => prev.map(p => p.id === product.id ? {...p, status: 'pending'} : p))}
className="text-[var(--text-secondary)] hover:text-[var(--color-primary)]"
>
<Edit className="w-4 h-4 mr-1" />
Modificar
</Button>
)}
</div>
</div>
</Card>
))}
</div>
{/* Progress Indicator */}
{stats.pending > 0 && (
<Card className="p-4 bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="flex items-center space-x-3">
<AlertCircle className="w-5 h-5 text-[var(--color-warning)]" />
<div>
<p className="font-medium text-[var(--text-primary)]">
{stats.pending} productos pendientes de revisión
</p>
<p className="text-sm text-[var(--text-secondary)]">
Revisa todos los productos antes de continuar al siguiente paso
</p>
</div>
</div>
</Card>
)}
{/* Help Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-3">
💡 Consejos para la revisión:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Confianza alta (90%+):</strong> Productos identificados con alta precisión</li>
<li> <strong>Confianza media (75-89%):</strong> Revisar nombres y categorías</li>
<li> <strong>Confianza baja (&lt;75%):</strong> Verificar que corresponden a tu catálogo</li>
<li> Usa las acciones masivas para aprobar/rechazar por categoría completa</li>
</ul>
</Card>
</div>
);
};

View File

@@ -0,0 +1,585 @@
import React, { useState, useEffect } from 'react';
import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
interface Supplier {
id: string;
name: string;
contact_person: string;
phone: string;
email: string;
address: string;
categories: string[];
payment_terms: string;
delivery_days: string[];
min_order_amount?: number;
notes?: string;
status: 'active' | 'inactive';
created_at: string;
}
// Mock suppliers
const mockSuppliers: Supplier[] = [
{
id: '1',
name: 'Molinos del Sur',
contact_person: 'Juan Pérez',
phone: '+1 555-0123',
email: 'ventas@molinosdelsur.com',
address: 'Av. Industrial 123, Zona Sur',
categories: ['Harinas', 'Granos'],
payment_terms: '30 días',
delivery_days: ['Lunes', 'Miércoles', 'Viernes'],
min_order_amount: 200,
notes: 'Proveedor principal de harinas, muy confiable',
status: 'active',
created_at: '2024-01-15'
},
{
id: '2',
name: 'Lácteos Premium',
contact_person: 'María González',
phone: '+1 555-0456',
email: 'pedidos@lacteospremium.com',
address: 'Calle Central 456, Centro',
categories: ['Lácteos', 'Mantequillas', 'Quesos'],
payment_terms: '15 días',
delivery_days: ['Martes', 'Jueves', 'Sábado'],
min_order_amount: 150,
status: 'active',
created_at: '2024-01-20'
}
];
const daysOfWeek = [
'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'
];
const commonCategories = [
'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos',
'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes'
];
export const SuppliersStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const [suppliers, setSuppliers] = useState<Supplier[]>(
data.suppliers || mockSuppliers
);
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all');
useEffect(() => {
onDataChange({
...data,
suppliers: suppliers,
suppliersConfigured: true // This step is optional
});
}, [suppliers]);
const handleAddSupplier = () => {
const newSupplier: Supplier = {
id: Date.now().toString(),
name: '',
contact_person: '',
phone: '',
email: '',
address: '',
categories: [],
payment_terms: '30 días',
delivery_days: [],
status: 'active',
created_at: new Date().toISOString().split('T')[0]
};
setEditingSupplier(newSupplier);
setIsAddingNew(true);
};
const handleSaveSupplier = (supplier: Supplier) => {
if (isAddingNew) {
setSuppliers(prev => [...prev, supplier]);
} else {
setSuppliers(prev => prev.map(s => s.id === supplier.id ? supplier : s));
}
setEditingSupplier(null);
setIsAddingNew(false);
};
const handleDeleteSupplier = (id: string) => {
if (window.confirm('¿Estás seguro de eliminar este proveedor?')) {
setSuppliers(prev => prev.filter(s => s.id !== id));
}
};
const toggleSupplierStatus = (id: string) => {
setSuppliers(prev => prev.map(s =>
s.id === id
? { ...s, status: s.status === 'active' ? 'inactive' : 'active' }
: s
));
};
const getFilteredSuppliers = () => {
return filterStatus === 'all'
? suppliers
: suppliers.filter(s => s.status === filterStatus);
};
const stats = {
total: suppliers.length,
active: suppliers.filter(s => s.status === 'active').length,
inactive: suppliers.filter(s => s.status === 'inactive').length,
categories: Array.from(new Set(suppliers.flatMap(s => s.categories))).length
};
return (
<div className="space-y-6">
{/* Optional Step Notice */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<p className="text-sm text-[var(--color-info)]">
💡 <strong>Paso Opcional:</strong> Puedes saltar este paso y configurar proveedores más tarde desde el módulo de compras
</p>
</Card>
{/* Quick Actions */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<p className="text-sm text-[var(--text-secondary)]">
{stats.total} proveedores configurados ({stats.active} activos)
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={handleAddSupplier}
>
<Plus className="w-4 h-4 mr-2" />
Agregar Proveedor
</Button>
</div>
</div>
</Card>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
<p className="text-xs text-[var(--text-secondary)]">Total</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.active}</p>
<p className="text-xs text-[var(--text-secondary)]">Activos</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--text-tertiary)]">{stats.inactive}</p>
<p className="text-xs text-[var(--text-secondary)]">Inactivos</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.categories}</p>
<p className="text-xs text-[var(--text-secondary)]">Categorías</p>
</Card>
</div>
{/* Filters */}
<Card className="p-4">
<div className="flex items-center space-x-4">
<label className="text-sm font-medium text-[var(--text-secondary)]">Estado:</label>
<div className="flex space-x-2">
{[
{ value: 'all', label: 'Todos' },
{ value: 'active', label: 'Activos' },
{ value: 'inactive', label: 'Inactivos' }
].map(filter => (
<button
key={filter.value}
onClick={() => setFilterStatus(filter.value as any)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filterStatus === filter.value
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
{filter.label}
</button>
))}
</div>
</div>
</Card>
{/* Suppliers List */}
<div className="space-y-4">
{getFilteredSuppliers().map((supplier) => (
<Card key={supplier.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Status Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
supplier.status === 'active'
? 'bg-[var(--color-success)]/10'
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
}`}>
<Truck className={`w-4 h-4 ${
supplier.status === 'active'
? 'text-[var(--color-success)]'
: 'text-[var(--text-tertiary)]'
}`} />
</div>
{/* Supplier Info */}
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{supplier.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant={supplier.status === 'active' ? 'green' : 'gray'}>
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
</Badge>
{supplier.categories.slice(0, 2).map((cat, idx) => (
<Badge key={idx} variant="blue">{cat}</Badge>
))}
{supplier.categories.length > 2 && (
<Badge variant="gray">+{supplier.categories.length - 2}</Badge>
)}
</div>
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)] mb-2">
<div>
<span className="text-[var(--text-tertiary)]">Contacto: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.contact_person}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Entrega: </span>
<span className="font-medium text-[var(--text-primary)]">
{supplier.delivery_days.slice(0, 2).join(', ')}
{supplier.delivery_days.length > 2 && ` +${supplier.delivery_days.length - 2}`}
</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Pago: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.payment_terms}</span>
</div>
{supplier.min_order_amount && (
<div>
<span className="text-[var(--text-tertiary)]">Mín: </span>
<span className="font-medium text-[var(--text-primary)]">${supplier.min_order_amount}</span>
</div>
)}
</div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<div className="flex items-center gap-1">
<Phone className="w-3 h-3" />
<span>{supplier.phone}</span>
</div>
<div className="flex items-center gap-1">
<Mail className="w-3 h-3" />
<span>{supplier.email}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
<span>{supplier.address}</span>
</div>
</div>
{supplier.notes && (
<div className="mt-3 p-2 bg-[var(--bg-secondary)] rounded text-sm">
<span className="text-[var(--text-tertiary)]">Notas: </span>
<span className="text-[var(--text-primary)]">{supplier.notes}</span>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="flex gap-2 ml-4 mt-1">
<Button
size="sm"
variant="outline"
onClick={() => setEditingSupplier(supplier)}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => toggleSupplierStatus(supplier.id)}
className={supplier.status === 'active'
? 'text-[var(--color-warning)] border-[var(--color-warning)] hover:bg-[var(--color-warning)]/10'
: 'text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10'
}
>
{supplier.status === 'active' ? 'Pausar' : 'Activar'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteSupplier(supplier.id)}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
))}
{getFilteredSuppliers().length === 0 && (
<Card className="p-8 text-center">
<Truck className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)] mb-4">No hay proveedores en esta categoría</p>
<Button onClick={handleAddSupplier}>
<Plus className="w-4 h-4 mr-2" />
Agregar Primer Proveedor
</Button>
</Card>
)}
</div>
{/* Edit Modal */}
{editingSupplier && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
{isAddingNew ? 'Agregar Proveedor' : 'Editar Proveedor'}
</h3>
<SupplierForm
supplier={editingSupplier}
onSave={handleSaveSupplier}
onCancel={() => {
setEditingSupplier(null);
setIsAddingNew(false);
}}
/>
</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">
🚚 Gestión de Proveedores:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Este paso es opcional</strong> - puedes configurar proveedores más tarde</li>
<li> Define categorías de productos para facilitar la búsqueda</li>
<li> Establece días de entrega y términos de pago</li>
<li> Configura montos mínimos de pedido para optimizar compras</li>
</ul>
</Card>
</div>
);
};
// Component for editing suppliers
interface SupplierFormProps {
supplier: Supplier;
onSave: (supplier: Supplier) => void;
onCancel: () => void;
}
const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel }) => {
const [formData, setFormData] = useState(supplier);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
alert('El nombre es requerido');
return;
}
if (!formData.contact_person.trim()) {
alert('El contacto es requerido');
return;
}
onSave(formData);
};
const toggleCategory = (category: string) => {
setFormData(prev => ({
...prev,
categories: prev.categories.includes(category)
? prev.categories.filter(c => c !== category)
: [...prev.categories, category]
}));
};
const toggleDeliveryDay = (day: string) => {
setFormData(prev => ({
...prev,
delivery_days: prev.delivery_days.includes(day)
? prev.delivery_days.filter(d => d !== day)
: [...prev.delivery_days, day]
}));
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Nombre de la Empresa *
</label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Molinos del Sur"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Persona de Contacto *
</label>
<Input
value={formData.contact_person}
onChange={(e) => setFormData(prev => ({ ...prev, contact_person: e.target.value }))}
placeholder="Juan Pérez"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Teléfono
</label>
<Input
value={formData.phone}
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
placeholder="+1 555-0123"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Email
</label>
<Input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="ventas@proveedor.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección
</label>
<Input
value={formData.address}
onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
placeholder="Av. Industrial 123, Zona Sur"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Categorías de Productos
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{commonCategories.map(category => (
<label key={category} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={formData.categories.includes(category)}
onChange={() => toggleCategory(category)}
className="rounded"
/>
<span className="text-sm text-[var(--text-primary)]">{category}</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Términos de Pago
</label>
<select
value={formData.payment_terms}
onChange={(e) => setFormData(prev => ({ ...prev, payment_terms: e.target.value }))}
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
<option value="Inmediato">Inmediato</option>
<option value="15 días">15 días</option>
<option value="30 días">30 días</option>
<option value="45 días">45 días</option>
<option value="60 días">60 días</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Pedido Mínimo
</label>
<Input
type="number"
value={formData.min_order_amount || ''}
onChange={(e) => setFormData(prev => ({ ...prev, min_order_amount: Number(e.target.value) }))}
placeholder="200"
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Días de Entrega
</label>
<div className="flex flex-wrap gap-2">
{daysOfWeek.map(day => (
<label key={day} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={formData.delivery_days.includes(day)}
onChange={() => toggleDeliveryDay(day)}
className="rounded"
/>
<span className="text-sm text-[var(--text-primary)]">{day}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Notas
</label>
<textarea
value={formData.notes || ''}
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
placeholder="Información adicional sobre el proveedor..."
className="w-full p-2 border border-[var(--border-secondary)] rounded resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
rows={3}
/>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
Guardar Proveedor
</Button>
</div>
</form>
);
};