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

677 lines
26 KiB
TypeScript
Raw Normal View History

2025-09-03 14:06:38 +02:00
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';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { salesService } from '../../../../api';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
2025-09-03 14:06:38 +02:00
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[];
}
// Data processing utility function
const processDataFile = async (
file: File,
onProgress: (progress: number, stage: string, message: string) => void,
validateSalesData: any,
generateInventorySuggestions: any
) => {
try {
// Stage 1: Validate file with sales service
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await validateSalesData(file);
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
}
// Stage 2: Store validation result for later import (after inventory setup)
onProgress(50, 'validating', 'Procesando datos identificados...');
// Stage 3: Generate AI suggestions with inventory service
onProgress(60, 'analyzing', 'Identificando productos únicos...');
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
console.log('DataProcessingStep - Validation result:', validationResult);
console.log('DataProcessingStep - Product list:', validationResult.product_list);
console.log('DataProcessingStep - Product list length:', validationResult.product_list?.length);
// Extract product list from validation result
const productList = validationResult.product_list || [];
console.log('DataProcessingStep - Generating AI suggestions with:', {
fileName: file.name,
productList: productList,
productListLength: productList.length
});
let suggestionsResult;
if (productList.length > 0) {
suggestionsResult = await generateInventorySuggestions(productList);
} else {
console.warn('DataProcessingStep - No products found, creating default suggestions');
suggestionsResult = {
suggestions: [],
total_products: validationResult.unique_products || 0,
business_model_analysis: {
model: 'production' as const,
recommendations: []
},
high_confidence_count: 0
};
}
console.log('DataProcessingStep - AI suggestions result:', suggestionsResult);
onProgress(90, 'analyzing', 'Generando recomendaciones con IA...');
onProgress(100, 'completed', 'Procesamiento completado');
// Combine results
const combinedResult = {
...validationResult,
salesDataFile: file, // Store file for later import after inventory setup
productsIdentified: suggestionsResult.total_products || validationResult.unique_products,
categoriesDetected: suggestionsResult.suggestions ?
new Set(suggestionsResult.suggestions.map(s => s.category)).size : 4,
businessModel: suggestionsResult.business_model_analysis?.model || 'production',
confidenceScore: suggestionsResult.high_confidence_count && suggestionsResult.total_products ?
Math.round((suggestionsResult.high_confidence_count / suggestionsResult.total_products) * 100) : 85,
recommendations: suggestionsResult.business_model_analysis?.recommendations || [],
aiSuggestions: suggestionsResult.suggestions || []
};
console.log('DataProcessingStep - Combined result:', combinedResult);
console.log('DataProcessingStep - Combined result aiSuggestions:', combinedResult.aiSuggestions);
console.log('DataProcessingStep - Combined result aiSuggestions length:', combinedResult.aiSuggestions?.length);
return combinedResult;
} catch (error) {
console.error('Data processing error:', error);
throw error;
2025-09-03 14:06:38 +02:00
}
};
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const user = useAuthUser();
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
const tenantLoading = useTenantLoading();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
// Use hooks for UI and direct service calls for now (until we extend hooks)
const { isLoading: inventoryLoading } = useInventory();
const { isLoading: salesLoading } = useSales();
const errorModal = useModal();
const { showToast } = useToast();
// Check if we're still loading user or tenant data
const isLoadingUserData = authLoading || tenantLoading;
// Get tenant ID from multiple sources with fallback
const getTenantId = (): string | null => {
const tenantId = currentTenant?.id || user?.tenant_id || null;
console.log('DataProcessingStep - getTenantId:', {
currentTenant: currentTenant?.id,
userTenantId: user?.tenant_id,
finalTenantId: tenantId,
isLoadingUserData,
authLoading,
tenantLoading,
user: user ? { id: user.id, email: user.email } : null
});
return tenantId;
};
// Check if tenant data is available (not loading and has ID)
const isTenantAvailable = (): boolean => {
return !isLoadingUserData && getTenantId() !== null;
};
2025-09-03 14:06:38 +02:00
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)) {
showToast({
title: 'Formato inválido',
message: 'Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)',
type: 'error'
});
2025-09-03 14:06:38 +02:00
return;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
showToast({
title: 'Archivo muy grande',
message: 'El archivo es demasiado grande. Máximo 10MB permitido.',
type: 'error'
});
2025-09-03 14:06:38 +02:00
return;
}
setUploadedFile(file);
setStage('validating');
setProgress(0);
try {
// Wait for user data to load if still loading
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
// Reset file state since we can't process it yet
setUploadedFile(null);
setStage('upload');
return;
}
const tenantId = getTenantId();
if (!tenantId) {
console.error('DataProcessingStep - No tenant ID available:', {
user,
currentTenant,
userTenantId: user?.tenant_id,
currentTenantId: currentTenant?.id,
isLoadingUserData,
authLoading,
tenantLoading
});
throw new Error('No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.');
}
console.log('DataProcessingStep - Starting file processing with tenant:', tenantId);
const result = await processDataFile(
2025-09-03 14:06:38 +02:00
file,
(newProgress, newStage, message) => {
setProgress(newProgress);
setStage(newStage as ProcessingStage);
setCurrentMessage(message);
},
salesService.validateSalesData.bind(salesService),
inventoryService.generateInventorySuggestions.bind(inventoryService)
2025-09-03 14:06:38 +02:00
);
setResults(result);
setStage('completed');
// Store results for next steps
onDataChange({
...data,
files: { ...data.files, salesData: file },
processingResults: result,
processingStage: 'completed',
processingProgress: 100
});
console.log('DataProcessingStep - File processing completed:', result);
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Procesamiento completado',
message: `Se procesaron ${result.total_records} registros y se identificaron ${result.unique_products} productos únicos.`,
source: 'onboarding'
});
2025-09-03 14:06:38 +02:00
} catch (error) {
console.error('DataProcessingStep - Processing error:', error);
console.error('DataProcessingStep - Error details:', {
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : null,
tenantInfo: {
user: user ? { id: user.id, tenant_id: user.tenant_id } : null,
currentTenant: currentTenant ? { id: currentTenant.id } : null
}
});
2025-09-03 14:06:38 +02:00
setStage('error');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
setCurrentMessage(errorMessage);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en el procesamiento',
message: errorMessage,
source: 'onboarding'
});
2025-09-03 14:06:38 +02:00
}
};
const downloadTemplate = async () => {
try {
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
return;
}
const tenantId = getTenantId();
if (!tenantId) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.',
source: 'onboarding'
});
return;
}
// Template download functionality can be implemented later if needed
console.warn('Template download not yet implemented in reorganized structure');
createAlert({
type: 'info',
category: 'system',
title: 'Descarga de plantilla no disponible',
message: 'Esta funcionalidad se implementará próximamente.'
});
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Plantilla descargada',
message: 'La plantilla de ventas se ha descargado correctamente',
source: 'onboarding'
});
} catch (error) {
console.error('Error downloading template:', error);
// Fallback to static template
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2025-09-03 14:06:38 +02:00
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);
}
2025-09-03 14:06:38 +02:00
};
const resetProcess = () => {
setStage('upload');
setUploadedFile(null);
setProgress(0);
setCurrentMessage('');
setResults(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="space-y-8">
{/* Loading state when tenant data is not available */}
{!isTenantAvailable() && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
Cargando datos de usuario...
</h3>
<p className="text-[var(--text-secondary)]">
Por favor espere mientras cargamos su información de tenant
</p>
</Card>
)}
2025-09-03 14:06:38 +02:00
{/* Improved Upload Stage */}
{stage === 'upload' && isTenantAvailable() && (
2025-09-03 14:06:38 +02:00
<>
<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>
);
};