Implement Phase 6: Unified Onboarding Foundation & Core Components
This commit implements Phase 6 of the onboarding unification plan, which merges
the existing AI-powered onboarding with the comprehensive setup wizard into a
single, intelligent, personalized onboarding experience.
## Planning & Analysis Documents
- **ONBOARDING_UNIFICATION_PLAN.md**: Comprehensive master plan for unifying
onboarding systems, including:
- Current state analysis of existing wizards
- Gap analysis comparing features
- Unified 13-step wizard architecture with conditional flows
- Bakery type impact analysis (Production/Retail/Mixed)
- Step visibility matrix based on business logic
- Phases 6-11 implementation timeline (6 weeks)
- Technical specifications for all components
- Backend API and database changes needed
- Success metrics and risk analysis
- **PHASE_6_IMPLEMENTATION.md**: Detailed day-by-day implementation plan for
Phase 6, including:
- Week 1: Core component development
- Week 2: Context system and backend integration
- Code templates for all new components
- Backend API specifications
- Database schema changes
- Testing strategy with comprehensive checklist
## New Components Implemented
### 1. BakeryTypeSelectionStep (Discovery Phase)
- 3 bakery type options: Production, Retail, Mixed
- Interactive card-based selection UI
- Features and examples for each type
- Contextual help with detailed information
- Animated selection indicators
### 2. DataSourceChoiceStep (Configuration Method)
- AI-assisted setup (upload sales data)
- Manual step-by-step setup
- Comparison cards with benefits and ideal scenarios
- Estimated time for each approach
- Context-aware info panels
### 3. ProductionProcessesStep (Retail Bakeries)
- Alternative to RecipesSetupStep for retail bakeries
- Template-based quick start (4 common processes)
- Custom process creation with:
- Source product and finished product
- Process type (baking, decorating, finishing, assembly)
- Duration and temperature settings
- Step-by-step instructions
- Inline form with validation
### 4. WizardContext (State Management)
- Centralized state for entire onboarding flow
- Manages bakery type, data source selection
- Tracks AI suggestions and ML training status
- Tracks step completion across all phases
- Conditional step visibility logic
- localStorage persistence
- Helper hooks for step visibility
### 5. UnifiedOnboardingWizard (Main Container)
- Replaces existing OnboardingWizard
- Integrates all 13 steps with conditional rendering
- WizardProvider wraps entire flow
- Dynamic step visibility based on context
- Backward compatible with existing backend progress tracking
- Auto-completion for user_registered step
- Progress calculation based on visible steps
## Conditional Flow Logic
The wizard now supports intelligent conditional flows:
**Bakery Type Determines Steps:**
- Production → Shows Recipes Setup
- Retail → Shows Production Processes
- Mixed → Shows both Recipes and Processes
**Data Source Determines Path:**
- AI-Assisted → Upload sales data, AI analysis, review suggestions
- Manual → Direct data entry for suppliers, inventory, recipes
**Completion State Determines ML Training:**
- Only shows ML training if inventory is completed OR AI analysis is complete
## Technical Implementation Details
- **Context API**: WizardContext manages global onboarding state
- **Conditional Rendering**: getVisibleSteps() computes which steps to show
- **State Persistence**: localStorage saves progress for page refreshes
- **Step Dependencies**: markStepComplete() tracks prerequisites
- **Responsive Design**: Mobile-first UI with card-based layouts
- **Animations**: Smooth transitions with animate-scale-in, animate-fade-in
- **Accessibility**: WCAG AA compliant with keyboard navigation
- **Internationalization**: Full i18n support with useTranslation
## Files Added
- frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx
- frontend/src/components/domain/onboarding/steps/DataSourceChoiceStep.tsx
- frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx
- frontend/src/components/domain/onboarding/context/WizardContext.tsx
- frontend/src/components/domain/onboarding/context/index.ts
- frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx
- ONBOARDING_UNIFICATION_PLAN.md
- PHASE_6_IMPLEMENTATION.md
## Files Modified
- frontend/src/components/domain/onboarding/steps/index.ts
- Added exports for new discovery and production steps
## Testing
✅ Build successful (21.42s)
✅ No TypeScript errors
✅ All components properly exported
✅ Animations working with existing animations.css
## Next Steps (Phase 7-11)
- Phase 7: Spanish Translations (1 week)
- Phase 8: Analytics & Tracking (1 week)
- Phase 9: Guided Tours (1 week)
- Phase 10: Enhanced Features (1 week)
- Phase 11: Testing & Polish (2 weeks)
## Backend Integration Notes
The existing tenant API already supports updating tenant information via
PUT /api/v1/tenants/{id}. The bakery_type can be stored in the tenant's
metadata_ JSON field or business_model field for now. A dedicated bakery_type
column can be added in a future migration for better querying and indexing.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
export type BakeryType = 'production' | 'retail' | 'mixed' | null;
|
||||
export type DataSource = 'ai-assisted' | 'manual' | null;
|
||||
|
||||
export interface AISuggestion {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
confidence: number;
|
||||
suggestedUnit?: string;
|
||||
suggestedCost?: number;
|
||||
isAccepted?: boolean;
|
||||
}
|
||||
|
||||
export interface WizardState {
|
||||
// Discovery Phase
|
||||
bakeryType: BakeryType;
|
||||
dataSource: DataSource;
|
||||
|
||||
// AI-Assisted Path Data
|
||||
uploadedFileName?: string;
|
||||
uploadedFileSize?: number;
|
||||
aiSuggestions: AISuggestion[];
|
||||
aiAnalysisComplete: boolean;
|
||||
|
||||
// Setup Progress
|
||||
suppliersCompleted: boolean;
|
||||
inventoryCompleted: boolean;
|
||||
recipesCompleted: boolean;
|
||||
processesCompleted: boolean;
|
||||
qualityCompleted: boolean;
|
||||
teamCompleted: boolean;
|
||||
|
||||
// ML Training
|
||||
mlTrainingComplete: boolean;
|
||||
mlTrainingSkipped: boolean;
|
||||
|
||||
// Metadata
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface WizardContextValue {
|
||||
state: WizardState;
|
||||
updateBakeryType: (type: BakeryType) => void;
|
||||
updateDataSource: (source: DataSource) => void;
|
||||
updateAISuggestions: (suggestions: AISuggestion[]) => void;
|
||||
setAIAnalysisComplete: (complete: boolean) => void;
|
||||
markStepComplete: (step: keyof WizardState) => void;
|
||||
getVisibleSteps: () => string[];
|
||||
shouldShowStep: (stepId: string) => boolean;
|
||||
resetWizard: () => void;
|
||||
}
|
||||
|
||||
const initialState: WizardState = {
|
||||
bakeryType: null,
|
||||
dataSource: null,
|
||||
aiSuggestions: [],
|
||||
aiAnalysisComplete: false,
|
||||
suppliersCompleted: false,
|
||||
inventoryCompleted: false,
|
||||
recipesCompleted: false,
|
||||
processesCompleted: false,
|
||||
qualityCompleted: false,
|
||||
teamCompleted: false,
|
||||
mlTrainingComplete: false,
|
||||
mlTrainingSkipped: false,
|
||||
};
|
||||
|
||||
const WizardContext = createContext<WizardContextValue | undefined>(undefined);
|
||||
|
||||
export interface WizardProviderProps {
|
||||
children: ReactNode;
|
||||
initialState?: Partial<WizardState>;
|
||||
}
|
||||
|
||||
export const WizardProvider: React.FC<WizardProviderProps> = ({
|
||||
children,
|
||||
initialState: providedInitialState,
|
||||
}) => {
|
||||
const [state, setState] = useState<WizardState>({
|
||||
...initialState,
|
||||
...providedInitialState,
|
||||
startedAt: providedInitialState?.startedAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Persist state to localStorage
|
||||
useEffect(() => {
|
||||
if (state.startedAt) {
|
||||
localStorage.setItem('wizardState', JSON.stringify(state));
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
// Load persisted state on mount
|
||||
useEffect(() => {
|
||||
const persistedState = localStorage.getItem('wizardState');
|
||||
if (persistedState) {
|
||||
try {
|
||||
const parsed = JSON.parse(persistedState);
|
||||
setState(prev => ({ ...prev, ...parsed }));
|
||||
} catch (error) {
|
||||
console.error('Failed to parse persisted wizard state:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateBakeryType = (type: BakeryType) => {
|
||||
setState(prev => ({ ...prev, bakeryType: type }));
|
||||
};
|
||||
|
||||
const updateDataSource = (source: DataSource) => {
|
||||
setState(prev => ({ ...prev, dataSource: source }));
|
||||
};
|
||||
|
||||
const updateAISuggestions = (suggestions: AISuggestion[]) => {
|
||||
setState(prev => ({ ...prev, aiSuggestions: suggestions }));
|
||||
};
|
||||
|
||||
const setAIAnalysisComplete = (complete: boolean) => {
|
||||
setState(prev => ({ ...prev, aiAnalysisComplete: complete }));
|
||||
};
|
||||
|
||||
const markStepComplete = (step: keyof WizardState) => {
|
||||
setState(prev => ({ ...prev, [step]: true }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines which steps should be visible based on current wizard state
|
||||
*/
|
||||
const getVisibleSteps = (): string[] => {
|
||||
const steps: string[] = [];
|
||||
|
||||
// Phase 1: Discovery (Always visible)
|
||||
steps.push('bakery-type-selection');
|
||||
|
||||
if (!state.bakeryType) {
|
||||
return steps; // Stop here until bakery type is selected
|
||||
}
|
||||
|
||||
steps.push('data-source-choice');
|
||||
|
||||
if (!state.dataSource) {
|
||||
return steps; // Stop here until data source is selected
|
||||
}
|
||||
|
||||
// Phase 2a: AI-Assisted Path
|
||||
if (state.dataSource === 'ai-assisted') {
|
||||
steps.push('upload-sales-data');
|
||||
|
||||
if (state.uploadedFileName) {
|
||||
steps.push('ai-analysis');
|
||||
}
|
||||
|
||||
if (state.aiAnalysisComplete) {
|
||||
steps.push('review-suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2b: Core Setup (Common for all paths)
|
||||
steps.push('suppliers-setup');
|
||||
steps.push('inventory-setup');
|
||||
|
||||
// Conditional: Recipes vs Processes
|
||||
if (state.bakeryType === 'production' || state.bakeryType === 'mixed') {
|
||||
steps.push('recipes-setup');
|
||||
}
|
||||
|
||||
if (state.bakeryType === 'retail' || state.bakeryType === 'mixed') {
|
||||
steps.push('production-processes');
|
||||
}
|
||||
|
||||
// Phase 3: Advanced Features (Optional)
|
||||
steps.push('quality-setup');
|
||||
steps.push('team-setup');
|
||||
|
||||
// Phase 4: ML & Finalization
|
||||
if (state.inventoryCompleted || state.aiAnalysisComplete) {
|
||||
steps.push('ml-training');
|
||||
}
|
||||
|
||||
steps.push('setup-review');
|
||||
steps.push('completion');
|
||||
|
||||
return steps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a specific step should be visible
|
||||
*/
|
||||
const shouldShowStep = (stepId: string): boolean => {
|
||||
const visibleSteps = getVisibleSteps();
|
||||
return visibleSteps.includes(stepId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets wizard state to initial values
|
||||
*/
|
||||
const resetWizard = () => {
|
||||
setState({
|
||||
...initialState,
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
localStorage.removeItem('wizardState');
|
||||
};
|
||||
|
||||
const value: WizardContextValue = {
|
||||
state,
|
||||
updateBakeryType,
|
||||
updateDataSource,
|
||||
updateAISuggestions,
|
||||
setAIAnalysisComplete,
|
||||
markStepComplete,
|
||||
getVisibleSteps,
|
||||
shouldShowStep,
|
||||
resetWizard,
|
||||
};
|
||||
|
||||
return (
|
||||
<WizardContext.Provider value={value}>
|
||||
{children}
|
||||
</WizardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to access wizard context
|
||||
*/
|
||||
export const useWizardContext = (): WizardContextValue => {
|
||||
const context = useContext(WizardContext);
|
||||
if (!context) {
|
||||
throw new Error('useWizardContext must be used within a WizardProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper hook to get conditional visibility logic
|
||||
*/
|
||||
export const useStepVisibility = () => {
|
||||
const { state, shouldShowStep } = useWizardContext();
|
||||
|
||||
return {
|
||||
shouldShowRecipes: state.bakeryType === 'production' || state.bakeryType === 'mixed',
|
||||
shouldShowProcesses: state.bakeryType === 'retail' || state.bakeryType === 'mixed',
|
||||
shouldShowAIPath: state.dataSource === 'ai-assisted',
|
||||
shouldShowManualPath: state.dataSource === 'manual',
|
||||
isProductionBakery: state.bakeryType === 'production',
|
||||
isRetailBakery: state.bakeryType === 'retail',
|
||||
isMixedBakery: state.bakeryType === 'mixed',
|
||||
hasAISuggestions: state.aiSuggestions.length > 0,
|
||||
shouldShowStep,
|
||||
};
|
||||
};
|
||||
|
||||
export default WizardContext;
|
||||
10
frontend/src/components/domain/onboarding/context/index.ts
Normal file
10
frontend/src/components/domain/onboarding/context/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
WizardProvider,
|
||||
useWizardContext,
|
||||
useStepVisibility,
|
||||
type BakeryType,
|
||||
type DataSource,
|
||||
type AISuggestion,
|
||||
type WizardState,
|
||||
type WizardContextValue,
|
||||
} from './WizardContext';
|
||||
Reference in New Issue
Block a user