Start integrating the onboarding flow with backend 11

This commit is contained in:
Urtzi Alfaro
2025-09-07 09:01:09 +02:00
parent a65aaf269a
commit 9005286ada
12 changed files with 392 additions and 734 deletions

View File

@@ -332,14 +332,8 @@ export {
// Hooks - Classification
export {
usePendingSuggestions,
useSuggestionHistory,
useBusinessModelAnalysis,
useClassifyProduct,
useClassifyProductsBatch,
useApproveClassification,
useUpdateSuggestion,
useDeleteSuggestion,
classificationKeys,
} from './hooks/classification';

View File

@@ -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<ProductSuggestionResponse> {
return apiClient.post<ProductSuggestionResponse>(
`${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<ProductSuggestionResponse[]> {
return apiClient.post<ProductSuggestionResponse[]>(
`${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<BusinessModelAnalysisResponse> {
return apiClient.get<BusinessModelAnalysisResponse>(
`${this.baseUrl}/${tenantId}/classification/business-model-analysis`
);
}
async approveClassification(
tenantId: string,
approvalData: ClassificationApprovalRequest
): Promise<ClassificationApprovalResponse> {
return apiClient.post<ClassificationApprovalResponse>(
`${this.baseUrl}/${tenantId}/classification/approve-suggestion`,
approvalData
);
}
async getPendingSuggestions(tenantId: string): Promise<ProductSuggestionResponse[]> {
return apiClient.get<ProductSuggestionResponse[]>(
`${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<ProductSuggestionResponse>
): Promise<ProductSuggestionResponse> {
return apiClient.put<ProductSuggestionResponse>(
`${this.baseUrl}/${tenantId}/classification/suggestions/${suggestionId}`,
updateData
);
}
}
export const classificationService = new ClassificationService();

View File

@@ -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<string, any>;
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<ProductSuggestionResponse>;
}
export interface ClassificationApprovalResponse {
suggestion_id: string;
approved: boolean;
created_ingredient?: {
id: string;
name: string;
};
message: string;
}

View File

@@ -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';

View File

@@ -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<OnboardingStepProps> = ({
// 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<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
// Use the new onboarding hooks
const {
processSalesFile,
generateInventorySuggestions,
salesProcessing: {
stage: onboardingStage,
progress: onboardingProgress,
@@ -58,37 +113,20 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
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<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
const [products, setProducts] = useState<Product[]>(() => {
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<string>('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<OnboardingStepProps> = ({
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<HTMLInputElement>(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<HTMLInputElement>(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<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
});
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<OnboardingStepProps> = ({
});
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<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
}
} 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<OnboardingStepProps> = ({
};
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<OnboardingStepProps> = ({
setLocalStage('upload');
setUploadedFile(null);
setLocalResults(null);
setProducts([]);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
@@ -340,6 +393,37 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
}
};
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 (
<div className="space-y-8">
{/* Loading state when tenant data is not available */}
@@ -357,7 +441,7 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
</Card>
)}
{/* Improved Upload Stage */}
{/* Upload Stage */}
{(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && (
<>
<div
@@ -418,7 +502,6 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
</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">
@@ -448,7 +531,7 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
</div>
</div>
{/* Improved Template Download Section */}
{/* 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">
@@ -540,26 +623,20 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
</Card>
)}
{/* Simplified Results Stage */}
{stage === 'completed' && results && (
{/* Review Stage - Combined validation results and product approval */}
{(stage === 'review' || (stage === 'completed' && products.length > 0)) && results && (
<div className="space-y-8">
{/* Success Header */}
{/* Success Header with Validation Results */}
<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!
¡Validación Completada!
</h3>
<p className="text-[var(--text-secondary)] max-w-2xl mx-auto">
{results.validationMessage || 'Tus datos han sido procesados exitosamente'}
{results.validationMessage || 'Tus datos han sido procesados exitosamente. Revisa y aprueba los productos detectados.'}
</p>
{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>
)}
</div>
{/* Enhanced Stats Cards */}
@@ -576,60 +653,182 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
<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>
</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>
{/* 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>
{/* Product Review Section */}
{products.length > 0 && (
<>
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{stats.total}</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Productos detectados</div>
</Card>
)}
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">{stats.approved}</div>
<div className="text-sm font-medium text-[var(--color-success)]">Aprobados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">{stats.rejected}</div>
<div className="text-sm font-medium text-[var(--color-error)]">Rechazados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="text-3xl font-bold text-[var(--color-warning)] mb-2">{stats.pending}</div>
<div className="text-sm font-medium text-[var(--color-warning)]">Pendientes</div>
</Card>
</div>
{/* 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>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'Todas las categorías' : category}
</option>
))}
</ul>
</Card>
)}
</div>
</select>
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{getFilteredProducts().length} productos
</Badge>
</div>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5"
>
Aprobar todos
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
Rechazar todos
</Button>
</div>
</div>
{/* Products List */}
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{product.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? '✓ Aprobado' :
product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{product.category}
</Badge>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getConfidenceColor(product.confidence)}`}>
{product.confidence}% confianza
</Badge>
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
{product.original_name && product.original_name !== product.name && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[120px]">Nombre original:</span>
<span className="font-medium text-[var(--text-primary)]">{product.original_name}</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Tipo:</span>
<span className="font-medium text-[var(--text-primary)]">
{product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Unidad:</span>
<span className="font-medium text-[var(--text-primary)]">{product.unit_of_measure}</span>
</div>
{product.sales_data && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Ventas:</span>
<span className="font-medium text-[var(--text-primary)]">{product.sales_data.total_quantity}</span>
</div>
)}
</div>
{product.notes && (
<div className="text-xs italic bg-[var(--bg-secondary)] p-2 rounded border-l-4 border-[var(--color-primary)]/30">
<span className="font-medium text-[var(--text-tertiary)]">Nota:</span> {product.notes}
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant={product.status === 'approved' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'approve')}
className={product.status === 'approved'
? 'bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white shadow-sm'
: 'text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5'
}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant={product.status === 'rejected' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'reject')}
className={product.status === 'rejected'
? 'bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white shadow-sm'
: 'text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5'
}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
</Button>
</div>
</div>
</Card>
))}
</div>
</>
)}
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📋 Revisión de Datos de Ventas:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Datos validados</strong> - Tu archivo ha sido procesado y validado correctamente</li>
<li> <strong>Productos detectados</strong> - La IA ha identificado automáticamente productos desde tus ventas</li>
<li> <strong>Revisa cuidadosamente</strong> - Aprueba o rechaza cada producto según sea correcto para tu negocio</li>
<li> <strong>Verifica nombres</strong> - Compara el nombre original vs. el nombre sugerido</li>
<li> <strong>Revisa clasificaciones</strong> - Confirma si son ingredientes o productos terminados</li>
<li> <strong>Usa filtros</strong> - Filtra por categoría para revisar productos similares</li>
<li> <strong>Acciones masivas</strong> - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso</li>
</ul>
</Card>
</div>
)}

