diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 8d5d0078..f57d9292 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -332,14 +332,8 @@ export { // Hooks - Classification export { - usePendingSuggestions, - useSuggestionHistory, - useBusinessModelAnalysis, useClassifyProduct, useClassifyProductsBatch, - useApproveClassification, - useUpdateSuggestion, - useDeleteSuggestion, classificationKeys, } from './hooks/classification'; diff --git a/frontend/src/api/services/classification.ts b/frontend/src/api/services/classification.ts index fd8338b4..263d2d94 100644 --- a/frontend/src/api/services/classification.ts +++ b/frontend/src/api/services/classification.ts @@ -5,10 +5,7 @@ import { apiClient } from '../client'; import { ProductClassificationRequest, BatchClassificationRequest, - ProductSuggestionResponse, - BusinessModelAnalysisResponse, - ClassificationApprovalRequest, - ClassificationApprovalResponse, + ProductSuggestionResponse } from '../types/classification'; export class ClassificationService { @@ -19,7 +16,7 @@ export class ClassificationService { classificationData: ProductClassificationRequest ): Promise { return apiClient.post( - `${this.baseUrl}/${tenantId}/classification/classify-product`, + `${this.baseUrl}/${tenantId}/inventory/classify-product`, classificationData ); } @@ -28,67 +25,20 @@ export class ClassificationService { tenantId: string, batchData: BatchClassificationRequest ): Promise { - return apiClient.post( - `${this.baseUrl}/${tenantId}/classification/classify-batch`, + const response = await apiClient.post<{ + suggestions: ProductSuggestionResponse[]; + business_model_analysis: any; + total_products: number; + high_confidence_count: number; + low_confidence_count: number; + }>( + `${this.baseUrl}/${tenantId}/inventory/classify-products-batch`, batchData ); + // Extract just the suggestions array from the response + return response.suggestions; } - async getBusinessModelAnalysis(tenantId: string): Promise { - return apiClient.get( - `${this.baseUrl}/${tenantId}/classification/business-model-analysis` - ); - } - - async approveClassification( - tenantId: string, - approvalData: ClassificationApprovalRequest - ): Promise { - return apiClient.post( - `${this.baseUrl}/${tenantId}/classification/approve-suggestion`, - approvalData - ); - } - - async getPendingSuggestions(tenantId: string): Promise { - return apiClient.get( - `${this.baseUrl}/${tenantId}/classification/pending-suggestions` - ); - } - - async getSuggestionHistory( - tenantId: string, - limit: number = 50, - offset: number = 0 - ): Promise<{ - items: ProductSuggestionResponse[]; - total: number; - }> { - const queryParams = new URLSearchParams(); - queryParams.append('limit', limit.toString()); - queryParams.append('offset', offset.toString()); - - return apiClient.get( - `${this.baseUrl}/${tenantId}/classification/suggestion-history?${queryParams.toString()}` - ); - } - - async deleteSuggestion(tenantId: string, suggestionId: string): Promise<{ message: string }> { - return apiClient.delete<{ message: string }>( - `${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}` - ); - } - - async updateSuggestion( - tenantId: string, - suggestionId: string, - updateData: Partial - ): Promise { - return apiClient.put( - `${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`, - updateData - ); - } } export const classificationService = new ClassificationService(); \ No newline at end of file diff --git a/frontend/src/api/types/classification.ts b/frontend/src/api/types/classification.ts index 2e531602..5ff6d09f 100644 --- a/frontend/src/api/types/classification.ts +++ b/frontend/src/api/types/classification.ts @@ -26,40 +26,4 @@ export interface ProductSuggestionResponse { is_seasonal: boolean; suggested_supplier?: string; notes?: string; -} - -export interface BusinessModelAnalysisResponse { - tenant_id: string; - analysis_date: string; - business_type: string; - primary_products: string[]; - seasonality_patterns: Record; - supplier_recommendations: Array<{ - category: string; - suppliers: string[]; - estimated_cost_savings: number; - }>; - inventory_optimization_suggestions: Array<{ - product_name: string; - current_stock_level: number; - suggested_stock_level: number; - reason: string; - }>; - confidence_score: number; -} - -export interface ClassificationApprovalRequest { - suggestion_id: string; - approved: boolean; - modifications?: Partial; -} - -export interface ClassificationApprovalResponse { - suggestion_id: string; - approved: boolean; - created_ingredient?: { - id: string; - name: string; - }; - message: string; } \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/index.ts b/frontend/src/components/domain/onboarding/index.ts index 29708272..a0a01551 100644 --- a/frontend/src/components/domain/onboarding/index.ts +++ b/frontend/src/components/domain/onboarding/index.ts @@ -3,8 +3,7 @@ export { default as OnboardingWizard } from './OnboardingWizard'; // Individual step components export { BakerySetupStep } from './steps/BakerySetupStep'; -export { DataProcessingStep } from './steps/DataProcessingStep'; -export { ReviewStep } from './steps/ReviewStep'; +export { HistoricalSalesValidationStep } from './steps/HistoricalSalesValidationStep'; export { InventorySetupStep } from './steps/InventorySetupStep'; export { SuppliersStep } from './steps/SuppliersStep'; export { MLTrainingStep } from './steps/MLTrainingStep'; diff --git a/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx b/frontend/src/components/domain/onboarding/steps/HistoricalSalesValidationStep.tsx similarity index 54% rename from frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx rename to frontend/src/components/domain/onboarding/steps/HistoricalSalesValidationStep.tsx index cf02d8b9..18122302 100644 --- a/frontend/src/components/domain/onboarding/steps/DataProcessingStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/HistoricalSalesValidationStep.tsx @@ -1,5 +1,5 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity } from 'lucide-react'; import { Button, Card, Badge } from '../../../ui'; import { OnboardingStepProps } from '../OnboardingWizard'; import { useModal } from '../../../../hooks/ui/useModal'; @@ -7,8 +7,9 @@ import { useToast } from '../../../../hooks/ui/useToast'; import { useOnboarding } from '../../../../hooks/business/onboarding'; import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store'; import { useCurrentTenant, useTenantLoading } from '../../../../stores'; +import type { ProductSuggestionResponse } from '../../../../api'; -type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error'; +type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'review' | 'completed' | 'error'; interface ProcessingResult { // Validation data @@ -31,15 +32,70 @@ interface ProcessingResult { recommendations: string[]; } -// This function has been replaced by the onboarding hooks +interface Product { + id: string; + name: string; + category: string; + confidence: number; + sales_count?: number; + estimated_price?: number; + status: 'approved' | 'rejected' | 'pending'; + notes?: string; + // Fields from API suggestion + suggestion_id?: string; + original_name: string; + suggested_name: string; + product_type: 'ingredient' | 'finished_product'; + unit_of_measure: string; + estimated_shelf_life_days: number; + requires_refrigeration: boolean; + requires_freezing: boolean; + is_seasonal: boolean; + suggested_supplier?: string; + sales_data?: { + total_quantity: number; + average_daily_sales: number; + peak_day: string; + frequency: number; + }; +} -export const DataProcessingStep: React.FC = ({ +// Convert API suggestions to Product interface +const convertSuggestionsToProducts = (suggestions: ProductSuggestionResponse[]): Product[] => { + const products = suggestions.map((suggestion, index) => ({ + id: suggestion.suggestion_id || `product-${index}`, + name: suggestion.suggested_name, + category: suggestion.category, + confidence: Math.round(suggestion.confidence_score * 100), + status: 'pending' as const, + // Store original API data + suggestion_id: suggestion.suggestion_id, + original_name: suggestion.original_name, + suggested_name: suggestion.suggested_name, + product_type: suggestion.product_type, + unit_of_measure: suggestion.unit_of_measure, + estimated_shelf_life_days: suggestion.estimated_shelf_life_days, + requires_refrigeration: suggestion.requires_refrigeration, + requires_freezing: suggestion.requires_freezing, + is_seasonal: suggestion.is_seasonal, + suggested_supplier: suggestion.suggested_supplier, + notes: suggestion.notes, + sales_data: suggestion.sales_data, + // Legacy fields for display + sales_count: suggestion.sales_data?.total_quantity || 0, + estimated_price: 0 // Price estimation not provided by current API + })); + + return products; +}; + +export const HistoricalSalesValidationStep: React.FC = ({ data, onDataChange, - onNext, - onPrevious, - isFirstStep, - isLastStep + _onNext, + _onPrevious, + _isFirstStep, + _isLastStep }) => { const user = useAuthUser(); const authLoading = useAuthLoading(); @@ -49,7 +105,6 @@ export const DataProcessingStep: React.FC = ({ // Use the new onboarding hooks const { processSalesFile, - generateInventorySuggestions, salesProcessing: { stage: onboardingStage, progress: onboardingProgress, @@ -58,37 +113,20 @@ export const DataProcessingStep: React.FC = ({ suggestions }, tenantCreation, - isLoading, error, clearError } = useOnboarding(); - const errorModal = useModal(); const toast = 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 => { - // Also check the onboarding data for tenant creation success const onboardingTenantId = data.bakery?.tenant_id; const tenantId = currentTenant?.id || user?.tenant_id || onboardingTenantId || null; - console.log('DataProcessingStep - getTenantId:', { - currentTenant: currentTenant?.id, - userTenantId: user?.tenant_id, - onboardingTenantId: onboardingTenantId, - finalTenantId: tenantId, - isLoadingUserData, - authLoading, - tenantLoading, - user: user ? { id: user.id, email: user.email } : null, - tenantCreationSuccess: data.tenantCreation?.isSuccess - }); return tenantId; }; - // Check if tenant data is available (not loading and has ID, OR tenant was created successfully) + // Check if tenant data is available const isTenantAvailable = (): boolean => { const hasAuth = !authLoading && user; const hasTenantId = getTenantId() !== null; @@ -96,42 +134,39 @@ export const DataProcessingStep: React.FC = ({ 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; }; - // Use onboarding hook state when available, fallback to local state + + // State management const [localStage, setLocalStage] = useState(data.processingStage || 'upload'); const [uploadedFile, setUploadedFile] = useState(data.files?.salesData || null); const [localResults, setLocalResults] = useState(data.processingResults || null); + const [products, setProducts] = useState(() => { + if (data.detectedProducts) { + return data.detectedProducts; + } + // Generate from existing suggestions if available + if (suggestions && suggestions.length > 0) { + return convertSuggestionsToProducts(suggestions); + } + return []; + }); + const [selectedCategory, setSelectedCategory] = useState('all'); // Derive current state from onboarding hooks or local state - // Priority: if local is 'completed' or 'error', use local; otherwise use onboarding state const stage = (localStage === 'completed' || localStage === 'error') ? localStage : (onboardingStage || localStage); const progress = onboardingProgress || 0; const currentMessage = onboardingMessage || ''; - const results = (validationResults && suggestions) ? { + const results = useMemo(() => (validationResults && suggestions) ? { ...validationResults, aiSuggestions: suggestions, - // Add calculated fields from backend response productsIdentified: validationResults.unique_products || validationResults.product_list?.length || 0, - categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0, + categoriesDetected: suggestions ? new Set(suggestions.map((s: ProductSuggestionResponse) => s.category)).size : 0, businessModel: 'production', confidenceScore: 85, recommendations: validationResults.summary?.suggestions || [], - // Backend response details totalRecords: validationResults.total_records || 0, validRecords: validationResults.valid_records || 0, invalidRecords: validationResults.invalid_records || 0, @@ -140,32 +175,62 @@ export const DataProcessingStep: React.FC = ({ estimatedProcessingTime: validationResults.summary?.estimated_processing_time_seconds || 0, detectedColumns: validationResults.summary?.detected_columns || [], validationMessage: validationResults.message || 'Validación completada' - } : localResults; - - // Debug logging for state changes - console.log('DataProcessingStep - State debug:', { - localStage, - onboardingStage, - finalStage: stage, - hasValidationResults: !!validationResults, - hasSuggestions: !!suggestions, - hasResults: !!results, - localResults: !!localResults - }); - const [dragActive, setDragActive] = useState(false); - - const fileInputRef = useRef(null); - const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile }); + } : localResults, [validationResults, suggestions, localResults]); + // Update products when suggestions change useEffect(() => { - // Only update if state actually changed - const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile }; + if (suggestions && suggestions.length > 0 && products.length === 0) { + const newProducts = convertSuggestionsToProducts(suggestions); + setProducts(newProducts); + } + }, [suggestions, products.length]); + + // Auto-progress to review stage when validation completes and suggestions are available + useEffect(() => { + if (stage === 'analyzing' && results && suggestions && suggestions.length > 0) { + setLocalStage('review'); + } + }, [stage, results, suggestions]); + + const [dragActive, setDragActive] = useState(false); + const fileInputRef = useRef(null); + const lastStateRef = useRef({ stage, progress, currentMessage, results, suggestions, uploadedFile, products }); + + // Memoized computed values + const approvedProducts = useMemo(() => + products.filter(p => p.status === 'approved'), + [products] + ); + + const reviewCompleted = useMemo(() => + products.length > 0 && products.every(p => p.status !== 'pending'), + [products] + ); + + const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))]; + + const getFilteredProducts = () => { + if (selectedCategory === 'all') { + return products; + } + return products.filter(p => p.category === selectedCategory); + }; + + const stats = { + total: products.length, + approved: products.filter(p => p.status === 'approved').length, + rejected: products.filter(p => p.status === 'rejected').length, + pending: products.filter(p => p.status === 'pending').length + }; + + // Update parent data when state changes + useEffect(() => { + const currentState = { stage, progress, currentMessage, results, suggestions, uploadedFile, products }; if (JSON.stringify(currentState) !== JSON.stringify(lastStateRef.current)) { lastStateRef.current = currentState; - // Update parent data when state changes onDataChange({ - processingStage: stage, + processingStage: stage === 'review' ? 'completed' : stage, processingProgress: progress, currentMessage: currentMessage, processingResults: results, @@ -173,10 +238,13 @@ export const DataProcessingStep: React.FC = ({ files: { ...data.files, salesData: uploadedFile - } + }, + detectedProducts: products, + approvedProducts, + reviewCompleted }); } - }, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data.files]); + }, [stage, progress, currentMessage, results, suggestions, uploadedFile, products, approvedProducts, reviewCompleted, onDataChange, data.files]); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -232,7 +300,6 @@ export const DataProcessingStep: React.FC = ({ try { // Wait for user data to load if still loading if (!isTenantAvailable()) { - console.log('Tenant not available, waiting...'); setUploadedFile(null); setLocalStage('upload'); toast.addToast('Por favor espere mientras cargamos su información...', { @@ -241,13 +308,6 @@ export const DataProcessingStep: React.FC = ({ }); return; } - - console.log('DataProcessingStep - Starting file processing', { - fileName: file.name, - fileSize: file.size, - fileType: file.type, - lastModified: file.lastModified - }); // Use the onboarding hook for file processing const success = await processSalesFile(file, (progress, stage, message) => { @@ -255,10 +315,13 @@ export const DataProcessingStep: React.FC = ({ }); if (success) { - setLocalStage('completed'); + setLocalStage('review'); - // If we have results from the onboarding hook, store them locally too + // Update products from suggestions if (validationResults && suggestions) { + const newProducts = convertSuggestionsToProducts(suggestions); + setProducts(newProducts); + const processedResults = { ...validationResults, aiSuggestions: suggestions, @@ -271,7 +334,7 @@ export const DataProcessingStep: React.FC = ({ setLocalResults(processedResults); } - toast.addToast('El archivo se procesó correctamente', { + toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', { title: 'Procesamiento completado', type: 'success' }); @@ -280,17 +343,7 @@ export const DataProcessingStep: React.FC = ({ } } catch (error) { - console.error('DataProcessingStep - Processing error:', error); - 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 - }); - + console.error('HistoricalSalesValidationStep - Processing error:', error); setLocalStage('error'); const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos'; @@ -302,7 +355,6 @@ export const DataProcessingStep: React.FC = ({ }; const downloadTemplate = () => { - // Provide a static CSV template 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 @@ -332,6 +384,7 @@ export const DataProcessingStep: React.FC = ({ setLocalStage('upload'); setUploadedFile(null); setLocalResults(null); + setProducts([]); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -340,6 +393,37 @@ export const DataProcessingStep: React.FC = ({ } }; + const handleProductAction = (productId: string, action: 'approve' | 'reject') => { + setProducts(prev => prev.map(product => + product.id === productId + ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } + : product + )); + }; + + const handleBulkAction = (action: 'approve' | 'reject') => { + const filteredProducts = getFilteredProducts(); + setProducts(prev => prev.map(product => + filteredProducts.some(fp => fp.id === product.id) + ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } + : product + )); + }; + + const getConfidenceColor = (confidence: number) => { + if (confidence >= 90) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; + if (confidence >= 75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20'; + return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; + case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; + default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]'; + } + }; + return (
{/* Loading state when tenant data is not available */} @@ -357,7 +441,7 @@ export const DataProcessingStep: React.FC = ({ )} - {/* Improved Upload Stage */} + {/* Upload Stage */} {(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && ( <>
= ({

- {/* Visual indicators */}
@@ -448,7 +531,7 @@ export const DataProcessingStep: React.FC = ({
- {/* Improved Template Download Section */} + {/* Template Download Section */}
@@ -540,26 +623,20 @@ export const DataProcessingStep: React.FC = ({ )} - {/* Simplified Results Stage */} - {stage === 'completed' && results && ( + {/* Review Stage - Combined validation results and product approval */} + {(stage === 'review' || (stage === 'completed' && products.length > 0)) && results && (
- {/* Success Header */} + {/* Success Header with Validation Results */}

- ¡Procesamiento Completado! + ¡Validación Completada!

- {results.validationMessage || 'Tus datos han sido procesados exitosamente'} + {results.validationMessage || 'Tus datos han sido procesados exitosamente. Revisa y aprueba los productos detectados.'}

- {results.fileSizeMb && ( -
- Archivo {results.fileFormat?.toUpperCase()} • {results.fileSizeMb.toFixed(2)} MB - {results.estimatedProcessingTime && ` • ${results.estimatedProcessingTime}s procesamiento`} -
- )}
{/* Enhanced Stats Cards */} @@ -576,60 +653,182 @@ export const DataProcessingStep: React.FC = ({

{results.invalidRecords || results.invalid_records || 0}

Inválidos

-

{results.productsIdentified}

Productos

- -
-

{results.confidenceScore}%

-

Confianza

-
- -
-

- {results.businessModel === 'artisan' ? 'Artesanal' : - results.businessModel === 'retail' ? 'Retail' : 'Híbrido'} -

-

Modelo

-
- {/* Additional Details from Backend */} - {(results.detectedColumns?.length > 0 || results.recommendations?.length > 0) && ( -
- {/* Detected Columns */} - {results.detectedColumns?.length > 0 && ( - -

Columnas Detectadas

-
- {results.detectedColumns.map((column, index) => ( - - {column} - - ))} -
+ {/* Product Review Section */} + {products.length > 0 && ( + <> + {/* Summary Stats */} +
+ +
{stats.total}
+
Productos detectados
- )} + + +
{stats.approved}
+
Aprobados
+
+ + +
{stats.rejected}
+
Rechazados
+
+ + +
{stats.pending}
+
Pendientes
+
+
- {/* Backend Recommendations */} - {results.recommendations?.length > 0 && ( - -

Recomendaciones

-
    - {results.recommendations.slice(0, 3).map((rec, index) => ( -
  • - - {rec} -
  • + {/* Controls */} +
    +
    + + + + {getFilteredProducts().length} productos + +
    + +
    + + +
    +
    + + {/* Products List */} +
    + {getFilteredProducts().map((product) => ( + +
    +
    +
    +

    {product.name}

    + + {product.status === 'approved' ? '✓ Aprobado' : + product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'} + +
    + +
    + + {product.category} + + + {product.confidence}% confianza + +
    + +
    + {product.original_name && product.original_name !== product.name && ( +
    + Nombre original: + {product.original_name} +
    + )} +
    +
    + Tipo: + + {product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'} + +
    +
    + Unidad: + {product.unit_of_measure} +
    + {product.sales_data && ( +
    + Ventas: + {product.sales_data.total_quantity} +
    + )} +
    + {product.notes && ( +
    + Nota: {product.notes} +
    + )} +
    +
    + +
    + + +
    +
    +
    + ))} +
    + )} + + {/* Information */} + +

    + 📋 Revisión de Datos de Ventas: +

    +
      +
    • Datos validados - Tu archivo ha sido procesado y validado correctamente
    • +
    • Productos detectados - La IA ha identificado automáticamente productos desde tus ventas
    • +
    • Revisa cuidadosamente - Aprueba o rechaza cada producto según sea correcto para tu negocio
    • +
    • Verifica nombres - Compara el nombre original vs. el nombre sugerido
    • +
    • Revisa clasificaciones - Confirma si son ingredientes o productos terminados
    • +
    • Usa filtros - Filtra por categoría para revisar productos similares
    • +
    • Acciones masivas - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso
    • +
    +
)} diff --git a/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx index a90d6dbf..9288d859 100644 --- a/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/InventorySetupStep.tsx @@ -125,16 +125,16 @@ export const InventorySetupStep: React.FC = ({ } // Try to get approved products from business hooks data first, then from component props const approvedProducts = data.approvedProducts || - allStepData?.['review']?.approvedProducts || - data.allStepData?.['review']?.approvedProducts; + allStepData?.['sales-validation']?.approvedProducts || + data.allStepData?.['sales-validation']?.approvedProducts; return generateInventoryFromProducts(approvedProducts || []); }); // Update items when approved products become available (for when component is already mounted) useEffect(() => { const approvedProducts = data.approvedProducts || - allStepData?.['review']?.approvedProducts || - data.allStepData?.['review']?.approvedProducts; + allStepData?.['sales-validation']?.approvedProducts || + data.allStepData?.['sales-validation']?.approvedProducts; if (approvedProducts && approvedProducts.length > 0 && items.length === 0) { const newItems = generateInventoryFromProducts(approvedProducts); @@ -153,12 +153,12 @@ export const InventorySetupStep: React.FC = ({ console.log('InventorySetup - Starting handleCreateInventory'); const approvedProducts = data.approvedProducts || - allStepData?.['review']?.approvedProducts || - data.allStepData?.['review']?.approvedProducts; + allStepData?.['sales-validation']?.approvedProducts || + data.allStepData?.['sales-validation']?.approvedProducts; console.log('InventorySetup - approvedProducts:', { fromDataProp: data.approvedProducts, - fromAllStepData: allStepData?.['review']?.approvedProducts, - fromDataAllStepData: data.allStepData?.['review']?.approvedProducts, + fromAllStepData: allStepData?.['sales-validation']?.approvedProducts, + fromDataAllStepData: data.allStepData?.['sales-validation']?.approvedProducts, finalProducts: approvedProducts, allStepDataKeys: Object.keys(allStepData || {}), dataKeys: Object.keys(data || {}) @@ -216,10 +216,10 @@ export const InventorySetupStep: React.FC = ({ }); // Now try to import sales data if available - const salesDataFile = data.allStepData?.['data-processing']?.salesDataFile || - allStepData?.['data-processing']?.salesDataFile; - const processingResults = data.allStepData?.['data-processing']?.processingResults || - allStepData?.['data-processing']?.processingResults; + const salesDataFile = data.allStepData?.['sales-validation']?.salesDataFile || + allStepData?.['sales-validation']?.salesDataFile; + const processingResults = data.allStepData?.['sales-validation']?.processingResults || + allStepData?.['sales-validation']?.processingResults; if (salesDataFile && processingResults?.is_valid && inventoryMapping) { try { diff --git a/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx b/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx deleted file mode 100644 index 51347f89..00000000 --- a/frontend/src/components/domain/onboarding/steps/ReviewStep.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react'; -import { Button, Card, Badge } from '../../../ui'; -import { OnboardingStepProps } from '../OnboardingWizard'; - -interface Product { - id: string; - name: string; - category: string; - confidence: number; - sales_count?: number; - estimated_price?: number; - status: 'approved' | 'rejected' | 'pending'; - notes?: string; - // Fields from API suggestion - suggestion_id?: string; - original_name: string; - suggested_name: string; - product_type: 'ingredient' | 'finished_product'; - unit_of_measure: string; - estimated_shelf_life_days: number; - requires_refrigeration: boolean; - requires_freezing: boolean; - is_seasonal: boolean; - suggested_supplier?: string; - sales_data?: { - total_quantity: number; - average_daily_sales: number; - peak_day: string; - frequency: number; - }; -} - -// Convert API suggestions to Product interface -const convertSuggestionsToProducts = (suggestions: any[]): Product[] => { - console.log('ReviewStep - convertSuggestionsToProducts called with:', suggestions); - - const products = suggestions.map((suggestion, index) => ({ - id: suggestion.suggestion_id || `product-${index}`, - name: suggestion.suggested_name, - category: suggestion.category, - confidence: Math.round(suggestion.confidence_score * 100), - status: 'pending' as const, - // Store original API data - suggestion_id: suggestion.suggestion_id, - original_name: suggestion.original_name, - suggested_name: suggestion.suggested_name, - product_type: suggestion.product_type, - unit_of_measure: suggestion.unit_of_measure, - estimated_shelf_life_days: suggestion.estimated_shelf_life_days, - requires_refrigeration: suggestion.requires_refrigeration, - requires_freezing: suggestion.requires_freezing, - is_seasonal: suggestion.is_seasonal, - suggested_supplier: suggestion.suggested_supplier, - notes: suggestion.notes, - sales_data: suggestion.sales_data, - // Legacy fields for display - sales_count: suggestion.sales_data?.total_quantity || 0, - estimated_price: 0 // Price estimation not provided by current API - })); - - console.log('ReviewStep - Converted products:', products); - return products; -}; - -export const ReviewStep: React.FC = ({ - data, - onDataChange, - onNext, - onPrevious, - isFirstStep, - isLastStep -}) => { - const createAlert = (alert: any) => { - console.log('Alert:', alert); - }; - - // Generate products from AI suggestions in processing results - const generateProductsFromResults = (results: any) => { - console.log('ReviewStep - generateProductsFromResults called with:', results); - console.log('ReviewStep - results keys:', Object.keys(results || {})); - console.log('ReviewStep - results.aiSuggestions:', results?.aiSuggestions); - console.log('ReviewStep - aiSuggestions length:', results?.aiSuggestions?.length); - console.log('ReviewStep - aiSuggestions type:', typeof results?.aiSuggestions); - console.log('ReviewStep - aiSuggestions is array:', Array.isArray(results?.aiSuggestions)); - - if (results?.aiSuggestions && results.aiSuggestions.length > 0) { - console.log('ReviewStep - Using AI suggestions:', results.aiSuggestions); - return convertSuggestionsToProducts(results.aiSuggestions); - } - // Fallback: create products from product list if no AI suggestions - if (results?.product_list) { - console.log('ReviewStep - Using fallback product list:', results.product_list); - return results.product_list.map((name: string, index: number) => ({ - id: `fallback-${index}`, - name, - original_name: name, - suggested_name: name, - category: 'Sin clasificar', - confidence: 50, - status: 'pending' as const, - product_type: 'finished_product' as const, - unit_of_measure: 'units', - estimated_shelf_life_days: 7, - requires_refrigeration: false, - requires_freezing: false, - is_seasonal: false - })); - } - return []; - }; - - const [products, setProducts] = useState(() => { - if (data.detectedProducts) { - return data.detectedProducts; - } - // Try to get processing results from current step data first, then from previous step data - const processingResults = data.processingResults || data.allStepData?.['data-processing']?.processingResults; - console.log('ReviewStep - Initializing with processingResults:', processingResults); - return generateProductsFromResults(processingResults); - }); - - // Check for empty products and show alert after component mounts - useEffect(() => { - const processingResults = data.processingResults || data.allStepData?.['data-processing']?.processingResults; - if (products.length === 0 && processingResults) { - createAlert({ - type: 'warning', - category: 'system', - priority: 'medium', - title: 'Sin productos detectados', - message: 'No se encontraron productos en los datos procesados. Verifique el archivo de ventas.', - source: 'onboarding' - }); - } - }, [products.length, data.processingResults, data.allStepData, createAlert]); - - const [selectedCategory, setSelectedCategory] = useState('all'); - - const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))]; - - // Memoize computed values to avoid unnecessary recalculations - const approvedProducts = useMemo(() => - products.filter(p => p.status === 'approved'), - [products] - ); - - const reviewCompleted = useMemo(() => - products.length > 0 && products.every(p => p.status !== 'pending'), - [products] - ); - - const [lastReviewCompleted, setLastReviewCompleted] = useState(false); - - const dataChangeRef = useRef({ products: [], approvedProducts: [], reviewCompleted: false }); - - // Update parent data when products change - useEffect(() => { - const currentState = { products, approvedProducts, reviewCompleted }; - const lastState = dataChangeRef.current; - - // Only call onDataChange if the state actually changed - if (JSON.stringify(currentState) !== JSON.stringify(lastState)) { - console.log('ReviewStep - Updating parent data with:', { - detectedProducts: products, - approvedProducts, - reviewCompleted, - approvedProductsCount: approvedProducts.length - }); - onDataChange({ - detectedProducts: products, - approvedProducts, - reviewCompleted - }); - dataChangeRef.current = currentState; - } - }, [products, approvedProducts, reviewCompleted, onDataChange]); - - // Handle review completion alert separately - useEffect(() => { - if (reviewCompleted && approvedProducts.length > 0 && !lastReviewCompleted) { - createAlert({ - type: 'success', - category: 'system', - priority: 'medium', - title: 'Revisión completada', - message: `Se aprobaron ${approvedProducts.length} de ${products.length} productos detectados.`, - source: 'onboarding' - }); - setLastReviewCompleted(true); - } - - if (!reviewCompleted && lastReviewCompleted) { - setLastReviewCompleted(false); - } - }, [reviewCompleted, approvedProducts.length, products.length, lastReviewCompleted, createAlert]); - - const handleProductAction = (productId: string, action: 'approve' | 'reject') => { - setProducts(prev => prev.map(product => - product.id === productId - ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } - : product - )); - }; - - const handleBulkAction = (action: 'approve' | 'reject') => { - const filteredProducts = getFilteredProducts(); - setProducts(prev => prev.map(product => - filteredProducts.some(fp => fp.id === product.id) - ? { ...product, status: action === 'approve' ? 'approved' : 'rejected' } - : product - )); - }; - - const getFilteredProducts = () => { - if (selectedCategory === 'all') { - return products; - } - return products.filter(p => p.category === selectedCategory); - }; - - const stats = { - total: products.length, - approved: products.filter(p => p.status === 'approved').length, - rejected: products.filter(p => p.status === 'rejected').length, - pending: products.filter(p => p.status === 'pending').length - }; - - const getConfidenceColor = (confidence: number) => { - if (confidence >= 90) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; - if (confidence >= 75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20'; - return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20'; - case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20'; - default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]'; - } - }; - - if (products.length === 0) { - return ( -
-
- -

- No se encontraron productos -

-

- No se pudieron detectar productos en el archivo procesado. - Verifique que el archivo contenga datos de ventas válidos. -

- -
-
- ); - } - - return ( -
- {/* Summary Stats */} -
- -
{stats.total}
-
Productos detectados
-
- - -
{stats.approved}
-
Aprobados
-
- - -
{stats.rejected}
-
Rechazados
-
- - -
{stats.pending}
-
Pendientes
-
-
- - {/* Controls */} -
-
- - - - {getFilteredProducts().length} productos - -
- -
- - -
-
- - {/* Products List */} -
- {getFilteredProducts().map((product) => ( - -
-
-
-

{product.name}

- - {product.status === 'approved' ? '✓ Aprobado' : - product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'} - -
- -
- - {product.category} - - - {product.confidence}% confianza - -
- -
- {product.original_name && product.original_name !== product.name && ( -
- Nombre original: - {product.original_name} -
- )} -
-
- Tipo: - - {product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'} - -
-
- Unidad: - {product.unit_of_measure} -
- {product.sales_data && ( -
- Ventas: - {product.sales_data.total_quantity} -
- )} -
- {product.notes && ( -
- Nota: {product.notes} -
- )} -
-
- -
- - -
-
-
- ))} -
- - {/* Information */} - -

- 📋 Revisión de Productos Detectados: -

-
    -
  • Revise cuidadosamente - Los productos fueron detectados automáticamente desde sus datos de ventas
  • -
  • Apruebe o rechace cada producto según sea correcto para su negocio
  • -
  • Verifique nombres - Compare el nombre original vs. el nombre sugerido
  • -
  • Revise clasificaciones - Confirme si son ingredientes o productos terminados
  • -
  • Use filtros - Filtre por categoría para revisar productos similares
  • -
  • Acciones masivas - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso
  • -
-
- -
- ); -}; \ No newline at end of file diff --git a/frontend/src/hooks/business/onboarding/steps.ts b/frontend/src/hooks/business/onboarding/steps.ts index 31ebea4f..6b2ba3d5 100644 --- a/frontend/src/hooks/business/onboarding/steps.ts +++ b/frontend/src/hooks/business/onboarding/steps.ts @@ -22,25 +22,15 @@ export const DEFAULT_STEPS: OnboardingStep[] = [ }, }, { - id: 'data-processing', + id: 'sales-validation', title: '📊 Validación de Ventas', - description: 'Valida tus datos de ventas y detecta productos automáticamente', + description: 'Sube, valida y aprueba tus datos de ventas históricas con IA', isRequired: true, isCompleted: false, validation: (data: OnboardingData) => { if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas'; - if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar'; + if (data.processingStage !== 'completed' && data.processingStage !== 'review') return 'El procesamiento debe completarse antes de continuar'; if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar'; - return null; - }, - }, - { - id: 'review', - title: '📋 Revisión', - description: 'Revisión de productos detectados por IA y resultados', - isRequired: true, - isCompleted: false, - validation: (data: OnboardingData) => { if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados'; const hasApprovedProducts = data.approvedProducts && data.approvedProducts.length > 0; if (!hasApprovedProducts) return 'Debes aprobar al menos un producto para continuar'; diff --git a/frontend/src/hooks/business/onboarding/types.ts b/frontend/src/hooks/business/onboarding/types.ts index dd3886a8..0ffc0f6b 100644 --- a/frontend/src/hooks/business/onboarding/types.ts +++ b/frontend/src/hooks/business/onboarding/types.ts @@ -29,7 +29,7 @@ export interface OnboardingData { files?: { salesData?: File; }; - processingStage?: 'upload' | 'validating' | 'analyzing' | 'completed' | 'error'; + processingStage?: 'upload' | 'validating' | 'analyzing' | 'review' | 'completed' | 'error'; processingResults?: { is_valid: boolean; total_records: number; @@ -44,8 +44,9 @@ export interface OnboardingData { }; }; - // Step 3: Review + // Step 3: Sales Validation (merged data processing + review) suggestions?: ProductSuggestionResponse[]; + detectedProducts?: any[]; // Products detected from AI analysis approvedSuggestions?: ProductSuggestionResponse[]; approvedProducts?: ProductSuggestionResponse[]; reviewCompleted?: boolean; diff --git a/frontend/src/hooks/business/onboarding/useOnboarding.ts b/frontend/src/hooks/business/onboarding/useOnboarding.ts index 4b9197f6..fc70216e 100644 --- a/frontend/src/hooks/business/onboarding/useOnboarding.ts +++ b/frontend/src/hooks/business/onboarding/useOnboarding.ts @@ -123,7 +123,7 @@ export const useOnboarding = () => { ): Promise => { const result = await salesProcessing.processFile(file, onProgress); if (result.success) { - updateStepData('data-processing', { + updateStepData('sales-validation', { files: { salesData: file }, processingStage: 'completed', processingResults: result.validationResults, diff --git a/frontend/src/hooks/business/onboarding/useTrainingOrchestration.ts b/frontend/src/hooks/business/onboarding/useTrainingOrchestration.ts index 8fa5d183..1869e4e1 100644 --- a/frontend/src/hooks/business/onboarding/useTrainingOrchestration.ts +++ b/frontend/src/hooks/business/onboarding/useTrainingOrchestration.ts @@ -152,14 +152,13 @@ export const useTrainingOrchestration = () => { const missingItems: string[] = []; // Get data from previous steps - const dataProcessingData = allStepData?.['data-processing']; - const reviewData = allStepData?.['review']; + const salesValidationData = allStepData?.['sales-validation']; const inventoryData = allStepData?.['inventory']; // Check if sales data was processed - const hasProcessingResults = dataProcessingData?.processingResults && - dataProcessingData.processingResults.is_valid && - dataProcessingData.processingResults.total_records > 0; + const hasProcessingResults = salesValidationData?.processingResults && + salesValidationData.processingResults.is_valid && + salesValidationData.processingResults.total_records > 0; // Check if sales data was imported (required for training) const hasImportResults = inventoryData?.salesImportResult && @@ -176,10 +175,10 @@ export const useTrainingOrchestration = () => { missingItems.push('Datos de ventas importados'); } - // Check if products were approved in review step - const hasApprovedProducts = reviewData?.approvedProducts && - reviewData.approvedProducts.length > 0 && - reviewData.reviewCompleted; + // Check if products were approved in sales validation step + const hasApprovedProducts = salesValidationData?.approvedProducts && + salesValidationData.approvedProducts.length > 0 && + salesValidationData.reviewCompleted; if (!hasApprovedProducts) { missingItems.push('Productos aprobados en revisión'); @@ -195,8 +194,8 @@ export const useTrainingOrchestration = () => { } // Check if we have enough data for training - if (dataProcessingData?.processingResults?.total_records && - dataProcessingData.processingResults.total_records < 10) { + if (salesValidationData?.processingResults?.total_records && + salesValidationData.processingResults.total_records < 10) { missingItems.push('Suficientes registros de ventas (mínimo 10)'); } diff --git a/frontend/src/pages/app/onboarding/OnboardingPage.tsx b/frontend/src/pages/app/onboarding/OnboardingPage.tsx index 9c3c847c..8933ebde 100644 --- a/frontend/src/pages/app/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/app/onboarding/OnboardingPage.tsx @@ -7,8 +7,7 @@ import { LoadingSpinner } from '../../../components/shared/LoadingSpinner'; // Step Components import { BakerySetupStep } from '../../../components/domain/onboarding/steps/BakerySetupStep'; -import { DataProcessingStep } from '../../../components/domain/onboarding/steps/DataProcessingStep'; -import { ReviewStep } from '../../../components/domain/onboarding/steps/ReviewStep'; +import { HistoricalSalesValidationStep } from '../../../components/domain/onboarding/steps/HistoricalSalesValidationStep'; import { InventorySetupStep } from '../../../components/domain/onboarding/steps/InventorySetupStep'; import { SuppliersStep } from '../../../components/domain/onboarding/steps/SuppliersStep'; import { MLTrainingStep } from '../../../components/domain/onboarding/steps/MLTrainingStep'; @@ -43,8 +42,7 @@ const OnboardingPage: React.FC = () => { // Map steps to components const stepComponents: { [key: string]: React.ComponentType } = { 'setup': BakerySetupStep, - 'data-processing': DataProcessingStep, - 'review': ReviewStep, + 'sales-validation': HistoricalSalesValidationStep, 'inventory': InventorySetupStep, 'suppliers': SuppliersStep, 'ml-training': MLTrainingStep,