Start integrating the onboarding flow with backend 14

This commit is contained in:
Urtzi Alfaro
2025-09-07 22:54:14 +02:00
parent 05898d504d
commit 0060b9cccb
13 changed files with 668 additions and 487 deletions

View File

@@ -34,11 +34,22 @@ export class OnboardingService {
): Promise<UserProgress> { ): Promise<UserProgress> {
// Backend uses current user from auth token, so userId parameter is ignored // Backend uses current user from auth token, so userId parameter is ignored
// Backend expects UpdateStepRequest format for completion // Backend expects UpdateStepRequest format for completion
return apiClient.put<UserProgress>(`${this.baseUrl}/step`, { const requestBody = {
step_name: stepName, step_name: stepName,
completed: true, completed: true,
data: data, data: data,
}); };
console.log(`🔄 API call to mark step "${stepName}" as completed:`, requestBody);
try {
const response = await apiClient.put<UserProgress>(`${this.baseUrl}/step`, requestBody);
console.log(`✅ Step "${stepName}" marked as completed successfully:`, response);
return response;
} catch (error) {
console.error(`❌ API error marking step "${stepName}" as completed:`, error);
throw error;
}
} }
async resetProgress(userId: string): Promise<UserProgress> { async resetProgress(userId: string): Promise<UserProgress> {

View File

@@ -120,7 +120,20 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
const tenantCreatedSuccessfully = tenantCreation.isSuccess; const tenantCreatedSuccessfully = tenantCreation.isSuccess;
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true; const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
return Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding)); const result = Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding));
console.log('🔄 SmartInventorySetup - isTenantAvailable check:', {
hasAuth,
authLoading,
hasUser: !!user,
hasTenantId,
tenantId: getTenantId(),
tenantCreatedSuccessfully,
tenantCreatedInOnboarding,
finalResult: result
});
return result;
}; };
// Local state // Local state
@@ -144,7 +157,14 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
// Update products when suggestions change // Update products when suggestions change
useEffect(() => { useEffect(() => {
console.log('🔄 SmartInventorySetup - suggestions effect:', {
suggestionsLength: suggestions?.length || 0,
productsLength: products.length,
suggestions: suggestions,
});
if (suggestions && suggestions.length > 0 && products.length === 0) { if (suggestions && suggestions.length > 0 && products.length === 0) {
console.log('✅ Converting suggestions to products and setting stage to review');
const newProducts = convertSuggestionsToCards(suggestions); const newProducts = convertSuggestionsToCards(suggestions);
setProducts(newProducts); setProducts(newProducts);
setLocalStage('review'); setLocalStage('review');
@@ -157,6 +177,17 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
: (onboardingStage === 'completed' ? 'review' : onboardingStage || localStage); : (onboardingStage === 'completed' ? 'review' : onboardingStage || localStage);
const progress = onboardingProgress || 0; const progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || ''; const currentMessage = onboardingMessage || '';
// Debug current stage
useEffect(() => {
console.log('🔄 SmartInventorySetup - Stage debug:', {
localStage,
onboardingStage,
finalStage: stage,
productsLength: products.length,
suggestionsLength: suggestions?.length || 0
});
}, [localStage, onboardingStage, stage, products.length, suggestions?.length]);
// Product stats // Product stats
const stats = useMemo(() => ({ const stats = useMemo(() => ({
@@ -247,15 +278,20 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
setLocalStage('validating'); setLocalStage('validating');
try { try {
console.log('🔄 SmartInventorySetup - Processing file:', file.name);
const success = await processSalesFile(file); const success = await processSalesFile(file);
console.log('🔄 SmartInventorySetup - Processing result:', { success });
if (success) { if (success) {
console.log('✅ File processed successfully, setting stage to review');
setLocalStage('review'); setLocalStage('review');
toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', { toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', {
title: 'Procesamiento completado', title: 'Procesamiento completado',
type: 'success' type: 'success'
}); });
} else { } else {
console.error('❌ File processing failed - processSalesFile returned false');
throw new Error('Error procesando el archivo'); throw new Error('Error procesando el archivo');
} }
} catch (error) { } catch (error) {

View File

@@ -39,10 +39,14 @@ export const useOnboardingActions = () => {
const nextStep = useCallback(async (): Promise<boolean> => { const nextStep = useCallback(async (): Promise<boolean> => {
try { try {
const currentStep = store.getCurrentStep(); const currentStep = store.getCurrentStep();
if (!currentStep) return false;
if (!currentStep) {
return false;
}
// Validate current step // Validate current step
const validation = validateCurrentStep(); const validation = validateCurrentStep();
if (validation) { if (validation) {
store.setError(validation); store.setError(validation);
return false; return false;
@@ -52,8 +56,21 @@ export const useOnboardingActions = () => {
// Handle step-specific actions before moving to next step // Handle step-specific actions before moving to next step
if (currentStep.id === 'setup') { if (currentStep.id === 'setup') {
const bakeryData = store.getStepData('setup').bakery; // IMPORTANT: Ensure user_registered step is completed first
if (bakeryData && !tenantCreation.isSuccess) { const userRegisteredCompleted = await progressTracking.markStepCompleted('user_registered', {});
if (!userRegisteredCompleted) {
console.error('❌ Failed to mark user_registered as completed');
store.setError('Failed to verify user registration status');
return false;
}
const stepData = store.getStepData('setup');
const bakeryData = stepData.bakery;
// Check if tenant creation is needed
const needsTenantCreation = bakeryData && !bakeryData.tenantCreated && !bakeryData.tenant_id;
if (needsTenantCreation) {
store.setLoading(true); store.setLoading(true);
const success = await tenantCreation.createTenant(bakeryData); const success = await tenantCreation.createTenant(bakeryData);
store.setLoading(false); store.setLoading(false);
@@ -62,6 +79,11 @@ export const useOnboardingActions = () => {
store.setError(tenantCreation.error || 'Error creating tenant'); store.setError(tenantCreation.error || 'Error creating tenant');
return false; return false;
} }
console.log('✅ Tenant created successfully');
// Wait a moment for backend to update state
await new Promise(resolve => setTimeout(resolve, 1000));
} }
} }
@@ -100,7 +122,13 @@ export const useOnboardingActions = () => {
// Save progress to backend // Save progress to backend
const stepData = store.getStepData(currentStep.id); const stepData = store.getStepData(currentStep.id);
await progressTracking.markStepCompleted(currentStep.id, stepData);
const markCompleted = await progressTracking.markStepCompleted(currentStep.id, stepData);
if (!markCompleted) {
console.error(`❌ Failed to mark step "${currentStep.id}" as completed`);
store.setError(`Failed to save progress for step "${currentStep.id}"`);
return false;
}
// Move to next step // Move to next step
if (store.nextStep()) { if (store.nextStep()) {
@@ -223,7 +251,7 @@ export const useOnboardingActions = () => {
store.setLoading(false); store.setLoading(false);
if (success) { if (success) {
console.log('✅ Onboarding fully completed!'); console.log('✅ Onboarding completed');
store.markStepCompleted(store.steps.length - 1); store.markStepCompleted(store.steps.length - 1);
// Navigate to dashboard after completion // Navigate to dashboard after completion

View File

@@ -61,6 +61,11 @@ const initialState = {
progress: null, progress: null,
}; };
// Debug logging for store initialization (only if there's an issue)
if (initialState.steps.length !== DEFAULT_STEPS.length) {
console.error('⚠️ Store initialization issue: steps count mismatch');
}
export const useOnboardingStore = create<OnboardingStore>()( export const useOnboardingStore = create<OnboardingStore>()(
devtools( devtools(
(set, get) => ({ (set, get) => ({

View File

@@ -1,33 +1,30 @@
/** /**
* Inventory setup service - Clean, standardized implementation * Inventory setup service - Simplified implementation
*/ */
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { import {
useCreateIngredient, useCreateIngredient,
useCreateSalesRecord, useCreateSalesRecord,
} from '../../../../api'; } from '../../../../api';
import { useCurrentTenant } from '../../../../stores'; import { useCurrentTenant } from '../../../../stores';
import { useOnboardingStore } from '../core/store'; import { useOnboardingStore } from '../core/store';
import { createServiceHook } from '../utils/createServiceHook'; import type { ProductSuggestionResponse } from '../core/types';
import type { InventorySetupState, ProductSuggestionResponse } from '../core/types';
const useInventorySetupService = createServiceHook<InventorySetupState>({
initialState: {
createdItems: [],
inventoryMapping: {},
salesImportResult: null,
isInventoryConfigured: false,
},
});
export const useInventorySetup = () => { export const useInventorySetup = () => {
const service = useInventorySetupService();
const createIngredientMutation = useCreateIngredient(); const createIngredientMutation = useCreateIngredient();
const createSalesRecordMutation = useCreateSalesRecord(); const createSalesRecordMutation = useCreateSalesRecord();
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const { setStepData } = useOnboardingStore(); const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createdItems, setCreatedItems] = useState<any[]>([]);
const [inventoryMapping, setInventoryMapping] = useState<{ [productName: string]: string }>({});
const [salesImportResult, setSalesImportResult] = useState<any | null>(null);
const [isInventoryConfigured, setIsInventoryConfigured] = useState(false);
const createInventoryFromSuggestions = useCallback(async ( const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[] suggestions: ProductSuggestionResponse[]
): Promise<{ ): Promise<{
@@ -35,29 +32,26 @@ export const useInventorySetup = () => {
createdItems?: any[]; createdItems?: any[];
inventoryMapping?: { [productName: string]: string }; inventoryMapping?: { [productName: string]: string };
}> => { }> => {
console.log('useInventorySetup - createInventoryFromSuggestions called with:', { console.log('🔄 Creating inventory from suggestions:', suggestions?.length, 'items');
suggestionsCount: suggestions?.length,
suggestions: suggestions?.slice(0, 3), // Log first 3 for debugging
tenantId: currentTenant?.id
});
if (!suggestions || suggestions.length === 0) { if (!suggestions || suggestions.length === 0) {
console.error('useInventorySetup - No suggestions provided'); console.error(' No suggestions provided');
service.setError('No hay sugerencias para crear el inventario'); setError('No hay sugerencias para crear el inventario');
return { success: false }; return { success: false };
} }
if (!currentTenant?.id) { if (!currentTenant?.id) {
console.error('useInventorySetup - No tenant ID available'); console.error(' No tenant ID available');
service.setError('No se pudo obtener información del tenant'); setError('No se pudo obtener información del tenant');
return { success: false }; return { success: false };
} }
const result = await service.executeAsync(async () => { setIsLoading(true);
const createdItems = []; setError(null);
const inventoryMapping: { [key: string]: string } = {};
try {
console.log('useInventorySetup - Creating ingredients from suggestions...'); const newCreatedItems = [];
const newInventoryMapping: { [key: string]: string } = {};
// Create ingredients from approved suggestions // Create ingredients from approved suggestions
for (const suggestion of suggestions) { for (const suggestion of suggestions) {
@@ -80,74 +74,54 @@ export const useInventorySetup = () => {
notes: suggestion.notes || `Producto clasificado automáticamente desde: ${suggestion.original_name}`, notes: suggestion.notes || `Producto clasificado automáticamente desde: ${suggestion.original_name}`,
}; };
console.log('useInventorySetup - Creating ingredient:', {
name: ingredientData.name,
category: ingredientData.category,
original_name: suggestion.original_name,
ingredientData: ingredientData,
tenantId: currentTenant.id,
apiUrl: `/tenants/${currentTenant.id}/ingredients`
});
const createdItem = await createIngredientMutation.mutateAsync({ const createdItem = await createIngredientMutation.mutateAsync({
tenantId: currentTenant.id, tenantId: currentTenant.id,
ingredientData, ingredientData,
}); });
console.log('useInventorySetup - Created ingredient successfully:', { newCreatedItems.push(createdItem);
id: createdItem.id,
name: createdItem.name
});
createdItems.push(createdItem);
// Map both original and suggested names to the same ingredient ID for flexibility // Map both original and suggested names to the same ingredient ID for flexibility
inventoryMapping[suggestion.original_name] = createdItem.id; newInventoryMapping[suggestion.original_name] = createdItem.id;
if (suggestion.suggested_name && suggestion.suggested_name !== suggestion.original_name) { if (suggestion.suggested_name && suggestion.suggested_name !== suggestion.original_name) {
inventoryMapping[suggestion.suggested_name] = createdItem.id; newInventoryMapping[suggestion.suggested_name] = createdItem.id;
} }
} catch (error) { } catch (error) {
console.error(`Error creating ingredient ${suggestion.suggested_name || suggestion.original_name}:`, error); console.error(`Error creating ingredient ${suggestion.suggested_name || suggestion.original_name}:`, error);
// Continue with other ingredients even if one fails // Continue with other ingredients even if one fails
} }
} }
if (createdItems.length === 0) { if (newCreatedItems.length === 0) {
throw new Error('No se pudo crear ningún elemento del inventario'); throw new Error('No se pudo crear ningún elemento del inventario');
} }
console.log('useInventorySetup - Successfully created ingredients:', { console.log('✅ Created', newCreatedItems.length, '/', suggestions.length, 'ingredients');
createdCount: createdItems.length,
totalSuggestions: suggestions.length,
inventoryMapping
});
// Update service state // Update state
service.setSuccess({ setCreatedItems(newCreatedItems);
...service.data, setInventoryMapping(newInventoryMapping);
createdItems, setIsInventoryConfigured(true);
inventoryMapping,
isInventoryConfigured: true,
});
// Update onboarding store // Update onboarding store
setStepData('smart-inventory-setup', { setStepData('smart-inventory-setup', {
inventoryItems: createdItems, inventoryItems: newCreatedItems,
inventoryMapping, inventoryMapping: newInventoryMapping,
inventoryConfigured: true, inventoryConfigured: true,
}); });
return { return {
createdItems, success: true,
inventoryMapping, createdItems: newCreatedItems,
inventoryMapping: newInventoryMapping,
}; };
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
return { setError(errorMessage);
success: result.success, return { success: false };
createdItems: result.data?.createdItems, } finally {
inventoryMapping: result.data?.inventoryMapping, setIsLoading(false);
}; }
}, [service, createIngredientMutation, currentTenant, setStepData]); }, [createIngredientMutation, currentTenant, setStepData]);
const importSalesData = useCallback(async ( const importSalesData = useCallback(async (
salesData: any, salesData: any,
@@ -158,7 +132,7 @@ export const useInventorySetup = () => {
message: string; message: string;
}> => { }> => {
if (!currentTenant?.id) { if (!currentTenant?.id) {
service.setError('No se pudo obtener información del tenant'); setError('No se pudo obtener información del tenant');
return { return {
success: false, success: false,
recordsCreated: 0, recordsCreated: 0,
@@ -167,7 +141,7 @@ export const useInventorySetup = () => {
} }
if (!salesData || !salesData.product_list) { if (!salesData || !salesData.product_list) {
service.setError('No hay datos de ventas para importar'); setError('No hay datos de ventas para importar');
return { return {
success: false, success: false,
recordsCreated: 0, recordsCreated: 0,
@@ -175,7 +149,10 @@ export const useInventorySetup = () => {
}; };
} }
const result = await service.executeAsync(async () => { setIsLoading(true);
setError(null);
try {
let recordsCreated = 0; let recordsCreated = 0;
// Process actual sales data and create sales records // Process actual sales data and create sales records
@@ -221,11 +198,8 @@ export const useInventorySetup = () => {
: 'No se pudieron importar registros de ventas', : 'No se pudieron importar registros de ventas',
}; };
// Update service state // Update state
service.setSuccess({ setSalesImportResult(importResult);
...service.data,
salesImportResult: importResult,
});
// Update onboarding store // Update onboarding store
setStepData('smart-inventory-setup', { setStepData('smart-inventory-setup', {
@@ -233,31 +207,49 @@ export const useInventorySetup = () => {
}); });
return { return {
success: recordsCreated > 0,
recordsCreated, recordsCreated,
message: importResult.message, message: importResult.message,
}; };
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error importando datos de ventas';
setError(errorMessage);
return {
success: false,
recordsCreated: 0,
message: errorMessage,
};
} finally {
setIsLoading(false);
}
}, [createSalesRecordMutation, currentTenant, setStepData]);
return { const clearError = useCallback(() => {
success: result.success, setError(null);
recordsCreated: result.data?.recordsCreated || 0, }, []);
message: result.data?.message || (result.error || 'Error importando datos de ventas'),
}; const reset = useCallback(() => {
}, [service, createSalesRecordMutation, currentTenant, setStepData]); setIsLoading(false);
setError(null);
setCreatedItems([]);
setInventoryMapping({});
setSalesImportResult(null);
setIsInventoryConfigured(false);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
createdItems: service.data?.createdItems || [], createdItems,
inventoryMapping: service.data?.inventoryMapping || {}, inventoryMapping,
salesImportResult: service.data?.salesImportResult || null, salesImportResult,
isInventoryConfigured: service.data?.isInventoryConfigured || false, isInventoryConfigured,
// Actions // Actions
createInventoryFromSuggestions, createInventoryFromSuggestions,
importSalesData, importSalesData,
clearError: service.clearError, clearError,
reset: service.reset, reset,
}; };
}; };

View File

@@ -1,77 +1,81 @@
/** /**
* Progress tracking service - Clean, standardized implementation * Progress tracking service - Simplified implementation
*/ */
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { onboardingService } from '../../../../api/services/onboarding'; import { onboardingService } from '../../../../api/services/onboarding';
import { createServiceHook } from '../utils/createServiceHook';
import type { ProgressTrackingState } from '../core/types';
import type { UserProgress } from '../../../../api/types/onboarding'; import type { UserProgress } from '../../../../api/types/onboarding';
const useProgressTrackingService = createServiceHook<ProgressTrackingState>({
initialState: {
progress: null,
isInitialized: false,
isCompleted: false,
completionPercentage: 0,
currentBackendStep: null,
},
});
export const useProgressTracking = () => { export const useProgressTracking = () => {
const service = useProgressTrackingService();
const initializationAttempted = useRef(false); const initializationAttempted = useRef(false);
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState<UserProgress | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [completionPercentage, setCompletionPercentage] = useState(0);
const [currentBackendStep, setCurrentBackendStep] = useState<string | null>(null);
// Load initial progress from backend // Load initial progress from backend
const loadProgress = useCallback(async (): Promise<UserProgress | null> => { const loadProgress = useCallback(async (): Promise<UserProgress | null> => {
const result = await service.executeAsync(async () => { setIsLoading(true);
const progress = await onboardingService.getUserProgress(''); setError(null);
// Update service state with additional computed values
const updatedData = {
...service.data!,
progress,
isInitialized: true,
isCompleted: progress?.fully_completed || false,
completionPercentage: progress?.completion_percentage || 0,
currentBackendStep: progress?.current_step || null,
};
service.setSuccess(updatedData);
return progress;
});
return result.data || null; try {
}, [service]); const progressData = await onboardingService.getUserProgress('');
// Update state
setProgress(progressData);
setIsInitialized(true);
setIsCompleted(progressData?.fully_completed || false);
setCompletionPercentage(progressData?.completion_percentage || 0);
setCurrentBackendStep(progressData?.current_step || null);
console.log('📊 Progress loaded:', progressData?.current_step, '(', progressData?.completion_percentage, '%)');
return progressData;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error loading progress';
setError(errorMessage);
return null;
} finally {
setIsLoading(false);
}
}, []);
// Mark a step as completed and save to backend // Mark a step as completed and save to backend
const markStepCompleted = useCallback(async ( const markStepCompleted = useCallback(async (
stepId: string, stepId: string,
data?: Record<string, any> data?: Record<string, any>
): Promise<boolean> => { ): Promise<boolean> => {
const result = await service.executeAsync(async () => { console.log(`🔄 Attempting to mark step "${stepId}" as completed with data:`, data);
setIsLoading(true);
setError(null);
try {
const updatedProgress = await onboardingService.markStepAsCompleted(stepId, data); const updatedProgress = await onboardingService.markStepAsCompleted(stepId, data);
// Update service state // Update state
service.setSuccess({ setProgress(updatedProgress);
...service.data!, setIsCompleted(updatedProgress?.fully_completed || false);
progress: updatedProgress, setCompletionPercentage(updatedProgress?.completion_percentage || 0);
isCompleted: updatedProgress?.fully_completed || false, setCurrentBackendStep(updatedProgress?.current_step || null);
completionPercentage: updatedProgress?.completion_percentage || 0,
currentBackendStep: updatedProgress?.current_step || null,
});
console.log(`✅ Step "${stepId}" marked as completed in backend`); console.log(`✅ Step "${stepId}" marked as completed in backend`);
return updatedProgress; return true;
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : `Error marking step "${stepId}" as completed`;
if (!result.success) { setError(errorMessage);
console.error(`❌ Error marking step "${stepId}" as completed:`, result.error); console.error(`❌ Error marking step "${stepId}" as completed:`, error);
console.error(`❌ Attempted to send data:`, data);
return false;
} finally {
setIsLoading(false);
} }
}, []);
return result.success;
}, [service]);
// Get the next step the user should work on // Get the next step the user should work on
const getNextStep = useCallback(async (): Promise<string> => { const getNextStep = useCallback(async (): Promise<string> => {
@@ -95,21 +99,28 @@ export const useProgressTracking = () => {
// Complete the entire onboarding process // Complete the entire onboarding process
const completeOnboarding = useCallback(async (): Promise<boolean> => { const completeOnboarding = useCallback(async (): Promise<boolean> => {
const result = await service.executeAsync(async () => { setIsLoading(true);
setError(null);
try {
const result = await onboardingService.completeOnboarding(); const result = await onboardingService.completeOnboarding();
if (result.success) { if (result.success) {
// Reload progress to get updated status // Reload progress to get updated status
await loadProgress(); await loadProgress();
console.log('🎉 Onboarding completed successfully!'); console.log('🎉 Onboarding completed successfully!');
return result; return true;
} }
throw new Error('Failed to complete onboarding'); throw new Error('Failed to complete onboarding');
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
return result.success; setError(errorMessage);
}, [service, loadProgress]); return false;
} finally {
setIsLoading(false);
}
}, [loadProgress]);
// Check if user can access a specific step // Check if user can access a specific step
const canAccessStep = useCallback(async (stepId: string): Promise<boolean> => { const canAccessStep = useCallback(async (stepId: string): Promise<boolean> => {
@@ -124,21 +135,25 @@ export const useProgressTracking = () => {
// Auto-load progress on hook initialization - PREVENT multiple attempts // Auto-load progress on hook initialization - PREVENT multiple attempts
useEffect(() => { useEffect(() => {
if (!service.data?.isInitialized && !initializationAttempted.current && !service.isLoading) { if (!isInitialized && !initializationAttempted.current && !isLoading) {
initializationAttempted.current = true; initializationAttempted.current = true;
loadProgress(); loadProgress();
} }
}, [service.data?.isInitialized, service.isLoading]); // Remove loadProgress from deps }, [isInitialized, isLoading, loadProgress]);
const clearError = useCallback(() => {
setError(null);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
progress: service.data?.progress || null, progress,
isInitialized: service.data?.isInitialized || false, isInitialized,
isCompleted: service.data?.isCompleted || false, isCompleted,
completionPercentage: service.data?.completionPercentage || 0, completionPercentage,
currentBackendStep: service.data?.currentBackendStep || null, currentBackendStep,
// Actions // Actions
loadProgress, loadProgress,
@@ -147,6 +162,6 @@ export const useProgressTracking = () => {
getResumePoint, getResumePoint,
completeOnboarding, completeOnboarding,
canAccessStep, canAccessStep,
clearError: service.clearError, clearError,
}; };
}; };

View File

@@ -1,141 +1,147 @@
/** /**
* Resume logic service - Clean, standardized implementation * Resume logic service - Simplified implementation
*/ */
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useOnboardingStore } from '../core/store'; import { useOnboardingStore } from '../core/store';
import { useProgressTracking } from './useProgressTracking'; import { useProgressTracking } from './useProgressTracking';
import { createServiceHook } from '../utils/createServiceHook';
import type { ResumeState } from '../core/types';
const useResumeLogicService = createServiceHook<ResumeState>({
initialState: {
isCheckingResume: false,
resumePoint: null,
shouldResume: false,
},
});
export const useResumeLogic = () => { export const useResumeLogic = () => {
const service = useResumeLogicService();
const navigate = useNavigate(); const navigate = useNavigate();
const progressTracking = useProgressTracking(); const progressTracking = useProgressTracking();
const { setCurrentStep } = useOnboardingStore(); const { setCurrentStep } = useOnboardingStore();
const resumeAttempted = useRef(false); const resumeAttempted = useRef(false);
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isCheckingResume, setIsCheckingResume] = useState(false);
const [resumePoint, setResumePoint] = useState<{ stepId: string; stepIndex: number } | null>(null);
const [shouldResume, setShouldResume] = useState(false);
// Check if user should resume onboarding // Check if user should resume onboarding
const checkForResume = useCallback(async (): Promise<boolean> => { const checkForResume = useCallback(async (): Promise<boolean> => {
const result = await service.executeAsync(async () => { setIsLoading(true);
// Set checking state setError(null);
service.setSuccess({ setIsCheckingResume(true);
...service.data!,
isCheckingResume: true,
});
try {
// Load user's progress from backend // Load user's progress from backend
await progressTracking.loadProgress(); await progressTracking.loadProgress();
if (!progressTracking.progress) { if (!progressTracking.progress) {
console.log('🔍 No progress found, starting from beginning'); console.log('🔍 No progress found, starting from beginning');
service.setSuccess({ setIsCheckingResume(false);
...service.data!, setShouldResume(false);
isCheckingResume: false,
shouldResume: false,
});
return false; return false;
} }
// If onboarding is already completed, don't resume // If onboarding is already completed, don't resume
if (progressTracking.isCompleted) { if (progressTracking.isCompleted) {
console.log('✅ Onboarding already completed, redirecting to dashboard'); console.log('✅ Onboarding completed, redirecting to dashboard');
navigate('/app/dashboard'); navigate('/app/dashboard');
service.setSuccess({ setIsCheckingResume(false);
...service.data!, setShouldResume(false);
isCheckingResume: false,
shouldResume: false,
});
return false; return false;
} }
// Get the resume point from backend // Get the resume point from backend
const resumePoint = await progressTracking.getResumePoint(); const resumePointData = await progressTracking.getResumePoint();
console.log('🔄 Resume point found:', resumePoint); console.log('🎯 Resuming from step:', resumePointData.stepId);
service.setSuccess({ setIsCheckingResume(false);
...service.data!, setResumePoint(resumePointData);
isCheckingResume: false, setShouldResume(true);
resumePoint,
shouldResume: true,
});
return true; return true;
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error checking resume';
return result.success; setError(errorMessage);
}, [service, progressTracking, navigate]); setIsCheckingResume(false);
setShouldResume(false);
console.error('❌ Resume check failed:', errorMessage);
return false;
} finally {
setIsLoading(false);
}
}, [progressTracking, navigate]);
// Resume the onboarding flow from the correct step // Resume the onboarding flow from the correct step
const resumeFlow = useCallback((): boolean => { const resumeFlow = useCallback((): boolean => {
if (!service.data?.resumePoint) { if (!resumePoint) {
console.warn('⚠️ No resume point available'); console.warn('⚠️ No resume point available');
return false; return false;
} }
try { try {
const { stepIndex } = service.data.resumePoint; const { stepIndex } = resumePoint;
console.log(`🎯 Resuming onboarding from step index: ${stepIndex}`);
// Navigate to the correct step in the flow // Navigate to the correct step in the flow
setCurrentStep(stepIndex); setCurrentStep(stepIndex);
console.log('✅ Successfully resumed onboarding flow'); console.log('✅ Resumed onboarding at step', stepIndex);
service.setSuccess({ setShouldResume(false);
...service.data!,
shouldResume: false,
});
return true; return true;
} catch (error) { } catch (error) {
console.error('❌ Error resuming flow:', error); console.error('❌ Error resuming flow:', error);
return false; return false;
} }
}, [service, setCurrentStep]); }, [resumePoint, setCurrentStep]);
// Handle automatic resume on mount // Handle automatic resume on mount
const handleAutoResume = useCallback(async () => { const handleAutoResume = useCallback(async () => {
const shouldResume = await checkForResume(); try {
// Add a timeout to prevent hanging indefinitely
if (shouldResume) { const timeoutPromise = new Promise<boolean>((_, reject) =>
// Wait a bit for state to update, then resume setTimeout(() => reject(new Error('Resume check timeout')), 10000)
setTimeout(() => { );
resumeFlow();
}, 100); const shouldResumeResult = await Promise.race([
checkForResume(),
timeoutPromise
]);
if (shouldResumeResult) {
// Wait a bit for state to update, then resume
setTimeout(() => {
resumeFlow();
}, 100);
}
} catch (error) {
console.error('❌ Auto-resume failed:', error);
// Reset the checking state in case of error
setIsCheckingResume(false);
setShouldResume(false);
} }
}, [checkForResume, resumeFlow]); }, [checkForResume, resumeFlow]);
// Auto-check for resume when the hook is first used - PREVENT multiple attempts // Auto-check for resume when the hook is first used - PREVENT multiple attempts
useEffect(() => { useEffect(() => {
if (progressTracking.isInitialized && !service.data?.isCheckingResume && !resumeAttempted.current) { if (progressTracking.isInitialized && !isCheckingResume && !resumeAttempted.current) {
resumeAttempted.current = true; resumeAttempted.current = true;
handleAutoResume(); handleAutoResume();
} }
}, [progressTracking.isInitialized, service.data?.isCheckingResume]); // Remove handleAutoResume from deps }, [progressTracking.isInitialized, isCheckingResume, handleAutoResume]);
const clearError = useCallback(() => {
setError(null);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
isCheckingResume: service.data?.isCheckingResume || false, isCheckingResume,
resumePoint: service.data?.resumePoint || null, resumePoint,
shouldResume: service.data?.shouldResume || false, shouldResume,
// Actions // Actions
checkForResume, checkForResume,
resumeFlow, resumeFlow,
handleAutoResume, handleAutoResume,
clearError: service.clearError, clearError,
// Progress tracking state for convenience // Progress tracking state for convenience
progress: progressTracking.progress, progress: progressTracking.progress,

View File

@@ -1,57 +1,43 @@
/** /**
* Sales processing service - Clean, standardized implementation * Sales processing service - Simplified implementation
*/ */
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { useClassifyProductsBatch, useValidateFileOnly } from '../../../../api'; import { useClassifyProductsBatch, useValidateFileOnly } from '../../../../api';
import { useCurrentTenant } from '../../../../stores'; import { useCurrentTenant } from '../../../../stores';
import { useOnboardingStore } from '../core/store'; import { useOnboardingStore } from '../core/store';
import { createServiceHook } from '../utils/createServiceHook'; import type { ProgressCallback, ProductSuggestionResponse } from '../core/types';
import type { SalesProcessingState, ProgressCallback, ProductSuggestionResponse } from '../core/types';
const useSalesProcessingService = createServiceHook<SalesProcessingState>({
initialState: {
stage: 'idle',
progress: 0,
currentMessage: '',
validationResults: null,
suggestions: null,
},
});
export const useSalesProcessing = () => { export const useSalesProcessing = () => {
const service = useSalesProcessingService();
const classifyProductsMutation = useClassifyProductsBatch(); const classifyProductsMutation = useClassifyProductsBatch();
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const { validateFile } = useValidateFileOnly(); const { validateFile } = useValidateFileOnly();
const { setStepData } = useOnboardingStore(); const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[] | null>(null);
const [stage, setStage] = useState<'idle' | 'validating' | 'analyzing' | 'completed' | 'error'>('idle');
const [progress, setProgress] = useState(0);
const [currentMessage, setCurrentMessage] = useState('');
const [validationResults, setValidationResults] = useState<any | null>(null);
const updateProgress = useCallback(( const updateProgress = useCallback((
progress: number, progressValue: number,
stage: SalesProcessingState['stage'], stageValue: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error',
message: string, message: string,
onProgress?: ProgressCallback onProgress?: ProgressCallback
) => { ) => {
service.setSuccess({ setProgress(progressValue);
...service.data, setStage(stageValue);
progress, setCurrentMessage(message);
stage, onProgress?.(progressValue, stageValue, message);
currentMessage: message, }, []);
});
onProgress?.(progress, stage, message);
}, [service]);
const extractProductList = useCallback((validationResult: any): string[] => { const extractProductList = useCallback((validationResult: any): string[] => {
console.log('extractProductList - Input validation result:', {
hasProductList: !!validationResult?.product_list,
productList: validationResult?.product_list,
hasSampleRecords: !!validationResult?.sample_records,
keys: Object.keys(validationResult || {})
});
// First try to use the direct product_list from backend response // First try to use the direct product_list from backend response
if (validationResult.product_list && Array.isArray(validationResult.product_list)) { if (validationResult.product_list && Array.isArray(validationResult.product_list)) {
console.log('extractProductList - Using direct product_list:', validationResult.product_list);
return validationResult.product_list; return validationResult.product_list;
} }
@@ -63,23 +49,22 @@ export const useSalesProcessing = () => {
productSet.add(record.product_name); productSet.add(record.product_name);
} }
}); });
const extractedList = Array.from(productSet); return Array.from(productSet);
console.log('extractProductList - Extracted from sample_records:', extractedList);
return extractedList;
} }
console.log('extractProductList - No products found, returning empty array');
return []; return [];
}, []); }, []);
const generateSuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[]> => { const generateSuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[]> => {
try { try {
if (!currentTenant?.id) { if (!currentTenant?.id) {
console.error('No tenant ID available for classification'); console.error('No tenant ID available for classification');
return []; return [];
} }
const response = await classifyProductsMutation.mutateAsync({ console.log('🔄 Generating suggestions for', productList.length, 'products');
const requestPayload = {
tenantId: currentTenant.id, tenantId: currentTenant.id,
batchData: { batchData: {
products: productList.map(name => ({ products: productList.map(name => ({
@@ -87,11 +72,15 @@ export const useSalesProcessing = () => {
description: '' description: ''
})) }))
} }
}); };
const response = await classifyProductsMutation.mutateAsync(requestPayload);
console.log('✅ Generated', response?.length || 0, 'suggestions');
return response || []; return response || [];
} catch (error) { } catch (error) {
console.error('Error generating inventory suggestions:', error); console.error('Error generating suggestions:', error);
return []; return [];
} }
}, [classifyProductsMutation, currentTenant]); }, [classifyProductsMutation, currentTenant]);
@@ -104,23 +93,17 @@ export const useSalesProcessing = () => {
validationResults?: any; validationResults?: any;
suggestions?: ProductSuggestionResponse[]; suggestions?: ProductSuggestionResponse[];
}> => { }> => {
service.setLoading(true); console.log('🚀 Processing file:', file.name);
service.setError(null); setIsLoading(true);
setError(null);
try { try {
// Stage 1: Validate file structure // Stage 1: Validate file structure
updateProgress(10, 'validating', 'Iniciando validación del archivo...', onProgress); updateProgress(10, 'validating', 'Iniciando validación del archivo...', onProgress);
updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress); updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress);
console.log('useSalesProcessing - currentTenant check:', {
currentTenant,
tenantId: currentTenant?.id,
hasTenant: !!currentTenant,
hasId: !!currentTenant?.id
});
if (!currentTenant?.id) { if (!currentTenant?.id) {
throw new Error(`No se pudo obtener información del tenant. Tenant: ${JSON.stringify(currentTenant)}`); throw new Error('No se pudo obtener información del tenant');
} }
const result = await validateFile( const result = await validateFile(
@@ -133,14 +116,6 @@ export const useSalesProcessing = () => {
} }
); );
console.log('useSalesProcessing - Backend result:', {
success: result.success,
hasValidationResult: !!result.validationResult,
validationResult: result.validationResult,
error: result.error,
fullResult: result
});
if (!result.success || !result.validationResult) { if (!result.success || !result.validationResult) {
throw new Error(result.error || 'Error en la validación del archivo'); throw new Error(result.error || 'Error en la validación del archivo');
} }
@@ -150,21 +125,12 @@ export const useSalesProcessing = () => {
product_list: extractProductList(result.validationResult), product_list: extractProductList(result.validationResult),
}; };
console.log('useSalesProcessing - Processed validation result:', { console.log('📊 File validated:', validationResult.product_list?.length, 'products found');
validationResult,
hasProductList: !!validationResult.product_list,
productListLength: validationResult.product_list?.length || 0,
productListSample: validationResult.product_list?.slice(0, 5)
});
updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress); updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress);
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) { if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
console.error('useSalesProcessing - No products found:', { console.error(' No products found in file');
hasValidationResult: !!validationResult,
hasProductList: !!validationResult?.product_list,
productListLength: validationResult?.product_list?.length
});
throw new Error('No se encontraron productos válidos en el archivo'); throw new Error('No se encontraron productos válidos en el archivo');
} }
@@ -177,14 +143,11 @@ export const useSalesProcessing = () => {
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress); updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
updateProgress(100, 'completed', 'Procesamiento completado exitosamente', onProgress); updateProgress(100, 'completed', 'Procesamiento completado exitosamente', onProgress);
// Update service state // Update state with suggestions
service.setSuccess({ setSuggestions(suggestions || []);
stage: 'completed', setValidationResults(validationResult);
progress: 100,
currentMessage: 'Procesamiento completado exitosamente', console.log('✅ Processing completed:', suggestions?.length || 0, 'suggestions generated');
validationResults: validationResult,
suggestions: suggestions || [],
});
// Update onboarding store // Update onboarding store
setStepData('smart-inventory-setup', { setStepData('smart-inventory-setup', {
@@ -202,29 +165,44 @@ export const useSalesProcessing = () => {
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo'; const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
service.setError(errorMessage); setError(errorMessage);
updateProgress(0, 'error', errorMessage, onProgress); updateProgress(0, 'error', errorMessage, onProgress);
return { return {
success: false, success: false,
}; };
} }
}, [service, updateProgress, currentTenant, validateFile, extractProductList, generateSuggestions, setStepData]); }, [updateProgress, currentTenant, validateFile, extractProductList, generateSuggestions, setStepData]);
const clearError = useCallback(() => {
setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setSuggestions(null);
setStage('idle');
setProgress(0);
setCurrentMessage('');
setValidationResults(null);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
stage: service.data?.stage || 'idle', stage,
progress: service.data?.progress || 0, progress,
currentMessage: service.data?.currentMessage || '', currentMessage,
validationResults: service.data?.validationResults || null, validationResults,
suggestions: service.data?.suggestions || null, suggestions,
// Actions // Actions
processFile, processFile,
generateSuggestions, generateSuggestions,
clearError: service.clearError, clearError,
reset: service.reset, reset,
}; };
}; };

View File

@@ -1,44 +1,47 @@
/** /**
* Tenant creation service - Clean, standardized implementation * Tenant creation service - Simplified implementation
*/ */
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { useRegisterBakery } from '../../../../api'; import { useRegisterBakery } from '../../../../api';
import { useTenantStore } from '../../../../stores/tenant.store'; import { useTenantStore } from '../../../../stores/tenant.store';
import { useOnboardingStore } from '../core/store'; import { useOnboardingStore } from '../core/store';
import { createServiceHook } from '../utils/createServiceHook';
import type { TenantCreationState } from '../core/types';
import type { BakeryRegistration, TenantResponse } from '../../../../api'; import type { BakeryRegistration, TenantResponse } from '../../../../api';
const useTenantCreationService = createServiceHook<TenantCreationState>({
initialState: {
tenantData: null,
},
});
export const useTenantCreation = () => { export const useTenantCreation = () => {
const service = useTenantCreationService();
const registerBakeryMutation = useRegisterBakery(); const registerBakeryMutation = useRegisterBakery();
const { setCurrentTenant, loadUserTenants } = useTenantStore(); const { setCurrentTenant, loadUserTenants } = useTenantStore();
const { setStepData } = useOnboardingStore(); const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSuccess, setIsSuccess] = useState(false);
const [tenantData, setTenantData] = useState<TenantResponse | null>(null);
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => { const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) { if (!bakeryData) {
service.setError('Los datos de la panadería son requeridos'); setError('Los datos de la panadería son requeridos');
return false; return false;
} }
const result = await service.executeAsync(async () => { setIsLoading(true);
setError(null);
try {
// Call API to register bakery // Call API to register bakery
const tenantResponse: TenantResponse = await registerBakeryMutation.mutateAsync(bakeryData); const tenantResponse: TenantResponse = await registerBakeryMutation.mutateAsync(bakeryData);
// Update tenant store // Update tenant store
console.log('useTenantCreation - Setting current tenant:', tenantResponse);
setCurrentTenant(tenantResponse); setCurrentTenant(tenantResponse);
// Reload user tenants // Reload user tenants
await loadUserTenants(); await loadUserTenants();
// Update state
setTenantData(tenantResponse);
setIsSuccess(true);
// Update onboarding data // Update onboarding data
setStepData('setup', { setStepData('setup', {
bakery: { bakery: {
@@ -48,23 +51,39 @@ export const useTenantCreation = () => {
} as any, } as any,
}); });
console.log('useTenantCreation - Tenant created and set successfully'); console.log(' Tenant created successfully');
return tenantResponse; return true;
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating tenant';
setError(errorMessage);
setIsSuccess(false);
return false;
} finally {
setIsLoading(false);
}
}, [registerBakeryMutation, setCurrentTenant, loadUserTenants, setStepData]);
return result.success; const clearError = useCallback(() => {
}, [service, registerBakeryMutation, setCurrentTenant, loadUserTenants, setStepData]); setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setIsSuccess(false);
setTenantData(null);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
isSuccess: service.isSuccess, isSuccess,
tenantData: service.tenantData, tenantData,
// Actions // Actions
createTenant, createTenant,
clearError: service.clearError, clearError,
reset: service.reset, reset,
}; };
}; };

View File

@@ -1,8 +1,8 @@
/** /**
* Training orchestration service - Clean, standardized implementation * Training orchestration service - Simplified implementation
*/ */
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
useCreateTrainingJob, useCreateTrainingJob,
useTrainingJobStatus, useTrainingJobStatus,
@@ -11,35 +11,42 @@ import {
import { useCurrentTenant } from '../../../../stores'; import { useCurrentTenant } from '../../../../stores';
import { useAuthUser } from '../../../../stores/auth.store'; import { useAuthUser } from '../../../../stores/auth.store';
import { useOnboardingStore } from '../core/store'; import { useOnboardingStore } from '../core/store';
import { createServiceHook } from '../utils/createServiceHook'; import type { TrainingLog, TrainingMetrics } from '../core/types';
import type { TrainingOrchestrationState, TrainingLog, TrainingMetrics } from '../core/types';
import type { TrainingJobResponse } from '../../../../api'; import type { TrainingJobResponse } from '../../../../api';
const useTrainingOrchestrationService = createServiceHook<TrainingOrchestrationState>({
initialState: {
status: 'idle',
progress: 0,
currentStep: '',
estimatedTimeRemaining: 0,
job: null,
logs: [],
metrics: null,
},
});
export const useTrainingOrchestration = () => { export const useTrainingOrchestration = () => {
const service = useTrainingOrchestrationService();
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const user = useAuthUser(); const user = useAuthUser();
const createTrainingJobMutation = useCreateTrainingJob(); const createTrainingJobMutation = useCreateTrainingJob();
const { setStepData, getAllStepData } = useOnboardingStore(); const { setStepData, getAllStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>('idle');
const [progress, setProgress] = useState(0);
const [currentStep, setCurrentStep] = useState('');
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(0);
const [job, setJob] = useState<TrainingJobResponse | null>(null);
const [logs, setLogs] = useState<TrainingLog[]>([]);
const [metrics, setMetrics] = useState<TrainingMetrics | null>(null);
const addLog = useCallback((message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
setLogs(prevLogs => [...prevLogs, newLog]);
}, []);
// Get job status when we have a job ID // Get job status when we have a job ID
const { data: jobStatus } = useTrainingJobStatus( const { data: jobStatus } = useTrainingJobStatus(
currentTenant?.id || '', currentTenant?.id || '',
service.data?.job?.job_id || '', job?.job_id || '',
{ {
enabled: !!currentTenant?.id && !!service.data?.job?.job_id && service.data?.status === 'training', enabled: !!currentTenant?.id && !!job?.job_id && status === 'training',
refetchInterval: 5000, refetchInterval: 5000,
} }
); );
@@ -47,44 +54,35 @@ export const useTrainingOrchestration = () => {
// WebSocket for real-time updates // WebSocket for real-time updates
const { data: wsData } = useTrainingWebSocket( const { data: wsData } = useTrainingWebSocket(
currentTenant?.id || '', currentTenant?.id || '',
service.data?.job?.job_id || '', job?.job_id || '',
(user as any)?.token, (user as any)?.token,
{ {
onProgress: (data: any) => { onProgress: (data: any) => {
service.setSuccess({ setProgress(data.progress?.percentage || progress);
...service.data!, setCurrentStep(data.progress?.current_step || currentStep);
progress: data.progress?.percentage || service.data?.progress || 0, setEstimatedTimeRemaining(data.progress?.estimated_time_remaining || estimatedTimeRemaining);
currentStep: data.progress?.current_step || service.data?.currentStep || '',
estimatedTimeRemaining: data.progress?.estimated_time_remaining || service.data?.estimatedTimeRemaining || 0,
});
addLog( addLog(
`${data.progress?.current_step} - ${data.progress?.products_completed}/${data.progress?.products_total} productos procesados (${data.progress?.percentage}%)`, `${data.progress?.current_step} - ${data.progress?.products_completed}/${data.progress?.products_total} productos procesados (${data.progress?.percentage}%)`,
'info' 'info'
); );
}, },
onCompleted: (data: any) => { onCompleted: (data: any) => {
service.setSuccess({ setStatus('completed');
...service.data!, setProgress(100);
status: 'completed', setMetrics({
progress: 100, accuracy: data.results.performance_metrics.accuracy,
metrics: { mape: data.results.performance_metrics.mape,
accuracy: data.results.performance_metrics.accuracy, mae: data.results.performance_metrics.mae,
mape: data.results.performance_metrics.mape, rmse: data.results.performance_metrics.rmse,
mae: data.results.performance_metrics.mae, r2_score: data.results.performance_metrics.r2_score || 0,
rmse: data.results.performance_metrics.rmse,
r2_score: data.results.performance_metrics.r2_score || 0,
},
}); });
addLog('¡Entrenamiento ML completado exitosamente!', 'success'); addLog('¡Entrenamiento ML completado exitosamente!', 'success');
addLog(`${data.results.successful_trainings} modelos creados exitosamente`, 'success'); addLog(`${data.results.successful_trainings} modelos creados exitosamente`, 'success');
addLog(`Duración total: ${Math.round(data.results.training_duration / 60)} minutos`, 'info'); addLog(`Duración total: ${Math.round(data.results.training_duration / 60)} minutos`, 'info');
}, },
onError: (data: any) => { onError: (data: any) => {
service.setError(data.error); setError(data.error);
service.setSuccess({ setStatus('failed');
...service.data!,
status: 'failed',
});
addLog(`Error en entrenamiento: ${data.error}`, 'error'); addLog(`Error en entrenamiento: ${data.error}`, 'error');
}, },
onStarted: (data: any) => { onStarted: (data: any) => {
@@ -95,29 +93,13 @@ export const useTrainingOrchestration = () => {
// Update status from polling when WebSocket is not available // Update status from polling when WebSocket is not available
useEffect(() => { useEffect(() => {
if (jobStatus && service.data?.job?.job_id === jobStatus.job_id) { if (jobStatus && job?.job_id === jobStatus.job_id) {
service.setSuccess({ setStatus(jobStatus.status as any);
...service.data!, setProgress(jobStatus.progress || progress);
status: jobStatus.status as any, setCurrentStep(jobStatus.current_step || currentStep);
progress: jobStatus.progress || service.data?.progress || 0, setEstimatedTimeRemaining(jobStatus.estimated_time_remaining || estimatedTimeRemaining);
currentStep: jobStatus.current_step || service.data?.currentStep || '',
estimatedTimeRemaining: jobStatus.estimated_time_remaining || service.data?.estimatedTimeRemaining || 0,
});
} }
}, [jobStatus, service.data?.job?.job_id, service]); }, [jobStatus, job?.job_id, progress, currentStep, estimatedTimeRemaining]);
const addLog = useCallback((message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
service.setSuccess({
...service.data!,
logs: [...(service.data?.logs || []), newLog]
});
}, [service]);
const validateTrainingData = useCallback(async (allStepData?: any): Promise<{ const validateTrainingData = useCallback(async (allStepData?: any): Promise<{
isValid: boolean; isValid: boolean;
@@ -126,7 +108,6 @@ export const useTrainingOrchestration = () => {
const missingItems: string[] = []; const missingItems: string[] = [];
const stepData = allStepData || getAllStepData(); const stepData = allStepData || getAllStepData();
console.log('Training Validation - All step data:', stepData);
// Get data from the smart-inventory-setup step (where sales and inventory are handled) // Get data from the smart-inventory-setup step (where sales and inventory are handled)
const smartInventoryData = stepData?.['smart-inventory-setup']; const smartInventoryData = stepData?.['smart-inventory-setup'];
@@ -181,18 +162,9 @@ export const useTrainingOrchestration = () => {
const isValid = missingItems.length === 0; const isValid = missingItems.length === 0;
console.log('Training Validation Result:', { if (!isValid) {
isValid, console.log('⚠️ Training validation failed:', missingItems.join(', '));
missingItems, }
smartInventoryData: {
hasFile: !!smartInventoryData?.files?.salesData,
hasProcessingResults,
hasImportResults,
hasApprovedProducts,
hasInventoryConfig,
totalRecords: smartInventoryData?.processingResults?.total_records || 0
}
});
return { isValid, missingItems }; return { isValid, missingItems };
}, [getAllStepData]); }, [getAllStepData]);
@@ -203,23 +175,21 @@ export const useTrainingOrchestration = () => {
endDate?: string; endDate?: string;
}): Promise<boolean> => { }): Promise<boolean> => {
if (!currentTenant?.id) { if (!currentTenant?.id) {
service.setError('No se pudo obtener información del tenant'); setError('No se pudo obtener información del tenant');
return false; return false;
} }
service.setLoading(true); setIsLoading(true);
service.setSuccess({ setStatus('validating');
...service.data!, setProgress(0);
status: 'validating', setError(null);
progress: 0,
});
addLog('Validando disponibilidad de datos...', 'info'); addLog('Validando disponibilidad de datos...', 'info');
const result = await service.executeAsync(async () => { try {
// Start training job // Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info'); addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const job = await createTrainingJobMutation.mutateAsync({ const trainingJob = await createTrainingJobMutation.mutateAsync({
tenantId: currentTenant.id, tenantId: currentTenant.id,
request: { request: {
products: options?.products, products: options?.products,
@@ -228,44 +198,61 @@ export const useTrainingOrchestration = () => {
} }
}); });
// Update service state // Update state
const updatedData = { setJob(trainingJob);
...service.data!, setStatus('training');
job,
status: 'training' as const,
};
service.setSuccess(updatedData);
// Update onboarding store // Update onboarding store
setStepData('ml-training', { setStepData('ml-training', {
trainingStatus: 'training', trainingStatus: 'training',
trainingJob: job, trainingJob: trainingJob,
}); });
addLog(`Trabajo de entrenamiento iniciado: ${job.job_id}`, 'success'); addLog(`Trabajo de entrenamiento iniciado: ${trainingJob.job_id}`, 'success');
return job; return true;
}); } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error starting training';
setError(errorMessage);
setStatus('failed');
return false;
} finally {
setIsLoading(false);
}
}, [currentTenant, createTrainingJobMutation, addLog, setStepData]);
return result.success; const clearError = useCallback(() => {
}, [currentTenant, createTrainingJobMutation, addLog, service, setStepData]); setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setStatus('idle');
setProgress(0);
setCurrentStep('');
setEstimatedTimeRemaining(0);
setJob(null);
setLogs([]);
setMetrics(null);
}, []);
return { return {
// State // State
isLoading: service.isLoading, isLoading,
error: service.error, error,
status: service.data?.status || 'idle', status,
progress: service.data?.progress || 0, progress,
currentStep: service.data?.currentStep || '', currentStep,
estimatedTimeRemaining: service.data?.estimatedTimeRemaining || 0, estimatedTimeRemaining,
job: service.data?.job || null, job,
logs: service.data?.logs || [], logs,
metrics: service.data?.metrics || null, metrics,
// Actions // Actions
startTraining, startTraining,
validateTrainingData, validateTrainingData,
addLog, addLog,
clearError: service.clearError, clearError,
reset: service.reset, reset,
}; };
}; };

View File

@@ -32,7 +32,7 @@ export const useOnboarding = () => {
return { return {
// Core state from store // Core state from store
currentStep: store.getCurrentStep(), currentStep: store.currentStep, // Return the index, not the step object
steps: store.steps, steps: store.steps,
data: store.data, data: store.data,
progress: store.getProgress(), progress: store.getProgress(),

View File

@@ -34,7 +34,6 @@ const OnboardingPage: React.FC = () => {
validateCurrentStep, validateCurrentStep,
createTenant, createTenant,
processSalesFile, processSalesFile,
generateInventorySuggestions,
createInventoryFromSuggestions, createInventoryFromSuggestions,
completeOnboarding, completeOnboarding,
clearError, clearError,
@@ -60,6 +59,17 @@ const OnboardingPage: React.FC = () => {
validation: step.validation validation: step.validation
})); }));
// Debug logging - only show if there's an error or important state change
if (error || isLoading || autoResume.isCheckingResume) {
console.log('OnboardingPage Status:', {
stepsLength: steps.length,
currentStep,
isLoading,
error,
isCheckingResume: autoResume.isCheckingResume,
});
}
const handleStepChange = (stepIndex: number, stepData: any) => { const handleStepChange = (stepIndex: number, stepData: any) => {
const stepId = steps[stepIndex]?.id; const stepId = steps[stepIndex]?.id;
if (stepId) { if (stepId) {
@@ -96,49 +106,102 @@ const OnboardingPage: React.FC = () => {
} }
}, [isAuthenticated, navigate]); }, [isAuthenticated, navigate]);
// Clear error when user navigates away // Clear error when user navigates away or when component mounts
useEffect(() => { useEffect(() => {
// Clear any existing errors when the page loads
if (error) {
console.log('🧹 Clearing existing error on mount:', error);
clearError();
}
return () => { return () => {
if (error) { if (error) {
clearError(); clearError();
} }
}; };
}, [error, clearError]); }, [clearError]); // Include clearError in dependencies
// Show loading while processing or checking for saved progress // Show loading while processing or checking for saved progress
if (isLoading || autoResume.isCheckingResume) { // Add a safety timeout - if checking resume takes more than 10 seconds, proceed anyway
const [loadingTimeoutReached, setLoadingTimeoutReached] = React.useState(false);
React.useEffect(() => {
if (isLoading || autoResume.isCheckingResume) {
const timeout = setTimeout(() => {
console.warn('⚠️ Loading timeout reached, proceeding with onboarding');
setLoadingTimeoutReached(true);
}, 10000); // 10 second timeout
return () => clearTimeout(timeout);
} else {
setLoadingTimeoutReached(false);
}
}, [isLoading, autoResume.isCheckingResume]);
if ((isLoading || autoResume.isCheckingResume) && !loadingTimeoutReached) {
const message = autoResume.isCheckingResume const message = autoResume.isCheckingResume
? "Verificando progreso guardado..." ? "Verificando progreso guardado..."
: "Procesando..."; : "Procesando...";
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="lg" message={message} /> <div className="flex flex-col items-center">
<LoadingSpinner size="lg" />
<div className="mt-4 text-lg text-gray-600">{message}</div>
</div>
</div> </div>
); );
} }
// Show error state // Show error state with better recovery options
if (error) { if (error && !loadingTimeoutReached) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center max-w-md">
<h2 className="text-xl font-semibold text-red-600 mb-4">Error en Onboarding</h2>
<p className="text-gray-600 mb-4">{error}</p>
<div className="space-y-2">
<button
onClick={() => {
console.log('🔄 User clicked retry - clearing error');
clearError();
}}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Continuar con el Onboarding
</button>
<button
onClick={() => {
console.log('🔄 User clicked reset');
reset();
}}
className="w-full px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
Reiniciar Desde el Principio
</button>
</div>
<p className="text-sm text-gray-500 mt-4">
Si el problema persiste, recarga la página
</p>
</div>
</div>
);
}
// Safety check: ensure we have valid steps and current step
if (wizardSteps.length === 0) {
console.error('❌ No wizard steps available, this should not happen');
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h2 className="text-xl font-semibold text-red-600 mb-4">Error en Onboarding</h2> <h2 className="text-xl font-semibold text-red-600 mb-4">Error de Configuración</h2>
<p className="text-gray-600 mb-4">{error}</p> <p className="text-gray-600 mb-4">No se pudieron cargar los pasos del onboarding.</p>
<div className="space-x-4"> <button
<button onClick={() => window.location.reload()}
onClick={clearError} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" >
> Recargar Página
Reintentar </button>
</button>
<button
onClick={reset}
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
Reiniciar
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -12,10 +12,9 @@ import structlog
# Import core modules # Import core modules
from app.core.config import settings from app.core.config import settings
from app.core.database import init_db, close_db from app.core.database import init_db, close_db, health_check as db_health_check
from app.api import ingredients, stock, classification from app.api import ingredients, stock, classification
from app.services.inventory_alert_service import InventoryAlertService from app.services.inventory_alert_service import InventoryAlertService
from shared.monitoring.health import router as health_router
from shared.monitoring.metrics import setup_metrics_early from shared.monitoring.metrics import setup_metrics_early
# Auth decorators are used in endpoints, no global setup needed # Auth decorators are used in endpoints, no global setup needed
@@ -127,7 +126,6 @@ async def general_exception_handler(request: Request, exc: Exception):
# Include routers # Include routers
app.include_router(health_router, prefix="/health", tags=["health"])
app.include_router(ingredients.router, prefix=settings.API_V1_STR) app.include_router(ingredients.router, prefix=settings.API_V1_STR)
app.include_router(stock.router, prefix=settings.API_V1_STR) app.include_router(stock.router, prefix=settings.API_V1_STR)
app.include_router(classification.router, prefix=settings.API_V1_STR) app.include_router(classification.router, prefix=settings.API_V1_STR)
@@ -154,6 +152,49 @@ async def root():
} }
@app.get("/health")
async def health_check():
"""Comprehensive health check endpoint"""
try:
# Check database health
db_health = await db_health_check()
# Check alert service health
alert_health = {"status": "not_initialized"}
if hasattr(app.state, 'alert_service') and app.state.alert_service:
try:
alert_health = await app.state.alert_service.health_check()
except Exception as e:
alert_health = {"status": "unhealthy", "error": str(e)}
# Determine overall health
overall_healthy = (
db_health and
alert_health.get("status") in ["healthy", "not_initialized"]
)
return {
"status": "healthy" if overall_healthy else "unhealthy",
"service": settings.SERVICE_NAME,
"version": settings.VERSION,
"database": "connected" if db_health else "disconnected",
"alert_service": alert_health,
"timestamp": structlog.get_logger().info("Health check requested")
}
except Exception as e:
logger.error("Health check failed", error=str(e))
return {
"status": "unhealthy",
"service": settings.SERVICE_NAME,
"version": settings.VERSION,
"database": "unknown",
"alert_service": {"status": "unknown"},
"error": str(e),
"timestamp": structlog.get_logger().info("Health check failed")
}
# Service info endpoint # Service info endpoint
@app.get(f"{settings.API_V1_STR}/info") @app.get(f"{settings.API_V1_STR}/info")
async def service_info(): async def service_info():