IMPORVE ONBOARDING STEPS
This commit is contained in:
@@ -1,372 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../contexts/AuthContext';
|
||||
import { useUserProgress, useMarkStepCompleted } from '../../../api/hooks/onboarding';
|
||||
import { StepProgress } from './components/StepProgress';
|
||||
import { StepNavigation } from './components/StepNavigation';
|
||||
import { Card, CardHeader, CardBody } from '../../ui/Card';
|
||||
import {
|
||||
WelcomeStep,
|
||||
SuppliersSetupStep,
|
||||
InventorySetupStep,
|
||||
RecipesSetupStep,
|
||||
QualitySetupStep,
|
||||
TeamSetupStep,
|
||||
ReviewSetupStep,
|
||||
CompletionStep
|
||||
} from './steps';
|
||||
|
||||
// Step weights for weighted progress calculation
|
||||
const STEP_WEIGHTS = {
|
||||
'setup-welcome': 5, // 2 min (light)
|
||||
'suppliers-setup': 10, // 5 min (moderate)
|
||||
'inventory-items-setup': 20, // 10 min (heavy)
|
||||
'recipes-setup': 20, // 10 min (heavy)
|
||||
'quality-setup': 15, // 7 min (moderate)
|
||||
'team-setup': 10, // 5 min (optional)
|
||||
'setup-review': 5, // 2 min (light, informational)
|
||||
'setup-completion': 5 // 2 min (light)
|
||||
};
|
||||
|
||||
export interface SetupStepConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
component: React.ComponentType<SetupStepProps>;
|
||||
minRequired?: number; // Minimum items to proceed
|
||||
isOptional?: boolean; // Can be skipped
|
||||
estimatedMinutes?: number; // For UI display
|
||||
weight: number; // For progress calculation
|
||||
}
|
||||
|
||||
export interface SetupStepProps {
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onComplete: (data?: any) => void;
|
||||
onSkip?: () => void;
|
||||
onUpdate?: (state: { itemsCount?: number; canContinue?: boolean }) => void;
|
||||
isFirstStep: boolean;
|
||||
isLastStep: boolean;
|
||||
canContinue?: boolean;
|
||||
}
|
||||
|
||||
export const SetupWizard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
// Define setup wizard steps (Steps 5-11 in overall onboarding)
|
||||
const SETUP_STEPS: SetupStepConfig[] = [
|
||||
{
|
||||
id: 'setup-welcome',
|
||||
title: t('setup_wizard:steps.welcome.title', 'Welcome & Setup Overview'),
|
||||
description: t('setup_wizard:steps.welcome.description', 'Let\'s set up your bakery operations'),
|
||||
component: WelcomeStep,
|
||||
isOptional: true,
|
||||
estimatedMinutes: 2,
|
||||
weight: STEP_WEIGHTS['setup-welcome']
|
||||
},
|
||||
{
|
||||
id: 'suppliers-setup',
|
||||
title: t('setup_wizard:steps.suppliers.title', 'Add Suppliers'),
|
||||
description: t('setup_wizard:steps.suppliers.description', 'Your ingredient and material providers'),
|
||||
component: SuppliersSetupStep,
|
||||
minRequired: 1,
|
||||
isOptional: false,
|
||||
estimatedMinutes: 5,
|
||||
weight: STEP_WEIGHTS['suppliers-setup']
|
||||
},
|
||||
{
|
||||
id: 'inventory-items-setup',
|
||||
title: t('setup_wizard:steps.inventory.title', 'Set Up Inventory Items'),
|
||||
description: t('setup_wizard:steps.inventory.description', 'Ingredients and materials you use'),
|
||||
component: InventorySetupStep,
|
||||
minRequired: 3,
|
||||
isOptional: false,
|
||||
estimatedMinutes: 10,
|
||||
weight: STEP_WEIGHTS['inventory-items-setup']
|
||||
},
|
||||
{
|
||||
id: 'recipes-setup',
|
||||
title: t('setup_wizard:steps.recipes.title', 'Create Recipes'),
|
||||
description: t('setup_wizard:steps.recipes.description', 'Your bakery\'s production formulas'),
|
||||
component: RecipesSetupStep,
|
||||
minRequired: 1,
|
||||
isOptional: false,
|
||||
estimatedMinutes: 10,
|
||||
weight: STEP_WEIGHTS['recipes-setup']
|
||||
},
|
||||
{
|
||||
id: 'quality-setup',
|
||||
title: t('setup_wizard:steps.quality.title', 'Define Quality Standards'),
|
||||
description: t('setup_wizard:steps.quality.description', 'Standards for consistent production'),
|
||||
component: QualitySetupStep,
|
||||
minRequired: 2,
|
||||
isOptional: true,
|
||||
estimatedMinutes: 7,
|
||||
weight: STEP_WEIGHTS['quality-setup']
|
||||
},
|
||||
{
|
||||
id: 'team-setup',
|
||||
title: t('setup_wizard:steps.team.title', 'Add Team Members'),
|
||||
description: t('setup_wizard:steps.team.description', 'Your bakery staff'),
|
||||
component: TeamSetupStep,
|
||||
minRequired: 0,
|
||||
isOptional: true,
|
||||
estimatedMinutes: 5,
|
||||
weight: STEP_WEIGHTS['team-setup']
|
||||
},
|
||||
{
|
||||
id: 'setup-review',
|
||||
title: t('setup_wizard:steps.review.title', 'Review Your Setup'),
|
||||
description: t('setup_wizard:steps.review.description', 'Confirm your configuration'),
|
||||
component: ReviewSetupStep,
|
||||
isOptional: false,
|
||||
estimatedMinutes: 2,
|
||||
weight: STEP_WEIGHTS['setup-review']
|
||||
},
|
||||
{
|
||||
id: 'setup-completion',
|
||||
title: t('setup_wizard:steps.completion.title', 'You\'re All Set!'),
|
||||
description: t('setup_wizard:steps.completion.description', 'Your bakery system is ready'),
|
||||
component: CompletionStep,
|
||||
isOptional: false,
|
||||
estimatedMinutes: 2,
|
||||
weight: STEP_WEIGHTS['setup-completion']
|
||||
}
|
||||
];
|
||||
|
||||
// State management
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [canContinue, setCanContinue] = useState(false);
|
||||
|
||||
// Handle updates from step components
|
||||
const handleStepUpdate = (state: { itemsCount?: number; canContinue?: boolean }) => {
|
||||
if (state.canContinue !== undefined) {
|
||||
setCanContinue(state.canContinue);
|
||||
}
|
||||
};
|
||||
|
||||
// Get user progress from backend
|
||||
const { data: userProgress, isLoading: isLoadingProgress } = useUserProgress(
|
||||
user?.id || '',
|
||||
{ enabled: !!user?.id }
|
||||
);
|
||||
|
||||
const markStepCompleted = useMarkStepCompleted();
|
||||
|
||||
// Calculate weighted progress percentage
|
||||
const calculateProgress = (): number => {
|
||||
if (!userProgress) return 0;
|
||||
|
||||
const totalWeight = Object.values(STEP_WEIGHTS).reduce((a, b) => a + b);
|
||||
let completedWeight = 0;
|
||||
|
||||
// Add weight of fully completed steps
|
||||
SETUP_STEPS.forEach((step, index) => {
|
||||
if (index < currentStepIndex) {
|
||||
const stepProgress = userProgress.steps.find(s => s.step_name === step.id);
|
||||
if (stepProgress?.completed) {
|
||||
completedWeight += step.weight;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add 50% of current step weight (user is midway through)
|
||||
const currentStep = SETUP_STEPS[currentStepIndex];
|
||||
completedWeight += currentStep.weight * 0.5;
|
||||
|
||||
return Math.round((completedWeight / totalWeight) * 100);
|
||||
};
|
||||
|
||||
const progressPercentage = calculateProgress();
|
||||
|
||||
// Initialize step index based on backend progress
|
||||
useEffect(() => {
|
||||
if (userProgress && !isInitialized) {
|
||||
console.log('🔄 Initializing setup wizard progress:', userProgress);
|
||||
|
||||
// Find first incomplete step
|
||||
let stepIndex = 0;
|
||||
for (let i = 0; i < SETUP_STEPS.length; i++) {
|
||||
const step = SETUP_STEPS[i];
|
||||
const stepProgress = userProgress.steps.find(s => s.step_name === step.id);
|
||||
|
||||
if (!stepProgress?.completed && stepProgress?.status !== 'skipped') {
|
||||
stepIndex = i;
|
||||
console.log(`📍 Resuming at step: "${step.id}" (index ${i})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all steps complete, go to last step
|
||||
if (stepIndex === 0 && SETUP_STEPS.every(step => {
|
||||
const stepProgress = userProgress.steps.find(s => s.step_name === step.id);
|
||||
return stepProgress?.completed || stepProgress?.status === 'skipped';
|
||||
})) {
|
||||
stepIndex = SETUP_STEPS.length - 1;
|
||||
console.log('✅ All steps completed, going to completion step');
|
||||
}
|
||||
|
||||
setCurrentStepIndex(stepIndex);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [userProgress, isInitialized]);
|
||||
|
||||
const currentStep = SETUP_STEPS[currentStepIndex];
|
||||
|
||||
// Navigation handlers
|
||||
const handleNext = () => {
|
||||
if (currentStepIndex < SETUP_STEPS.length - 1) {
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
setCanContinue(false); // Reset for next step
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStepIndex > 0) {
|
||||
setCurrentStepIndex(currentStepIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = async () => {
|
||||
if (!user?.id || !currentStep.isOptional) return;
|
||||
|
||||
console.log(`⏭️ Skipping step: "${currentStep.id}"`);
|
||||
|
||||
try {
|
||||
// Mark step as skipped (not completed)
|
||||
await markStepCompleted.mutateAsync({
|
||||
userId: user.id,
|
||||
stepName: currentStep.id,
|
||||
data: {
|
||||
skipped: true,
|
||||
skipped_at: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Step "${currentStep.id}" marked as skipped`);
|
||||
|
||||
// Move to next step
|
||||
handleNext();
|
||||
} catch (error) {
|
||||
console.error(`❌ Error skipping step "${currentStep.id}":`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStepComplete = async (data?: any) => {
|
||||
if (!user?.id) {
|
||||
console.error('User ID not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent concurrent mutations
|
||||
if (markStepCompleted.isPending) {
|
||||
console.warn(`⚠️ Step completion already in progress for "${currentStep.id}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🎯 Completing step: "${currentStep.id}" with data:`, data);
|
||||
|
||||
try {
|
||||
// Mark step as completed in backend
|
||||
await markStepCompleted.mutateAsync({
|
||||
userId: user.id,
|
||||
stepName: currentStep.id,
|
||||
data: {
|
||||
...data,
|
||||
completed_at: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Successfully completed step: "${currentStep.id}"`);
|
||||
|
||||
// Handle completion step navigation
|
||||
if (currentStep.id === 'setup-completion') {
|
||||
console.log('🎉 Setup wizard completed! Navigating to dashboard...');
|
||||
navigate('/app/dashboard');
|
||||
} else {
|
||||
// Auto-advance to next step
|
||||
handleNext();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error completing step "${currentStep.id}":`, error);
|
||||
|
||||
const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error';
|
||||
alert(`${t('setup_wizard:errors.step_failed', 'Error completing step')} "${currentStep.title}": ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state while initializing
|
||||
if (isLoadingProgress || !isInitialized) {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||
<Card padding="lg" shadow="lg">
|
||||
<CardBody>
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
{t('common:loading', 'Loading your setup progress...')}
|
||||
</p>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StepComponent = currentStep.component;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 space-y-6">
|
||||
{/* Progress Header */}
|
||||
<StepProgress
|
||||
steps={SETUP_STEPS}
|
||||
currentStepIndex={currentStepIndex}
|
||||
progressPercentage={progressPercentage}
|
||||
userProgress={userProgress}
|
||||
/>
|
||||
|
||||
{/* Step Content */}
|
||||
<Card shadow="lg" padding="none">
|
||||
<CardHeader padding="lg" divider>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-[var(--color-primary)] rounded-full flex items-center justify-center text-white text-sm font-bold">
|
||||
{currentStepIndex + 1}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
<p className="text-[var(--text-secondary)] text-sm">
|
||||
{currentStep.description}
|
||||
</p>
|
||||
</div>
|
||||
{currentStep.estimatedMinutes && (
|
||||
<div className="hidden sm:block text-sm text-[var(--text-tertiary)]">
|
||||
⏱️ ~{currentStep.estimatedMinutes} min
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody padding="lg">
|
||||
<StepComponent
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
onComplete={handleStepComplete}
|
||||
onSkip={handleSkip}
|
||||
onUpdate={handleStepUpdate}
|
||||
isFirstStep={currentStepIndex === 0}
|
||||
isLastStep={currentStepIndex === SETUP_STEPS.length - 1}
|
||||
canContinue={canContinue}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export { SetupWizard } from './SetupWizard';
|
||||
export type { SetupStepConfig, SetupStepProps } from './SetupWizard';
|
||||
// SetupWizard.tsx has been deleted - setup is now integrated into UnifiedOnboardingWizard
|
||||
// Individual setup steps are still used by UnifiedOnboardingWizard
|
||||
export * from './steps';
|
||||
export * from './components';
|
||||
|
||||
Reference in New Issue
Block a user