Simplify the onboardinf flow components

This commit is contained in:
Urtzi Alfaro
2025-09-08 17:19:00 +02:00
parent 201817a1be
commit 2e1e696cb5
32 changed files with 1431 additions and 6366 deletions

View File

@@ -1,225 +0,0 @@
# Onboarding Hooks - Complete Clean Architecture
## Overview
This onboarding system has been **completely refactored** with no legacy code or backwards compatibility. It follows modern clean architecture principles with:
- **Standardized patterns** across all services
- **Zero circular dependencies**
- **Centralized state management** with Zustand
- **Type-safe interfaces** throughout
- **Service isolation** with standardized APIs
- **Factory pattern** for service hooks
## Architecture
### 🏗️ Core Layer (`core/`)
**`store.ts`** - Centralized Zustand store
- Single source of truth for all onboarding state
- Atomic operations with devtools integration
- Computed getters for derived state
**`actions.ts`** - Business logic orchestration
- Coordinates between services
- Handles complex step transitions
- Manages validation and error states
**`types.ts`** - Complete type definitions
- Unified interfaces for all services
- Standardized error and state patterns
- Re-exports for external consumption
### 🔧 Services Layer (`services/`)
All services follow the same standardized pattern using `createServiceHook`:
**`useTenantCreation.ts`** - Bakery registration and tenant setup
**`useSalesProcessing.ts`** - File validation and AI classification
**`useInventorySetup.ts`** - Inventory creation and sales import
**`useTrainingOrchestration.ts`** - ML model training workflow
**`useProgressTracking.ts`** - Backend progress synchronization
**`useResumeLogic.ts`** - Flow resumption management
### 🛠️ Utils Layer (`utils/`)
**`createServiceHook.ts`** - Factory for standardized service hooks
- Eliminates duplicate patterns
- Provides consistent async execution
- Standardized error handling
## New File Structure
```
src/hooks/business/onboarding/
├── core/
│ ├── store.ts # Zustand centralized store
│ ├── actions.ts # Business logic orchestration
│ ├── types.ts # Complete type definitions
│ └── useAutoResume.ts # Auto-resume wrapper
├── services/
│ ├── useTenantCreation.ts # Tenant service
│ ├── useSalesProcessing.ts # Sales processing service
│ ├── useInventorySetup.ts # Inventory service
│ ├── useTrainingOrchestration.ts # Training service
│ ├── useProgressTracking.ts # Progress service
│ └── useResumeLogic.ts # Resume service
├── utils/
│ └── createServiceHook.ts # Service factory
├── config/
│ └── steps.ts # Step definitions and validation
├── useOnboarding.ts # Main unified hook
└── index.ts # Clean exports
```
## Component Usage
### Primary Interface - useOnboarding
```typescript
import { useOnboarding } from '../hooks/business/onboarding';
const OnboardingComponent = () => {
const {
// Core state
currentStep,
steps,
data,
progress,
isLoading,
error,
// Service states (when needed)
tenantCreation: { isLoading: tenantLoading, isSuccess },
salesProcessing: { stage, progress: fileProgress, suggestions },
inventorySetup: { createdItems, inventoryMapping },
trainingOrchestration: { status, logs, metrics },
// Actions
nextStep,
previousStep,
updateStepData,
createTenant,
processSalesFile,
startTraining,
completeOnboarding,
clearError,
} = useOnboarding();
// Clean, consistent API across all functionality
};
```
### Auto-Resume Functionality
```typescript
import { useAutoResume } from '../hooks/business/onboarding';
const OnboardingPage = () => {
const { isCheckingResume, completionPercentage } = useAutoResume();
if (isCheckingResume) {
return <LoadingSpinner message="Checking saved progress..." />;
}
// Continue with onboarding flow
};
```
## Key Improvements
### ✅ **Eliminated All Legacy Issues**
1. **No Circular Dependencies** - Clear dependency hierarchy
2. **No Massive God Hooks** - Focused, single-responsibility services
3. **No Inconsistent Patterns** - Standardized service factory
4. **No Type Confusion** - Clean, unified type system
5. **No Duplicate Code** - DRY principles throughout
### ✅ **Modern Patterns**
1. **Zustand Store** - Performant, devtools-enabled state
2. **Factory Pattern** - Consistent service creation
3. **Service Composition** - Clean separation of concerns
4. **Type-Safe** - Full TypeScript coverage
5. **Async-First** - Proper error handling and loading states
### ✅ **Developer Experience**
1. **Predictable API** - Same patterns across all services
2. **Easy Testing** - Isolated, mockable services
3. **Clear Documentation** - Self-documenting code structure
4. **Performance** - Optimized renders and state updates
5. **Debugging** - Zustand devtools integration
## Benefits for Components
### Before (Legacy)
```typescript
// Inconsistent APIs, complex imports, circular deps
import { useOnboarding } from './useOnboarding';
import { useAutoResume } from './useAutoResume'; // Circular dependency!
const {
// 50+ properties mixed together
currentStep, data, isLoading, tenantCreation: { isLoading: tenantLoading },
salesProcessing: { stage, progress }, // Nested complexity
} = useOnboarding();
```
### After (Clean)
```typescript
// Clean, consistent, predictable
import { useOnboarding, useAutoResume } from '../hooks/business/onboarding';
const onboarding = useOnboarding();
const autoResume = useAutoResume();
// Clear separation, no circular deps, standardized patterns
```
## Migration Path
### ❌ Removed (No Backwards Compatibility)
- Old `useOnboarding` (400+ lines)
- Old `useOnboardingData`
- Old `useOnboardingFlow`
- Old `useAutoResume`
- All inconsistent service hooks
### ✅ New Clean Interfaces
- Unified `useOnboarding` hook
- Standardized service hooks
- Centralized store management
- Factory-created services
## Advanced Usage
### Direct Service Access
```typescript
import { useSalesProcessing, useInventorySetup } from '../hooks/business/onboarding';
// When you need direct service control
const salesService = useSalesProcessing();
const inventoryService = useInventorySetup();
```
### Custom Service Creation
```typescript
import { createServiceHook } from '../hooks/business/onboarding';
// Create new services following the same pattern
const useMyCustomService = createServiceHook({
initialState: { customData: null },
// ... configuration
});
```
## Performance
- **Zustand** - Minimal re-renders, optimized updates
- **Memoized Selectors** - Computed values cached
- **Service Isolation** - Independent loading states
- **Factory Pattern** - Reduced bundle size
This is a **complete rewrite** with zero legacy baggage. Modern, maintainable, and built for the future.

View File

