Start integrating the onboarding flow with backend 7

This commit is contained in:
Urtzi Alfaro
2025-09-05 22:46:28 +02:00
parent 069954981a
commit 548a2ddd11
28 changed files with 5544 additions and 1014 deletions

View File

@@ -0,0 +1,21 @@
/**
* Onboarding hooks index
* Exports all focused onboarding hooks
*/
// Types
export type * from './types';
// Steps configuration
export { DEFAULT_STEPS, getStepById, getStepIndex, canAccessStep, calculateProgress } from './steps';
// Focused hooks
export { useOnboardingFlow } from './useOnboardingFlow';
export { useOnboardingData } from './useOnboardingData';
export { useTenantCreation } from './useTenantCreation';
export { useSalesProcessing } from './useSalesProcessing';
export { useInventorySetup } from './useInventorySetup';
export { useTrainingOrchestration } from './useTrainingOrchestration';
// Main orchestrating hook
export { useOnboarding } from './useOnboarding';

View File

@@ -0,0 +1,145 @@
/**
* Onboarding step definitions and validation logic
*/
import type { OnboardingStep, OnboardingData } from './types';
export 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_type) return 'El tipo 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';
const hasApprovedProducts = data.approvedProducts && data.approvedProducts.length > 0;
if (!hasApprovedProducts) return 'Debes aprobar al menos un producto para continuar';
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';
// Check if sales data was imported successfully
const hasImportResults = data.salesImportResult &&
(data.salesImportResult.records_created > 0 ||
data.salesImportResult.success === true ||
data.salesImportResult.imported === true);
if (!hasImportResults) return 'Debes importar los datos de ventas al inventario';
return null;
},
},
{
id: 'suppliers',
title: '🏪 Proveedores',
description: 'Configuración de proveedores y asociaciones',
isRequired: false,
isCompleted: false,
// Optional step - no strict validation required
},
{
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,
// Completion step - no additional validation needed
},
];
/**
* Get step by ID
*/
export const getStepById = (stepId: string): OnboardingStep | undefined => {
return DEFAULT_STEPS.find(step => step.id === stepId);
};
/**
* Get step index by ID
*/
export const getStepIndex = (stepId: string): number => {
return DEFAULT_STEPS.findIndex(step => step.id === stepId);
};
/**
* Check if all required steps before given step are completed
*/
export const canAccessStep = (stepIndex: number, completedSteps: boolean[]): boolean => {
for (let i = 0; i < stepIndex; i++) {
if (DEFAULT_STEPS[i].isRequired && !completedSteps[i]) {
return false;
}
}
return true;
};
/**
* Calculate onboarding progress
*/
export const calculateProgress = (completedSteps: boolean[]): {
completedCount: number;
totalRequired: number;
percentage: number;
} => {
const requiredSteps = DEFAULT_STEPS.filter(step => step.isRequired);
const completedRequired = requiredSteps.filter((step, index) => {
const stepIndex = getStepIndex(step.id);
return completedSteps[stepIndex];
});
return {
completedCount: completedRequired.length,
totalRequired: requiredSteps.length,
percentage: Math.round((completedRequired.length / requiredSteps.length) * 100),
};
};

View File

@@ -0,0 +1,111 @@
/**
* Shared types for onboarding hooks
*/
import type {
BakeryRegistration,
ProductSuggestionResponse,
BusinessModelAnalysisResponse,
User
} from '../../../api';
// Re-export for convenience
export type { ProductSuggestionResponse, BusinessModelAnalysisResponse };
export interface OnboardingStep {
id: string;
title: string;
description: string;
isRequired: boolean;
isCompleted: boolean;
validation?: (data: OnboardingData) => string | null;
}
export interface OnboardingData {
// Step 1: Setup
bakery?: BakeryRegistration;
// 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?: ProductSuggestionResponse[];
approvedSuggestions?: ProductSuggestionResponse[];
approvedProducts?: ProductSuggestionResponse[];
reviewCompleted?: boolean;
// Step 4: Inventory
inventoryItems?: any[];
inventoryMapping?: { [productName: string]: string };
inventoryConfigured?: boolean;
salesImportResult?: {
success: boolean;
imported: boolean;
records_created: number;
message: string;
};
// Step 5: Suppliers
suppliers?: any[];
supplierMappings?: any[];
// Step 6: ML Training
trainingStatus?: 'idle' | 'validating' | 'training' | 'completed' | 'failed';
trainingProgress?: number;
trainingJob?: any;
trainingLogs?: any[];
trainingMetrics?: any;
autoStartTraining?: boolean;
// Step 7: Completion
completionStats?: {
totalProducts: number;
inventoryItems: number;
suppliersConfigured: number;
mlModelAccuracy: number;
estimatedTimeSaved: string;
completionScore: number;
};
// Cross-step data sharing
allStepData?: { [stepId: string]: any };
}
export interface OnboardingProgress {
currentStep: number;
totalSteps: number;
completedSteps: number;
isComplete: boolean;
progressPercentage: number;
}
export interface OnboardingError {
step?: string;
message: string;
details?: any;
}
// Processing progress callback
export type ProgressCallback = (progress: number, stage: string, message: string) => void;
// Step validation function
export type StepValidator = (data: OnboardingData) => string | null;
// Step update callback
export type StepDataUpdater = (stepId: string, data: Partial<OnboardingData>) => void;

View File

