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.
33 KiB
Phase 6: Foundation & Integration - Detailed Implementation Plan
Overview
This phase merges the existing AI-powered onboarding with the new comprehensive setup wizard into a unified, intelligent system with conditional flows based on bakery type and data source.
Duration: 2 weeks Team: 1 senior developer + 1 mid-level developer Dependencies: Analysis complete ✅
Week 1: Core Components & Context System
Day 1-2: Bakery Type Selection Step
File: /frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx
Component Structure:
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { SetupStepProps } from '../SetupWizard';
interface BakeryType {
id: 'production' | 'retail' | 'mixed';
icon: string;
name: string;
description: string;
features: string[];
examples: string[];
color: string;
}
export const BakeryTypeSelectionStep: React.FC<SetupStepProps> = ({
onUpdate,
onComplete
}) => {
const { t } = useTranslation();
const [selectedType, setSelectedType] = useState<string | null>(null);
const bakeryTypes: BakeryType[] = [
{
id: 'production',
icon: '🥖',
name: t('bakery_type.production.name', 'Panadería de Producción'),
description: t('bakery_type.production.desc', 'Producimos desde cero usando ingredientes'),
features: [
t('bakery_type.production.feature1', 'Gestión completa de recetas'),
t('bakery_type.production.feature2', 'Control de ingredientes'),
t('bakery_type.production.feature3', 'Procesos de producción completos'),
t('bakery_type.production.feature4', 'Cálculo de costos detallado')
],
examples: [
t('bakery_type.production.example1', 'Pan artesanal'),
t('bakery_type.production.example2', 'Bollería'),
t('bakery_type.production.example3', 'Repostería')
],
color: 'from-amber-500 to-orange-600'
},
{
id: 'retail',
icon: '🛒',
name: t('bakery_type.retail.name', 'Panadería de Reventa/Acabado'),
description: t('bakery_type.retail.desc', 'Recibimos productos terminados o semi-elaborados'),
features: [
t('bakery_type.retail.feature1', 'Gestión de productos terminados'),
t('bakery_type.retail.feature2', 'Procesos de horneado simple'),
t('bakery_type.retail.feature3', 'Gestión de proveedores'),
t('bakery_type.retail.feature4', 'Control de calidad')
],
examples: [
t('bakery_type.retail.example1', 'Hornear productos precocidos'),
t('bakery_type.retail.example2', 'Venta de productos terminados'),
t('bakery_type.retail.example3', 'Acabado de productos par-baked')
],
color: 'from-blue-500 to-indigo-600'
},
{
id: 'mixed',
icon: '🏭',
name: t('bakery_type.mixed.name', 'Panadería Mixta'),
description: t('bakery_type.mixed.desc', 'Combinamos producción propia y reventa'),
features: [
t('bakery_type.mixed.feature1', 'Todas las funcionalidades'),
t('bakery_type.mixed.feature2', 'Máxima flexibilidad'),
t('bakery_type.mixed.feature3', 'Gestión completa de operaciones'),
t('bakery_type.mixed.feature4', 'Ideal para negocios en crecimiento')
],
examples: [
t('bakery_type.mixed.example1', 'Producción propia + Reventa'),
t('bakery_type.mixed.example2', 'Combinación de modelos')
],
color: 'from-purple-500 to-pink-600'
}
];
const handleSelect = (typeId: string) => {
setSelectedType(typeId);
onUpdate?.({
itemsCount: 1,
canContinue: true,
});
};
const handleContinue = async () => {
if (!selectedType) return;
// Save bakery type to context and tenant
await onComplete?.({
bakeryType: selectedType
});
};
return (
<div className="space-y-6">
{/* Header */}
<div className="text-center">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
{t('bakery_type.title', '¿Qué tipo de panadería tienes?')}
</h2>
<p className="text-[var(--text-secondary)]">
{t('bakery_type.subtitle', 'Esto nos ayudará a personalizar tu experiencia')}
</p>
</div>
{/* Bakery Type Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{bakeryTypes.map((type) => (
<button
key={type.id}
onClick={() => handleSelect(type.id)}
className={`
relative p-6 border-2 rounded-xl transition-all
${selectedType === type.id
? 'border-[var(--color-primary)] shadow-lg scale-105'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:shadow-md'
}
text-left group
`}
>
{/* Selected Indicator */}
{selectedType === type.id && (
<div className="absolute top-3 right-3">
<svg className="w-6 h-6 text-[var(--color-success)]" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
)}
{/* Icon */}
<div className={`text-6xl mb-4 bg-gradient-to-br ${type.color} bg-clip-text text-transparent`}>
{type.icon}
</div>
{/* Name & Description */}
<h3 className="font-bold text-lg text-[var(--text-primary)] mb-2">
{type.name}
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
{type.description}
</p>
{/* Features */}
<div className="space-y-2 mb-4">
<p className="text-xs font-semibold text-[var(--text-tertiary)] uppercase">
{t('bakery_type.features', 'Características')}:
</p>
<ul className="space-y-1">
{type.features.map((feature, idx) => (
<li key={idx} className="flex items-start gap-2 text-sm text-[var(--text-secondary)]">
<svg className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span>{feature}</span>
</li>
))}
</ul>
</div>
{/* Examples */}
<div className="pt-3 border-t border-[var(--border-secondary)]">
<p className="text-xs font-semibold text-[var(--text-tertiary)] uppercase mb-1">
{t('bakery_type.examples', 'Ejemplos')}:
</p>
<p className="text-xs text-[var(--text-secondary)]">
{type.examples.join(', ')}
</p>
</div>
</button>
))}
</div>
{/* Help Text */}
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-[var(--color-info)] mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-[var(--text-primary)] mb-1">
{t('bakery_type.help_title', '¿No estás seguro?')}
</p>
<p className="text-sm text-[var(--text-secondary)]">
{t('bakery_type.help_desc', 'Puedes elegir "Panadería Mixta" si combinas producción propia con reventa de productos. Podrás personalizar más adelante.')}
</p>
</div>
</div>
</div>
</div>
);
};
API Integration:
// Update tenant with bakery type
const updateTenantBakeryType = async (tenantId: string, bakeryType: string) => {
await apiClient.put(`/api/v1/tenants/${tenantId}/settings`, {
bakery_type: bakeryType
});
};
Tasks:
- Create component file
- Implement UI with 3 cards
- Add hover/selected states
- Integrate with API to save bakery type
- Add unit tests
- Add Storybook story
Day 3-4: Data Source Choice Step
File: /frontend/src/components/domain/onboarding/steps/DataSourceChoiceStep.tsx
Component Structure:
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { SetupStepProps } from '../SetupWizard';
interface DataSourceOption {
id: 'ai' | 'manual';
icon: React.ReactNode;
name: string;
description: string;
duration: string;
benefits: string[];
recommended?: boolean;
}
export const DataSourceChoiceStep: React.FC<SetupStepProps> = ({
onUpdate,
onComplete
}) => {
const { t } = useTranslation();
const [selectedSource, setSelectedSource] = useState<string | null>(null);
const dataSources: DataSourceOption[] = [
{
id: 'ai',
icon: (
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
),
name: t('data_source.ai.name', 'Subir datos de ventas (Recomendado)'),
description: t('data_source.ai.desc', 'Deja que la IA analice tus ventas y configure automáticamente tu inventario'),
duration: t('data_source.ai.duration', '~5 minutos'),
benefits: [
t('data_source.ai.benefit1', 'Configuración automática basada en tus datos reales'),
t('data_source.ai.benefit2', 'Recomendaciones inteligentes de productos'),
t('data_source.ai.benefit3', 'Análisis de patrones de venta'),
t('data_source.ai.benefit4', 'Mucho más rápido que la configuración manual')
],
recommended: true
},
{
id: 'manual',
icon: (
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
),
name: t('data_source.manual.name', 'Configuración manual'),
description: t('data_source.manual.desc', 'Configura todo desde cero usando nuestras plantillas y guías'),
duration: t('data_source.manual.duration', '~15 minutos'),
benefits: [
t('data_source.manual.benefit1', 'Control total sobre cada detalle'),
t('data_source.manual.benefit2', 'No necesitas datos históricos'),
t('data_source.manual.benefit3', 'Plantillas predefinidas incluidas'),
t('data_source.manual.benefit4', 'Ideal para negocios nuevos')
],
recommended: false
}
];
const handleSelect = (sourceId: string) => {
setSelectedSource(sourceId);
onUpdate?.({
itemsCount: 1,
canContinue: true,
});
};
const handleContinue = async () => {
if (!selectedSource) return;
await onComplete?.({
dataSource: selectedSource
});
};
return (
<div className="space-y-6">
{/* Header */}
<div className="text-center">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-2">
{t('data_source.title', '¿Cómo quieres configurar tu inventario?')}
</h2>
<p className="text-[var(--text-secondary)]">
{t('data_source.subtitle', 'Elige el método que mejor se adapte a tu situación')}
</p>
</div>
{/* Data Source Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{dataSources.map((source) => (
<button
key={source.id}
onClick={() => handleSelect(source.id)}
className={`
relative p-6 border-2 rounded-xl transition-all
${selectedSource === source.id
? 'border-[var(--color-primary)] shadow-lg scale-105'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:shadow-md'
}
text-left
`}
>
{/* Recommended Badge */}
{source.recommended && (
<div className="absolute top-3 right-3 px-3 py-1 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] text-white text-xs font-semibold rounded-full">
{t('data_source.recommended', 'Recomendado')}
</div>
)}
{/* Selected Indicator */}
{selectedSource === source.id && !source.recommended && (
<div className="absolute top-3 right-3">
<svg className="w-6 h-6 text-[var(--color-success)]" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
)}
{/* Icon */}
<div className="text-[var(--color-primary)] mb-4">
{source.icon}
</div>
{/* Name & Duration */}
<div className="mb-2">
<h3 className="font-bold text-lg text-[var(--text-primary)]">
{source.name}
</h3>
<p className="text-sm text-[var(--color-primary)] font-medium">
⏱️ {source.duration}
</p>
</div>
{/* Description */}
<p className="text-sm text-[var(--text-secondary)] mb-4">
{source.description}
</p>
{/* Benefits */}
<ul className="space-y-2">
{source.benefits.map((benefit, idx) => (
<li key={idx} className="flex items-start gap-2 text-sm text-[var(--text-secondary)]">
<svg className="w-4 h-4 text-[var(--color-success)] mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span>{benefit}</span>
</li>
))}
</ul>
</button>
))}
</div>
{/* Additional Info */}
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-4">
{/* AI Path Info */}
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 className="font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
<svg className="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
{t('data_source.ai_info_title', 'Ruta con IA')}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
{t('data_source.ai_info', 'Necesitarás un archivo CSV, Excel o JSON con tus datos de ventas históricos. La IA analizará tus productos y configurará automáticamente el inventario.')}
</p>
</div>
{/* Manual Path Info */}
<div className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<h4 className="font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
{t('data_source.manual_info_title', 'Ruta Manual')}
</h4>
<p className="text-sm text-[var(--text-secondary)]">
{t('data_source.manual_info', 'Te guiaremos paso a paso para agregar proveedores, ingredientes y recetas. Incluimos plantillas para facilitar el proceso.')}
</p>
</div>
</div>
</div>
);
};
Tasks:
- Create component file
- Implement UI with 2 cards
- Add recommended badge for AI path
- Add info boxes
- Integrate with wizard context
- Add unit tests
- Add Storybook story
Day 5: Production Processes Step (for Retail Bakeries)
File: /frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx
Component Structure: (See PHASE_6_DETAILED_SPEC.md for full implementation)
API Endpoints:
// Backend: Create production process endpoint
POST /api/v1/tenants/{tenant_id}/production/processes
Body: {
product_id: string;
process_name: string;
description: string;
steps: Array<{
order: number;
instruction: string;
duration_minutes: number;
temperature_celsius?: number;
}>;
duration_minutes: number;
temperature_celsius?: number;
}
Tasks:
- Create component file
- Implement process form
- Add process templates library
- Create backend API endpoint
- Create database table
- Add unit tests
- Add integration tests
Week 2: Context System & Integration
Day 6-7: Wizard Context Provider
File: /frontend/src/contexts/WizardContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';
interface WizardContextType {
// Discovery
bakeryType?: 'production' | 'retail' | 'mixed';
dataSource?: 'ai' | 'manual';
// AI Path Data
salesDataUploaded: boolean;
aiSuggestions: ProductSuggestion[];
selectedSuggestions: string[];
// Setup Data
suppliers: Supplier[];
inventory: InventoryItem[];
recipes: Recipe[];
processes: ProductionProcess[];
qualityTemplates: QualityTemplate[];
teamMembers: TeamMember[];
// ML Training
mlTrainingJobId?: string;
mlTrainingStatus?: 'pending' | 'in_progress' | 'completed' | 'failed';
mlTrainingProgress?: number;
// Actions
setBakeryType: (type: 'production' | 'retail' | 'mixed') => void;
setDataSource: (source: 'ai' | 'manual') => void;
setAISuggestions: (suggestions: ProductSuggestion[]) => void;
addSupplier: (supplier: Supplier) => void;
addInventoryItem: (item: InventoryItem) => void;
addRecipe: (recipe: Recipe) => void;
addProcess: (process: ProductionProcess) => void;
reset: () => void;
}
const WizardContext = createContext<WizardContextType | undefined>(undefined);
export const WizardProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [bakeryType, setBakeryType] = useState<'production' | 'retail' | 'mixed'>();
const [dataSource, setDataSource] = useState<'ai' | 'manual'>();
const [salesDataUploaded, setSalesDataUploaded] = useState(false);
const [aiSuggestions, setAISuggestions] = useState<ProductSuggestion[]>([]);
const [selectedSuggestions, setSelectedSuggestions] = useState<string[]>([]);
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [recipes, setRecipes] = useState<Recipe[]>([]);
const [processes, setProcesses] = useState<ProductionProcess[]>([]);
const [qualityTemplates, setQualityTemplates] = useState<QualityTemplate[]>([]);
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [mlTrainingJobId, setMLTrainingJobId] = useState<string>();
const [mlTrainingStatus, setMLTrainingStatus] = useState<string>();
const [mlTrainingProgress, setMLTrainingProgress] = useState<number>(0);
const addSupplier = useCallback((supplier: Supplier) => {
setSuppliers(prev => [...prev, supplier]);
}, []);
const addInventoryItem = useCallback((item: InventoryItem) => {
setInventory(prev => [...prev, item]);
}, []);
const addRecipe = useCallback((recipe: Recipe) => {
setRecipes(prev => [...prev, recipe]);
}, []);
const addProcess = useCallback((process: ProductionProcess) => {
setProcesses(prev => [...prev, process]);
}, []);
const reset = useCallback(() => {
setBakeryType(undefined);
setDataSource(undefined);
setSalesDataUploaded(false);
setAISuggestions([]);
setSelectedSuggestions([]);
setSuppliers([]);
setInventory([]);
setRecipes([]);
setProcesses([]);
setQualityTemplates([]);
setTeamMembers([]);
setMLTrainingJobId(undefined);
setMLTrainingStatus(undefined);
setMLTrainingProgress(0);
}, []);
const value: WizardContextType = {
bakeryType,
dataSource,
salesDataUploaded,
aiSuggestions,
selectedSuggestions,
suppliers,
inventory,
recipes,
processes,
qualityTemplates,
teamMembers,
mlTrainingJobId,
mlTrainingStatus,
mlTrainingProgress,
setBakeryType,
setDataSource,
setAISuggestions,
addSupplier,
addInventoryItem,
addRecipe,
addProcess,
reset
};
return (
<WizardContext.Provider value={value}>
{children}
</WizardContext.Provider>
);
};
export const useWizardContext = () => {
const context = useContext(WizardContext);
if (context === undefined) {
throw new Error('useWizardContext must be used within a WizardProvider');
}
return context;
};
Tasks:
- Create context file
- Implement state management
- Add persistence to localStorage
- Add TypeScript types
- Add hooks for context consumption
- Add unit tests
Day 8-9: Conditional Step Logic
File: /frontend/src/components/domain/onboarding/SetupWizard.tsx (update existing)
Add Step Visibility Logic:
import { useWizardContext } from '../../../contexts/WizardContext';
const getVisibleSteps = (
bakeryType?: string,
dataSource?: string,
hasSalesData?: boolean
): StepConfig[] => {
const steps: StepConfig[] = [];
// Phase 1: Discovery
steps.push(
{
id: 'bakery-type',
title: t('steps.bakery_type.title', 'Tipo de Panadería'),
description: t('steps.bakery_type.description', 'Selecciona tu modelo de negocio'),
component: BakeryTypeSelectionStep,
weight: 5,
estimatedMinutes: 2
},
{
id: 'data-choice',
title: t('steps.data_choice.title', 'Fuente de Datos'),
description: t('steps.data_choice.description', 'Elige cómo configurar'),
component: DataSourceChoiceStep,
weight: 5,
estimatedMinutes: 1
}
);
// Phase 2a: AI-Assisted Path (conditional)
if (dataSource === 'ai') {
steps.push(
{
id: 'upload-sales',
title: t('steps.upload_sales.title', 'Subir Datos'),
description: t('steps.upload_sales.description', 'Datos históricos de ventas'),
component: UploadSalesDataStep, // Reuse existing
weight: 15,
estimatedMinutes: 5
}
);
}
// Phase 2b: Core Setup (always shown)
steps.push(
{
id: 'suppliers-setup',
title: t('steps.suppliers.title', 'Proveedores'),
description: t('steps.suppliers.description', 'Tus proveedores'),
component: SuppliersSetupStep, // From new wizard
minRequired: 1,
weight: 10,
estimatedMinutes: 5
},
{
id: 'inventory-setup',
title: t('steps.inventory.title', 'Inventario'),
description: t('steps.inventory.description', 'Ingredientes y productos'),
component: InventorySetupStep, // Enhanced from new wizard
minRequired: 3,
weight: 20,
estimatedMinutes: 10
}
);
// Step 8a: Recipes (conditional - production/mixed only)
if (bakeryType === 'production' || bakeryType === 'mixed') {
steps.push({
id: 'recipes-setup',
title: t('steps.recipes.title', 'Recetas'),
description: t('steps.recipes.description', 'Tus fórmulas de producción'),
component: RecipesSetupStep, // From new wizard
minRequired: 1,
weight: 20,
estimatedMinutes: 10
});
}
// Step 8b: Production Processes (conditional - retail/mixed only)
if (bakeryType === 'retail' || bakeryType === 'mixed') {
steps.push({
id: 'production-processes',
title: t('steps.processes.title', 'Procesos de Producción'),
description: t('steps.processes.description', 'Instrucciones de horneado'),
component: ProductionProcessesStep, // New component
minRequired: 1,
weight: 15,
estimatedMinutes: 7
});
}
// Phase 3: Advanced Features (optional)
steps.push(
{
id: 'quality-setup',
title: t('steps.quality.title', 'Calidad'),
description: t('steps.quality.description', 'Estándares de calidad'),
component: QualitySetupStep, // From new wizard
isOptional: true,
weight: 15,
estimatedMinutes: 7
},
{
id: 'team-setup',
title: t('steps.team.title', 'Equipo'),
description: t('steps.team.description', 'Miembros del equipo'),
component: TeamSetupStep, // From new wizard
isOptional: true,
weight: 10,
estimatedMinutes: 5
}
);
// Phase 4: ML & Finalization
if (hasSalesData) {
steps.push({
id: 'ml-training',
title: t('steps.ml_training.title', 'Entrenamiento IA'),
description: t('steps.ml_training.description', 'Entrenar modelos predictivos'),
component: MLTrainingStep, // Reuse existing
weight: 10,
estimatedMinutes: 5
});
}
steps.push(
{
id: 'review',
title: t('steps.review.title', 'Revisar'),
description: t('steps.review.description', 'Confirma tu configuración'),
component: ReviewSetupStep, // From new wizard
weight: 5,
estimatedMinutes: 2
},
{
id: 'completion',
title: t('steps.completion.title', '¡Listo!'),
description: t('steps.completion.description', 'Sistema configurado'),
component: CompletionStep, // From new wizard
weight: 5,
estimatedMinutes: 2
}
);
return steps;
};
Update SetupWizard Component:
export const SetupWizard: React.FC = () => {
const { bakeryType, dataSource } = useWizardContext();
const [currentStepIndex, setCurrentStepIndex] = useState(0);
// Dynamically calculate visible steps
const visibleSteps = useMemo(() => {
return getVisibleSteps(bakeryType, dataSource, salesDataUploaded);
}, [bakeryType, dataSource, salesDataUploaded]);
// ... rest of wizard logic
};
Tasks:
- Implement getVisibleSteps function
- Update SetupWizard to use dynamic steps
- Add step dependency validation
- Update progress calculation
- Test all conditional paths
- Add integration tests
Day 10: Backend Integration
Backend Tasks:
1. Add bakery_type to tenants table:
ALTER TABLE tenants ADD COLUMN bakery_type VARCHAR(50);
ALTER TABLE tenants ADD COLUMN data_source VARCHAR(50);
2. Create production_processes table:
CREATE TABLE production_processes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE,
process_name VARCHAR(200) NOT NULL,
description TEXT,
steps JSONB NOT NULL DEFAULT '[]',
duration_minutes INTEGER,
temperature_celsius INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id),
CONSTRAINT fk_product FOREIGN KEY (product_id) REFERENCES inventory_items(id)
);
CREATE INDEX idx_production_processes_tenant ON production_processes(tenant_id);
CREATE INDEX idx_production_processes_product ON production_processes(product_id);
3. Create API endpoint for production processes:
File: /services/production/app/api/production_processes.py
from fastapi import APIRouter, Depends, HTTPException
from uuid import UUID
from typing import List
from app.models.production_process import ProductionProcess, ProductionProcessCreate
from app.services.auth import get_current_user
from app.database import get_db
router = APIRouter()
@router.post("/tenants/{tenant_id}/production/processes")
async def create_production_process(
tenant_id: UUID,
process: ProductionProcessCreate,
current_user = Depends(get_current_user),
db = Depends(get_db)
):
# Validate tenant access
# Create production process
# Return created process
pass
@router.get("/tenants/{tenant_id}/production/processes")
async def get_production_processes(
tenant_id: UUID,
current_user = Depends(get_current_user),
db = Depends(get_db)
) -> List[ProductionProcess]:
# Validate tenant access
# Fetch processes
# Return processes
pass
4. Update onboarding step dependencies:
File: /services/auth/app/api/onboarding_progress.py
Update STEP_DEPENDENCIES to include new steps:
STEP_DEPENDENCIES = {
"bakery-type": ["user_registered"],
"data-choice": ["user_registered", "bakery-type"],
"upload-sales": ["user_registered", "bakery-type", "data-choice"],
"suppliers-setup": ["user_registered", "bakery-type", "data-choice"],
"inventory-setup": ["user_registered", "bakery-type", "suppliers-setup"],
"recipes-setup": ["user_registered", "bakery-type", "inventory-setup"],
"production-processes": ["user_registered", "bakery-type", "inventory-setup"],
"quality-setup": ["user_registered", "bakery-type", "inventory-setup"],
"team-setup": ["user_registered", "bakery-type"],
"ml-training": ["user_registered", "bakery-type", "inventory-setup"],
"review": ["user_registered", "bakery-type", "inventory-setup"],
"completion": ["user_registered", "bakery-type", "review"]
}
Tasks:
- Add database migrations
- Create production processes API
- Update tenant settings endpoint
- Update step dependencies
- Add backend unit tests
- Add API integration tests
Testing Strategy
Unit Tests
Component Tests:
- BakeryTypeSelectionStep - 3 type cards render, selection works
- DataSourceChoiceStep - 2 option cards render, selection works
- ProductionProcessesStep - Form renders, validation works
- WizardContext - State management works correctly
- Conditional step logic - Steps show/hide based on context
API Tests:
- POST /tenants/{id}/settings - Saves bakery type
- POST /production/processes - Creates process
- GET /production/processes - Fetches processes
- Step dependencies - Validates correctly
Integration Tests
End-to-End Flows:
- Production + AI path: Full flow works
- Production + Manual path: Full flow works
- Retail + AI path: Full flow works
- Retail + Manual path: Full flow works
- Mixed + AI path: Full flow works
- Mixed + Manual path: Full flow works
Manual Testing Checklist
- All step transitions work
- Context persists across navigation
- Backend saves data correctly
- Progress tracking works
- Can go back and change selections
- Conditional steps appear/disappear correctly
- All UI states (loading, error, success) work
- Mobile responsive
- Accessibility (keyboard navigation, screen readers)
Deliverables
✅ Week 1:
- BakeryTypeSelectionStep component
- DataSourceChoiceStep component
- ProductionProcessesStep component
- Component tests
- Storybook stories
✅ Week 2: 6. WizardContext provider 7. Conditional step logic 8. Backend database changes 9. Backend API endpoints 10. Integration tests
Success Criteria
- All 6 onboarding paths work end-to-end
- Context persists correctly
- Backend stores all new data
- Tests have >80% coverage
- No TypeScript errors
- Build succeeds
- Performance: Wizard loads in <2s
- Accessibility: WCAG AA compliant
Next Phase Preview
Phase 7: Spanish Translations
- Comprehensive Spanish i18n
- 1000+ translation strings
- Translation review by native speaker
- Default language set to Spanish
Resources Needed
- Senior Developer (Days 1-10): Architecture, context system, integration
- Mid Developer (Days 1-10): Component implementation, testing
- Backend Developer (Days 10): Database migrations, API endpoints
- Designer (Days 1-3): Review mockups for new steps
- QA (Days 8-10): Integration testing
Ready to start? Let's build Phase 6! 🚀