@@ -1,187 +0,0 @@
/**
* Onboarding step definitions and validation logic
*/
import type { OnboardingStep, OnboardingData } from '../core/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: 'smart-inventory-setup',
title: '📦 Inventario Inteligente',
description: 'Sube datos de ventas, configura inventario y crea tu catálogo de productos',
isRequired: true,
isCompleted: false,
validation: (data: OnboardingData) => {
console.log('Smart Inventory Step Validation - Data:', {
hasSalesData: !!data.files?.salesData,
processingStage: data.processingStage,
isValid: data.processingResults?.is_valid,
reviewCompleted: data.reviewCompleted,
approvedProductsCount: data.approvedProducts?.length || 0,
inventoryConfigured: data.inventoryConfigured,
salesImportResult: data.salesImportResult
});
if (!data.files?.salesData) return 'Debes cargar el archivo de datos de ventas';
if (data.processingStage !== 'completed' && data.processingStage !== 'review') return 'El procesamiento debe completarse antes de continuar';
if (!data.processingResults?.is_valid) return 'Los datos deben ser válidos para continuar';
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';
// Check if ready for automatic inventory creation and sales import
// If inventory is already configured, check if sales data was imported
if (data.inventoryConfigured) {
const hasImportResults = data.salesImportResult &&
(data.salesImportResult.records_created > 0 ||
data.salesImportResult.success === true ||
data.salesImportResult.imported === true);
if (!hasImportResults) {
console.log('Smart Inventory Step Validation - Sales import validation failed:', {
hasSalesImportResult: !!data.salesImportResult,
salesImportResult: data.salesImportResult,
inventoryConfigured: data.inventoryConfigured
});
return 'Los datos de ventas históricos deben estar importados para continuar al entrenamiento de IA.';
}
} else {
// If inventory is not configured yet, ensure all prerequisites are ready
// The actual creation will happen automatically on "Next Step"
console.log('Smart Inventory Step Validation - Ready for automatic inventory creation:', {
hasApprovedProducts: hasApprovedProducts,
reviewCompleted: data.reviewCompleted,
readyForCreation: hasApprovedProducts && data.reviewCompleted
});
}
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) => {
console.log('ML Training Step Validation - Data:', {
inventoryConfigured: data.inventoryConfigured,
hasSalesFile: !!data.files?.salesData,
processingResults: data.processingResults,
salesImportResult: data.salesImportResult,
trainingStatus: data.trainingStatus
});
// CRITICAL PREREQUISITE 1: Inventory must be configured
if (!data.inventoryConfigured) {
return 'Debes configurar el inventario antes de entrenar el modelo de IA';
}
// CRITICAL PREREQUISITE 2: Sales file must be uploaded and processed
if (!data.files?.salesData) {
return 'Debes cargar un archivo de datos de ventas históricos para entrenar el modelo';
}
// CRITICAL PREREQUISITE 3: Sales data must be processed and valid
if (!data.processingResults?.is_valid) {
return 'Los datos de ventas deben ser procesados y validados antes del entrenamiento';
}
// CRITICAL PREREQUISITE 4: Sales data must be imported to backend
const hasSalesDataImported = data.salesImportResult &&
(data.salesImportResult.records_created > 0 ||
data.salesImportResult.success === true);
if (!hasSalesDataImported && data.trainingStatus !== 'completed') {
return 'Los datos de ventas históricos deben estar importados en el sistema para iniciar el entrenamiento del modelo de IA';
}
// CRITICAL PREREQUISITE 5: Training must be completed to proceed
if (data.trainingStatus !== 'completed') {
return 'El entrenamiento del modelo de IA debe completarse antes de continuar';
}
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) => {
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

@@ -1,338 +0,0 @@
/**
* Core onboarding actions - Business logic orchestration
*/
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOnboardingStore } from './store';
import { useTenantCreation } from '../services/useTenantCreation';
import { useSalesProcessing } from '../services/useSalesProcessing';
import { useInventorySetup } from '../services/useInventorySetup';
import { useTrainingOrchestration } from '../services/useTrainingOrchestration';
import { useProgressTracking } from '../services/useProgressTracking';
import { getStepById } from '../config/steps';
import type { ProductSuggestionResponse } from '../core/types';
import type { BakeryRegistration } from '../../../../api';
export const useOnboardingActions = () => {
const navigate = useNavigate();
const store = useOnboardingStore();
// Service hooks
const tenantCreation = useTenantCreation();
const salesProcessing = useSalesProcessing();
const inventorySetup = useInventorySetup();
const trainingOrchestration = useTrainingOrchestration();
const progressTracking = useProgressTracking();
const validateCurrentStep = useCallback((): string | null => {
const currentStep = store.getCurrentStep();
if (!currentStep) return null;
const stepConfig = getStepById(currentStep.id);
if (stepConfig?.validation) {
return stepConfig.validation(store.data);
}
return null;
}, [store]);
const nextStep = useCallback(async (): Promise<boolean> => {
try {
const currentStep = store.getCurrentStep();
if (!currentStep) {
return false;
}
// Validate current step
const validation = validateCurrentStep();
if (validation) {
store.setError(validation);
return false;
}
store.setError(null);
// Handle step-specific actions before moving to next step
if (currentStep.id === 'setup') {
// IMPORTANT: Ensure user_registered step is completed first
const userRegisteredCompleted = await progressTracking.markStepCompleted('user_registered', {});
if (!userRegisteredCompleted) {
console.error('❌ Failed to mark user_registered as completed');
store.setError('Failed to verify user registration status');
return false;
}
const stepData = store.getStepData('setup');
const bakeryData = stepData.bakery;
// Check if tenant creation is needed
const needsTenantCreation = bakeryData && !bakeryData.tenantCreated && !bakeryData.tenant_id;
if (needsTenantCreation) {
store.setLoading(true);
const success = await tenantCreation.createTenant(bakeryData);
store.setLoading(false);
if (!success) {
store.setError(tenantCreation.error || 'Error creating tenant');
return false;
}
console.log('✅ Tenant created successfully');
// Wait a moment for backend to update state
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (currentStep.id === 'smart-inventory-setup') {
const inventoryData = store.getStepData('smart-inventory-setup');
if (inventoryData?.approvedProducts?.length > 0 && !inventoryData?.inventoryConfigured) {
store.setLoading(true);
// Create inventory from approved products
const inventoryResult = await inventorySetup.createInventoryFromSuggestions(inventoryData.approvedProducts);
if (!inventoryResult.success) {
store.setLoading(false);
store.setError(inventorySetup.error || 'Error creating inventory');
return false;
}
// Import sales data after inventory creation
if (inventoryData?.processingResults && inventoryResult?.inventoryMapping) {
const salesImportResult = await inventorySetup.importSalesData(
inventoryData.processingResults,
inventoryResult.inventoryMapping
);
if (!salesImportResult.success) {
store.setLoading(false);
store.setError(inventorySetup.error || 'Error importing sales data');
return false;
}
}
store.setLoading(false);
}
}
// Save progress to backend
const stepData = store.getStepData(currentStep.id);
const markCompleted = await progressTracking.markStepCompleted(currentStep.id, stepData);
if (!markCompleted) {
console.error(`❌ Failed to mark step "${currentStep.id}" as completed`);
store.setError(`Failed to save progress for step "${currentStep.id}"`);
return false;
}
// Move to next step
if (store.nextStep()) {
store.markStepCompleted(store.currentStep - 1);
return true;
}
return false;
} catch (error) {
console.error('Error in nextStep:', error);
store.setError(error instanceof Error ? error.message : 'Error moving to next step');
store.setLoading(false);
return false;
}
}, [store, validateCurrentStep, tenantCreation, inventorySetup, progressTracking]);
const previousStep = useCallback((): boolean => {
return store.previousStep();
}, [store]);
const goToStep = useCallback((stepIndex: number): boolean => {
return store.goToStep(stepIndex);
}, [store]);
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
store.setLoading(true);
const success = await tenantCreation.createTenant(bakeryData);
store.setLoading(false);
if (!success) {
store.setError(tenantCreation.error || 'Error creating tenant');
}
return success;
}, [store, tenantCreation]);
const processSalesFile = useCallback(async (file: File): Promise<boolean> => {
console.log('🎬 Actions - processSalesFile started');
store.setLoading(true);
const result = await salesProcessing.processFile(file);
console.log('🎬 Actions - processFile result:', result);
store.setLoading(false);
if (!result.success) {
console.error('❌ Actions - Processing failed:', salesProcessing.error);
store.setError(salesProcessing.error || 'Error processing sales file');
} else {
console.log('✅ Actions - Processing succeeded');
}
return result.success;
}, [store, salesProcessing]);
const generateProductSuggestions = useCallback(async (productList: string[]): Promise<boolean> => {
console.log('🎬 Actions - generateProductSuggestions started for', productList.length, 'products');
store.setLoading(true);
const result = await salesProcessing.generateProductSuggestions(productList);
console.log('🎬 Actions - generateProductSuggestions result:', result);
store.setLoading(false);
if (!result.success) {
console.error('❌ Actions - Suggestions generation failed:', result.error);
store.setError(result.error || 'Error generating product suggestions');
} else {
console.log('✅ Actions - Product suggestions generated successfully');
}
return result.success;
}, [store, salesProcessing]);
const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[]
): Promise<boolean> => {
store.setLoading(true);
const result = await inventorySetup.createInventoryFromSuggestions(suggestions);
store.setLoading(false);
if (!result.success) {
store.setError(inventorySetup.error || 'Error creating inventory');
}
return result.success;
}, [store, inventorySetup]);
const importSalesData = useCallback(async (
salesData: any,
inventoryMapping: { [productName: string]: string }
): Promise<boolean> => {
store.setLoading(true);
const result = await inventorySetup.importSalesData(salesData, inventoryMapping);
store.setLoading(false);
if (!result.success) {
store.setError(inventorySetup.error || 'Error importing sales data');
}
return result.success;
}, [store, inventorySetup]);
const startTraining = useCallback(async (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}): Promise<boolean> => {
// Validate training prerequisites
const validation = await trainingOrchestration.validateTrainingData(store.getAllStepData());
if (!validation.isValid) {
store.setError(`Training prerequisites not met: ${validation.missingItems.join(', ')}`);
return false;
}
store.setLoading(true);
const success = await trainingOrchestration.startTraining(options);
store.setLoading(false);
if (!success) {
store.setError(trainingOrchestration.error || 'Error starting training');
}
return success;
}, [store, trainingOrchestration]);
const completeOnboarding = useCallback(async (): Promise<boolean> => {
try {
store.setLoading(true);
// Mark final completion
const completionStats = {
totalProducts: store.data.processingResults?.unique_products || 0,
inventoryItems: store.data.inventoryItems?.length || 0,
suppliersConfigured: store.data.suppliers?.length || 0,
mlModelAccuracy: store.data.trainingMetrics?.accuracy || 0,
estimatedTimeSaved: '2-3 horas por día',
completionScore: 95,
};
store.setStepData('completion', { completionStats });
// Complete in backend
const success = await progressTracking.completeOnboarding();
store.setLoading(false);
if (success) {
console.log('✅ Onboarding completed');
store.markStepCompleted(store.steps.length - 1);
// Navigate to dashboard after completion
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
return true;
} else {
store.setError('Error completing onboarding');
return false;
}
} catch (error) {
console.error('Error completing onboarding:', error);
store.setError(error instanceof Error ? error.message : 'Error completing onboarding');
store.setLoading(false);
return false;
}
}, [store, progressTracking, navigate]);
const clearError = useCallback(() => {
store.setError(null);
tenantCreation.clearError();
salesProcessing.clearError();
inventorySetup.clearError();
trainingOrchestration.clearError();
progressTracking.clearError();
}, [store, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration, progressTracking]);
const reset = useCallback(() => {
store.reset();
tenantCreation.reset();
salesProcessing.reset();
inventorySetup.reset();
trainingOrchestration.reset();
}, [store, tenantCreation, salesProcessing, inventorySetup, trainingOrchestration]);
return {
// Navigation actions
nextStep,
previousStep,
goToStep,
validateCurrentStep,
// Step-specific actions
createTenant,
processSalesFile,
generateProductSuggestions, // New function for separated suggestion generation
createInventoryFromSuggestions,
importSalesData,
startTraining,
completeOnboarding,
// Utility actions
clearError,
reset,
};
};