@@ -0,0 +1,300 @@
/**
* Inventory setup hook for creating inventory from suggestions
*/
import { useState, useCallback } from 'react';
import {
useCreateIngredient,
useCreateSalesRecord,
} from '../../../api';
import { useCurrentTenant } from '../../../stores';
import type { ProductSuggestionResponse } from './types';
interface InventorySetupState {
isLoading: boolean;
error: string | null;
createdItems: any[];
inventoryMapping: { [productName: string]: string };
salesImportResult: {
success: boolean;
imported: boolean;
records_created: number;
message: string;
} | null;
isInventoryConfigured: boolean;
}
interface InventorySetupActions {
createInventoryFromSuggestions: (
suggestions: ProductSuggestionResponse[]
) => Promise<{
success: boolean;
createdItems?: any[];
inventoryMapping?: { [productName: string]: string };
}>;
importSalesData: (
salesData: any,
inventoryMapping: { [productName: string]: string }
) => Promise<{
success: boolean;
recordsCreated: number;
message: string;
}>;
clearError: () => void;
reset: () => void;
}
export const useInventorySetup = () => {
const [state, setState] = useState<InventorySetupState>({
isLoading: false,
error: null,
createdItems: [],
inventoryMapping: {},
salesImportResult: null,
isInventoryConfigured: false,
});
const createIngredientMutation = useCreateIngredient();
const createSalesRecordMutation = useCreateSalesRecord();
const currentTenant = useCurrentTenant();
const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[]
): Promise<{
success: boolean;
createdItems?: any[];
inventoryMapping?: { [productName: string]: string };
}> => {
if (!suggestions || suggestions.length === 0) {
setState(prev => ({
...prev,
error: 'No hay sugerencias para crear el inventario',
}));
return { success: false };
}
if (!currentTenant?.id) {
setState(prev => ({
...prev,
error: 'No se pudo obtener información del tenant',
}));
return { success: false };
}
setState(prev => ({
...prev,
isLoading: true,
error: null
}));
try {
const createdItems = [];
const inventoryMapping: { [key: string]: string } = {};
// Create ingredients from approved suggestions
for (const suggestion of suggestions) {
try {
const ingredientData = {
name: suggestion.name,
category: suggestion.category || 'Sin categoría',
description: suggestion.description || '',
unit_of_measure: suggestion.unit_of_measure || 'unidad',
cost_per_unit: suggestion.cost_per_unit || 0,
supplier_info: suggestion.supplier_info || {},
nutritional_info: suggestion.nutritional_info || {},
storage_requirements: suggestion.storage_requirements || {},
allergen_info: suggestion.allergen_info || {},
is_active: true,
};
const createdItem = await createIngredientMutation.mutateAsync({
tenantId: currentTenant.id,
ingredientData,
});
createdItems.push(createdItem);
inventoryMapping[suggestion.name] = createdItem.id;
} catch (error) {
console.error(`Error creating ingredient ${suggestion.name}:`, error);
// Continue with other ingredients even if one fails
}
}
if (createdItems.length === 0) {
throw new Error('No se pudo crear ningún elemento del inventario');
}
setState(prev => ({
...prev,
isLoading: false,
createdItems,
inventoryMapping,
isInventoryConfigured: true,
}));
return {
success: true,
createdItems,
inventoryMapping,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creando el inventario';
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage
}));
return { success: false };
}
}, [createIngredientMutation, currentTenant]);
const importSalesData = useCallback(async (
salesData: any,
inventoryMapping: { [productName: string]: string }
): Promise<{
success: boolean;
recordsCreated: number;
message: string;
}> => {
if (!currentTenant?.id) {
setState(prev => ({
...prev,
error: 'No se pudo obtener información del tenant',
}));
return {
success: false,
recordsCreated: 0,
message: 'Error: No se pudo obtener información del tenant',
};
}
if (!salesData || !salesData.product_list) {
setState(prev => ({
...prev,
error: 'No hay datos de ventas para importar',
}));
return {
success: false,
recordsCreated: 0,
message: 'Error: No hay datos de ventas para importar',
};
}
setState(prev => ({
...prev,
isLoading: true,
error: null
}));
try {
let recordsCreated = 0;
// Process actual sales data and create sales records
if (salesData.raw_data && Array.isArray(salesData.raw_data)) {
for (const salesRecord of salesData.raw_data) {
try {
// Map product name to inventory product ID
const inventoryProductId = inventoryMapping[salesRecord.product_name];
if (inventoryProductId) {
const salesRecordData = {
date: salesRecord.date,
product_name: salesRecord.product_name,
inventory_product_id: inventoryProductId,
quantity_sold: salesRecord.quantity,
unit_price: salesRecord.unit_price,
total_revenue: salesRecord.total_amount || (salesRecord.quantity * salesRecord.unit_price),
channel: salesRecord.channel || 'tienda',
customer_info: salesRecord.customer_info || {},
notes: salesRecord.notes || '',
};
await createSalesRecordMutation.mutateAsync({
tenantId: currentTenant.id,
salesData: salesRecordData,
});
recordsCreated++;
}
} catch (error) {
console.error('Error creating sales record:', error);
// Continue with next record
}
}
}
const result = {
success: recordsCreated > 0,
imported: recordsCreated > 0,
records_created: recordsCreated,
message: recordsCreated > 0
? `Se importaron ${recordsCreated} registros de ventas exitosamente`
: 'No se pudieron importar registros de ventas',
};
setState(prev => ({
...prev,
isLoading: false,
salesImportResult: result,
}));
return {
success: result.success,
recordsCreated,
message: result.message,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error importando datos de ventas';
const result = {
success: false,
imported: false,
records_created: 0,
message: errorMessage,
};
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage,
salesImportResult: result,
}));
return {
success: false,
recordsCreated: 0,
message: errorMessage,
};
}
}, [createSalesRecordMutation, currentTenant]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const reset = useCallback(() => {
setState({
isLoading: false,
error: null,
createdItems: [],
inventoryMapping: {},
salesImportResult: null,
isInventoryConfigured: false,
});
}, []);
return {
// State
isLoading: state.isLoading,
error: state.error,
createdItems: state.createdItems,
inventoryMapping: state.inventoryMapping,
salesImportResult: state.salesImportResult,
isInventoryConfigured: state.isInventoryConfigured,
// Actions
createInventoryFromSuggestions,
importSalesData,
clearError,
reset,
} satisfies InventorySetupState & InventorySetupActions;
};

