Start integrating the onboarding flow with backend 4

This commit is contained in:
Urtzi Alfaro
2025-09-05 12:55:26 +02:00
parent 0faaa25e58
commit 3fe1f17610
26 changed files with 2161 additions and 1002 deletions

View 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,
};
};