diff --git a/frontend/src/api/services/onboarding.service.ts b/frontend/src/api/services/onboarding.service.ts index 828cb899..5a78331f 100644 --- a/frontend/src/api/services/onboarding.service.ts +++ b/frontend/src/api/services/onboarding.service.ts @@ -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; +} + +// 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 { + async validateFileAndExtractProducts(tenantId: string, file: File): Promise { 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 { + 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 { + // 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 */ diff --git a/frontend/src/components/onboarding/SmartHistoricalDataImport.tsx b/frontend/src/components/onboarding/SmartHistoricalDataImport.tsx index b101d594..c7adf106 100644 --- a/frontend/src/components/onboarding/SmartHistoricalDataImport.tsx +++ b/frontend/src/components/onboarding/SmartHistoricalDataImport.tsx @@ -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 = ({ 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 = ({ })); } 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 = ({ ); - case 'analysis': + case 'validation': return (
-
- +
+

- 🧠 Analizando tu archivo con IA... + 📋 Validando archivo...

- Esto puede tomar unos momentos mientras clasificamos tus productos + Verificando formato y extrayendo productos únicos

@@ -493,16 +530,48 @@ const SmartHistoricalDataImport: React.FC = ({ {state.file?.name}
-
+
+
Paso 1 de 4: Validación
+
+
+ ); + + case 'suggestions': + return ( +
+
+ +
+

+ 🧠 Generando sugerencias con IA... +

+

+ Clasificando productos y analizando tu modelo de negocio +

+
+ {state.validationResult && ( +
+
+ + + {state.validationResult.unique_products} productos únicos encontrados + +
+
+ )} +
+
+
+
Paso 2 de 4: Clasificación IA
); 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 = ({ ¡Análisis Completado! 🎉

- Hemos encontrado {analysisResult.total_products_found} productos y + Hemos encontrado {suggestionsResult.total_products} productos y sugerimos {approvedCount} para tu inventario

+
+ âš¡ Procesado en {suggestionsResult.processing_time_seconds.toFixed(1)}s +
- {renderBusinessModelInsight(analysisResult.business_model_analysis)} + {renderBusinessModelInsight(suggestionsResult.business_model_analysis)}
@@ -579,15 +651,15 @@ const SmartHistoricalDataImport: React.FC = ({ {visibleSuggestions?.map(renderSuggestionCard)}
- {analysisResult.warnings.length > 0 && ( + {state.validationResult?.validation_warnings && state.validationResult.validation_warnings.length > 0 && (
-

Advertencias

+

Advertencias de Validación

    - {analysisResult.warnings.map((warning, idx) => ( -
  • • {warning}
  • + {state.validationResult.validation_warnings.map((warning, idx) => ( +
  • • {warning.message || warning}
  • ))}
@@ -630,6 +702,9 @@ const SmartHistoricalDataImport: React.FC = ({ case 'creation': case 'import': const isCreating = state.phase === 'creation'; + const stepNumber = isCreating ? 3 : 4; + const stepProgress = isCreating ? 75 : 90; + return (
@@ -662,11 +737,17 @@ const SmartHistoricalDataImport: React.FC = ({ )}
-
+
+
+
+

+ {isCreating ? 'Creando inventario...' : 'Procesando importación final...'} +

+ Paso {stepNumber} de 4
-

- {isCreating ? 'Creando inventario...' : 'Procesando importación final...'} -

); diff --git a/frontend/src/pages/inventory/InventoryPage.tsx b/frontend/src/pages/inventory/InventoryPage.tsx index bd84077f..d0334707 100644 --- a/frontend/src/pages/inventory/InventoryPage.tsx +++ b/frontend/src/pages/inventory/InventoryPage.tsx @@ -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' }`} > - +
)} diff --git a/frontend/src/pages/recipes/RecipesPage.tsx b/frontend/src/pages/recipes/RecipesPage.tsx index 9805cfdf..15986fa6 100644 --- a/frontend/src/pages/recipes/RecipesPage.tsx +++ b/frontend/src/pages/recipes/RecipesPage.tsx @@ -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' }`} > - +