View File

@@ -0,0 +1,306 @@
/**
* Main onboarding hook - orchestrates all focused onboarding hooks
* This is the primary hook that components should use
*/
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthUser } from '../../../stores/auth.store';
import { useCurrentTenant } from '../../../stores';
import { useOnboardingFlow } from './useOnboardingFlow';
import { useOnboardingData } from './useOnboardingData';
import { useTenantCreation } from './useTenantCreation';
import { useSalesProcessing } from './useSalesProcessing';
import { useInventorySetup } from './useInventorySetup';
import { useTrainingOrchestration } from './useTrainingOrchestration';
import type {
OnboardingData,
ProgressCallback,
ProductSuggestionResponse
} from './types';
import type { BakeryRegistration } from '../../../api';
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: BakeryRegistration) => Promise<boolean>;
processSalesFile: (file: File, onProgress?: ProgressCallback) => Promise<boolean>;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<boolean>;
importSalesData: (salesData: any, inventoryMapping: { [productName: string]: string }) => Promise<boolean>;
startTraining: (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}) => Promise<boolean>;
// Completion
completeOnboarding: () => Promise<boolean>;
// Utilities
clearError: () => void;
reset: () => void;
}
export const useOnboarding = () => {
const navigate = useNavigate();
const user = useAuthUser();
const currentTenant = useCurrentTenant();
// Focused hooks
const flow = useOnboardingFlow();
const data = useOnboardingData();
const tenantCreation = useTenantCreation();
const salesProcessing = useSalesProcessing();
const inventorySetup = useInventorySetup();
const trainingOrchestration = useTrainingOrchestration();
// Navigation actions
const nextStep = useCallback((): boolean => {
const validation = validateCurrentStep();
if (validation) {
return false;
}
if (flow.nextStep()) {
flow.markStepCompleted(flow.currentStep - 1);
return true;
}
return false;
}, [flow]);
const previousStep = useCallback((): boolean => {
return flow.previousStep();
}, [flow]);
const goToStep = useCallback((stepIndex: number): boolean => {
return flow.goToStep(stepIndex);
}, [flow]);
// Data management
const updateStepData = useCallback((stepId: string, stepData: Partial<OnboardingData>) => {
data.updateStepData(stepId, stepData);
}, [data]);
const validateCurrentStep = useCallback((): string | null => {
const currentStep = flow.getCurrentStep();
const validationResult = data.validateStep(currentStep.id);
// Also check for specific step validations
if (validationResult) return validationResult;
return null;
}, [flow, data]);
// Step-specific actions
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
const success = await tenantCreation.createTenant(bakeryData);
if (success) {
updateStepData('setup', { bakery: bakeryData });
}
return success;
}, [tenantCreation, updateStepData]);
const processSalesFile = useCallback(async (
file: File,
onProgress?: ProgressCallback
): Promise<boolean> => {
const result = await salesProcessing.processFile(file, onProgress);
if (result.success) {
updateStepData('data-processing', {
files: { salesData: file },
processingStage: 'completed',
processingResults: result.validationResults,
suggestions: result.suggestions || []
});
}
return result.success;
}, [salesProcessing, updateStepData]);
const generateInventorySuggestions = useCallback(async (
productList: string[]
): Promise<ProductSuggestionResponse[] | null> => {
return salesProcessing.generateSuggestions(productList);
}, [salesProcessing]);
const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[]
): Promise<boolean> => {
const result = await inventorySetup.createInventoryFromSuggestions(suggestions);
if (result.success) {
updateStepData('inventory', {
inventoryItems: result.createdItems,
inventoryMapping: result.inventoryMapping,
inventoryConfigured: true,
});
}
return result.success;
}, [inventorySetup, updateStepData]);
const importSalesData = useCallback(async (
salesData: any,
inventoryMapping: { [productName: string]: string }
): Promise<boolean> => {
const result = await inventorySetup.importSalesData(salesData, inventoryMapping);
if (result.success) {
updateStepData('inventory', {
salesImportResult: {
success: result.success,
imported: true,
records_created: result.recordsCreated,
message: result.message,
},
});
}
return result.success;
}, [inventorySetup, updateStepData]);
const startTraining = useCallback(async (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}): Promise<boolean> => {
// First validate training data requirements
const allStepData = data.getAllStepData();
const validation = await trainingOrchestration.validateTrainingData(allStepData);
if (!validation.isValid) {
return false;
}
const success = await trainingOrchestration.startTraining(options);
if (success) {
updateStepData('ml-training', {
trainingStatus: 'training',
trainingJob: trainingOrchestration.job,
trainingLogs: trainingOrchestration.logs,
});
}
return success;
}, [trainingOrchestration, data, updateStepData]);
const completeOnboarding = useCallback(async (): Promise<boolean> => {
// Mark final completion
updateStepData('completion', {
completionStats: {
totalProducts: data.data.processingResults?.unique_products || 0,
inventoryItems: data.data.inventoryItems?.length || 0,
suppliersConfigured: data.data.suppliers?.length || 0,
mlModelAccuracy: data.data.trainingMetrics?.accuracy || 0,
estimatedTimeSaved: '2-3 horas por día',
completionScore: 95,
},
});
flow.markStepCompleted(flow.steps.length - 1);
// Navigate to dashboard after completion
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
return true;
}, [data, flow, navigate, updateStepData]);
// Utilities
const clearError = useCallback(() => {
data.clearError();
tenantCreation.clearError();
salesProcessing.clearError();
inventorySetup.clearError();
trainingOrchestration.clearError();
}, [data, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration]);
const reset = useCallback(() => {
flow.resetFlow();
data.resetData();
tenantCreation.reset();
salesProcessing.reset();
inventorySetup.reset();
trainingOrchestration.reset();
}, [flow, data, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration]);
// Determine overall loading and error state
const isLoading = tenantCreation.isLoading ||
salesProcessing.isLoading ||
inventorySetup.isLoading ||
trainingOrchestration.isLoading;
const error = data.error?.message ||
tenantCreation.error ||
salesProcessing.error ||
inventorySetup.error ||
trainingOrchestration.error;
return {
// State from flow management
currentStep: flow.currentStep,
steps: flow.steps,
progress: flow.getProgress(),
// State from data management
data: data.data,
allStepData: data.getAllStepData(),
// State from individual hooks
tenantCreation: {
isLoading: tenantCreation.isLoading,
isSuccess: tenantCreation.isSuccess,
},
salesProcessing: {
stage: salesProcessing.stage,
progress: salesProcessing.progress,
currentMessage: salesProcessing.currentMessage,
validationResults: salesProcessing.validationResults,
suggestions: salesProcessing.suggestions,
},
inventorySetup: {
createdItems: inventorySetup.createdItems,
inventoryMapping: inventorySetup.inventoryMapping,
salesImportResult: inventorySetup.salesImportResult,
isInventoryConfigured: inventorySetup.isInventoryConfigured,
},
trainingOrchestration: {
status: trainingOrchestration.status,
progress: trainingOrchestration.progress,
currentStep: trainingOrchestration.currentStep,
estimatedTimeRemaining: trainingOrchestration.estimatedTimeRemaining,
job: trainingOrchestration.job,
logs: trainingOrchestration.logs,
metrics: trainingOrchestration.metrics,
},
// Overall state
isLoading,
error,
user,
currentTenant,
// Actions
nextStep,
previousStep,
goToStep,
updateStepData,
validateCurrentStep,
createTenant,
processSalesFile,
generateInventorySuggestions,
createInventoryFromSuggestions,
importSalesData,
startTraining,
completeOnboarding,
clearError,
reset,
} satisfies ReturnType<typeof useOnboardingFlow> &
ReturnType<typeof useOnboardingData> &
{ [key: string]: any } &
OnboardingActions;
};

