Start integrating the onboarding flow with backend 11
This commit is contained in:
@@ -332,14 +332,8 @@ export {
|
||||
|
||||
// Hooks - Classification
|
||||
export {
|
||||
usePendingSuggestions,
|
||||
useSuggestionHistory,
|
||||
useBusinessModelAnalysis,
|
||||
useClassifyProduct,
|
||||
useClassifyProductsBatch,
|
||||
useApproveClassification,
|
||||
useUpdateSuggestion,
|
||||
useDeleteSuggestion,
|
||||
classificationKeys,
|
||||
} from './hooks/classification';
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user