View File

@@ -1,200 +0,0 @@
/**
* Onboarding store - Centralized state management with Zustand
* Handles all onboarding data, steps, and global state
*/
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import type { OnboardingData, OnboardingStep, OnboardingProgress } from './types';
import { DEFAULT_STEPS } from '../config/steps';
interface OnboardingStore {
// Flow state
currentStep: number;
steps: OnboardingStep[];
// Data state
data: OnboardingData;
// UI state
isLoading: boolean;
error: string | null;
// Progress state
progress: OnboardingProgress | null;
// Actions - Flow management
setCurrentStep: (step: number) => void;
nextStep: () => boolean;
previousStep: () => boolean;
goToStep: (stepIndex: number) => boolean;
markStepCompleted: (stepIndex: number) => void;
// Actions - Data management
setStepData: (stepId: string, stepData: Partial<OnboardingData>) => void;
clearStepData: (stepId: string) => void;
// Actions - UI state
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
// Actions - Progress
setProgress: (progress: OnboardingProgress) => void;
// Actions - Utilities
reset: () => void;
// Getters
getCurrentStep: () => OnboardingStep;
getStepData: (stepId: string) => any;
getAllStepData: () => { [stepId: string]: any };
getProgress: () => OnboardingProgress;
canNavigateToStep: (stepIndex: number) => boolean;
}
const initialState = {
currentStep: 0,
steps: DEFAULT_STEPS.map(step => ({ ...step, isCompleted: false })),
data: { allStepData: {} },
isLoading: false,
error: null,
progress: null,
};
// Debug logging for store initialization (only if there's an issue)
if (initialState.steps.length !== DEFAULT_STEPS.length) {
console.error('⚠️ Store initialization issue: steps count mismatch');
}
export const useOnboardingStore = create<OnboardingStore>()(
devtools(
(set, get) => ({
...initialState,
// Flow management actions
setCurrentStep: (step: number) => {
set({ currentStep: step }, false, 'setCurrentStep');
},
nextStep: () => {
const { currentStep, steps } = get();
if (currentStep < steps.length - 1) {
set({ currentStep: currentStep + 1 }, false, 'nextStep');
return true;
}
return false;
},
previousStep: () => {
const { currentStep } = get();
if (currentStep > 0) {
set({ currentStep: currentStep - 1 }, false, 'previousStep');
return true;
}
return false;
},
goToStep: (stepIndex: number) => {
const { steps, canNavigateToStep } = get();
if (stepIndex >= 0 && stepIndex < steps.length && canNavigateToStep(stepIndex)) {
set({ currentStep: stepIndex }, false, 'goToStep');
return true;
}
return false;
},
markStepCompleted: (stepIndex: number) => {
set((state) => ({
steps: state.steps.map((step, index) =>
index === stepIndex ? { ...step, isCompleted: true } : step
),
}), false, 'markStepCompleted');
},
// Data management actions
setStepData: (stepId: string, stepData: Partial<OnboardingData>) => {
set((state) => ({
data: {
...state.data,
...stepData,
allStepData: {
...state.data.allStepData,
[stepId]: {
...state.data.allStepData?.[stepId],
...stepData,
},
},
},
}), false, 'setStepData');
},
clearStepData: (stepId: string) => {
set((state) => ({
data: {
...state.data,
allStepData: {
...state.data.allStepData,
[stepId]: undefined,
},
},
}), false, 'clearStepData');
},
// UI state actions
setLoading: (loading: boolean) => {
set({ isLoading: loading }, false, 'setLoading');
},
setError: (error: string | null) => {
set({ error, isLoading: false }, false, 'setError');
},
// Progress actions
setProgress: (progress: OnboardingProgress) => {
set({ progress }, false, 'setProgress');
},
// Utility actions
reset: () => {
set(initialState, false, 'reset');
},
// Getters
getCurrentStep: () => {
const { steps, currentStep } = get();
return steps[currentStep];
},
getStepData: (stepId: string) => {
const { data } = get();
return data.allStepData?.[stepId] || {};
},
getAllStepData: () => {
const { data } = get();
return data.allStepData || {};
},
getProgress: () => {
const { steps, currentStep } = get();
const completedSteps = steps.filter(step => step.isCompleted).length;
const requiredSteps = steps.filter(step => step.isRequired).length;
return {
currentStep,
totalSteps: steps.length,
completedSteps,
isComplete: completedSteps === requiredSteps,
progressPercentage: Math.round((completedSteps / requiredSteps) * 100),
};
},
canNavigateToStep: (stepIndex: number) => {
const { steps } = get();
// Allow navigation to any step for now - can add more complex logic later
return stepIndex >= 0 && stepIndex < steps.length;
},
}),
{ name: 'onboarding-store' }
)
);

View File