View File

@@ -0,0 +1,130 @@
/**
* Data persistence and validation for onboarding
*/
import { useState, useCallback } from 'react';
import { getStepById } from './steps';
import type { OnboardingData, OnboardingError, StepValidator } from './types';
interface OnboardingDataState {
data: OnboardingData;
error: OnboardingError | null;
}
interface OnboardingDataActions {
updateStepData: (stepId: string, stepData: Partial<OnboardingData>) => void;
validateStep: (stepId: string) => string | null;
clearError: () => void;
resetData: () => void;
getStepData: (stepId: string) => any;
setAllStepData: (allData: { [stepId: string]: any }) => void;
getAllStepData: () => { [stepId: string]: any };
}
export const useOnboardingData = () => {
const [state, setState] = useState<OnboardingDataState>({
data: {
allStepData: {},
},
error: null,
});
const updateStepData = useCallback((stepId: string, stepData: Partial<OnboardingData>) => {
setState(prev => ({
...prev,
data: {
...prev.data,
...stepData,
// Also store in allStepData for cross-step access
allStepData: {
...prev.data.allStepData,
[stepId]: {
...prev.data.allStepData?.[stepId],
...stepData,
},
},
},
error: null, // Clear error when data is updated successfully
}));
}, []);
const validateStep = useCallback((stepId: string): string | null => {
const step = getStepById(stepId);
if (step?.validation) {
try {
const validationResult = step.validation(state.data);
if (validationResult) {
setState(prev => ({
...prev,
error: {
step: stepId,
message: validationResult,
},
}));
return validationResult;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Validation error';
setState(prev => ({
...prev,
error: {
step: stepId,
message: errorMessage,
details: error,
},
}));
return errorMessage;
}
}
return null;
}, [state.data]);
const clearError = useCallback(() => {
setState(prev => ({
...prev,
error: null,
}));
}, []);
const resetData = useCallback(() => {
setState({
data: {
allStepData: {},
},
error: null,
});
}, []);
const getStepData = useCallback((stepId: string): any => {
return state.data.allStepData?.[stepId] || {};
}, [state.data.allStepData]);
const setAllStepData = useCallback((allData: { [stepId: string]: any }) => {
setState(prev => ({
...prev,
data: {
...prev.data,
allStepData: allData,
},
}));
}, []);
const getAllStepData = useCallback((): { [stepId: string]: any } => {
return state.data.allStepData || {};
}, [state.data.allStepData]);
return {
// State
data: state.data,
error: state.error,
// Actions
updateStepData,
validateStep,
clearError,
resetData,
getStepData,
setAllStepData,
getAllStepData,
} satisfies OnboardingDataState & OnboardingDataActions;
};

View File

@@ -0,0 +1,122 @@
/**
* Navigation and step management for onboarding
*/
import { useState, useCallback } from 'react';
import { DEFAULT_STEPS, canAccessStep, calculateProgress } from './steps';
import type { OnboardingStep, OnboardingProgress } from './types';
interface OnboardingFlowState {
currentStep: number;
steps: OnboardingStep[];
}
interface OnboardingFlowActions {
nextStep: () => boolean;
previousStep: () => boolean;
goToStep: (stepIndex: number) => boolean;
getCurrentStep: () => OnboardingStep;
getProgress: () => OnboardingProgress;
canNavigateToStep: (stepIndex: number) => boolean;
markStepCompleted: (stepIndex: number) => void;
resetFlow: () => void;
}
export const useOnboardingFlow = () => {
const [state, setState] = useState<OnboardingFlowState>({
currentStep: 0,
steps: DEFAULT_STEPS.map(step => ({ ...step })), // Create a copy
});
const nextStep = useCallback((): boolean => {
if (state.currentStep < state.steps.length - 1) {
setState(prev => ({
...prev,
currentStep: prev.currentStep + 1,
}));
return true;
}
return false;
}, [state.currentStep, state.steps.length]);
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) {
const completedSteps = state.steps.map(step => step.isCompleted);
if (canAccessStep(stepIndex, completedSteps)) {
setState(prev => ({
...prev,
currentStep: stepIndex,
}));
return true;
}
}
return false;
}, [state.steps]);
const getCurrentStep = useCallback((): OnboardingStep => {
return state.steps[state.currentStep];
}, [state.currentStep, state.steps]);
const getProgress = useCallback((): OnboardingProgress => {
const completedSteps = state.steps.map(step => step.isCompleted);
const progress = calculateProgress(completedSteps);
return {
currentStep: state.currentStep,
totalSteps: state.steps.length,
completedSteps: progress.completedCount,
isComplete: progress.completedCount === progress.totalRequired,
progressPercentage: progress.percentage,
};
}, [state.currentStep, state.steps]);
const canNavigateToStep = useCallback((stepIndex: number): boolean => {
const completedSteps = state.steps.map(step => step.isCompleted);
return canAccessStep(stepIndex, completedSteps);
}, [state.steps]);
const markStepCompleted = useCallback((stepIndex: number): void => {
setState(prev => ({
...prev,
steps: prev.steps.map((step, index) =>
index === stepIndex
? { ...step, isCompleted: true }
: step
),
}));
}, []);
const resetFlow = useCallback((): void => {
setState({
currentStep: 0,
steps: DEFAULT_STEPS.map(step => ({ ...step, isCompleted: false })),
});
}, []);
return {
// State
currentStep: state.currentStep,
steps: state.steps,
// Actions
nextStep,
previousStep,
goToStep,
getCurrentStep,
getProgress,
canNavigateToStep,
markStepCompleted,
resetFlow,
} satisfies OnboardingFlowState & OnboardingFlowActions;
};