View File

@@ -125,16 +125,16 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
}
// 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<OnboardingStepProps> = ({
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<OnboardingStepProps> = ({
});
// 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 {

View File

@@ -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<OnboardingStepProps> = ({
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<Product[]>(() => {
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<string>('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 (
<div className="space-y-8">
<div className="text-center py-16">
<AlertCircle className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-gray-600 mb-2">
No se encontraron productos
</h3>
<p className="text-gray-500 mb-6 max-w-md mx-auto">
No se pudieron detectar productos en el archivo procesado.
Verifique que el archivo contenga datos de ventas válidos.
</p>
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Volver al paso anterior
</Button>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{stats.total}</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Productos detectados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">{stats.approved}</div>
<div className="text-sm font-medium text-[var(--color-success)]">Aprobados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">{stats.rejected}</div>
<div className="text-sm font-medium text-[var(--color-error)]">Rechazados</div>
</Card>
<Card className="p-6 text-center bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="text-3xl font-bold text-[var(--color-warning)] mb-2">{stats.pending}</div>
<div className="text-sm font-medium text-[var(--color-warning)]">Pendientes</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'Todas las categorías' : category}
</option>
))}
</select>
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{getFilteredProducts().length} productos
</Badge>
</div>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5"
>
Aprobar todos
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
Rechazar todos
</Button>
</div>
</div>
{/* Products List */}
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{product.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? '✓ Aprobado' :
product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{product.category}
</Badge>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getConfidenceColor(product.confidence)}`}>
{product.confidence}% confianza
</Badge>
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
{product.original_name && product.original_name !== product.name && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[120px]">Nombre original:</span>
<span className="font-medium text-[var(--text-primary)]">{product.original_name}</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Tipo:</span>
<span className="font-medium text-[var(--text-primary)]">
{product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Unidad:</span>
<span className="font-medium text-[var(--text-primary)]">{product.unit_of_measure}</span>
</div>
{product.sales_data && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Ventas:</span>
<span className="font-medium text-[var(--text-primary)]">{product.sales_data.total_quantity}</span>
</div>
)}
</div>
{product.notes && (
<div className="text-xs italic bg-[var(--bg-secondary)] p-2 rounded border-l-4 border-[var(--color-primary)]/30">
<span className="font-medium text-[var(--text-tertiary)]">Nota:</span> {product.notes}
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant={product.status === 'approved' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'approve')}
className={product.status === 'approved'
? 'bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white shadow-sm'
: 'text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5'
}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant={product.status === 'rejected' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'reject')}
className={product.status === 'rejected'
? 'bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white shadow-sm'
: 'text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5'
}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📋 Revisión de Productos Detectados:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Revise cuidadosamente</strong> - Los productos fueron detectados automáticamente desde sus datos de ventas</li>
<li> <strong>Apruebe o rechace</strong> cada producto según sea correcto para su negocio</li>
<li> <strong>Verifique nombres</strong> - Compare el nombre original vs. el nombre sugerido</li>
<li> <strong>Revise clasificaciones</strong> - Confirme si son ingredientes o productos terminados</li>
<li> <strong>Use filtros</strong> - Filtre por categoría para revisar productos similares</li>
<li> <strong>Acciones masivas</strong> - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso</li>
</ul>
</Card>
</div>
);
};

View File

@@ -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';

View File

@@ -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;

View File

@@ -123,7 +123,7 @@ export const useOnboarding = () => {
): Promise<boolean> => {
const result = await salesProcessing.processFile(file, onProgress);
if (result.success) {
updateStepData('data-processing', {
updateStepData('sales-validation', {
files: { salesData: file },
processingStage: 'completed',
processingResults: result.validationResults,

View File

@@ -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)');
}

View File

@@ -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<any> } = {
'setup': BakerySetupStep,
'data-processing': DataProcessingStep,
'review': ReviewStep,
'sales-validation': HistoricalSalesValidationStep,
'inventory': InventorySetupStep,
'suppliers': SuppliersStep,
'ml-training': MLTrainingStep,