Start integrating the onboarding flow with backend 14
This commit is contained in:
@@ -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> {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user