View File

@@ -0,0 +1,221 @@
/**
* Sales file processing and validation hook
*/
import { useState, useCallback } from 'react';
import { useClassifyProductsBatch, useValidateAndImportFile } from '../../../api';
import { useCurrentTenant } from '../../../stores';
import type { ProductSuggestionResponse, ProgressCallback } from './types';
interface SalesProcessingState {
isLoading: boolean;
error: string | null;
stage: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error';
progress: number;
currentMessage: string;
validationResults: any | null;
suggestions: ProductSuggestionResponse[] | null;
}
interface SalesProcessingActions {
processFile: (file: File, onProgress?: ProgressCallback) => Promise<{
success: boolean;
validationResults?: any;
suggestions?: ProductSuggestionResponse[];
}>;
generateSuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
clearError: () => void;
reset: () => void;
}
export const useSalesProcessing = () => {
const [state, setState] = useState<SalesProcessingState>({
isLoading: false,
error: null,
stage: 'idle',
progress: 0,
currentMessage: '',
validationResults: null,
suggestions: null,
});
const classifyProductsMutation = useClassifyProductsBatch();
const currentTenant = useCurrentTenant();
const { processFile: processFileImport } = useValidateAndImportFile();
const updateProgress = useCallback((progress: number, stage: string, message: string, onProgress?: ProgressCallback) => {
setState(prev => ({
...prev,
progress,
stage: stage as any,
currentMessage: message,
}));
onProgress?.(progress, stage, message);
}, []);
const processFile = useCallback(async (
file: File,
onProgress?: ProgressCallback
): Promise<{
success: boolean;
validationResults?: any;
suggestions?: ProductSuggestionResponse[];
}> => {
setState(prev => ({
...prev,
isLoading: true,
error: null,
stage: 'idle',
progress: 0,
}));
try {
// Stage 1: Validate file structure
updateProgress(10, 'validating', 'Iniciando validación del archivo...', onProgress);
updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress);
// Use the actual data import hook for file validation
if (!currentTenant?.id) {
throw new Error('No se pudo obtener información del tenant');
}
const result = await processFileImport(
currentTenant.id,
file,
{
skipValidation: false,
onProgress: (stage, progress, message) => {
updateProgress(progress, stage, message, onProgress);
}
}
);
if (!result.success || !result.validationResult) {
throw new Error(result.error || 'Error en la validación del archivo');
}
const validationResult = {
...result.validationResult,
product_list: extractProductList(result.validationResult),
};
updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress);
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos válidos en el archivo');
}
// Stage 2: Generate AI suggestions
updateProgress(60, 'analyzing', 'Identificando productos únicos...', onProgress);
updateProgress(70, 'analyzing', 'Generando sugerencias de IA...', onProgress);
const suggestions = await generateSuggestions(validationResult.product_list);
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
updateProgress(100, 'completed', 'Procesamiento completado exitosamente', onProgress);
setState(prev => ({
...prev,
isLoading: false,
stage: 'completed',
validationResults: validationResult,
suggestions: suggestions || [],
}));
return {
success: true,
validationResults: validationResult,
suggestions: suggestions || [],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage,
stage: 'error',
}));
updateProgress(0, 'error', errorMessage, onProgress);
return {
success: false,
};
}
}, [updateProgress, currentTenant, processFileImport]);
// Helper to extract product list from validation result
const extractProductList = useCallback((validationResult: any): string[] => {
// Extract unique product names from sample records
if (validationResult.sample_records && Array.isArray(validationResult.sample_records)) {
const productSet = new Set<string>();
validationResult.sample_records.forEach((record: any) => {
if (record.product_name) {
productSet.add(record.product_name);
}
});
return Array.from(productSet);
}
return [];
}, []);
const generateSuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[] | null> => {
try {
if (!currentTenant?.id) {
console.error('No tenant ID available for classification');
return [];
}
const response = await classifyProductsMutation.mutateAsync({
tenantId: currentTenant.id,
batchData: {
products: productList.map(name => ({
product_name: name,
description: ''
}))
}
});
return response || [];
} catch (error) {
console.error('Error generating inventory suggestions:', error);
// Don't throw here - suggestions are optional
return [];
}
}, [classifyProductsMutation, currentTenant]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const reset = useCallback(() => {
setState({
isLoading: false,
error: null,
stage: 'idle',
progress: 0,
currentMessage: '',
validationResults: null,
suggestions: null,
});
}, []);
return {
// State
isLoading: state.isLoading,
error: state.error,
stage: state.stage,
progress: state.progress,
currentMessage: state.currentMessage,
validationResults: state.validationResults,
suggestions: state.suggestions,
// Actions
processFile,
generateSuggestions,
clearError,
reset,
} satisfies SalesProcessingState & SalesProcessingActions;
};

