Files
bakery-ia/frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx
2025-12-19 13:10:24 +01:00

457 lines
22 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { SetupStepProps } from '../types';
import { useQualityTemplates, useCreateQualityTemplate } from '../../../../api/hooks/qualityTemplates';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { QualityCheckType, ProcessStage, QualityCheckTemplate, QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates';
export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
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; // Keep undefined if not available - backend requires valid UUID
// 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: true, // Always allow continuing since this step is optional
});
}, [templates.length, onUpdate]);
// Validation
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!userId) {
newErrors.form = t('common:error_loading_user', 'User not loaded. Please wait or refresh the page.');
setErrors(newErrors);
return false;
}
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 as QualityCheckType) || QualityCheckType.VISUAL,
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{t('setup_wizard:why_this_matters', 'Why This Matters')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('setup_wizard:quality.why', 'Quality checks ensure consistent output and help you identify issues early. Define what "good" looks like for each stage of production.')}
</p>
</div>
{/* 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.recommended_met', 'Recommended amount met')}
</div>
) : (
<div className="text-xs text-[var(--text-tertiary)]">
{t('setup_wizard:quality.recommended', '2+ recommended (optional)')}
</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={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Check type clicked:', option.value, 'current:', formData.check_type);
setFormData(prev => ({ ...prev, check_type: option.value }));
}}
className={`p - 3 text - left border - 2 rounded - lg transition - all cursor - pointer ${formData.check_type === option.value
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 shadow-lg ring-2 ring-[var(--color-primary)]/30'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} `}
>
<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={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Stage clicked:', option.value);
const isSelected = formData.applicable_stages.includes(option.value);
setFormData(prev => ({
...prev,
applicable_stages: isSelected
? prev.applicable_stages.filter(s => s !== option.value)
: [...prev.applicable_stages, option.value]
}));
}}
className={`p - 2 text - sm text - left border - 2 rounded - lg transition - all cursor - pointer ${formData.applicable_stages.includes(option.value)
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/20 text-[var(--color-primary)] font-semibold shadow-md ring-1 ring-[var(--color-primary)]/30'
: 'border-[var(--border-secondary)] text-[var(--text-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
} `}
>
{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>
{errors.form && (
<div className="p-3 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg text-sm text-[var(--color-error)]">
{errors.form}
</div>
)}
<div className="flex gap-2 pt-2">
<button
type="submit"
disabled={createTemplateMutation.isPending || !userId}
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>
)}
{/* Navigation buttons */}
{!isAdding && onComplete && (
<div className="flex items-center justify-between gap-4 pt-6 mt-6 border-t border-[var(--border-secondary)]">
<div className="flex gap-2">
<button
type="button"
onClick={onPrevious}
className="px-4 py-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors font-medium"
>
{t('common:previous', 'Anterior')}
</button>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => onComplete()}
disabled={canContinue === false}
className="px-6 py-2.5 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 flex items-center gap-2"
>
{t('common:next', 'Continuar →')}
</button>
</div>
</div>
)}
</div>
);
};