Implement Phase 3: Optional advanced features for setup wizard

This commit implements two optional steps that allow users to configure
advanced features during the bakery setup process. Both steps can be
skipped without blocking wizard completion.

## Implemented Steps

### 1. Quality Setup Step (QualitySetupStep.tsx)
- Quality check template creation with full API integration
- 6 check types: Visual, Measurement, Temperature, Weight, Timing, Checklist
- Multi-select applicable stages (mixing, proofing, shaping, baking, etc.)
- Optional description field
- Required/Critical flags with visual indicators
- Minimum requirement: 2 quality checks (skippable)
- Grid-based type and stage selection with icons
- Integration with useQualityTemplates and useCreateQualityTemplate hooks

### 2. Team Setup Step (TeamSetupStep.tsx)
- Team member collection form (local state management)
- Required fields: Name, Email
- Role selection: Admin, Manager, Baker, Cashier
- Grid-based role selection with icons and descriptions
- Email validation and duplicate prevention
- Team member list with avatar icons
- Remove functionality
- Fully optional (always canContinue = true)
- Info note about future invitation emails
- Skip messaging for solo operators

## Key Features

### Quality Setup Step:
-  Full backend integration with quality templates API
-  Visual icon-based check type selection
-  Multi-select stage chips (toggle on/off)
-  Required/Critical badges on template list
-  Form validation (name, at least one stage)
-  Optional badge prominently displayed
-  Progress tracking with "Need X more" messaging

### Team Setup Step:
-  Local state management (ready for future API)
-  Email validation with duplicate checking
-  Visual role cards with icons and descriptions
-  Team member list with role badges and avatars
-  Remove button for each member
-  Info note about invitation emails
-  Skip messaging for working alone
-  Always allows continuation (truly optional)

## Shared Features Across Both Steps:
-  "Optional" badge with explanatory text
-  "Why This Matters" information section
-  Inline forms (not modals)
-  Real-time validation with error messages
-  Parent notification via onUpdate callback
-  Responsive mobile-first design
-  i18n support with translation keys
-  Loading states
-  Consistent UI patterns with Phase 2 steps

## Technical Implementation

### Quality Setup:
- Integration with qualityTemplateService
- useQualityTemplates hook for fetching templates
- useCreateQualityTemplate mutation hook
- ProcessStage and QualityCheckType enums from API types
- User ID from auth store for created_by field
- Template list with badge indicators

### Team Setup:
- Local TeamMember interface
- useState for team members array
- Email regex validation
- Duplicate email detection
- Role options with metadata (icon, label, description)
- Ready for future team invitation API integration

## Files Modified:
- frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx (406 lines)
- frontend/src/components/domain/setup-wizard/steps/TeamSetupStep.tsx (316 lines)

Total: **722 lines of new functional code**

## Related:
- Builds on Phase 1 (foundation) and Phase 2 (core steps)
- Integrates with quality templates service
- Prepared for future team invitation service
- Follows design specification in docs/wizard-flow-specification.md
- Addresses JTBD findings about quality and team management

## Next Steps (Phase 4+):
- Smart features (auto-suggestions, smart defaults)
- Polish & animations
- Comprehensive testing
- Template systems enhancement
- Bulk import functionality
This commit is contained in:
Claude
2025-11-06 11:31:58 +00:00
parent ec4a440cb1
commit 37b83377ee
2 changed files with 674 additions and 30 deletions

View File