View File

@@ -0,0 +1,96 @@
/**
* Tenant creation hook for bakery registration
*/
import { useState, useCallback } from 'react';
import { useRegisterBakery } from '../../../api';
import type { BakeryRegistration } from '../../../api';
interface TenantCreationState {
isLoading: boolean;
error: string | null;
isSuccess: boolean;
tenantData: BakeryRegistration | null;
}
interface TenantCreationActions {
createTenant: (bakeryData: BakeryRegistration) => Promise<boolean>;
clearError: () => void;
reset: () => void;
}
export const useTenantCreation = () => {
const [state, setState] = useState<TenantCreationState>({
isLoading: false,
error: null,
isSuccess: false,
tenantData: null,
});
const registerBakeryMutation = useRegisterBakery();
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) {
setState(prev => ({
...prev,
error: 'Los datos de la panadería son requeridos',
}));
return false;
}
setState(prev => ({
...prev,
isLoading: true,
error: null,
isSuccess: false
}));
try {
await registerBakeryMutation.mutateAsync(bakeryData);
setState(prev => ({
...prev,
isLoading: false,
isSuccess: true,
tenantData: bakeryData,
}));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error al crear la panadería';
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage,
isSuccess: false,
}));
return false;
}
}, [registerBakeryMutation]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const reset = useCallback(() => {
setState({
isLoading: false,
error: null,
isSuccess: false,
tenantData: null,
});
}, []);
return {
// State
isLoading: state.isLoading,
error: state.error,
isSuccess: state.isSuccess,
tenantData: state.tenantData,
// Actions
createTenant,
clearError,
reset,
} satisfies TenantCreationState & TenantCreationActions;
};

View File

