215 lines
6.6 KiB
TypeScript
215 lines
6.6 KiB
TypeScript
|
|
// frontend/src/utils/onboardingRouter.ts
|
||
|
|
/**
|
||
|
|
* Onboarding Router Utility
|
||
|
|
* Determines user's next step based on their current progress
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { onboardingService } from '../api/services/onboarding.service';
|
||
|
|
import type { UserProgress } from '../api/services/onboarding.service';
|
||
|
|
|
||
|
|
export type OnboardingStep =
|
||
|
|
| 'user_registered'
|
||
|
|
| 'bakery_registered'
|
||
|
|
| 'sales_data_uploaded'
|
||
|
|
| 'training_completed'
|
||
|
|
| 'dashboard_accessible';
|
||
|
|
|
||
|
|
export type NextAction =
|
||
|
|
| 'register'
|
||
|
|
| 'login'
|
||
|
|
| 'onboarding_bakery'
|
||
|
|
| 'onboarding_data'
|
||
|
|
| 'onboarding_training'
|
||
|
|
| 'dashboard'
|
||
|
|
| 'landing';
|
||
|
|
|
||
|
|
export interface RoutingDecision {
|
||
|
|
nextAction: NextAction;
|
||
|
|
currentStep: OnboardingStep | null;
|
||
|
|
message?: string;
|
||
|
|
canSkip?: boolean;
|
||
|
|
completionPercentage: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class OnboardingRouter {
|
||
|
|
/**
|
||
|
|
* Determine next action for authenticated user
|
||
|
|
*/
|
||
|
|
static async getNextActionForUser(userProgress?: UserProgress): Promise<RoutingDecision> {
|
||
|
|
try {
|
||
|
|
const progress = userProgress || await onboardingService.getUserProgress();
|
||
|
|
|
||
|
|
// Check if user has fully completed onboarding or has high completion percentage
|
||
|
|
const isFullyCompleted = progress.fully_completed || progress.completion_percentage >= 80;
|
||
|
|
const currentStep = progress.current_step as OnboardingStep;
|
||
|
|
|
||
|
|
let nextAction: NextAction;
|
||
|
|
|
||
|
|
if (isFullyCompleted || currentStep === 'training_completed' || currentStep === 'dashboard_accessible') {
|
||
|
|
// User can access dashboard
|
||
|
|
nextAction = 'dashboard';
|
||
|
|
} else {
|
||
|
|
// Use step-based routing
|
||
|
|
nextAction = this.mapStepToAction(currentStep);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
nextAction,
|
||
|
|
currentStep,
|
||
|
|
completionPercentage: progress.completion_percentage,
|
||
|
|
message: this.getStepMessage(currentStep),
|
||
|
|
canSkip: this.canSkipStep(currentStep)
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error getting user progress:', error);
|
||
|
|
|
||
|
|
// Fallback logic when API fails
|
||
|
|
return {
|
||
|
|
nextAction: 'onboarding_bakery',
|
||
|
|
currentStep: 'bakery_registered',
|
||
|
|
completionPercentage: 0,
|
||
|
|
message: 'Let\'s set up your bakery information'
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine next action for unauthenticated user
|
||
|
|
*/
|
||
|
|
static getNextActionForGuest(): RoutingDecision {
|
||
|
|
return {
|
||
|
|
nextAction: 'landing',
|
||
|
|
currentStep: null,
|
||
|
|
completionPercentage: 0,
|
||
|
|
message: 'Welcome to PanIA - AI for your bakery'
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user can access dashboard with current progress
|
||
|
|
*/
|
||
|
|
static async canAccessDashboard(): Promise<boolean> {
|
||
|
|
try {
|
||
|
|
const progress = await onboardingService.getUserProgress();
|
||
|
|
return progress.fully_completed || progress.completion_percentage >= 80;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error checking dashboard access:', error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get dynamic onboarding step within the flow
|
||
|
|
*/
|
||
|
|
static async getOnboardingStepFromProgress(): Promise<{
|
||
|
|
step: number;
|
||
|
|
totalSteps: number;
|
||
|
|
canProceed: boolean;
|
||
|
|
}> {
|
||
|
|
try {
|
||
|
|
const progress = await onboardingService.getUserProgress();
|
||
|
|
const currentStep = progress.current_step as OnboardingStep;
|
||
|
|
|
||
|
|
const stepMap: Record<OnboardingStep, number> = {
|
||
|
|
'user_registered': 1,
|
||
|
|
'bakery_registered': 2,
|
||
|
|
'sales_data_uploaded': 3,
|
||
|
|
'training_completed': 4,
|
||
|
|
'dashboard_accessible': 5
|
||
|
|
};
|
||
|
|
|
||
|
|
const currentStepNumber = stepMap[currentStep] || 1;
|
||
|
|
|
||
|
|
return {
|
||
|
|
step: Math.max(currentStepNumber - 1, 1), // Convert to 1-based onboarding steps
|
||
|
|
totalSteps: 4, // We have 4 onboarding steps (excluding user registration)
|
||
|
|
canProceed: progress.completion_percentage > 0
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error getting onboarding step:', error);
|
||
|
|
return {
|
||
|
|
step: 1,
|
||
|
|
totalSteps: 4,
|
||
|
|
canProceed: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update progress when user completes an action
|
||
|
|
*/
|
||
|
|
static async completeStep(
|
||
|
|
step: OnboardingStep,
|
||
|
|
data?: Record<string, any>
|
||
|
|
): Promise<UserProgress> {
|
||
|
|
return await onboardingService.completeStep(step, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user has completed a specific step
|
||
|
|
*/
|
||
|
|
static async hasCompletedStep(step: OnboardingStep): Promise<boolean> {
|
||
|
|
try {
|
||
|
|
const progress = await onboardingService.getUserProgress();
|
||
|
|
const stepStatus = progress.steps.find((s: any) => s.step_name === step);
|
||
|
|
return stepStatus?.completed || false;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error checking step completion for ${step}:`, error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get user-friendly message for current step
|
||
|
|
*/
|
||
|
|
private static getStepMessage(step: OnboardingStep): string {
|
||
|
|
const messages: Record<OnboardingStep, string> = {
|
||
|
|
'user_registered': 'Welcome! Your account has been created successfully.',
|
||
|
|
'bakery_registered': 'Let\'s set up your bakery information to get started.',
|
||
|
|
'sales_data_uploaded': 'Upload your historical sales data for better predictions.',
|
||
|
|
'training_completed': 'Great! Your AI model is ready. Welcome to your dashboard!',
|
||
|
|
'dashboard_accessible': 'Welcome back! You\'re ready to use your AI-powered dashboard.'
|
||
|
|
};
|
||
|
|
|
||
|
|
return messages[step] || 'Continue setting up your account';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map step to corresponding frontend action
|
||
|
|
*/
|
||
|
|
private static mapStepToAction(step: OnboardingStep): NextAction {
|
||
|
|
const actionMap: Record<OnboardingStep, NextAction> = {
|
||
|
|
'user_registered': 'onboarding_bakery',
|
||
|
|
'bakery_registered': 'onboarding_bakery',
|
||
|
|
'sales_data_uploaded': 'onboarding_data',
|
||
|
|
'training_completed': 'dashboard', // ✅ Users can access dashboard when training is completed
|
||
|
|
'dashboard_accessible': 'dashboard'
|
||
|
|
};
|
||
|
|
|
||
|
|
return actionMap[step] || 'onboarding_bakery';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user can skip a specific step
|
||
|
|
*/
|
||
|
|
private static canSkipStep(step: OnboardingStep): boolean {
|
||
|
|
// Define which steps can be skipped
|
||
|
|
const skippableSteps: OnboardingStep[] = [
|
||
|
|
'training_completed' // Users can access dashboard even if training is still in progress
|
||
|
|
];
|
||
|
|
|
||
|
|
return skippableSteps.includes(step);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper functions for components
|
||
|
|
export const useOnboardingRouter = () => {
|
||
|
|
return {
|
||
|
|
getNextActionForUser: OnboardingRouter.getNextActionForUser,
|
||
|
|
getNextActionForGuest: OnboardingRouter.getNextActionForGuest,
|
||
|
|
canAccessDashboard: OnboardingRouter.canAccessDashboard,
|
||
|
|
getOnboardingStepFromProgress: OnboardingRouter.getOnboardingStepFromProgress,
|
||
|
|
completeStep: OnboardingRouter.completeStep,
|
||
|
|
hasCompletedStep: OnboardingRouter.hasCompletedStep,
|
||
|
|
};
|
||
|
|
};
|