Fix new services implementation 1

This commit is contained in:
Urtzi Alfaro
2025-08-13 21:41:00 +02:00
parent 16b8a9d50c
commit 262b3dc9c4
13 changed files with 1702 additions and 1210 deletions

View File

@@ -56,6 +56,28 @@ export interface BusinessModelAnalysis {
recommendations: string[];
}
// Step 1: File validation result
export interface FileValidationResult {
is_valid: boolean;
total_records: number;
unique_products: number;
product_list: string[];
validation_errors: any[];
validation_warnings: any[];
summary: Record<string, any>;
}
// Step 2: AI suggestions result
export interface ProductSuggestionsResult {
suggestions: InventorySuggestion[];
business_model_analysis: BusinessModelAnalysis;
total_products: number;
high_confidence_count: number;
low_confidence_count: number;
processing_time_seconds: number;
}
// Legacy support - will be deprecated
export interface OnboardingAnalysisResult {
total_products_found: number;
inventory_suggestions: InventorySuggestion[];
@@ -143,16 +165,16 @@ export class OnboardingService {
return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`);
}
// ========== AUTOMATED INVENTORY CREATION METHODS ==========
// ========== NEW 4-STEP AUTOMATED INVENTORY CREATION METHODS ==========
/**
* Phase 1: Analyze sales data and get AI suggestions
* Step 1: Validate file and extract unique products
*/
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
async validateFileAndExtractProducts(tenantId: string, file: File): Promise<FileValidationResult> {
const formData = new FormData();
formData.append('file', file);
return apiClient.post(`/tenants/${tenantId}/onboarding/analyze`, formData, {
return apiClient.post(`/tenants/${tenantId}/onboarding/validate-file`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
@@ -160,7 +182,26 @@ export class OnboardingService {
}
/**
* Phase 2: Create inventory from approved suggestions
* Step 2: Generate AI-powered inventory suggestions
*/
async generateInventorySuggestions(
tenantId: string,
file: File,
productList: string[]
): Promise<ProductSuggestionsResult> {
const formData = new FormData();
formData.append('file', file);
formData.append('product_list', JSON.stringify(productList));
return apiClient.post(`/tenants/${tenantId}/onboarding/generate-suggestions`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* Step 3: Create inventory from approved suggestions
*/
async createInventoryFromSuggestions(
tenantId: string,
@@ -176,7 +217,7 @@ export class OnboardingService {
}
/**
* Phase 3: Import sales data with inventory mapping
* Step 4: Final sales data import with inventory mapping
*/
async importSalesWithInventory(
tenantId: string,
@@ -194,6 +235,35 @@ export class OnboardingService {
});
}
// ========== LEGACY METHODS (for backward compatibility) ==========
/**
* @deprecated Use the new 4-step flow instead
* Phase 1: Analyze sales data and get AI suggestions (OLD METHOD)
*/
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
// This method will use the new flow under the hood for backward compatibility
const validationResult = await this.validateFileAndExtractProducts(tenantId, file);
if (!validationResult.is_valid) {
throw new Error(`File validation failed: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`);
}
const suggestionsResult = await this.generateInventorySuggestions(tenantId, file, validationResult.product_list);
// Convert to legacy format
return {
total_products_found: suggestionsResult.total_products,
inventory_suggestions: suggestionsResult.suggestions,
business_model_analysis: suggestionsResult.business_model_analysis,
import_job_id: `legacy-${Date.now()}`,
status: 'completed',
processed_rows: validationResult.total_records,
errors: validationResult.validation_errors.map(e => e.message || String(e)),
warnings: validationResult.validation_warnings.map(w => w.message || String(w))
};
}
/**
* Get business model guidance based on analysis
*/

View File

@@ -21,6 +21,8 @@ import {
import toast from 'react-hot-toast';
import {
FileValidationResult,
ProductSuggestionsResult,
OnboardingAnalysisResult,
InventorySuggestion,
BusinessModelAnalysis,
@@ -35,12 +37,13 @@ interface SmartHistoricalDataImportProps {
onBack?: () => void;
}
type ImportPhase = 'upload' | 'analysis' | 'review' | 'creation' | 'import' | 'complete';
type ImportPhase = 'upload' | 'validation' | 'suggestions' | 'review' | 'creation' | 'import' | 'complete';
interface PhaseState {
phase: ImportPhase;
file?: File;
analysisResult?: OnboardingAnalysisResult;
validationResult?: FileValidationResult;
suggestionsResult?: ProductSuggestionsResult;
reviewedSuggestions?: InventorySuggestion[];
creationResult?: InventoryCreationResult;
importResult?: SalesImportResult;
@@ -57,22 +60,56 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
const handleFileUpload = useCallback(async (file: File) => {
setState(prev => ({ ...prev, file, phase: 'analysis' }));
setState(prev => ({ ...prev, file, phase: 'validation' }));
setIsProcessing(true);
try {
toast.loading('🧠 Analizando tu archivo con IA...', { id: 'analysis' });
// Step 1: Validate file and extract products
toast.loading('📋 Validando archivo...', { id: 'validation' });
const analysisResult = await onboardingService.analyzeSalesDataForOnboarding(tenantId, file);
const validationResult = await onboardingService.validateFileAndExtractProducts(tenantId, file);
toast.success(`¡Análisis completado! ${analysisResult.total_products_found} productos encontrados`, {
id: 'analysis'
if (!validationResult.is_valid) {
throw new Error(`Archivo inválido: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`);
}
toast.success(`¡Archivo válido! ${validationResult.unique_products} productos únicos encontrados`, {
id: 'validation'
});
setState(prev => ({ ...prev, validationResult, phase: 'suggestions' }));
// Step 2: Generate AI suggestions
setTimeout(() => handleGenerateSuggestions(file, validationResult.product_list), 1000);
} catch (error: any) {
toast.error('Error al validar el archivo', { id: 'validation' });
setState(prev => ({
...prev,
error: error.message || 'Error de validación',
phase: 'upload'
}));
} finally {
setIsProcessing(false);
}
}, [tenantId]);
const handleGenerateSuggestions = useCallback(async (file: File, productList: string[]) => {
setIsProcessing(true);
try {
toast.loading('🧠 Generando sugerencias con IA...', { id: 'suggestions' });
const suggestionsResult = await onboardingService.generateInventorySuggestions(tenantId, file, productList);
toast.success(`¡${suggestionsResult.total_products} productos clasificados! ${suggestionsResult.high_confidence_count} con alta confianza`, {
id: 'suggestions'
});
setState(prev => ({
...prev,
analysisResult,
reviewedSuggestions: analysisResult.inventory_suggestions.map(s => ({
suggestionsResult,
reviewedSuggestions: suggestionsResult.suggestions.map(s => ({
...s,
user_approved: s.confidence_score >= 0.7
})),
@@ -80,11 +117,11 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
}));
} catch (error: any) {
toast.error('Error al analizar el archivo', { id: 'analysis' });
toast.error('Error al generar sugerencias', { id: 'suggestions' });
setState(prev => ({
...prev,
error: error.message || 'Error desconocido',
phase: 'upload'
error: error.message || 'Error en sugerencias de IA',
phase: 'validation'
}));
} finally {
setIsProcessing(false);
@@ -475,17 +512,17 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
</div>
);
case 'analysis':
case 'validation':
return (
<div className="text-center py-12">
<div className="w-20 h-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
<Brain className="w-10 h-10 text-white" />
<div className="w-20 h-20 bg-gradient-to-r from-blue-500 to-green-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
<CheckCircle2 className="w-10 h-10 text-white" />
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-3">
🧠 Analizando tu archivo con IA...
📋 Validando archivo...
</h2>
<p className="text-gray-600 mb-6">
Esto puede tomar unos momentos mientras clasificamos tus productos
Verificando formato y extrayendo productos únicos
</p>
<div className="bg-white rounded-lg shadow-sm p-4 max-w-md mx-auto">
<div className="flex items-center justify-between text-sm text-gray-600">
@@ -493,16 +530,48 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
<span className="font-medium">{state.file?.name}</span>
</div>
<div className="mt-2 bg-gray-200 rounded-full h-2">
<div className="bg-gradient-to-r from-blue-500 to-purple-500 h-2 rounded-full w-1/2 animate-pulse"></div>
<div className="bg-gradient-to-r from-blue-500 to-green-500 h-2 rounded-full w-1/3 animate-pulse"></div>
</div>
<div className="mt-2 text-xs text-gray-500">Paso 1 de 4: Validación</div>
</div>
</div>
);
case 'suggestions':
return (
<div className="text-center py-12">
<div className="w-20 h-20 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
<Brain className="w-10 h-10 text-white" />
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-3">
🧠 Generando sugerencias con IA...
</h2>
<p className="text-gray-600 mb-6">
Clasificando productos y analizando tu modelo de negocio
</p>
<div className="bg-white rounded-lg shadow-sm p-4 max-w-md mx-auto">
{state.validationResult && (
<div className="mb-4">
<div className="flex items-center justify-center space-x-2 text-green-600 mb-2">
<CheckCircle2 className="w-4 h-4" />
<span className="text-sm font-medium">
{state.validationResult.unique_products} productos únicos encontrados
</span>
</div>
</div>
)}
<div className="bg-gray-200 rounded-full h-2">
<div className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full w-2/3 animate-pulse"></div>
</div>
<div className="mt-2 text-xs text-gray-500">Paso 2 de 4: Clasificación IA</div>
</div>
</div>
);
case 'review':
if (!state.analysisResult) return null;
if (!state.suggestionsResult) return null;
const { analysisResult, reviewedSuggestions } = state;
const { suggestionsResult, reviewedSuggestions } = state;
const approvedCount = reviewedSuggestions?.filter(s => s.user_approved).length || 0;
const highConfidenceCount = reviewedSuggestions?.filter(s => s.confidence_score >= 0.7).length || 0;
const visibleSuggestions = showAllSuggestions
@@ -519,12 +588,15 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
¡Análisis Completado! 🎉
</h2>
<p className="text-gray-600">
Hemos encontrado <strong>{analysisResult.total_products_found} productos</strong> y
Hemos encontrado <strong>{suggestionsResult.total_products} productos</strong> y
sugerimos <strong>{approvedCount} para tu inventario</strong>
</p>
<div className="mt-2 text-sm text-gray-500">
Procesado en {suggestionsResult.processing_time_seconds.toFixed(1)}s
</div>
</div>
{renderBusinessModelInsight(analysisResult.business_model_analysis)}
{renderBusinessModelInsight(suggestionsResult.business_model_analysis)}
<div className="bg-white border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
@@ -579,15 +651,15 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
{visibleSuggestions?.map(renderSuggestionCard)}
</div>
{analysisResult.warnings.length > 0 && (
{state.validationResult?.validation_warnings && state.validationResult.validation_warnings.length > 0 && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
<div className="flex">
<AlertTriangle className="h-5 w-5 text-amber-400" />
<div className="ml-3">
<h4 className="text-sm font-medium text-amber-800">Advertencias</h4>
<h4 className="text-sm font-medium text-amber-800">Advertencias de Validación</h4>
<ul className="mt-2 text-sm text-amber-700 space-y-1">
{analysisResult.warnings.map((warning, idx) => (
<li key={idx}> {warning}</li>
{state.validationResult.validation_warnings.map((warning, idx) => (
<li key={idx}> {warning.message || warning}</li>
))}
</ul>
</div>
@@ -630,6 +702,9 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
case 'creation':
case 'import':
const isCreating = state.phase === 'creation';
const stepNumber = isCreating ? 3 : 4;
const stepProgress = isCreating ? 75 : 90;
return (
<div className="text-center py-12">
<div className="w-20 h-20 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
@@ -662,11 +737,17 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
)}
<div className="bg-gray-200 rounded-full h-3">
<div className="bg-gradient-to-r from-green-400 to-blue-500 h-3 rounded-full w-3/4 animate-pulse"></div>
<div
className="bg-gradient-to-r from-green-400 to-blue-500 h-3 rounded-full animate-pulse transition-all duration-500"
style={{ width: `${stepProgress}%` }}
/>
</div>
<div className="flex justify-between items-center mt-2">
<p className="text-sm text-gray-500">
{isCreating ? 'Creando inventario...' : 'Procesando importación final...'}
</p>
<span className="text-xs text-gray-400">Paso {stepNumber} de 4</span>
</div>
<p className="text-sm text-gray-500 mt-2">
{isCreating ? 'Creando inventario...' : 'Procesando importación final...'}
</p>
</div>
</div>
);

View File

@@ -5,7 +5,7 @@ import {
Filter,
Download,
Upload,
Grid3X3,
LayoutGrid,
List,
Package,
TrendingDown,
@@ -143,6 +143,14 @@ const InventoryPage: React.FC = () => {
console.log('View details:', item);
};
// Handle view item by ID (for alerts)
const handleViewItemById = (itemId: string) => {
const item = items.find(item => item.id === itemId);
if (item) {
handleItemViewDetails(item);
}
};
// Handle alert acknowledgment
const handleAcknowledgeAlert = async (alertId: string) => {
await acknowledgeAlert(alertId);
@@ -302,7 +310,7 @@ const InventoryPage: React.FC = () => {
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<Grid3X3 className="w-4 h-4" />
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
@@ -529,7 +537,7 @@ const InventoryPage: React.FC = () => {
alerts={alerts}
onAcknowledge={handleAcknowledgeAlert}
onAcknowledgeAll={handleBulkAcknowledgeAlerts}
onViewItem={handleItemViewDetails}
onViewItem={handleViewItemById}
/>
</div>
)}

View File

@@ -4,7 +4,7 @@ import {
Search,
Plus,
Filter,
Grid3X3,
LayoutGrid,
List,
ChefHat,
TrendingUp,
@@ -304,7 +304,7 @@ const RecipesPage: React.FC = () => {
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<Grid3X3 className="w-4 h-4" />
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}