Fix new services implementation 1
This commit is contained in:
@@ -56,6 +56,28 @@ export interface BusinessModelAnalysis {
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
// Step 1: File validation result
|
||||
export interface FileValidationResult {
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
unique_products: number;
|
||||
product_list: string[];
|
||||
validation_errors: any[];
|
||||
validation_warnings: any[];
|
||||
summary: Record<string, any>;
|
||||
}
|
||||
|
||||
// Step 2: AI suggestions result
|
||||
export interface ProductSuggestionsResult {
|
||||
suggestions: InventorySuggestion[];
|
||||
business_model_analysis: BusinessModelAnalysis;
|
||||
total_products: number;
|
||||
high_confidence_count: number;
|
||||
low_confidence_count: number;
|
||||
processing_time_seconds: number;
|
||||
}
|
||||
|
||||
// Legacy support - will be deprecated
|
||||
export interface OnboardingAnalysisResult {
|
||||
total_products_found: number;
|
||||
inventory_suggestions: InventorySuggestion[];
|
||||
@@ -143,16 +165,16 @@ export class OnboardingService {
|
||||
return apiClient.get(`${this.baseEndpoint}/can-access/${stepName}`);
|
||||
}
|
||||
|
||||
// ========== AUTOMATED INVENTORY CREATION METHODS ==========
|
||||
// ========== NEW 4-STEP AUTOMATED INVENTORY CREATION METHODS ==========
|
||||
|
||||
/**
|
||||
* Phase 1: Analyze sales data and get AI suggestions
|
||||
* Step 1: Validate file and extract unique products
|
||||
*/
|
||||
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
|
||||
async validateFileAndExtractProducts(tenantId: string, file: File): Promise<FileValidationResult> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return apiClient.post(`/tenants/${tenantId}/onboarding/analyze`, formData, {
|
||||
return apiClient.post(`/tenants/${tenantId}/onboarding/validate-file`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -160,7 +182,26 @@ export class OnboardingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Create inventory from approved suggestions
|
||||
* Step 2: Generate AI-powered inventory suggestions
|
||||
*/
|
||||
async generateInventorySuggestions(
|
||||
tenantId: string,
|
||||
file: File,
|
||||
productList: string[]
|
||||
): Promise<ProductSuggestionsResult> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('product_list', JSON.stringify(productList));
|
||||
|
||||
return apiClient.post(`/tenants/${tenantId}/onboarding/generate-suggestions`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Create inventory from approved suggestions
|
||||
*/
|
||||
async createInventoryFromSuggestions(
|
||||
tenantId: string,
|
||||
@@ -176,7 +217,7 @@ export class OnboardingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3: Import sales data with inventory mapping
|
||||
* Step 4: Final sales data import with inventory mapping
|
||||
*/
|
||||
async importSalesWithInventory(
|
||||
tenantId: string,
|
||||
@@ -194,6 +235,35 @@ export class OnboardingService {
|
||||
});
|
||||
}
|
||||
|
||||
// ========== LEGACY METHODS (for backward compatibility) ==========
|
||||
|
||||
/**
|
||||
* @deprecated Use the new 4-step flow instead
|
||||
* Phase 1: Analyze sales data and get AI suggestions (OLD METHOD)
|
||||
*/
|
||||
async analyzeSalesDataForOnboarding(tenantId: string, file: File): Promise<OnboardingAnalysisResult> {
|
||||
// This method will use the new flow under the hood for backward compatibility
|
||||
const validationResult = await this.validateFileAndExtractProducts(tenantId, file);
|
||||
|
||||
if (!validationResult.is_valid) {
|
||||
throw new Error(`File validation failed: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`);
|
||||
}
|
||||
|
||||
const suggestionsResult = await this.generateInventorySuggestions(tenantId, file, validationResult.product_list);
|
||||
|
||||
// Convert to legacy format
|
||||
return {
|
||||
total_products_found: suggestionsResult.total_products,
|
||||
inventory_suggestions: suggestionsResult.suggestions,
|
||||
business_model_analysis: suggestionsResult.business_model_analysis,
|
||||
import_job_id: `legacy-${Date.now()}`,
|
||||
status: 'completed',
|
||||
processed_rows: validationResult.total_records,
|
||||
errors: validationResult.validation_errors.map(e => e.message || String(e)),
|
||||
warnings: validationResult.validation_warnings.map(w => w.message || String(w))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get business model guidance based on analysis
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import {
|
||||
FileValidationResult,
|
||||
ProductSuggestionsResult,
|
||||
OnboardingAnalysisResult,
|
||||
InventorySuggestion,
|
||||
BusinessModelAnalysis,
|
||||
@@ -35,12 +37,13 @@ interface SmartHistoricalDataImportProps {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
type ImportPhase = 'upload' | 'analysis' | 'review' | 'creation' | 'import' | 'complete';
|
||||
type ImportPhase = 'upload' | 'validation' | 'suggestions' | 'review' | 'creation' | 'import' | 'complete';
|
||||
|
||||
interface PhaseState {
|
||||
phase: ImportPhase;
|
||||
file?: File;
|
||||
analysisResult?: OnboardingAnalysisResult;
|
||||
validationResult?: FileValidationResult;
|
||||
suggestionsResult?: ProductSuggestionsResult;
|
||||
reviewedSuggestions?: InventorySuggestion[];
|
||||
creationResult?: InventoryCreationResult;
|
||||
importResult?: SalesImportResult;
|
||||
@@ -57,22 +60,56 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
|
||||
|
||||
const handleFileUpload = useCallback(async (file: File) => {
|
||||
setState(prev => ({ ...prev, file, phase: 'analysis' }));
|
||||
setState(prev => ({ ...prev, file, phase: 'validation' }));
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
toast.loading('🧠 Analizando tu archivo con IA...', { id: 'analysis' });
|
||||
// Step 1: Validate file and extract products
|
||||
toast.loading('📋 Validando archivo...', { id: 'validation' });
|
||||
|
||||
const analysisResult = await onboardingService.analyzeSalesDataForOnboarding(tenantId, file);
|
||||
const validationResult = await onboardingService.validateFileAndExtractProducts(tenantId, file);
|
||||
|
||||
toast.success(`¡Análisis completado! ${analysisResult.total_products_found} productos encontrados`, {
|
||||
id: 'analysis'
|
||||
if (!validationResult.is_valid) {
|
||||
throw new Error(`Archivo inválido: ${validationResult.validation_errors.map(e => e.message || e).join(', ')}`);
|
||||
}
|
||||
|
||||
toast.success(`¡Archivo válido! ${validationResult.unique_products} productos únicos encontrados`, {
|
||||
id: 'validation'
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, validationResult, phase: 'suggestions' }));
|
||||
|
||||
// Step 2: Generate AI suggestions
|
||||
setTimeout(() => handleGenerateSuggestions(file, validationResult.product_list), 1000);
|
||||
|
||||
} catch (error: any) {
|
||||
toast.error('Error al validar el archivo', { id: 'validation' });
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error.message || 'Error de validación',
|
||||
phase: 'upload'
|
||||
}));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
const handleGenerateSuggestions = useCallback(async (file: File, productList: string[]) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
toast.loading('🧠 Generando sugerencias con IA...', { id: 'suggestions' });
|
||||
|
||||
const suggestionsResult = await onboardingService.generateInventorySuggestions(tenantId, file, productList);
|
||||
|
||||
toast.success(`¡${suggestionsResult.total_products} productos clasificados! ${suggestionsResult.high_confidence_count} con alta confianza`, {
|
||||
id: 'suggestions'
|
||||
});
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
analysisResult,
|
||||
reviewedSuggestions: analysisResult.inventory_suggestions.map(s => ({
|
||||
suggestionsResult,
|
||||
reviewedSuggestions: suggestionsResult.suggestions.map(s => ({
|
||||
...s,
|
||||
user_approved: s.confidence_score >= 0.7
|
||||
})),
|
||||
@@ -80,11 +117,11 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
}));
|
||||
|
||||
} catch (error: any) {
|
||||
toast.error('Error al analizar el archivo', { id: 'analysis' });
|
||||
toast.error('Error al generar sugerencias', { id: 'suggestions' });
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error.message || 'Error desconocido',
|
||||
phase: 'upload'
|
||||
error: error.message || 'Error en sugerencias de IA',
|
||||
phase: 'validation'
|
||||
}));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
@@ -475,17 +512,17 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'analysis':
|
||||
case 'validation':
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-20 h-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
|
||||
<Brain className="w-10 h-10 text-white" />
|
||||
<div className="w-20 h-20 bg-gradient-to-r from-blue-500 to-green-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
|
||||
<CheckCircle2 className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-3">
|
||||
🧠 Analizando tu archivo con IA...
|
||||
📋 Validando archivo...
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Esto puede tomar unos momentos mientras clasificamos tus productos
|
||||
Verificando formato y extrayendo productos únicos
|
||||
</p>
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 max-w-md mx-auto">
|
||||
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||
@@ -493,16 +530,48 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
<span className="font-medium">{state.file?.name}</span>
|
||||
</div>
|
||||
<div className="mt-2 bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-500 h-2 rounded-full w-1/2 animate-pulse"></div>
|
||||
<div className="bg-gradient-to-r from-blue-500 to-green-500 h-2 rounded-full w-1/3 animate-pulse"></div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">Paso 1 de 4: Validación</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'suggestions':
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-20 h-20 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
|
||||
<Brain className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-3">
|
||||
🧠 Generando sugerencias con IA...
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Clasificando productos y analizando tu modelo de negocio
|
||||
</p>
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 max-w-md mx-auto">
|
||||
{state.validationResult && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-center space-x-2 text-green-600 mb-2">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">
|
||||
{state.validationResult.unique_products} productos únicos encontrados
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full w-2/3 animate-pulse"></div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">Paso 2 de 4: Clasificación IA</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'review':
|
||||
if (!state.analysisResult) return null;
|
||||
if (!state.suggestionsResult) return null;
|
||||
|
||||
const { analysisResult, reviewedSuggestions } = state;
|
||||
const { suggestionsResult, reviewedSuggestions } = state;
|
||||
const approvedCount = reviewedSuggestions?.filter(s => s.user_approved).length || 0;
|
||||
const highConfidenceCount = reviewedSuggestions?.filter(s => s.confidence_score >= 0.7).length || 0;
|
||||
const visibleSuggestions = showAllSuggestions
|
||||
@@ -519,12 +588,15 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
¡Análisis Completado! 🎉
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
Hemos encontrado <strong>{analysisResult.total_products_found} productos</strong> y
|
||||
Hemos encontrado <strong>{suggestionsResult.total_products} productos</strong> y
|
||||
sugerimos <strong>{approvedCount} para tu inventario</strong>
|
||||
</p>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
⚡ Procesado en {suggestionsResult.processing_time_seconds.toFixed(1)}s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderBusinessModelInsight(analysisResult.business_model_analysis)}
|
||||
{renderBusinessModelInsight(suggestionsResult.business_model_analysis)}
|
||||
|
||||
<div className="bg-white border rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -579,15 +651,15 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
{visibleSuggestions?.map(renderSuggestionCard)}
|
||||
</div>
|
||||
|
||||
{analysisResult.warnings.length > 0 && (
|
||||
{state.validationResult?.validation_warnings && state.validationResult.validation_warnings.length > 0 && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
|
||||
<div className="flex">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-400" />
|
||||
<div className="ml-3">
|
||||
<h4 className="text-sm font-medium text-amber-800">Advertencias</h4>
|
||||
<h4 className="text-sm font-medium text-amber-800">Advertencias de Validación</h4>
|
||||
<ul className="mt-2 text-sm text-amber-700 space-y-1">
|
||||
{analysisResult.warnings.map((warning, idx) => (
|
||||
<li key={idx}>• {warning}</li>
|
||||
{state.validationResult.validation_warnings.map((warning, idx) => (
|
||||
<li key={idx}>• {warning.message || warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -630,6 +702,9 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
case 'creation':
|
||||
case 'import':
|
||||
const isCreating = state.phase === 'creation';
|
||||
const stepNumber = isCreating ? 3 : 4;
|
||||
const stepProgress = isCreating ? 75 : 90;
|
||||
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-20 h-20 bg-gradient-to-r from-green-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
|
||||
@@ -662,11 +737,17 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
|
||||
)}
|
||||
|
||||
<div className="bg-gray-200 rounded-full h-3">
|
||||
<div className="bg-gradient-to-r from-green-400 to-blue-500 h-3 rounded-full w-3/4 animate-pulse"></div>
|
||||
<div
|
||||
className="bg-gradient-to-r from-green-400 to-blue-500 h-3 rounded-full animate-pulse transition-all duration-500"
|
||||
style={{ width: `${stepProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{isCreating ? 'Creando inventario...' : 'Procesando importación final...'}
|
||||
</p>
|
||||
<span className="text-xs text-gray-400">Paso {stepNumber} de 4</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
{isCreating ? 'Creando inventario...' : 'Procesando importación final...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Filter,
|
||||
Download,
|
||||
Upload,
|
||||
Grid3X3,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Package,
|
||||
TrendingDown,
|
||||
@@ -143,6 +143,14 @@ const InventoryPage: React.FC = () => {
|
||||
console.log('View details:', item);
|
||||
};
|
||||
|
||||
// Handle view item by ID (for alerts)
|
||||
const handleViewItemById = (itemId: string) => {
|
||||
const item = items.find(item => item.id === itemId);
|
||||
if (item) {
|
||||
handleItemViewDetails(item);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle alert acknowledgment
|
||||
const handleAcknowledgeAlert = async (alertId: string) => {
|
||||
await acknowledgeAlert(alertId);
|
||||
@@ -302,7 +310,7 @@ const InventoryPage: React.FC = () => {
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
@@ -529,7 +537,7 @@ const InventoryPage: React.FC = () => {
|
||||
alerts={alerts}
|
||||
onAcknowledge={handleAcknowledgeAlert}
|
||||
onAcknowledgeAll={handleBulkAcknowledgeAlerts}
|
||||
onViewItem={handleItemViewDetails}
|
||||
onViewItem={handleViewItemById}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Search,
|
||||
Plus,
|
||||
Filter,
|
||||
Grid3X3,
|
||||
LayoutGrid,
|
||||
List,
|
||||
ChefHat,
|
||||
TrendingUp,
|
||||
@@ -304,7 +304,7 @@ const RecipesPage: React.FC = () => {
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
|
||||
Reference in New Issue
Block a user