Start integrating the onboarding flow with backend 7
This commit is contained in:
21
frontend/src/hooks/business/onboarding/index.ts
Normal file
21
frontend/src/hooks/business/onboarding/index.ts
Normal 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';
|
||||
145
frontend/src/hooks/business/onboarding/steps.ts
Normal file
145
frontend/src/hooks/business/onboarding/steps.ts
Normal 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),
|
||||
};
|
||||
};
|
||||
111
frontend/src/hooks/business/onboarding/types.ts
Normal file
111
frontend/src/hooks/business/onboarding/types.ts
Normal 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;
|
||||
300
frontend/src/hooks/business/onboarding/useInventorySetup.ts
Normal file
300
frontend/src/hooks/business/onboarding/useInventorySetup.ts
Normal 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;
|
||||
};
|
||||
306
frontend/src/hooks/business/onboarding/useOnboarding.ts
Normal file
306
frontend/src/hooks/business/onboarding/useOnboarding.ts
Normal 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;
|
||||
};
|
||||
130
frontend/src/hooks/business/onboarding/useOnboardingData.ts
Normal file
130
frontend/src/hooks/business/onboarding/useOnboardingData.ts
Normal 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;
|
||||
};
|
||||
122
frontend/src/hooks/business/onboarding/useOnboardingFlow.ts
Normal file
122
frontend/src/hooks/business/onboarding/useOnboardingFlow.ts
Normal 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;
|
||||
};
|
||||
221
frontend/src/hooks/business/onboarding/useSalesProcessing.ts
Normal file
221
frontend/src/hooks/business/onboarding/useSalesProcessing.ts
Normal 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;
|
||||
};
|
||||
96
frontend/src/hooks/business/onboarding/useTenantCreation.ts
Normal file
96
frontend/src/hooks/business/onboarding/useTenantCreation.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user