@@ -0,0 +1,303 @@
/**
* ML model training orchestration hook
*/
import { useState, useCallback, useEffect } from 'react';
import {
useCreateTrainingJob,
useTrainingJobStatus,
useTrainingWebSocket,
} from '../../../api';
import { useCurrentTenant } from '../../../stores';
import { useAuthUser } from '../../../stores/auth.store';
import type { TrainingJobResponse, TrainingMetrics } from '../../../api';
interface TrainingOrchestrationState {
isLoading: boolean;
error: string | null;
status: 'idle' | 'validating' | 'training' | 'completed' | 'failed';
progress: number;
currentStep: string;
estimatedTimeRemaining: number;
job: TrainingJobResponse | null;
logs: TrainingLog[];
metrics: TrainingMetrics | null;
}
interface TrainingLog {
timestamp: string;
message: string;
level: 'info' | 'warning' | 'error' | 'success';
}
interface TrainingOrchestrationActions {
startTraining: (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}) => Promise<boolean>;
validateTrainingData: (allStepData: any) => Promise<{
isValid: boolean;
missingItems: string[];
}>;
clearError: () => void;
reset: () => void;
addLog: (message: string, level?: TrainingLog['level']) => void;
}
export const useTrainingOrchestration = () => {
const [state, setState] = useState<TrainingOrchestrationState>({
isLoading: false,
error: null,
status: 'idle',
progress: 0,
currentStep: '',
estimatedTimeRemaining: 0,
job: null,
logs: [],
metrics: null,
});
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const createTrainingJobMutation = useCreateTrainingJob();
// Get job status when we have a job ID
const { data: jobStatus } = useTrainingJobStatus(
currentTenant?.id || '',
state.job?.job_id || '',
{
enabled: !!currentTenant?.id && !!state.job?.job_id && state.status === 'training',
refetchInterval: 5000,
}
);
// WebSocket for real-time updates
const { data: wsData } = useTrainingWebSocket(
currentTenant?.id || '',
state.job?.job_id || '',
user?.token,
{
onProgress: (data) => {
setState(prev => ({
...prev,
progress: data.progress?.percentage || prev.progress,
currentStep: data.progress?.current_step || prev.currentStep,
estimatedTimeRemaining: data.progress?.estimated_time_remaining || prev.estimatedTimeRemaining,
}));
addLog(
`${data.progress?.current_step} - ${data.progress?.products_completed}/${data.progress?.products_total} productos procesados (${data.progress?.percentage}%)`,
'info'
);
},
onCompleted: (data) => {
setState(prev => ({
...prev,
status: 'completed',
progress: 100,
metrics: {
accuracy: data.results.performance_metrics.accuracy,
mape: data.results.performance_metrics.mape,
mae: data.results.performance_metrics.mae,
rmse: data.results.performance_metrics.rmse,
},
}));
addLog('¡Entrenamiento ML completado exitosamente!', 'success');
addLog(`${data.results.successful_trainings} modelos creados exitosamente`, 'success');
addLog(`Duración total: ${Math.round(data.results.training_duration / 60)} minutos`, 'info');
},
onError: (data) => {
setState(prev => ({
...prev,
status: 'failed',
error: data.error,
}));
addLog(`Error en entrenamiento: ${data.error}`, 'error');
},
onStarted: (data) => {
addLog(`Trabajo de entrenamiento iniciado: ${data.job_id}`, 'success');
},
}
);
// Update status from polling when WebSocket is not available
useEffect(() => {
if (jobStatus && state.job?.job_id === jobStatus.job_id) {
setState(prev => ({
...prev,
status: jobStatus.status as any,
progress: jobStatus.progress || prev.progress,
currentStep: jobStatus.current_step || prev.currentStep,
estimatedTimeRemaining: jobStatus.estimated_time_remaining || prev.estimatedTimeRemaining,
}));
}
}, [jobStatus, state.job?.job_id]);
const addLog = useCallback((message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
setState(prev => ({
...prev,
logs: [...prev.logs, newLog]
}));
}, []);
const validateTrainingData = useCallback(async (allStepData: any): Promise<{
isValid: boolean;
missingItems: string[];
}> => {
const missingItems: string[] = [];
// Get data from previous steps
const dataProcessingData = allStepData?.['data-processing'];
const reviewData = allStepData?.['review'];
const inventoryData = allStepData?.['inventory'];
// Check if sales data was processed
const hasProcessingResults = dataProcessingData?.processingResults &&
dataProcessingData.processingResults.is_valid &&
dataProcessingData.processingResults.total_records > 0;
// Check if sales data was imported (required for training)
const hasImportResults = inventoryData?.salesImportResult &&
(inventoryData.salesImportResult.records_created > 0 ||
inventoryData.salesImportResult.success === true ||
inventoryData.salesImportResult.imported === true);
if (!hasProcessingResults) {
missingItems.push('Datos de ventas validados');
}
// Sales data must be imported for ML training to work
if (!hasImportResults) {
missingItems.push('Datos de ventas importados');
}
// Check if products were approved in review step
const hasApprovedProducts = reviewData?.approvedProducts &&
reviewData.approvedProducts.length > 0 &&
reviewData.reviewCompleted;
if (!hasApprovedProducts) {
missingItems.push('Productos aprobados en revisión');
}
// Check if inventory was configured
const hasInventoryConfig = inventoryData?.inventoryConfigured &&
inventoryData?.inventoryItems &&
inventoryData.inventoryItems.length > 0;
if (!hasInventoryConfig) {
missingItems.push('Inventario configurado');
}
// Check if we have enough data for training
if (dataProcessingData?.processingResults?.total_records &&
dataProcessingData.processingResults.total_records < 10) {
missingItems.push('Suficientes registros de ventas (mínimo 10)');
}
return {
isValid: missingItems.length === 0,
missingItems
};
}, []);
const startTraining = useCallback(async (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}): Promise<boolean> => {
if (!currentTenant?.id) {
setState(prev => ({
...prev,
error: 'No se pudo obtener información del tenant',
}));
return false;
}
setState(prev => ({
...prev,
isLoading: true,
error: null,
status: 'validating',
progress: 0,
}));
addLog('Validando disponibilidad de datos...', 'info');
try {
// Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const job = await createTrainingJobMutation.mutateAsync({
tenantId: currentTenant.id,
request: {
products: options?.products,
start_date: options?.startDate,
end_date: options?.endDate,
}
});
setState(prev => ({
...prev,
job,
status: 'training',
isLoading: false,
}));
addLog(`Trabajo de entrenamiento iniciado: ${job.job_id}`, 'success');
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error al iniciar entrenamiento';
setState(prev => ({
...prev,
isLoading: false,
error: errorMessage,
status: 'failed',
}));
addLog(`Error: ${errorMessage}`, 'error');
return false;
}
}, [currentTenant, createTrainingJobMutation, addLog]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const reset = useCallback(() => {
setState({
isLoading: false,
error: null,
status: 'idle',
progress: 0,
currentStep: '',
estimatedTimeRemaining: 0,
job: null,
logs: [],
metrics: null,
});
}, []);
return {
// State
isLoading: state.isLoading,
error: state.error,
status: state.status,
progress: state.progress,
currentStep: state.currentStep,
estimatedTimeRemaining: state.estimatedTimeRemaining,
job: state.job,
logs: state.logs,
metrics: state.metrics,
// Actions
startTraining,
validateTrainingData,
clearError,
reset,
addLog,
} satisfies TrainingOrchestrationState & TrainingOrchestrationActions;
};