@@ -1,205 +0,0 @@
/**
* Core types for the entire onboarding system
*/
import type {
BakeryRegistration,
TrainingJobResponse,
UserProgress,
} from '../../../../api';
// Re-export TrainingMetrics locally
export interface TrainingMetrics {
accuracy: number;
mape: number;
mae: number;
rmse: number;
r2_score: number;
}
// Re-export ProductSuggestionResponse type locally since it's used across many files
export interface ProductSuggestionResponse {
suggestion_id: string;
original_name: string;
suggested_name: string;
product_type: string;
category: string;
unit_of_measure: string;
confidence_score: number;
estimated_shelf_life_days?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
notes?: string;
sales_data?: {
total_quantity: number;
average_daily_sales: number;
peak_day: string;
frequency: number;
};
}
// Base service state pattern
export interface ServiceState<T = any> {
data: T | null;
isLoading: boolean;
error: string | null;
isSuccess: boolean;
}
// Base service actions pattern
export interface ServiceActions {
clearError: () => void;
reset: () => void;
}
// Onboarding step definitions
export interface OnboardingStep {
id: string;
title: string;
description: string;
isRequired: boolean;
isCompleted: boolean;
validation?: (data: OnboardingData) => string | null;
}
// Complete onboarding data structure
export interface OnboardingData {
// Step 1: Setup
bakery?: BakeryRegistration;
// Step 2: Smart Inventory Setup
files?: {
salesData?: File;
};
processingStage?: 'upload' | 'validating' | 'validated' | 'analyzing' | 'review' | '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;
};
};
suggestions?: ProductSuggestionResponse[];
detectedProducts?: any[];
approvedSuggestions?: ProductSuggestionResponse[];
approvedProducts?: ProductSuggestionResponse[];
reviewCompleted?: boolean;
inventoryItems?: any[];
inventoryMapping?: { [productName: string]: string };
inventoryConfigured?: boolean;
salesImportResult?: {
success: boolean;
imported: boolean;
records_created: number;
message: string;
};
// Step 3: Suppliers
suppliers?: any[];
supplierMappings?: any[];
// Step 4: ML Training
trainingStatus?: 'idle' | 'validating' | 'training' | 'completed' | 'failed';
trainingProgress?: number;
trainingJob?: TrainingJobResponse;
trainingLogs?: TrainingLog[];
trainingMetrics?: TrainingMetrics;
autoStartTraining?: boolean;
// Step 5: 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;
}
// Training types
export interface TrainingLog {
timestamp: string;
message: string;
level: 'info' | 'warning' | 'error' | 'success';
}
// Progress callback
export type ProgressCallback = (progress: number, stage: string, message: string) => void;
// Step validation function
export type StepValidator = (data: OnboardingData) => string | null;
// Service-specific state types
export interface TenantCreationState extends ServiceState<BakeryRegistration> {
tenantData: BakeryRegistration | null;
}
export interface SalesProcessingState extends ServiceState<any> {
stage: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error';
progress: number;
currentMessage: string;
validationResults: any | null;
suggestions: ProductSuggestionResponse[] | null;
}
export interface InventorySetupState extends ServiceState<any> {
createdItems: any[];
inventoryMapping: { [productName: string]: string };
salesImportResult: {
success: boolean;
imported: boolean;
records_created: number;
message: string;
} | null;
isInventoryConfigured: boolean;
}
export interface TrainingOrchestrationState extends ServiceState<TrainingJobResponse> {
status: 'idle' | 'validating' | 'training' | 'completed' | 'failed';
progress: number;
currentStep: string;
estimatedTimeRemaining: number;
job: TrainingJobResponse | null;
logs: TrainingLog[];
metrics: TrainingMetrics | null;
}
export interface ProgressTrackingState extends ServiceState<UserProgress> {
progress: UserProgress | null;
isInitialized: boolean;
isCompleted: boolean;
completionPercentage: number;
currentBackendStep: string | null;
}
export interface ResumeState extends ServiceState {
isCheckingResume: boolean;
resumePoint: { stepId: string; stepIndex: number } | null;
shouldResume: boolean;
}

View File

@@ -1,20 +0,0 @@
/**
* Auto-resume hook - Simple wrapper around resume logic service
*/
import { useResumeLogic } from '../services/useResumeLogic';
export const useAutoResume = () => {
const resumeLogic = useResumeLogic();
return {
// State
isCheckingResume: resumeLogic.isCheckingResume,
isCompleted: resumeLogic.isCompleted,
completionPercentage: resumeLogic.completionPercentage,
// Actions
checkForSavedProgress: resumeLogic.checkForResume,
resumeFromSavedProgress: resumeLogic.resumeFlow,
};
};

View File

@@ -1,29 +0,0 @@
/**
* Onboarding hooks index - Complete clean architecture
* No legacy code, no backwards compatibility, clean modern patterns
*/
// Types (re-export core types for external usage)
export type * from './core/types';
// Steps configuration
export { DEFAULT_STEPS, getStepById, getStepIndex, canAccessStep, calculateProgress } from './config/steps';
// Core architecture (for advanced usage)
export { useOnboardingStore } from './core/store';
export { useOnboardingActions } from './core/actions';
// Service hooks (for direct service access when needed)
export { useTenantCreation } from './services/useTenantCreation';
export { useSalesProcessing } from './services/useSalesProcessing';
export { useInventorySetup } from './services/useInventorySetup';
export { useTrainingOrchestration } from './services/useTrainingOrchestration';
export { useProgressTracking } from './services/useProgressTracking';
export { useResumeLogic } from './services/useResumeLogic';
// Main hooks - PRIMARY INTERFACE for components
export { useOnboarding } from './useOnboarding';
export { useAutoResume } from './core/useAutoResume';
// Utility
export { createServiceHook } from './utils/createServiceHook';

View File