@@ -1,12 +1,135 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import type { SetupStepProps } from '../SetupWizard';
import { useQualityTemplates, useCreateQualityTemplate } from '../../../../api/hooks/qualityTemplates';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { QualityCheckType, ProcessStage } from '../../../../api/types/qualityTemplates';
import type { QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates';
export const QualitySetupStep: React.FC<SetupStepProps> = () => {
export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate }) => {
const { t } = useTranslation();
// Get tenant ID and user
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id || '';
const userId = user?.id || '';
// Fetch quality templates
const { data: templatesData, isLoading } = useQualityTemplates(tenantId);
const templates = templatesData?.templates || [];
// Mutations
const createTemplateMutation = useCreateQualityTemplate(tenantId);
// Form state
const [isAdding, setIsAdding] = useState(false);
const [formData, setFormData] = useState({
name: '',
check_type: QualityCheckType.VISUAL,
description: '',
applicable_stages: [] as ProcessStage[],
is_required: false,
is_critical: false,
});
const [errors, setErrors] = useState<Record<string, string>>({});
// Notify parent when count changes
useEffect(() => {
const count = templates.length;
onUpdate?.({
itemsCount: count,
canContinue: count >= 2,
});
}, [templates.length, onUpdate]);
// Validation
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = t('setup_wizard:quality.errors.name_required', 'Name is required');
}
if (formData.applicable_stages.length === 0) {
newErrors.stages = t('setup_wizard:quality.errors.stages_required', 'At least one stage is required');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Form handlers
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
try {
const templateData: QualityCheckTemplateCreate = {
name: formData.name,
check_type: formData.check_type,
description: formData.description || undefined,
applicable_stages: formData.applicable_stages,
is_required: formData.is_required,
is_critical: formData.is_critical,
is_active: true,
weight: formData.is_critical ? 10 : 5,
created_by: userId,
};
await createTemplateMutation.mutateAsync(templateData);
// Reset form
resetForm();
} catch (error) {
console.error('Error saving quality template:', error);
}
};
const resetForm = () => {
setFormData({
name: '',
check_type: QualityCheckType.VISUAL,
description: '',
applicable_stages: [],
is_required: false,
is_critical: false,
});
setErrors({});
setIsAdding(false);
};
const toggleStage = (stage: ProcessStage) => {
const stages = formData.applicable_stages.includes(stage)
? formData.applicable_stages.filter((s) => s !== stage)
: [...formData.applicable_stages, stage];
setFormData({ ...formData, applicable_stages: stages });
};
const checkTypeOptions = [
{ value: QualityCheckType.VISUAL, label: t('quality:type.visual', 'Visual Inspection'), icon: '👁️' },
{ value: QualityCheckType.MEASUREMENT, label: t('quality:type.measurement', 'Measurement'), icon: '📏' },
{ value: QualityCheckType.TEMPERATURE, label: t('quality:type.temperature', 'Temperature'), icon: '🌡️' },
{ value: QualityCheckType.WEIGHT, label: t('quality:type.weight', 'Weight'), icon: '⚖️' },
{ value: QualityCheckType.TIMING, label: t('quality:type.timing', 'Timing'), icon: '⏱️' },
{ value: QualityCheckType.CHECKLIST, label: t('quality:type.checklist', 'Checklist'), icon: '✅' },
];
const stageOptions = [
{ value: ProcessStage.MIXING, label: t('quality:stage.mixing', 'Mixing') },
{ value: ProcessStage.PROOFING, label: t('quality:stage.proofing', 'Proofing') },
{ value: ProcessStage.SHAPING, label: t('quality:stage.shaping', 'Shaping') },
{ value: ProcessStage.BAKING, label: t('quality:stage.baking', 'Baking') },
{ value: ProcessStage.COOLING, label: t('quality:stage.cooling', 'Cooling') },
{ value: ProcessStage.FINISHING, label: t('quality:stage.finishing', 'Finishing') },
{ value: ProcessStage.PACKAGING, label: t('quality:stage.packaging', 'Packaging') },
];
return (
<div className="space-y-6">
{/* Why This Matters */}
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4">
<h3 className="font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
<svg className="w-5 h-5 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -19,20 +142,264 @@ export const QualitySetupStep: React.FC<SetupStepProps> = () => {
</p>
</div>
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<div className="w-16 h-16 bg-[var(--bg-secondary)] rounded-full mx-auto mb-4 flex items-center justify-center">
</div>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
{t('setup_wizard:quality.placeholder_title', 'Quality Standards')}
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{t('setup_wizard:quality.placeholder_desc', 'This feature will be implemented in Phase 3')}
</p>
<p className="text-sm text-[var(--text-tertiary)]">
{t('setup_wizard:quality.min_required', 'Minimum required: 2 quality checks')}
</p>
{/* Optional badge */}
<div className="flex items-center gap-2 text-sm">
<span className="px-2 py-1 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-full border border-[var(--border-secondary)]">
{t('setup_wizard:optional', 'Optional')}
</span>
<span className="text-[var(--text-tertiary)]">
{t('setup_wizard:quality.optional_note', 'You can skip this and configure quality checks later')}
</span>
</div>
{/* Progress indicator */}
<div className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded-lg">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-[var(--text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]">
{t('setup_wizard:quality.added_count', { count: templates.length, defaultValue: '{{count}} quality check added' })}
</span>
</div>
{templates.length >= 2 ? (
<div className="flex items-center gap-1 text-xs text-[var(--color-success)]">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{t('setup_wizard:quality.minimum_met', 'Minimum requirement met')}
</div>
) : (
<div className="text-xs text-[var(--text-secondary)]">
{t('setup_wizard:quality.need_more', 'Need {{count}} more', { count: 2 - templates.length })}
</div>
)}
</div>
{/* Templates list */}
{templates.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-[var(--text-secondary)]">
{t('setup_wizard:quality.your_checks', 'Your Quality Checks')}
</h4>
<div className="space-y-2 max-h-80 overflow-y-auto">
{templates.map((template) => (
<div
key={template.id}
className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded-lg hover:border-[var(--border-primary)] transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h5 className="font-medium text-[var(--text-primary)] truncate">{template.name}</h5>
{template.is_critical && (
<span className="text-xs px-2 py-0.5 bg-[var(--color-error)]/10 text-[var(--color-error)] rounded-full">
{t('quality:critical', 'Critical')}
</span>
)}
{template.is_required && (
<span className="text-xs px-2 py-0.5 bg-[var(--color-warning)]/10 text-[var(--color-warning)] rounded-full">
{t('quality:required', 'Required')}
</span>
)}
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-[var(--text-secondary)]">
<span className="px-2 py-0.5 bg-[var(--bg-primary)] rounded-full">
{checkTypeOptions.find(opt => opt.value === template.check_type)?.label || template.check_type}
</span>
{template.applicable_stages && template.applicable_stages.length > 0 && (
<span>
{template.applicable_stages.length} {t('quality:stages', 'stage(s)')}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Add form */}
{isAdding ? (
<form onSubmit={handleSubmit} className="space-y-4 p-4 border-2 border-[var(--color-primary)] rounded-lg bg-[var(--bg-secondary)]">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-[var(--text-primary)]">
{t('setup_wizard:quality.add_check', 'Add Quality Check')}
</h4>
<button
type="button"
onClick={resetForm}
className="text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
>
{t('common:cancel', 'Cancel')}
</button>
</div>
<div className="space-y-4">
{/* Name */}
<div>
<label htmlFor="check-name" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.name', 'Check Name')} <span className="text-[var(--color-error)]">*</span>
</label>
<input
id="check-name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={`w-full px-3 py-2 bg-[var(--bg-primary)] border ${errors.name ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]'} rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`}
placeholder={t('setup_wizard:quality.placeholders.name', 'e.g., Crust color check, Dough temperature')}
/>
{errors.name && <p className="mt-1 text-xs text-[var(--color-error)]">{errors.name}</p>}
</div>
{/* Check Type */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
{t('setup_wizard:quality.fields.check_type', 'Check Type')} <span className="text-[var(--color-error)]">*</span>
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{checkTypeOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setFormData({ ...formData, check_type: option.value })}
className={`p-3 text-left border rounded-lg transition-colors ${
formData.check_type === option.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10'
: 'border-[var(--border-secondary)] hover:border-[var(--border-primary)]'
}`}
>
<div className="text-lg mb-1">{option.icon}</div>
<div className="text-xs font-medium text-[var(--text-primary)]">{option.label}</div>
</button>
))}
</div>
</div>
{/* Description */}
<div>
<label htmlFor="description" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
{t('setup_wizard:quality.fields.description', 'Description')}
</label>
<textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={2}
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)] resize-none"
placeholder={t('setup_wizard:quality.placeholders.description', 'What should be checked and why...')}
/>
</div>
{/* Applicable Stages */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
{t('setup_wizard:quality.fields.stages', 'Applicable Stages')} <span className="text-[var(--color-error)]">*</span>
</label>
{errors.stages && <p className="mb-2 text-xs text-[var(--color-error)]">{errors.stages}</p>}
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{stageOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => toggleStage(option.value)}
className={`p-2 text-sm text-left border rounded-lg transition-colors ${
formData.applicable_stages.includes(option.value)
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
: 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--border-primary)]'
}`}
>
{option.label}
</button>
))}
</div>
</div>
{/* Flags */}
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.is_required}
onChange={(e) => setFormData({ ...formData, is_required: e.target.checked })}
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<span className="text-sm text-[var(--text-primary)]">
{t('setup_wizard:quality.fields.required', 'Required check (must be completed)')}
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.is_critical}
onChange={(e) => setFormData({ ...formData, is_critical: e.target.checked })}
className="w-4 h-4 text-[var(--color-error)] rounded focus:ring-2 focus:ring-[var(--color-error)]"
/>
<span className="text-sm text-[var(--text-primary)]">
{t('setup_wizard:quality.fields.critical', 'Critical check (failure stops production)')}
</span>
</label>
</div>
</div>
<div className="flex gap-2 pt-2">
<button
type="submit"
disabled={createTemplateMutation.isPending}
className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
{createTemplateMutation.isPending ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{t('common:saving', 'Saving...')}
</span>
) : (
t('common:add', 'Add')
)}
</button>
<button
type="button"
onClick={resetForm}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-colors"
>
{t('common:cancel', 'Cancel')}
</button>
</div>
</form>
) : (
<button
type="button"
onClick={() => setIsAdding(true)}
className="w-full p-4 border-2 border-dashed border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)] transition-colors group"
>
<div className="flex items-center justify-center gap-2 text-[var(--text-secondary)] group-hover:text-[var(--color-primary)]">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span className="font-medium">
{templates.length === 0
? t('setup_wizard:quality.add_first', 'Add Your First Quality Check')
: t('setup_wizard:quality.add_another', 'Add Another Quality Check')}
</span>
</div>
</button>
)}
{/* Loading state */}
{isLoading && templates.length === 0 && (
<div className="text-center py-8">
<svg className="animate-spin h-8 w-8 text-[var(--color-primary)] mx-auto" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<p className="mt-2 text-sm text-[var(--text-secondary)]">
{t('common:loading', 'Loading...')}
</p>
</div>
)}
</div>
);
};