997 lines
33 KiB
Markdown
997 lines
33 KiB
Markdown
|
|
# 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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
// 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:**
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```typescript
|
||
|
|
// 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`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```sql
|
||
|
|
ALTER TABLE tenants ADD COLUMN bakery_type VARCHAR(50);
|
||
|
|
ALTER TABLE tenants ADD COLUMN data_source VARCHAR(50);
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Create production_processes table:**
|
||
|
|
```sql
|
||
|
|
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`
|
||
|
|
|
||
|
|
```python
|
||
|
|
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:
|
||
|
|
|
||
|
|
```python
|
||
|
|
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:**
|
||
|
|
1. BakeryTypeSelectionStep component
|
||
|
|
2. DataSourceChoiceStep component
|
||
|
|
3. ProductionProcessesStep component
|
||
|
|
4. Component tests
|
||
|
|
5. 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! 🚀**
|