@@ -1,255 +0,0 @@
/**
* Inventory setup service - Simplified implementation
*/
import { useCallback, useState } from 'react';
import {
useCreateIngredient,
useCreateSalesRecord,
} from '../../../../api';
import { useCurrentTenant } from '../../../../stores';
import { useOnboardingStore } from '../core/store';
import type { ProductSuggestionResponse } from '../core/types';
export const useInventorySetup = () => {
const createIngredientMutation = useCreateIngredient();
const createSalesRecordMutation = useCreateSalesRecord();
const currentTenant = useCurrentTenant();
const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createdItems, setCreatedItems] = useState<any[]>([]);
const [inventoryMapping, setInventoryMapping] = useState<{ [productName: string]: string }>({});
const [salesImportResult, setSalesImportResult] = useState<any | null>(null);
const [isInventoryConfigured, setIsInventoryConfigured] = useState(false);
const createInventoryFromSuggestions = useCallback(async (
suggestions: ProductSuggestionResponse[]
): Promise<{
success: boolean;
createdItems?: any[];
inventoryMapping?: { [productName: string]: string };
}> => {
console.log('🔄 Creating inventory from suggestions:', suggestions?.length, 'items');
if (!suggestions || suggestions.length === 0) {
console.error('❌ No suggestions provided');
setError('No hay sugerencias para crear el inventario');
return { success: false };
}
if (!currentTenant?.id) {
console.error('❌ No tenant ID available');
setError('No se pudo obtener información del tenant');
return { success: false };
}
setIsLoading(true);
setError(null);
try {
const newCreatedItems = [];
const newInventoryMapping: { [key: string]: string } = {};
// Create ingredients from approved suggestions
for (const suggestion of suggestions) {
try {
const ingredientData = {
name: suggestion.suggested_name || suggestion.original_name,
category: suggestion.category || 'Sin categoría',
description: suggestion.notes || '',
unit_of_measure: suggestion.unit_of_measure || 'units',
minimum_stock_level: 10,
maximum_stock_level: 100,
low_stock_threshold: 10,
reorder_point: 15,
reorder_quantity: 50,
shelf_life_days: suggestion.estimated_shelf_life_days || 30,
requires_refrigeration: suggestion.requires_refrigeration || false,
requires_freezing: suggestion.requires_freezing || false,
is_seasonal: suggestion.is_seasonal || false,
cost_per_unit: 0,
notes: suggestion.notes || `Producto clasificado automáticamente desde: ${suggestion.original_name}`,
};
const createdItem = await createIngredientMutation.mutateAsync({
tenantId: currentTenant.id,
ingredientData,
});
newCreatedItems.push(createdItem);
// Map both original and suggested names to the same ingredient ID for flexibility
newInventoryMapping[suggestion.original_name] = createdItem.id;
if (suggestion.suggested_name && suggestion.suggested_name !== suggestion.original_name) {
newInventoryMapping[suggestion.suggested_name] = createdItem.id;
}
} catch (error) {
console.error(`❌ Error creating ingredient ${suggestion.suggested_name || suggestion.original_name}:`, error);
// Continue with other ingredients even if one fails
}
}
if (newCreatedItems.length === 0) {
throw new Error('No se pudo crear ningún elemento del inventario');
}
console.log('✅ Created', newCreatedItems.length, '/', suggestions.length, 'ingredients');
// Update state
setCreatedItems(newCreatedItems);
setInventoryMapping(newInventoryMapping);
setIsInventoryConfigured(true);
// Update onboarding store
setStepData('smart-inventory-setup', {
inventoryItems: newCreatedItems,
inventoryMapping: newInventoryMapping,
inventoryConfigured: true,
});
return {
success: true,
createdItems: newCreatedItems,
inventoryMapping: newInventoryMapping,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
setError(errorMessage);
return { success: false };
} finally {
setIsLoading(false);
}
}, [createIngredientMutation, currentTenant, setStepData]);
const importSalesData = useCallback(async (
salesData: any,
inventoryMapping: { [productName: string]: string }
): Promise<{
success: boolean;
recordsCreated: number;
message: string;
}> => {
if (!currentTenant?.id) {
setError('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) {
setError('No hay datos de ventas para importar');
return {
success: false,
recordsCreated: 0,
message: 'Error: No hay datos de ventas para importar',
};
}
setIsLoading(true);
setError(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 importResult = {
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',
};
// Update state
setSalesImportResult(importResult);
// Update onboarding store
setStepData('smart-inventory-setup', {
salesImportResult: importResult,
});
return {
success: recordsCreated > 0,
recordsCreated,
message: importResult.message,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error importando datos de ventas';
setError(errorMessage);
return {
success: false,
recordsCreated: 0,
message: errorMessage,
};
} finally {
setIsLoading(false);
}
}, [createSalesRecordMutation, currentTenant, setStepData]);
const clearError = useCallback(() => {
setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setCreatedItems([]);
setInventoryMapping({});
setSalesImportResult(null);
setIsInventoryConfigured(false);
}, []);
return {
// State
isLoading,
error,
createdItems,
inventoryMapping,
salesImportResult,
isInventoryConfigured,
// Actions
createInventoryFromSuggestions,
importSalesData,
clearError,
reset,
};
};

View File

@@ -1,167 +0,0 @@
/**
* Progress tracking service - Simplified implementation
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { onboardingService } from '../../../../api/services/onboarding';
import type { UserProgress } from '../../../../api/types/onboarding';
export const useProgressTracking = () => {
const initializationAttempted = useRef(false);
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState<UserProgress | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [completionPercentage, setCompletionPercentage] = useState(0);
const [currentBackendStep, setCurrentBackendStep] = useState<string | null>(null);
// Load initial progress from backend
const loadProgress = useCallback(async (): Promise<UserProgress | null> => {
setIsLoading(true);
setError(null);
try {
const progressData = await onboardingService.getUserProgress('');
// Update state
setProgress(progressData);
setIsInitialized(true);
setIsCompleted(progressData?.fully_completed || false);
setCompletionPercentage(progressData?.completion_percentage || 0);
setCurrentBackendStep(progressData?.current_step || null);
console.log('📊 Progress loaded:', progressData?.current_step, '(', progressData?.completion_percentage, '%)');
return progressData;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error loading progress';
setError(errorMessage);
return null;
} finally {
setIsLoading(false);
}
}, []);
// Mark a step as completed and save to backend
const markStepCompleted = useCallback(async (
stepId: string,
data?: Record<string, any>
): Promise<boolean> => {
console.log(`🔄 Attempting to mark step "${stepId}" as completed with data:`, data);
setIsLoading(true);
setError(null);
try {
const updatedProgress = await onboardingService.markStepAsCompleted(stepId, data);
// Update state
setProgress(updatedProgress);
setIsCompleted(updatedProgress?.fully_completed || false);
setCompletionPercentage(updatedProgress?.completion_percentage || 0);
setCurrentBackendStep(updatedProgress?.current_step || null);
console.log(`✅ Step "${stepId}" marked as completed in backend`);
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : `Error marking step "${stepId}" as completed`;
setError(errorMessage);
console.error(`❌ Error marking step "${stepId}" as completed:`, error);
console.error(`❌ Attempted to send data:`, data);
return false;
} finally {
setIsLoading(false);
}
}, []);
// Get the next step the user should work on
const getNextStep = useCallback(async (): Promise<string> => {
try {
return await onboardingService.getNextStepId();
} catch (error) {
console.error('Error getting next step:', error);
return 'setup';
}
}, []);
// Get the step and index where user should resume
const getResumePoint = useCallback(async (): Promise<{ stepId: string; stepIndex: number }> => {
try {
return await onboardingService.getResumeStep();
} catch (error) {
console.error('Error getting resume point:', error);
return { stepId: 'setup', stepIndex: 0 };
}
}, []);
// Complete the entire onboarding process
const completeOnboarding = useCallback(async (): Promise<boolean> => {
setIsLoading(true);
setError(null);
try {
const result = await onboardingService.completeOnboarding();
if (result.success) {
// Reload progress to get updated status
await loadProgress();
console.log('🎉 Onboarding completed successfully!');
return true;
}
throw new Error('Failed to complete onboarding');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
setError(errorMessage);
return false;
} finally {
setIsLoading(false);
}
}, [loadProgress]);
// Check if user can access a specific step
const canAccessStep = useCallback(async (stepId: string): Promise<boolean> => {
try {
const result = await onboardingService.canAccessStep(stepId);
return result.can_access;
} catch (error) {
console.error(`Error checking access for step "${stepId}":`, error);
return true; // Allow access on error
}
}, []);
// Auto-load progress on hook initialization - PREVENT multiple attempts
useEffect(() => {
if (!isInitialized && !initializationAttempted.current && !isLoading) {
initializationAttempted.current = true;
loadProgress();
}
}, [isInitialized, isLoading, loadProgress]);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
// State
isLoading,
error,
progress,
isInitialized,
isCompleted,
completionPercentage,
currentBackendStep,
// Actions
loadProgress,
markStepCompleted,
getNextStep,
getResumePoint,
completeOnboarding,
canAccessStep,
clearError,
};
};

View File

@@ -1,151 +0,0 @@
/**
* Resume logic service - Simplified implementation
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOnboardingStore } from '../core/store';
import { useProgressTracking } from './useProgressTracking';
export const useResumeLogic = () => {
const navigate = useNavigate();
const progressTracking = useProgressTracking();
const { setCurrentStep } = useOnboardingStore();
const resumeAttempted = useRef(false);
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isCheckingResume, setIsCheckingResume] = useState(false);
const [resumePoint, setResumePoint] = useState<{ stepId: string; stepIndex: number } | null>(null);
const [shouldResume, setShouldResume] = useState(false);
// Check if user should resume onboarding
const checkForResume = useCallback(async (): Promise<boolean> => {
setIsLoading(true);
setError(null);
setIsCheckingResume(true);
try {
// Load user's progress from backend
await progressTracking.loadProgress();
if (!progressTracking.progress) {
console.log('🔍 No progress found, starting from beginning');
setIsCheckingResume(false);
setShouldResume(false);
return false;
}
// If onboarding is already completed, don't resume
if (progressTracking.isCompleted) {
console.log('✅ Onboarding completed, redirecting to dashboard');
navigate('/app/dashboard');
setIsCheckingResume(false);
setShouldResume(false);
return false;
}
// Get the resume point from backend
const resumePointData = await progressTracking.getResumePoint();
console.log('🎯 Resuming from step:', resumePointData.stepId);
setIsCheckingResume(false);
setResumePoint(resumePointData);
setShouldResume(true);
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error checking resume';
setError(errorMessage);
setIsCheckingResume(false);
setShouldResume(false);
console.error('❌ Resume check failed:', errorMessage);
return false;
} finally {
setIsLoading(false);
}
}, [progressTracking, navigate]);
// Resume the onboarding flow from the correct step
const resumeFlow = useCallback((): boolean => {
if (!resumePoint) {
console.warn('⚠️ No resume point available');
return false;
}
try {
const { stepIndex } = resumePoint;
// Navigate to the correct step in the flow
setCurrentStep(stepIndex);
console.log('✅ Resumed onboarding at step', stepIndex);
setShouldResume(false);
return true;
} catch (error) {
console.error('❌ Error resuming flow:', error);
return false;
}
}, [resumePoint, setCurrentStep]);
// Handle automatic resume on mount
const handleAutoResume = useCallback(async () => {
try {
// Add a timeout to prevent hanging indefinitely
const timeoutPromise = new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('Resume check timeout')), 10000)
);
const shouldResumeResult = await Promise.race([
checkForResume(),
timeoutPromise
]);
if (shouldResumeResult) {
// Wait a bit for state to update, then resume
setTimeout(() => {
resumeFlow();
}, 100);
}
} catch (error) {
console.error('❌ Auto-resume failed:', error);
// Reset the checking state in case of error
setIsCheckingResume(false);
setShouldResume(false);
}
}, [checkForResume, resumeFlow]);
// Auto-check for resume when the hook is first used - PREVENT multiple attempts
useEffect(() => {
if (progressTracking.isInitialized && !isCheckingResume && !resumeAttempted.current) {
resumeAttempted.current = true;
handleAutoResume();
}
}, [progressTracking.isInitialized, isCheckingResume, handleAutoResume]);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
// State
isLoading,
error,
isCheckingResume,
resumePoint,
shouldResume,
// Actions
checkForResume,
resumeFlow,
handleAutoResume,
clearError,
// Progress tracking state for convenience
progress: progressTracking.progress,
isCompleted: progressTracking.isCompleted,
completionPercentage: progressTracking.completionPercentage,
};
};

View File

@@ -1,317 +0,0 @@
/**
* Sales processing service - Simplified implementation
*/
import { useCallback, useState } from 'react';
import { useClassifyProductsBatch, useValidateFileOnly } from '../../../../api';
import { useCurrentTenant } from '../../../../stores';
import { useOnboardingStore } from '../core/store';
import type { ProgressCallback, ProductSuggestionResponse } from '../core/types';
export const useSalesProcessing = () => {
const classifyProductsMutation = useClassifyProductsBatch();
const currentTenant = useCurrentTenant();
const { validateFile } = useValidateFileOnly();
const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[]>([]);
const [stage, setStage] = useState<'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error'>('idle');
const [progress, setProgress] = useState(0);
const [currentMessage, setCurrentMessage] = useState('');
const [validationResults, setValidationResults] = useState<any | null>(null);
const updateProgress = useCallback((
progressValue: number,
stageValue: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error',
message: string,
onProgress?: ProgressCallback
) => {
setProgress(progressValue);
setStage(stageValue);
setCurrentMessage(message);
onProgress?.(progressValue, stageValue, message);
}, []);
const extractProductList = useCallback((validationResult: any): string[] => {
// First try to use the direct product_list from backend response
if (validationResult.product_list && Array.isArray(validationResult.product_list)) {
return validationResult.product_list;
}
// Fallback: 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[]> => {
try {
if (!currentTenant?.id) {
console.error('❌ No tenant ID available for classification');
return [];
}
console.log('🔄 Generating suggestions for', productList.length, 'products');
const requestPayload = {
tenantId: currentTenant.id,
batchData: {
products: productList.map(name => ({
product_name: name,
description: ''
}))
}
};
console.log('📡 Making API request to:', `/tenants/${currentTenant.id}/inventory/classify-products-batch`);
// Add timeout to the API call with shorter timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('API timeout: The inventory service may be overloaded. Please try again in a few moments.')), 15000); // 15 second timeout
});
const response = await Promise.race([
classifyProductsMutation.mutateAsync(requestPayload),
timeoutPromise
]);
console.log('✅ Generated', response?.length || 0, 'suggestions');
return response || [];
} catch (error) {
const isTimeout = error instanceof Error && error.message.includes('timeout');
const isNetworkError = error instanceof Error && (error.message.includes('fetch') || error.message.includes('network'));
console.error('❌ Error generating suggestions:', {
error: error,
message: error instanceof Error ? error.message : 'Unknown error',
isTimeout,
isNetworkError
});
// Re-throw timeout/network errors so they can be handled properly by the UI
if (isTimeout || isNetworkError) {
throw error;
}
return [];
}
}, [classifyProductsMutation, currentTenant]);
const processFile = useCallback(async (
file: File,
onProgress?: ProgressCallback
): Promise<{
success: boolean;
validationResults?: any;
suggestions?: ProductSuggestionResponse[];
}> => {
console.log('🚀 Processing file:', file.name);
setIsLoading(true);
setError(null);
try {
// Stage 1: Validate file structure
updateProgress(10, 'validating', 'Iniciando validación del archivo...', onProgress);
updateProgress(20, 'validating', 'Validando estructura del archivo...', onProgress);
if (!currentTenant?.id) {
throw new Error('No se pudo obtener información del tenant');
}
const result = await validateFile(
currentTenant.id,
file,
{
onProgress: (stage, progress, message) => {
updateProgress(progress, stage as any, 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),
};
console.log('📊 File validated:', validationResult.product_list?.length, 'products found');
updateProgress(40, 'validating', 'Verificando integridad de datos...', onProgress);
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
console.error('❌ No products found in file');
throw new Error('No se encontraron productos válidos en el archivo');
}
// Stage 2: File validation completed - WAIT FOR USER CONFIRMATION
updateProgress(100, 'validated', 'Archivo validado correctamente. Esperando confirmación del usuario...', onProgress);
// Store validation results and wait for user action
setValidationResults(validationResult);
console.log('✅ File validation completed:', validationResult.product_list?.length, 'products found');
// Update onboarding store - ONLY with validation results
setStepData('smart-inventory-setup', {
files: { salesData: file },
processingStage: 'validated', // Changed from 'completed'
processingResults: validationResult,
// DON'T set suggestions here - they will be generated later
});
console.log('📊 Updated onboarding store with suggestions');
return {
success: true,
validationResults: validationResult,
// No suggestions returned from processFile - they will be generated separately
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
setError(errorMessage);
updateProgress(0, 'error', errorMessage, onProgress);
return {
success: false,
};
} finally {
setIsLoading(false);
}
}, [updateProgress, currentTenant, validateFile, extractProductList, setStepData]);
const generateProductSuggestions = useCallback(async (
productList: string[],
onProgress?: ProgressCallback
): Promise<{
success: boolean;
suggestions?: ProductSuggestionResponse[];
error?: string;
}> => {
console.log('🚀 Generating product suggestions for', productList.length, 'products');
setIsLoading(true);
setError(null);
try {
updateProgress(10, 'analyzing', 'Iniciando análisis de productos...', onProgress);
updateProgress(30, 'analyzing', 'Identificando productos únicos...', onProgress);
updateProgress(50, 'analyzing', 'Generando sugerencias de IA...', onProgress);
let suggestions: ProductSuggestionResponse[] = [];
let suggestionError: string | null = null;
try {
updateProgress(70, 'analyzing', 'Consultando servicios de IA...', onProgress);
suggestions = await generateSuggestions(productList);
console.log('🔍 Generated suggestions:', {
suggestionsReceived: suggestions?.length || 0,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error generando sugerencias';
suggestionError = errorMessage;
console.error('❌ Suggestions generation failed:', errorMessage);
// Create basic suggestions from product names as fallback
updateProgress(80, 'analyzing', 'Preparando productos básicos...', onProgress);
suggestions = productList.map((productName, index) => ({
suggestion_id: `manual-${index}`,
original_name: productName,
suggested_name: productName,
product_type: 'ingredient',
category: 'Sin categoría',
unit_of_measure: 'units',
confidence_score: 0.5,
estimated_shelf_life_days: 30,
requires_refrigeration: false,
requires_freezing: false,
is_seasonal: false,
notes: 'Clasificación manual - El servicio de IA no está disponible temporalmente'
}));
console.log('🔧 Created fallback suggestions:', suggestions.length);
}
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
updateProgress(100, 'completed', 'Sugerencias generadas correctamente', onProgress);
// Update state with suggestions
setSuggestions(suggestions || []);
console.log('✅ Suggestions generation completed:', suggestions?.length || 0, 'suggestions');
// Update onboarding store with suggestions
setStepData('smart-inventory-setup', (prevData) => ({
...prevData,
processingStage: 'completed',
suggestions: suggestions || [],
suggestionError: suggestionError,
}));
return {
success: true,
suggestions: suggestions || [],
error: suggestionError || undefined,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error generando sugerencias';
setError(errorMessage);
updateProgress(0, 'error', errorMessage, onProgress);
return {
success: false,
error: errorMessage,
};
} finally {
setIsLoading(false);
}
}, [updateProgress, generateSuggestions, setStepData]);
const clearError = useCallback(() => {
setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setSuggestions([]);
setStage('idle');
setProgress(0);
setCurrentMessage('');
setValidationResults(null);
}, []);
return {
// State
isLoading,
error,
stage,
progress,
currentMessage,
validationResults,
suggestions,
// Actions
processFile,
generateProductSuggestions, // New separated function
clearError,
reset,
};
};

View File

@@ -1,89 +0,0 @@
/**
* Tenant creation service - Simplified implementation
*/
import { useCallback, useState } from 'react';
import { useRegisterBakery } from '../../../../api';
import { useTenantStore } from '../../../../stores/tenant.store';
import { useOnboardingStore } from '../core/store';
import type { BakeryRegistration, TenantResponse } from '../../../../api';
export const useTenantCreation = () => {
const registerBakeryMutation = useRegisterBakery();
const { setCurrentTenant, loadUserTenants } = useTenantStore();
const { setStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSuccess, setIsSuccess] = useState(false);
const [tenantData, setTenantData] = useState<TenantResponse | null>(null);
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) {
setError('Los datos de la panadería son requeridos');
return false;
}
setIsLoading(true);
setError(null);
try {
// Call API to register bakery
const tenantResponse: TenantResponse = await registerBakeryMutation.mutateAsync(bakeryData);
// Update tenant store
setCurrentTenant(tenantResponse);
// Reload user tenants
await loadUserTenants();
// Update state
setTenantData(tenantResponse);
setIsSuccess(true);
// Update onboarding data
setStepData('setup', {
bakery: {
...bakeryData,
tenantCreated: true,
tenant_id: tenantResponse.id,
} as any,
});
console.log('✅ Tenant created successfully');
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating tenant';
setError(errorMessage);
setIsSuccess(false);
return false;
} finally {
setIsLoading(false);
}
}, [registerBakeryMutation, setCurrentTenant, loadUserTenants, setStepData]);
const clearError = useCallback(() => {
setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setIsSuccess(false);
setTenantData(null);
}, []);
return {
// State
isLoading,
error,
isSuccess,
tenantData,
// Actions
createTenant,
clearError,
reset,
};
};

View File

@@ -1,258 +0,0 @@
/**
* Training orchestration service - Simplified implementation
*/
import { useCallback, useEffect, useState } from 'react';
import {
useCreateTrainingJob,
useTrainingJobStatus,
useTrainingWebSocket,
} from '../../../../api';
import { useCurrentTenant } from '../../../../stores';
import { useAuthUser } from '../../../../stores/auth.store';
import { useOnboardingStore } from '../core/store';
import type { TrainingLog, TrainingMetrics } from '../core/types';
import type { TrainingJobResponse } from '../../../../api';
export const useTrainingOrchestration = () => {
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const createTrainingJobMutation = useCreateTrainingJob();
const { setStepData, getAllStepData } = useOnboardingStore();
// Simple, direct state management
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>('idle');
const [progress, setProgress] = useState(0);
const [currentStep, setCurrentStep] = useState('');
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(0);
const [job, setJob] = useState<TrainingJobResponse | null>(null);
const [logs, setLogs] = useState<TrainingLog[]>([]);
const [metrics, setMetrics] = useState<TrainingMetrics | null>(null);
const addLog = useCallback((message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
setLogs(prevLogs => [...prevLogs, newLog]);
}, []);
// Get job status when we have a job ID
const { data: jobStatus } = useTrainingJobStatus(
currentTenant?.id || '',
job?.job_id || '',
{
enabled: !!currentTenant?.id && !!job?.job_id && status === 'training',
refetchInterval: 5000,
}
);
// WebSocket for real-time updates
const { data: wsData } = useTrainingWebSocket(
currentTenant?.id || '',
job?.job_id || '',
(user as any)?.token,
{
onProgress: (data: any) => {
setProgress(data.progress?.percentage || progress);
setCurrentStep(data.progress?.current_step || currentStep);
setEstimatedTimeRemaining(data.progress?.estimated_time_remaining || estimatedTimeRemaining);
addLog(
`${data.progress?.current_step} - ${data.progress?.products_completed}/${data.progress?.products_total} productos procesados (${data.progress?.percentage}%)`,
'info'
);
},
onCompleted: (data: any) => {
setStatus('completed');
setProgress(100);
setMetrics({
accuracy: data.results.performance_metrics.accuracy,
mape: data.results.performance_metrics.mape,
mae: data.results.performance_metrics.mae,
rmse: data.results.performance_metrics.rmse,
r2_score: data.results.performance_metrics.r2_score || 0,
});
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: any) => {
setError(data.error);
setStatus('failed');
addLog(`Error en entrenamiento: ${data.error}`, 'error');
},
onStarted: (data: any) => {
addLog(`Trabajo de entrenamiento iniciado: ${data.job_id}`, 'success');
},
}
);
// Update status from polling when WebSocket is not available
useEffect(() => {
if (jobStatus && job?.job_id === jobStatus.job_id) {
setStatus(jobStatus.status as any);
setProgress(jobStatus.progress || progress);
setCurrentStep(jobStatus.current_step || currentStep);
setEstimatedTimeRemaining(jobStatus.estimated_time_remaining || estimatedTimeRemaining);
}
}, [jobStatus, job?.job_id, progress, currentStep, estimatedTimeRemaining]);
const validateTrainingData = useCallback(async (allStepData?: any): Promise<{
isValid: boolean;
missingItems: string[];
}> => {
const missingItems: string[] = [];
const stepData = allStepData || getAllStepData();
// Get data from the smart-inventory-setup step (where sales and inventory are handled)
const smartInventoryData = stepData?.['smart-inventory-setup'];
// CRITICAL REQUIREMENT 1: Sales file must be uploaded
if (!smartInventoryData?.files?.salesData) {
missingItems.push('Archivo de datos de ventas históricos');
}
// CRITICAL REQUIREMENT 2: Sales data must be processed and valid
const hasProcessingResults = smartInventoryData?.processingResults &&
smartInventoryData.processingResults.is_valid &&
smartInventoryData.processingResults.total_records > 0;
if (!hasProcessingResults) {
missingItems.push('Datos de ventas procesados y validados');
}
// CRITICAL REQUIREMENT 3: Sales data must be imported to backend (MANDATORY for training)
const hasImportResults = smartInventoryData?.salesImportResult &&
(smartInventoryData.salesImportResult.records_created > 0 ||
smartInventoryData.salesImportResult.success === true ||
smartInventoryData.salesImportResult.imported === true);
if (!hasImportResults) {
missingItems.push('Datos de ventas históricos importados al sistema');
}
// CRITICAL REQUIREMENT 4: Products must be approved
const hasApprovedProducts = smartInventoryData?.approvedProducts &&
smartInventoryData.approvedProducts.length > 0 &&
smartInventoryData.reviewCompleted;
if (!hasApprovedProducts) {
missingItems.push('Productos revisados y aprobados');
}
// CRITICAL REQUIREMENT 5: Inventory must be configured
const hasInventoryConfig = smartInventoryData?.inventoryConfigured &&
smartInventoryData?.inventoryItems &&
smartInventoryData.inventoryItems.length > 0;
if (!hasInventoryConfig) {
missingItems.push('Inventario configurado con productos');
}
// CRITICAL REQUIREMENT 6: Minimum data volume for training
if (smartInventoryData?.processingResults?.total_records &&
smartInventoryData.processingResults.total_records < 10) {
missingItems.push('Suficientes registros de ventas para entrenar (mínimo 10)');
}
const isValid = missingItems.length === 0;
if (!isValid) {
console.log('⚠️ Training validation failed:', missingItems.join(', '));
}
return { isValid, missingItems };
}, [getAllStepData]);
const startTraining = useCallback(async (options?: {
products?: string[];
startDate?: string;
endDate?: string;
}): Promise<boolean> => {
if (!currentTenant?.id) {
setError('No se pudo obtener información del tenant');
return false;
}
setIsLoading(true);
setStatus('validating');
setProgress(0);
setError(null);
addLog('Validando disponibilidad de datos...', 'info');
try {
// Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const trainingJob = await createTrainingJobMutation.mutateAsync({
tenantId: currentTenant.id,
request: {
products: options?.products,
start_date: options?.startDate,
end_date: options?.endDate,
}
});
// Update state
setJob(trainingJob);
setStatus('training');
// Update onboarding store
setStepData('ml-training', {
trainingStatus: 'training',
trainingJob: trainingJob,
});
addLog(`Trabajo de entrenamiento iniciado: ${trainingJob.job_id}`, 'success');
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error starting training';
setError(errorMessage);
setStatus('failed');
return false;
} finally {
setIsLoading(false);
}
}, [currentTenant, createTrainingJobMutation, addLog, setStepData]);
const clearError = useCallback(() => {
setError(null);
}, []);
const reset = useCallback(() => {
setIsLoading(false);
setError(null);
setStatus('idle');
setProgress(0);
setCurrentStep('');
setEstimatedTimeRemaining(0);
setJob(null);
setLogs([]);
setMetrics(null);
}, []);
return {
// State
isLoading,
error,
status,
progress,
currentStep,
estimatedTimeRemaining,
job,
logs,
metrics,
// Actions
startTraining,
validateTrainingData,
addLog,
clearError,
reset,
};
};

