IMPORVE ONBOARDING STEPS

This commit is contained in:
Urtzi Alfaro
2025-11-09 09:22:08 +01:00
parent 4678f96f8f
commit cbe19a3cd1
27 changed files with 2801 additions and 1149 deletions

View File

@@ -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>
);
};

View File

@@ -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';