Fixed two critical issues in the setup wizard: 1. **Missing onUpdate callback**: - Added onUpdate prop to SetupStepProps interface - Created handleStepUpdate callback in SetupWizard to receive canContinue updates - Passed onUpdate to all step components - Added onUpdate implementation in SuppliersSetupStep (was missing) - This fixes the issue where Continue/Skip buttons were incorrectly disabled for optional steps 2. **Button interaction issues in QualitySetupStep**: - Added explicit e.preventDefault() and e.stopPropagation() to Check Type buttons - Added explicit e.preventDefault() and e.stopPropagation() to Applicable Stages buttons - Added cursor-pointer class for better UX - This fixes the issue where buttons weren't responding to clicks The Quality Setup and Suppliers Setup steps now properly: - Show enabled Continue button when requirements are met - Show Skip button for optional steps - Allow clicking Check Type and Applicable Stages buttons without form submission interference
414 lines
19 KiB
TypeScript
414 lines
19 KiB
TypeScript
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> = ({ 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: true, // Always allow continuing since this step is optional
|
|
});
|
|
}, [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">
|
|
<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();
|
|
setFormData({ ...formData, check_type: option.value });
|
|
}}
|
|
className={`p-3 text-left border rounded-lg transition-colors cursor-pointer ${
|
|
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={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
toggleStage(option.value);
|
|
}}
|
|
className={`p-2 text-sm text-left border rounded-lg transition-colors cursor-pointer ${
|
|
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>
|
|
);
|
|
};
|