View File

@@ -1,145 +0,0 @@
/**
* Main onboarding hook - Clean, unified interface for components
* This is the primary hook that all components should use
*/
import { useAuthUser } from '../../../stores/auth.store';
import { useCurrentTenant } from '../../../stores';
import { useOnboardingStore } from './core/store';
import { useOnboardingActions } from './core/actions';
import { useTenantCreation } from './services/useTenantCreation';
import { useSalesProcessing } from './services/useSalesProcessing';
import { useInventorySetup } from './services/useInventorySetup';
import { useTrainingOrchestration } from './services/useTrainingOrchestration';
import { useProgressTracking } from './services/useProgressTracking';
import { useResumeLogic } from './services/useResumeLogic';
export const useOnboarding = () => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
// Core store and actions
const store = useOnboardingStore();
const actions = useOnboardingActions();
// Service hooks for detailed state access
const tenantCreation = useTenantCreation();
const salesProcessing = useSalesProcessing();
const inventorySetup = useInventorySetup();
const trainingOrchestration = useTrainingOrchestration();
const progressTracking = useProgressTracking();
const resumeLogic = useResumeLogic();
return {
// Core state from store
currentStep: store.currentStep, // Return the index, not the step object
steps: store.steps,
data: store.data,
progress: store.getProgress(),
isLoading: store.isLoading,
error: store.error,
// User context
user,
currentTenant,
// Step data helpers
stepData: {
setup: store.getStepData('setup'),
'smart-inventory-setup': store.getStepData('smart-inventory-setup'),
suppliers: store.getStepData('suppliers'),
'ml-training': store.getStepData('ml-training'),
completion: store.getStepData('completion'),
},
// Service states (for components that need detailed service info)
tenantCreation: {
isLoading: tenantCreation.isLoading,
isSuccess: tenantCreation.isSuccess,
error: tenantCreation.error,
tenantData: tenantCreation.tenantData,
},
salesProcessing: {
isLoading: salesProcessing.isLoading,
error: salesProcessing.error,
stage: salesProcessing.stage,
progress: salesProcessing.progress,
currentMessage: salesProcessing.currentMessage,
validationResults: salesProcessing.validationResults,
suggestions: Array.isArray(salesProcessing.suggestions) ? salesProcessing.suggestions : [],
},
inventorySetup: {
isLoading: inventorySetup.isLoading,
error: inventorySetup.error,
createdItems: inventorySetup.createdItems,
inventoryMapping: inventorySetup.inventoryMapping,
salesImportResult: inventorySetup.salesImportResult,
isInventoryConfigured: inventorySetup.isInventoryConfigured,
},
trainingOrchestration: {
isLoading: trainingOrchestration.isLoading,
error: trainingOrchestration.error,
status: trainingOrchestration.status,
progress: trainingOrchestration.progress,
currentStep: trainingOrchestration.currentStep,
estimatedTimeRemaining: trainingOrchestration.estimatedTimeRemaining,
job: trainingOrchestration.job,
logs: trainingOrchestration.logs,
metrics: trainingOrchestration.metrics,
},
progressTracking: {
isLoading: progressTracking.isLoading,
error: progressTracking.error,
progress: progressTracking.progress,
isCompleted: progressTracking.isCompleted,
completionPercentage: progressTracking.completionPercentage,
isInitialized: progressTracking.isInitialized,
currentBackendStep: progressTracking.currentBackendStep,
},
resumeLogic: {
isCheckingResume: resumeLogic.isCheckingResume,
resumePoint: resumeLogic.resumePoint,
shouldResume: resumeLogic.shouldResume,
progress: resumeLogic.progress,
isCompleted: resumeLogic.isCompleted,
completionPercentage: resumeLogic.completionPercentage,
},
// Actions from the core actions hook
nextStep: actions.nextStep,
previousStep: actions.previousStep,
goToStep: actions.goToStep,
validateCurrentStep: actions.validateCurrentStep,
// Step data management
updateStepData: store.setStepData,
clearStepData: store.clearStepData,
// Step-specific actions
createTenant: actions.createTenant,
processSalesFile: actions.processSalesFile,
generateProductSuggestions: actions.generateProductSuggestions, // New separated function
createInventoryFromSuggestions: actions.createInventoryFromSuggestions,
importSalesData: actions.importSalesData,
startTraining: actions.startTraining,
completeOnboarding: actions.completeOnboarding,
// Service-specific actions (for components that need direct service access)
generateSuggestions: salesProcessing.generateSuggestions,
addTrainingLog: trainingOrchestration.addLog,
validateTrainingData: trainingOrchestration.validateTrainingData,
// Resume actions
checkForSavedProgress: resumeLogic.checkForResume,
resumeFromSavedProgress: resumeLogic.resumeFlow,
// Utility actions
clearError: actions.clearError,
reset: actions.reset,
};
};