View File

@@ -1,536 +0,0 @@
/**
* Onboarding business hook for managing the complete onboarding workflow
*/
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthUser } from '../../stores/auth.store';
import {
// Auth hooks
useAuthProfile,
// Tenant hooks
useRegisterBakery,
// Sales hooks
useValidateSalesRecord,
// Inventory hooks
useClassifyProductsBatch,
useCreateIngredient,
// Classification hooks
useBusinessModelAnalysis,
// Types
type User,
type BakeryRegistration,
type ProductSuggestionResponse,
type BusinessModelAnalysisResponse,
type ProductClassificationRequest,
} from '../../api';
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?: BakeryRegistration;
// 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?: ProductSuggestionResponse[];
approvedSuggestions?: ProductSuggestionResponse[];
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: User['onboarding_status'] | 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: BakeryRegistration) => Promise<boolean>;
processSalesFile: (file: File, onProgress: (progress: number, stage: string, message: string) => void) => Promise<boolean>;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<any | null>;
getBusinessModelGuide: (model: string) => Promise<BusinessModelAnalysisResponse | null>;
downloadTemplate: (templateData: any, 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_type) return 'El tipo 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();
// React Query hooks
const { data: profile } = useAuthProfile();
const registerBakeryMutation = useRegisterBakery();
const validateSalesMutation = useValidateSalesRecord();
const classifyProductsMutation = useClassifyProductsBatch();
const businessModelMutation = useBusinessModelAnalysis();
// 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) {
setState(prev => ({ ...prev, error: validation }));
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: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) return false;
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
await registerBakeryMutation.mutateAsync({
bakeryData
});
updateStepData('setup', { bakery: bakeryData });
setState(prev => ({ ...prev, isLoading: false }));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
return false;
}
}, [updateStepData, registerBakeryMutation]);
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 validateSalesMutation.mutateAsync({
// Convert file to the expected format for validation
data: {
// This would need to be adapted based on the actual API structure
file_data: file
}
});
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos válidos 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 || []
});
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, validateSalesMutation]);
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[] | null> => {
try {
const response = await classifyProductsMutation.mutateAsync({
products: productList.map(name => ({ name, description: '' }))
});
return response.suggestions || [];
} catch (error) {
console.error('Error generating inventory suggestions:', error);
return null;
}
}, [classifyProductsMutation]);
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestionResponse[]): Promise<any | null> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Create ingredients from approved suggestions
const createdItems = [];
const inventoryMapping: { [key: string]: string } = {};
for (const suggestion of suggestions) {
// This would need to be adapted based on actual API structure
const createdItem = await useCreateIngredient().mutateAsync({
name: suggestion.name,
category: suggestion.category,
// Map other suggestion properties to ingredient properties
});
createdItems.push(createdItem);
inventoryMapping[suggestion.name] = createdItem.id;
}
updateStepData('inventory', {
inventoryItems: createdItems,
inventoryMapping,
inventoryConfigured: true
});
setState(prev => ({ ...prev, isLoading: false }));
return { created_items: createdItems, inventory_mapping: inventoryMapping };
} 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: string): Promise<BusinessModelAnalysisResponse | null> => {
try {
const response = await businessModelMutation.mutateAsync({
business_model: model,
// Include any other required parameters
});
return response;
} catch (error) {
console.error('Error getting business model guide:', error);
return null;
}
}, [businessModelMutation]);
const downloadTemplate = useCallback((templateData: any, filename: string, format: 'csv' | 'json' = 'csv') => {
// Create and download template file
const content = format === 'json' ? JSON.stringify(templateData, null, 2) : convertToCSV(templateData);
const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.${format}`;
a.click();
URL.revokeObjectURL(url);
}, []);
const convertToCSV = (data: any): string => {
// Simple CSV conversion - this should be adapted based on the actual data structure
if (Array.isArray(data)) {
const headers = Object.keys(data[0] || {}).join(',');
const rows = data.map(item => Object.values(item).join(',')).join('\n');
return `${headers}\n${rows}`;
}
return JSON.stringify(data);
};
const checkOnboardingStatus = useCallback(async () => {
setState(prev => ({ ...prev, isLoading: true }));
try {
// Use the profile data to get onboarding status
setState(prev => ({
...prev,
onboardingStatus: profile?.onboarding_status || null,
isInitialized: true,
isLoading: false
}));
} catch (error) {
setState(prev => ({
...prev,
isInitialized: true,
isLoading: false
}));
}
}, [profile]);
const completeOnboarding = useCallback(async (): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Mark onboarding as completed - this would typically involve an API call
// For now, we'll simulate success and navigate to dashboard
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;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
return false;
}
}, [state.data, 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,
};
};