Add onboarding flow improvements
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user