Start integrating the onboarding flow with backend 4
This commit is contained in:
561
frontend/src/hooks/business/useOnboarding.ts
Normal file
561
frontend/src/hooks/business/useOnboarding.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
/**
|
||||
* Onboarding business hook for managing the complete onboarding workflow
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { inventoryService } from '../../services/api/inventory.service';
|
||||
import { salesService } from '../../services/api/sales.service';
|
||||
import { authService } from '../../services/api/auth.service';
|
||||
import { tenantService } from '../../services/api/tenant.service';
|
||||
import { useAuthUser } from '../../stores/auth.store';
|
||||
import { useAlertActions } from '../../stores/alerts.store';
|
||||
import {
|
||||
ProductSuggestion,
|
||||
ProductSuggestionsResponse,
|
||||
InventoryCreationResponse
|
||||
} from '../../types/inventory.types';
|
||||
import {
|
||||
BusinessModelGuide,
|
||||
BusinessModelType,
|
||||
TemplateData
|
||||
} from '../../types/sales.types';
|
||||
import { OnboardingStatus } from '../../types/auth.types';
|
||||
|
||||
export interface OnboardingStep {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isRequired: boolean;
|
||||
isCompleted: boolean;
|
||||
validation?: (data: any) => string | null;
|
||||
}
|
||||
|
||||
export interface OnboardingData {
|
||||
// Step 1: Setup
|
||||
bakery?: {
|
||||
name: string;
|
||||
business_model: BusinessModelType;
|
||||
address: string;
|
||||
city: string;
|
||||
postal_code: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Step 2: Data Processing
|
||||
files?: {
|
||||
salesData?: File;
|
||||
};
|
||||
processingStage?: 'upload' | 'validating' | 'analyzing' | 'completed' | 'error';
|
||||
processingResults?: {
|
||||
is_valid: boolean;
|
||||
total_records: number;
|
||||
unique_products: number;
|
||||
product_list: string[];
|
||||
validation_errors: string[];
|
||||
validation_warnings: string[];
|
||||
summary: {
|
||||
date_range: string;
|
||||
total_sales: number;
|
||||
average_daily_sales: number;
|
||||
};
|
||||
};
|
||||
|
||||
// Step 3: Review
|
||||
suggestions?: ProductSuggestion[];
|
||||
approvedSuggestions?: ProductSuggestion[];
|
||||
reviewCompleted?: boolean;
|
||||
|
||||
// Step 4: Inventory
|
||||
inventoryItems?: any[];
|
||||
inventoryMapping?: { [productName: string]: string };
|
||||
inventoryConfigured?: boolean;
|
||||
|
||||
// Step 5: Suppliers
|
||||
suppliers?: any[];
|
||||
supplierMappings?: any[];
|
||||
|
||||
// Step 6: ML Training
|
||||
trainingStatus?: 'not_started' | 'in_progress' | 'completed' | 'failed';
|
||||
modelAccuracy?: number;
|
||||
|
||||
// Step 7: Completion
|
||||
completionStats?: {
|
||||
totalProducts: number;
|
||||
inventoryItems: number;
|
||||
suppliersConfigured: number;
|
||||
mlModelAccuracy: number;
|
||||
estimatedTimeSaved: string;
|
||||
completionScore: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface OnboardingState {
|
||||
currentStep: number;
|
||||
steps: OnboardingStep[];
|
||||
data: OnboardingData;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
isInitialized: boolean;
|
||||
onboardingStatus: OnboardingStatus | null;
|
||||
}
|
||||
|
||||
interface OnboardingActions {
|
||||
// Navigation
|
||||
nextStep: () => boolean;
|
||||
previousStep: () => boolean;
|
||||
goToStep: (stepIndex: number) => boolean;
|
||||
|
||||
// Data Management
|
||||
updateStepData: (stepId: string, data: Partial<OnboardingData>) => void;
|
||||
validateCurrentStep: () => string | null;
|
||||
|
||||
// Step-specific Actions
|
||||
createTenant: (bakeryData: OnboardingData['bakery']) => Promise<boolean>;
|
||||
processSalesFile: (file: File, onProgress: (progress: number, stage: string, message: string) => void) => Promise<boolean>;
|
||||
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionsResponse | null>;
|
||||
createInventoryFromSuggestions: (suggestions: ProductSuggestion[]) => Promise<InventoryCreationResponse | null>;
|
||||
getBusinessModelGuide: (model: BusinessModelType) => Promise<BusinessModelGuide | null>;
|
||||
downloadTemplate: (templateData: TemplateData, filename: string, format?: 'csv' | 'json') => void;
|
||||
|
||||
// Completion
|
||||
completeOnboarding: () => Promise<boolean>;
|
||||
checkOnboardingStatus: () => Promise<void>;
|
||||
|
||||
// Utilities
|
||||
clearError: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_STEPS: OnboardingStep[] = [
|
||||
{
|
||||
id: 'setup',
|
||||
title: '🏢 Setup',
|
||||
description: 'Configuración básica de tu panadería y creación del tenant',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
|
||||
if (!data.bakery?.business_model) return 'El modelo de negocio es requerido';
|
||||
if (!data.bakery?.address) return 'La dirección es requerida';
|
||||
if (!data.bakery?.city) return 'La ciudad es requerida';
|
||||
if (!data.bakery?.postal_code) return 'El código postal es requerido';
|
||||
if (!data.bakery?.phone) return 'El teléfono es requerido';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'data-processing',
|
||||
title: '📊 Validación de Ventas',
|
||||
description: 'Valida tus datos de ventas y detecta productos automáticamente',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
|
||||
if (data.processingStage !== 'completed') return 'El procesamiento debe completarse antes de continuar';
|
||||
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: '📋 Revisión',
|
||||
description: 'Revisión de productos detectados por IA y resultados',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (!data.reviewCompleted) return 'Debes revisar y aprobar los productos detectados';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inventory',
|
||||
title: '⚙️ Inventario',
|
||||
description: 'Configuración de inventario e importación de datos de ventas',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (!data.inventoryConfigured) return 'Debes configurar el inventario básico';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'suppliers',
|
||||
title: '🏪 Proveedores',
|
||||
description: 'Configuración de proveedores y asociaciones',
|
||||
isRequired: false,
|
||||
isCompleted: false,
|
||||
},
|
||||
{
|
||||
id: 'ml-training',
|
||||
title: '🎯 Inteligencia',
|
||||
description: 'Creación de tu asistente inteligente personalizado',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (data.trainingStatus !== 'completed') return 'El entrenamiento del modelo debe completarse';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'completion',
|
||||
title: '🎉 Listo',
|
||||
description: 'Finalización y preparación para usar la plataforma',
|
||||
isRequired: true,
|
||||
isCompleted: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
const [state, setState] = useState<OnboardingState>({
|
||||
currentStep: 0,
|
||||
steps: DEFAULT_STEPS,
|
||||
data: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isInitialized: false,
|
||||
onboardingStatus: null,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthUser();
|
||||
const { createAlert } = useAlertActions();
|
||||
|
||||
// Initialize onboarding status
|
||||
useEffect(() => {
|
||||
if (user && !state.isInitialized) {
|
||||
checkOnboardingStatus();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Navigation
|
||||
const nextStep = useCallback((): boolean => {
|
||||
const currentStepData = state.steps[state.currentStep];
|
||||
const validation = validateCurrentStep();
|
||||
|
||||
if (validation) {
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'validation',
|
||||
priority: 'high',
|
||||
title: 'Validación fallida',
|
||||
message: validation,
|
||||
source: 'onboarding'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.currentStep < state.steps.length - 1) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentStep: prev.currentStep + 1,
|
||||
steps: prev.steps.map((step, index) =>
|
||||
index === prev.currentStep
|
||||
? { ...step, isCompleted: true }
|
||||
: step
|
||||
)
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [state.currentStep, state.steps, createAlert]);
|
||||
|
||||
const previousStep = useCallback((): boolean => {
|
||||
if (state.currentStep > 0) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentStep: prev.currentStep - 1,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [state.currentStep]);
|
||||
|
||||
const goToStep = useCallback((stepIndex: number): boolean => {
|
||||
if (stepIndex >= 0 && stepIndex < state.steps.length) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentStep: stepIndex,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [state.steps.length]);
|
||||
|
||||
// Data Management
|
||||
const updateStepData = useCallback((stepId: string, data: Partial<OnboardingData>) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
data: { ...prev.data, ...data }
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const validateCurrentStep = useCallback((): string | null => {
|
||||
const currentStepData = state.steps[state.currentStep];
|
||||
if (currentStepData?.validation) {
|
||||
return currentStepData.validation(state.data);
|
||||
}
|
||||
return null;
|
||||
}, [state.currentStep, state.steps, state.data]);
|
||||
|
||||
// Step-specific Actions
|
||||
const createTenant = useCallback(async (bakeryData: OnboardingData['bakery']): Promise<boolean> => {
|
||||
if (!bakeryData) return false;
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tenantService.createTenant({
|
||||
name: bakeryData.name,
|
||||
description: bakeryData.description || '',
|
||||
business_type: bakeryData.business_model,
|
||||
settings: {
|
||||
address: bakeryData.address,
|
||||
city: bakeryData.city,
|
||||
postal_code: bakeryData.postal_code,
|
||||
phone: bakeryData.phone,
|
||||
email: bakeryData.email,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
updateStepData('setup', { bakery: bakeryData });
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'medium',
|
||||
title: 'Tenant creado',
|
||||
message: 'Tu panadería ha sido configurada exitosamente',
|
||||
source: 'onboarding'
|
||||
});
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error creating tenant');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: 'Error al crear tenant',
|
||||
message: errorMessage,
|
||||
source: 'onboarding'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, [updateStepData, createAlert]);
|
||||
|
||||
const processSalesFile = useCallback(async (
|
||||
file: File,
|
||||
onProgress: (progress: number, stage: string, message: string) => void
|
||||
): Promise<boolean> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Stage 1: Validate file
|
||||
onProgress(20, 'validating', 'Validando estructura del archivo...');
|
||||
const validationResult = await salesService.validateSalesData(file);
|
||||
|
||||
onProgress(40, 'validating', 'Verificando integridad de datos...');
|
||||
|
||||
if (!validationResult.is_valid) {
|
||||
throw new Error('Archivo de datos inválido');
|
||||
}
|
||||
|
||||
if (!validationResult.product_list || validationResult.product_list.length === 0) {
|
||||
throw new Error('No se encontraron productos en el archivo');
|
||||
}
|
||||
|
||||
// Stage 2: Generate AI suggestions
|
||||
onProgress(60, 'analyzing', 'Identificando productos únicos...');
|
||||
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
|
||||
|
||||
const suggestions = await generateInventorySuggestions(validationResult.product_list);
|
||||
|
||||
onProgress(100, 'completed', 'Procesamiento completado');
|
||||
|
||||
updateStepData('data-processing', {
|
||||
files: { salesData: file },
|
||||
processingStage: 'completed',
|
||||
processingResults: validationResult,
|
||||
suggestions: suggestions?.suggestions || []
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error processing file';
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
data: {
|
||||
...prev.data,
|
||||
processingStage: 'error'
|
||||
}
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}, [updateStepData]);
|
||||
|
||||
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionsResponse | null> => {
|
||||
try {
|
||||
const response = await inventoryService.generateInventorySuggestions(productList);
|
||||
return response.success ? response.data : null;
|
||||
} catch (error) {
|
||||
console.error('Error generating inventory suggestions:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestion[]): Promise<InventoryCreationResponse | null> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await inventoryService.createInventoryFromSuggestions(suggestions);
|
||||
|
||||
if (response.success) {
|
||||
updateStepData('inventory', {
|
||||
inventoryItems: response.data.created_items,
|
||||
inventoryMapping: response.data.inventory_mapping,
|
||||
inventoryConfigured: true
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error creating inventory');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
return null;
|
||||
}
|
||||
}, [updateStepData]);
|
||||
|
||||
const getBusinessModelGuide = useCallback(async (model: BusinessModelType): Promise<BusinessModelGuide | null> => {
|
||||
try {
|
||||
const response = await salesService.getBusinessModelGuide(model);
|
||||
return response.success ? response.data : null;
|
||||
} catch (error) {
|
||||
console.error('Error getting business model guide:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const downloadTemplate = useCallback((templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv') => {
|
||||
salesService.downloadTemplate(templateData, filename, format);
|
||||
}, []);
|
||||
|
||||
const checkOnboardingStatus = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, isLoading: true }));
|
||||
|
||||
try {
|
||||
const response = await authService.checkOnboardingStatus();
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
onboardingStatus: response.success ? response.data : null,
|
||||
isInitialized: true,
|
||||
isLoading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isInitialized: true,
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const completeOnboarding = useCallback(async (): Promise<boolean> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await authService.completeOnboarding({
|
||||
completedAt: new Date().toISOString(),
|
||||
data: state.data
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: '¡Onboarding completado!',
|
||||
message: 'Has completado exitosamente la configuración inicial',
|
||||
source: 'onboarding'
|
||||
});
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
|
||||
}));
|
||||
|
||||
// Navigate to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
navigate('/app/dashboard');
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error completing onboarding');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: 'Error al completar onboarding',
|
||||
message: errorMessage,
|
||||
source: 'onboarding'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}, [state.data, createAlert, navigate]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState({
|
||||
currentStep: 0,
|
||||
steps: DEFAULT_STEPS.map(step => ({ ...step, isCompleted: false })),
|
||||
data: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isInitialized: false,
|
||||
onboardingStatus: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
nextStep,
|
||||
previousStep,
|
||||
goToStep,
|
||||
updateStepData,
|
||||
validateCurrentStep,
|
||||
createTenant,
|
||||
processSalesFile,
|
||||
generateInventorySuggestions,
|
||||
createInventoryFromSuggestions,
|
||||
getBusinessModelGuide,
|
||||
downloadTemplate,
|
||||
completeOnboarding,
|
||||
checkOnboardingStatus,
|
||||
clearError,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user