View File

@@ -1,103 +0,0 @@
/**
* Universal service hook factory - creates standardized service hooks
* This eliminates all the duplicate patterns across service hooks
*/
import { useState, useCallback } from 'react';
import type { ServiceState, ServiceActions } from '../core/types';
export interface ServiceHookConfig<T> {
initialState?: Partial<T>;
onSuccess?: (data: any, state: T) => T;
onError?: (error: string, state: T) => T;
resetState?: () => T;
}
export function createServiceHook<TState extends ServiceState, TActions extends ServiceActions = ServiceActions>(
config: ServiceHookConfig<TState> = {}
) {
return function useService() {
const defaultState: ServiceState = {
data: null,
isLoading: false,
error: null,
isSuccess: false,
};
const [state, setState] = useState<TState>({
...defaultState,
...config.initialState,
} as TState);
const setLoading = useCallback((loading: boolean) => {
setState(prev => ({ ...prev, isLoading: loading }));
}, []);
const setError = useCallback((error: string | null) => {
setState(prev => ({
...prev,
error,
isLoading: false,
isSuccess: false
}));
}, []);
const setSuccess = useCallback((data: any) => {
setState(prev => {
const newState = {
...prev,
data,
isLoading: false,
error: null,
isSuccess: true
};
return config.onSuccess ? config.onSuccess(data, newState) : newState;
});
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const reset = useCallback(() => {
if (config.resetState) {
setState(config.resetState());
} else {
setState({
...defaultState,
...config.initialState,
} as TState);
}
}, []);
const executeAsync = useCallback(async <T>(
asyncFn: () => Promise<T>
): Promise<{ success: boolean; data?: T; error?: string }> => {
try {
setLoading(true);
const result = await asyncFn();
setSuccess(result);
return { success: true, data: result };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setError(errorMessage);
return { success: false, error: errorMessage };
}
}, [setLoading, setSuccess, setError]);
return {
// State
...state,
// Core actions
setLoading,
setError,
setSuccess,
clearError,
reset,
// Async execution helper
executeAsync,
};
};
}