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';
|
2025-09-05 12:55:26 +02:00
|
|
|
import { useModal } from '../../../../hooks/ui/useModal';
|
|
|
|
|
import { useToast } from '../../../../hooks/ui/useToast';
|
2025-09-05 22:46:28 +02:00
|
|
|
import { useOnboarding } from '../../../../hooks/business/onboarding';
|
2025-09-04 18:59:56 +02:00
|
|
|
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
|
2025-09-05 22:51:39 +02:00
|
|
|
import { useCurrentTenant, useTenantLoading } from '../../../../stores';
|
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[];
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
// This function has been replaced by the onboarding hooks
|
2025-09-03 14:06:38 +02:00
|
|
|
|
|
|
|
|
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
|
|
|
|
|
data,
|
|
|
|
|
onDataChange,
|
|
|
|
|
onNext,
|
|
|
|
|
onPrevious,
|
|
|
|
|
isFirstStep,
|
|
|
|
|
isLastStep
|
|
|
|
|
}) => {
|
2025-09-04 18:59:56 +02:00
|
|
|
const user = useAuthUser();
|
|
|
|
|
const authLoading = useAuthLoading();
|
|
|
|
|
const currentTenant = useCurrentTenant();
|
|
|
|
|
const tenantLoading = useTenantLoading();
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
// Use the new onboarding hooks
|
|
|
|
|
const {
|
|
|
|
|
processSalesFile,
|
|
|
|
|
generateInventorySuggestions,
|
|
|
|
|
salesProcessing: {
|
|
|
|
|
stage: onboardingStage,
|
|
|
|
|
progress: onboardingProgress,
|
|
|
|
|
currentMessage: onboardingMessage,
|
|
|
|
|
validationResults,
|
|
|
|
|
suggestions
|
|
|
|
|
},
|
2025-09-06 19:40:47 +02:00
|
|
|
tenantCreation,
|
2025-09-05 22:46:28 +02:00
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
clearError
|
|
|
|
|
} = useOnboarding();
|
|
|
|
|
|
2025-09-05 12:55:26 +02:00
|
|
|
const errorModal = useModal();
|
2025-09-05 22:46:28 +02:00
|
|
|
const toast = useToast();
|
2025-09-05 12:55:26 +02:00
|
|
|
|
2025-09-04 18:59:56 +02:00
|
|
|
// 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 => {
|
2025-09-06 19:40:47 +02:00
|
|
|
// Also check the onboarding data for tenant creation success
|
|
|
|
|
const onboardingTenantId = data.bakery?.tenant_id;
|
|
|
|
|
const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null;
|
2025-09-04 18:59:56 +02:00
|
|
|
console.log('DataProcessingStep - getTenantId:', {
|
|
|
|
|
currentTenant: currentTenant?.id,
|
|
|
|
|
userTenantId: user?.tenant_id,
|
2025-09-06 19:40:47 +02:00
|
|
|
onboardingTenantId: onboardingTenantId,
|
2025-09-04 18:59:56 +02:00
|
|
|
finalTenantId: tenantId,
|
|
|
|
|
isLoadingUserData,
|
|
|
|
|
authLoading,
|
|
|
|
|
tenantLoading,
|
2025-09-06 19:40:47 +02:00
|
|
|
user: user ? { id: user.id, email: user.email } : null,
|
|
|
|
|
tenantCreationSuccess: data.tenantCreation?.isSuccess
|
2025-09-04 18:59:56 +02:00
|
|
|
});
|
|
|
|
|
return tenantId;
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-06 19:40:47 +02:00
|
|
|
// Check if tenant data is available (not loading and has ID, OR tenant was created successfully)
|
2025-09-04 18:59:56 +02:00
|
|
|
const isTenantAvailable = (): boolean => {
|
2025-09-06 19:40:47 +02:00
|
|
|
const hasAuth = !authLoading && user;
|
|
|
|
|
const hasTenantId = getTenantId() !== null;
|
|
|
|
|
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
|
|
|
|
|
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
|
|
|
|
|
|
|
|
|
|
const isAvailable = hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding);
|
|
|
|
|
console.log('DataProcessingStep - isTenantAvailable:', {
|
|
|
|
|
hasAuth,
|
|
|
|
|
hasTenantId,
|
|
|
|
|
tenantCreatedSuccessfully,
|
|
|
|
|
tenantCreatedInOnboarding,
|
|
|
|
|
isAvailable,
|
|
|
|
|
authLoading,
|
|
|
|
|
tenantLoading,
|
|
|
|
|
tenantCreationFromHook: tenantCreation,
|
|
|
|
|
bakeryData: data.bakery
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return isAvailable;
|
2025-09-04 18:59:56 +02:00
|
|
|
};
|
2025-09-05 22:46:28 +02:00
|
|
|
// Use onboarding hook state when available, fallback to local state
|
|
|
|
|
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
|
2025-09-03 14:06:38 +02:00
|
|
|
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
|
2025-09-05 22:46:28 +02:00
|
|
|
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
|
|
|
|
|
|
|
|
|
|
// Derive current state from onboarding hooks or local state
|
2025-09-06 19:40:47 +02:00
|
|
|
// Priority: if local is 'completed' or 'error', use local; otherwise use onboarding state
|
|
|
|
|
const stage = (localStage === 'completed' || localStage === 'error')
|
|
|
|
|
? localStage
|
|
|
|
|
: (onboardingStage || localStage);
|
2025-09-05 22:46:28 +02:00
|
|
|
const progress = onboardingProgress || 0;
|
|
|
|
|
const currentMessage = onboardingMessage || '';
|
|
|
|
|
const results = (validationResults && suggestions) ? {
|
|
|
|
|
...validationResults,
|
|
|
|
|
aiSuggestions: suggestions,
|
2025-09-06 19:40:47 +02:00
|
|
|
// Add calculated fields from backend response
|
|
|
|
|
productsIdentified: validationResults.unique_products || validationResults.product_list?.length || 0,
|
2025-09-05 22:46:28 +02:00
|
|
|
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
|
|
|
|
|
businessModel: 'production',
|
|
|
|
|
confidenceScore: 85,
|
2025-09-06 19:40:47 +02:00
|
|
|
recommendations: validationResults.summary?.suggestions || [],
|
|
|
|
|
// Backend response details
|
|
|
|
|
totalRecords: validationResults.total_records || 0,
|
|
|
|
|
validRecords: validationResults.valid_records || 0,
|
|
|
|
|
invalidRecords: validationResults.invalid_records || 0,
|
|
|
|
|
fileFormat: validationResults.summary?.file_format || 'csv',
|
|
|
|
|
fileSizeMb: validationResults.summary?.file_size_mb || 0,
|
|
|
|
|
estimatedProcessingTime: validationResults.summary?.estimated_processing_time_seconds || 0,
|
|
|
|
|
detectedColumns: validationResults.summary?.detected_columns || [],
|
|
|
|
|
validationMessage: validationResults.message || 'Validación completada'
|
2025-09-05 22:46:28 +02:00
|
|
|
} : localResults;
|
2025-09-06 19:40:47 +02:00
|
|
|
|
|
|
|
|
// Debug logging for state changes
|
|
|
|
|
console.log('DataProcessingStep - State debug:', {
|
|
|
|
|
localStage,
|
|
|
|
|
onboardingStage,
|
|
|
|
|
finalStage: stage,
|
|
|
|
|
hasValidationResults: !!validationResults,
|
|
|
|
|
hasSuggestions: !!suggestions,
|
|
|
|
|
hasResults: !!results,
|
|
|
|
|
localResults: !!localResults
|
|
|
|
|
});
|
2025-09-03 14:06:38 +02:00
|
|
|
const [dragActive, setDragActive] = useState(false);
|
|
|
|
|
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
2025-09-06 19:40:47 +02:00
|
|
|
const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile });
|
2025-09-03 14:06:38 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-09-06 19:40:47 +02:00
|
|
|
// Only update if state actually changed
|
|
|
|
|
const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile };
|
|
|
|
|
if (JSON.stringify(currentState) !== JSON.stringify(lastStateRef.current)) {
|
|
|
|
|
lastStateRef.current = currentState;
|
|
|
|
|
|
|
|
|
|
// Update parent data when state changes
|
|
|
|
|
onDataChange({
|
|
|
|
|
processingStage: stage,
|
|
|
|
|
processingProgress: progress,
|
|
|
|
|
currentMessage: currentMessage,
|
|
|
|
|
processingResults: results,
|
|
|
|
|
suggestions: suggestions,
|
|
|
|
|
files: {
|
|
|
|
|
...data.files,
|
|
|
|
|
salesData: uploadedFile
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data.files]);
|
2025-09-03 14:06:38 +02:00
|
|
|
|
|
|
|
|
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
|
2025-09-05 22:46:28 +02:00
|
|
|
const validExtensions = ['.csv', '.xlsx', '.xls', '.json'];
|
2025-09-03 14:06:38 +02:00
|
|
|
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
|
|
|
|
|
|
|
|
|
if (!validExtensions.includes(fileExtension)) {
|
2025-09-05 22:46:28 +02:00
|
|
|
toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', {
|
2025-09-05 12:55:26 +02:00
|
|
|
title: 'Formato inválido',
|
|
|
|
|
type: 'error'
|
|
|
|
|
});
|
2025-09-03 14:06:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check file size (max 10MB)
|
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
2025-09-05 22:46:28 +02:00
|
|
|
toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', {
|
2025-09-05 12:55:26 +02:00
|
|
|
title: 'Archivo muy grande',
|
|
|
|
|
type: 'error'
|
|
|
|
|
});
|
2025-09-03 14:06:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setUploadedFile(file);
|
2025-09-05 22:46:28 +02:00
|
|
|
setLocalStage('validating');
|
2025-09-03 14:06:38 +02:00
|
|
|
|
|
|
|
|
try {
|
2025-09-04 18:59:56 +02:00
|
|
|
// Wait for user data to load if still loading
|
|
|
|
|
if (!isTenantAvailable()) {
|
2025-09-05 22:46:28 +02:00
|
|
|
console.log('Tenant not available, waiting...');
|
2025-09-04 18:59:56 +02:00
|
|
|
setUploadedFile(null);
|
2025-09-05 22:46:28 +02:00
|
|
|
setLocalStage('upload');
|
|
|
|
|
toast.addToast('Por favor espere mientras cargamos su información...', {
|
|
|
|
|
title: 'Esperando datos de usuario',
|
|
|
|
|
type: 'info'
|
2025-09-04 18:59:56 +02:00
|
|
|
});
|
2025-09-05 22:46:28 +02:00
|
|
|
return;
|
2025-09-04 18:59:56 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-06 19:40:47 +02:00
|
|
|
console.log('DataProcessingStep - Starting file processing', {
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
fileType: file.type,
|
|
|
|
|
lastModified: file.lastModified
|
|
|
|
|
});
|
2025-09-04 18:59:56 +02:00
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
// Use the onboarding hook for file processing
|
|
|
|
|
const success = await processSalesFile(file, (progress, stage, message) => {
|
|
|
|
|
console.log(`Processing: ${progress}% - ${stage} - ${message}`);
|
2025-09-04 18:59:56 +02:00
|
|
|
});
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
if (success) {
|
|
|
|
|
setLocalStage('completed');
|
2025-09-06 19:40:47 +02:00
|
|
|
|
|
|
|
|
// If we have results from the onboarding hook, store them locally too
|
|
|
|
|
if (validationResults && suggestions) {
|
|
|
|
|
const processedResults = {
|
|
|
|
|
...validationResults,
|
|
|
|
|
aiSuggestions: suggestions,
|
|
|
|
|
productsIdentified: validationResults.product_list?.length || 0,
|
|
|
|
|
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
|
|
|
|
|
businessModel: 'production',
|
|
|
|
|
confidenceScore: 85,
|
|
|
|
|
recommendations: []
|
|
|
|
|
};
|
|
|
|
|
setLocalResults(processedResults);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
toast.addToast('El archivo se procesó correctamente', {
|
|
|
|
|
title: 'Procesamiento completado',
|
|
|
|
|
type: 'success'
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Error procesando el archivo');
|
|
|
|
|
}
|
2025-09-04 18:59:56 +02:00
|
|
|
|
2025-09-03 14:06:38 +02:00
|
|
|
} catch (error) {
|
2025-09-04 18:59:56 +02:00
|
|
|
console.error('DataProcessingStep - Processing error:', error);
|
2025-09-06 19:40:47 +02:00
|
|
|
console.error('DataProcessingStep - Error details:', {
|
|
|
|
|
error,
|
|
|
|
|
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
|
|
|
errorStack: error instanceof Error ? error.stack : undefined,
|
|
|
|
|
uploadedFile: file?.name,
|
|
|
|
|
fileSize: file?.size,
|
|
|
|
|
fileType: file?.type,
|
|
|
|
|
localUploadedFile: uploadedFile?.name
|
|
|
|
|
});
|
2025-09-04 18:59:56 +02:00
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
setLocalStage('error');
|
2025-09-04 18:59:56 +02:00
|
|
|
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
toast.addToast(errorMessage, {
|
2025-09-04 18:59:56 +02:00
|
|
|
title: 'Error en el procesamiento',
|
2025-09-05 22:46:28 +02:00
|
|
|
type: 'error'
|
2025-09-04 18:59:56 +02:00
|
|
|
});
|
2025-09-03 14:06:38 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
const downloadTemplate = () => {
|
|
|
|
|
// Provide a static CSV 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`;
|
|
|
|
|
|
2025-09-05 22:46:28 +02:00
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
link.setAttribute('href', url);
|
|
|
|
|
link.setAttribute('download', 'plantilla_ventas.csv');
|
|
|
|
|
link.style.visibility = 'hidden';
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
|
|
|
|
toast.addToast('La plantilla se descargó correctamente.', {
|
|
|
|
|
title: 'Plantilla descargada',
|
|
|
|
|
type: 'success'
|
|
|
|
|
});
|
2025-09-03 14:06:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetProcess = () => {
|
2025-09-05 22:46:28 +02:00
|
|
|
setLocalStage('upload');
|
2025-09-03 14:06:38 +02:00
|
|
|
setUploadedFile(null);
|
2025-09-05 22:46:28 +02:00
|
|
|
setLocalResults(null);
|
2025-09-03 14:06:38 +02:00
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.value = '';
|
|
|
|
|
}
|
2025-09-05 22:46:28 +02:00
|
|
|
if (error) {
|
|
|
|
|
clearError();
|
|
|
|
|
}
|
2025-09-03 14:06:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-8">
|
2025-09-04 18:59:56 +02:00
|
|
|
{/* 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 */}
|
2025-09-05 22:46:28 +02:00
|
|
|
{(stage === 'idle' || localStage === '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">
|
2025-09-06 19:40:47 +02:00
|
|
|
{results.validationMessage || 'Tus datos han sido procesados exitosamente'}
|
2025-09-03 14:06:38 +02:00
|
|
|
</p>
|
2025-09-06 19:40:47 +02:00
|
|
|
{results.fileSizeMb && (
|
|
|
|
|
<div className="text-xs text-[var(--text-tertiary)] mt-2">
|
|
|
|
|
Archivo {results.fileFormat?.toUpperCase()} • {results.fileSizeMb.toFixed(2)} MB
|
|
|
|
|
{results.estimatedProcessingTime && ` • ${results.estimatedProcessingTime}s procesamiento`}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-03 14:06:38 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-06 19:40:47 +02:00
|
|
|
{/* Enhanced Stats Cards */}
|
2025-09-03 14:06:38 +02:00
|
|
|
<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">
|
2025-09-06 19:40:47 +02:00
|
|
|
<p className="text-2xl font-bold text-[var(--color-info)]">{results.totalRecords || results.total_records}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Total Registros</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-success)]">{results.validRecords || results.valid_records}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Válidos</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
|
|
|
|
|
<p className="text-2xl font-bold text-[var(--color-error)]">{results.invalidRecords || results.invalid_records || 0}</p>
|
|
|
|
|
<p className="text-sm text-[var(--text-secondary)]">Inválidos</p>
|
2025-09-03 14:06:38 +02:00
|
|
|
</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>
|
2025-09-06 19:40:47 +02:00
|
|
|
|
|
|
|
|
{/* Additional Details from Backend */}
|
|
|
|
|
{(results.detectedColumns?.length > 0 || results.recommendations?.length > 0) && (
|
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
|
|
|
{/* Detected Columns */}
|
|
|
|
|
{results.detectedColumns?.length > 0 && (
|
|
|
|
|
<Card className="p-4">
|
|
|
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Columnas Detectadas</h4>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{results.detectedColumns.map((column, index) => (
|
|
|
|
|
<span key={index}
|
|
|
|
|
className="px-2 py-1 bg-[var(--color-primary)]/10 text-[var(--color-primary)] text-xs rounded-full border border-[var(--color-primary)]/20">
|
|
|
|
|
{column}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Backend Recommendations */}
|
|
|
|
|
{results.recommendations?.length > 0 && (
|
|
|
|
|
<Card className="p-4">
|
|
|
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Recomendaciones</h4>
|
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
{results.recommendations.slice(0, 3).map((rec, index) => (
|
|
|
|
|
<li key={index} className="text-sm text-[var(--text-secondary)] flex items-start">
|
|
|
|
|
<span className="text-[var(--color-success)] mr-2">•</span>
|
|
|
|
|
{rec}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-03 14:06:38 +02:00
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
};
|