Improve frontend 5
This commit is contained in:
@@ -2,11 +2,174 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import { KeyValueEditor } from '../../../ui/KeyValueEditor/KeyValueEditor';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
import { Info } from 'lucide-react';
|
||||
import {
|
||||
Info,
|
||||
Eye,
|
||||
Ruler,
|
||||
Thermometer,
|
||||
Weight,
|
||||
CheckSquare,
|
||||
Clock,
|
||||
ClipboardList
|
||||
} from 'lucide-react';
|
||||
|
||||
// Single comprehensive step with all fields
|
||||
const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
// STEP 1: Quality Check Type Selection
|
||||
const QualityCheckTypeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
|
||||
const handleTypeSelect = (type: string) => {
|
||||
onDataChange?.({ ...data, checkType: type });
|
||||
};
|
||||
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
const checkTypes = [
|
||||
{
|
||||
value: 'visual',
|
||||
icon: Eye,
|
||||
titleKey: 'qualityTemplate.checkTypes.visual',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.visual'
|
||||
},
|
||||
{
|
||||
value: 'measurement',
|
||||
icon: Ruler,
|
||||
titleKey: 'qualityTemplate.checkTypes.measurement',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.measurement'
|
||||
},
|
||||
{
|
||||
value: 'temperature',
|
||||
icon: Thermometer,
|
||||
titleKey: 'qualityTemplate.checkTypes.temperature',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.temperature'
|
||||
},
|
||||
{
|
||||
value: 'weight',
|
||||
icon: Weight,
|
||||
titleKey: 'qualityTemplate.checkTypes.weight',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.weight'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
icon: CheckSquare,
|
||||
titleKey: 'qualityTemplate.checkTypes.boolean',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.boolean'
|
||||
},
|
||||
{
|
||||
value: 'timing',
|
||||
icon: Clock,
|
||||
titleKey: 'qualityTemplate.checkTypes.timing',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.timing'
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
icon: ClipboardList,
|
||||
titleKey: 'qualityTemplate.checkTypes.checklist',
|
||||
descriptionKey: 'qualityTemplate.checkTypeDescriptions.checklist'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
{t('qualityTemplate.selectCheckType')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.selectCheckTypeDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Check Type Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{checkTypes.map((type) => {
|
||||
const IconComponent = type.icon;
|
||||
const isSelected = data.checkType === type.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
onClick={() => handleTypeSelect(type.value)}
|
||||
className={`p-5 border-2 rounded-lg transition-all text-left ${
|
||||
isSelected
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5 shadow-md'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`p-3 rounded-lg ${
|
||||
isSelected
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-secondary)] text-[var(--text-tertiary)]'
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-[var(--text-primary)] mb-1">
|
||||
{t(type.titleKey)}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t(type.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<AdvancedOptionsSection
|
||||
title={t('qualityTemplate.sections.additionalIdentifiers')}
|
||||
description={t('qualityTemplate.sections.additionalIdentifiersDescription')}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Template Code */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.templateCode')}
|
||||
<Tooltip content={t('qualityTemplate.fields.templateCodeTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.templateCode || ''}
|
||||
onChange={(e) => handleFieldChange('templateCode', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.templateCodePlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.category')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.category || ''}
|
||||
onChange={(e) => handleFieldChange('category', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.categoryPlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionsSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// STEP 2: Essential Configuration
|
||||
const EssentialConfigurationStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
|
||||
@@ -14,51 +177,69 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
const handleStageToggle = (stage: string) => {
|
||||
const currentStages = data.applicableStages || [];
|
||||
const newStages = currentStages.includes(stage)
|
||||
? currentStages.filter((s: string) => s !== stage)
|
||||
: [...currentStages, stage];
|
||||
handleFieldChange('applicableStages', newStages);
|
||||
};
|
||||
|
||||
const isMeasurementType = ['measurement', 'temperature', 'weight'].includes(data.checkType);
|
||||
|
||||
const processStages = [
|
||||
'mixing',
|
||||
'proofing',
|
||||
'shaping',
|
||||
'baking',
|
||||
'cooling',
|
||||
'packaging',
|
||||
'finishing'
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
{t('qualityTemplate.templateDetails')}
|
||||
{t('qualityTemplate.essentialConfiguration')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.fillRequiredInfo')}
|
||||
{t('qualityTemplate.essentialConfigurationDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Required Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
{/* Core Fields */}
|
||||
<div className="space-y-4">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
value={data.name || ''}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.namePlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.checkType')} *
|
||||
{t('qualityTemplate.fields.description')}
|
||||
</label>
|
||||
<select
|
||||
value={data.checkType}
|
||||
onChange={(e) => handleFieldChange('checkType', e.target.value)}
|
||||
<textarea
|
||||
value={data.description || ''}
|
||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
<option value="product_quality">{t('qualityTemplate.checkTypes.product_quality')}</option>
|
||||
<option value="process_hygiene">{t('qualityTemplate.checkTypes.process_hygiene')}</option>
|
||||
<option value="equipment">{t('qualityTemplate.checkTypes.equipment')}</option>
|
||||
<option value="safety">{t('qualityTemplate.checkTypes.safety')}</option>
|
||||
<option value="cleaning">{t('qualityTemplate.checkTypes.cleaning')}</option>
|
||||
<option value="temperature">{t('qualityTemplate.checkTypes.temperature')}</option>
|
||||
<option value="documentation">{t('qualityTemplate.checkTypes.documentation')}</option>
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Weight */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.weight')} *
|
||||
@@ -68,8 +249,8 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.weight}
|
||||
onChange={(e) => handleFieldChange('weight', e.target.value)}
|
||||
value={data.weight || 5.0}
|
||||
onChange={(e) => handleFieldChange('weight', parseFloat(e.target.value))}
|
||||
placeholder="5.0"
|
||||
step="0.1"
|
||||
min="0"
|
||||
@@ -77,82 +258,189 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Applicable Stages */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.applicableStages')}
|
||||
<Tooltip content={t('qualityTemplate.fields.applicableStagesTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{processStages.map((stage) => {
|
||||
const isSelected = (data.applicableStages || []).includes(stage);
|
||||
return (
|
||||
<button
|
||||
key={stage}
|
||||
type="button"
|
||||
onClick={() => handleStageToggle(stage)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
isSelected
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--color-primary)]/10'
|
||||
}`}
|
||||
>
|
||||
{t(`qualityTemplate.processStages.${stage}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-2">
|
||||
{t('qualityTemplate.fields.applicableStagesHelp')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="border-t border-[var(--border-primary)] pt-4">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">{t('qualityTemplate.sections.basicInformation')}</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Measurement-Specific Fields */}
|
||||
{isMeasurementType && (
|
||||
<div className="border-t border-[var(--border-primary)] pt-4">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
{t('qualityTemplate.sections.measurementSpecifications')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Min Value */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.minValue')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.minValue || ''}
|
||||
onChange={(e) => handleFieldChange('minValue', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="0"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Value */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.maxValue')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.maxValue || ''}
|
||||
onChange={(e) => handleFieldChange('maxValue', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="100"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Target Value */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.targetValue')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.targetValue || ''}
|
||||
onChange={(e) => handleFieldChange('targetValue', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="50"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Unit */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.unit')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.unit || ''}
|
||||
onChange={(e) => handleFieldChange('unit', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.unitPlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tolerance Percentage */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.tolerancePercentage')}
|
||||
<Tooltip content={t('qualityTemplate.fields.toleranceTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.tolerancePercentage || ''}
|
||||
onChange={(e) => handleFieldChange('tolerancePercentage', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="5.0"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="100"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Options */}
|
||||
<AdvancedOptionsSection
|
||||
title={t('qualityTemplate.sections.additionalDetails')}
|
||||
description={t('qualityTemplate.sections.additionalDetailsDescription')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Instructions */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.templateCode')} ({t('common.optional')})
|
||||
<Tooltip content={t('qualityTemplate.fields.templateCodeTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.templateCode}
|
||||
onChange={(e) => handleFieldChange('templateCode', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.templateCodePlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.version')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.version}
|
||||
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||
placeholder="1.0"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.description')}
|
||||
{t('qualityTemplate.fields.instructions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.description}
|
||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.fields.applicableStages')}
|
||||
<Tooltip content={t('qualityTemplate.fields.applicableStagesTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.applicableStages}
|
||||
onChange={(e) => handleFieldChange('applicableStages', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.applicablePlaceholder')}
|
||||
value={data.instructions || ''}
|
||||
onChange={(e) => handleFieldChange('instructions', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.instructionsPlaceholder')}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionsSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// STEP 3: Quality Criteria & Settings
|
||||
const QualityCriteriaSettingsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
{t('qualityTemplate.criteriaAndSettings')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.criteriaAndSettingsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scoring Configuration */}
|
||||
<div className="border-t border-[var(--border-primary)] pt-4">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">{t('qualityTemplate.sections.scoringConfiguration')}</h4>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{t('qualityTemplate.sections.scoringConfiguration')}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Scoring Method */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.scoringMethods.scoringMethod')}
|
||||
{t('qualityTemplate.fields.scoringMethod')}
|
||||
</label>
|
||||
<select
|
||||
value={data.scoringMethod}
|
||||
value={data.scoringMethod || 'weighted_average'}
|
||||
onChange={(e) => handleFieldChange('scoringMethod', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
@@ -163,17 +451,18 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Pass Threshold */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.passThresholdPercent')}
|
||||
<Tooltip content={t('tooltips.passThreshold')}>
|
||||
{t('qualityTemplate.fields.passThreshold')}
|
||||
<Tooltip content={t('qualityTemplate.fields.passThresholdTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.passThreshold}
|
||||
onChange={(e) => handleFieldChange('passThreshold', e.target.value)}
|
||||
value={data.passThreshold || 70}
|
||||
onChange={(e) => handleFieldChange('passThreshold', parseFloat(e.target.value))}
|
||||
placeholder="70.0"
|
||||
step="0.1"
|
||||
min="0"
|
||||
@@ -182,32 +471,34 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Frequency Days */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.frequencyDays')}
|
||||
<Tooltip content={t('tooltips.frequencyDays')}>
|
||||
{t('qualityTemplate.fields.frequencyDays')}
|
||||
<Tooltip content={t('qualityTemplate.fields.frequencyDaysTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.frequencyDays}
|
||||
onChange={(e) => handleFieldChange('frequencyDays', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')}
|
||||
value={data.frequencyDays || ''}
|
||||
onChange={(e) => handleFieldChange('frequencyDays', e.target.value ? parseInt(e.target.value) : null)}
|
||||
placeholder={t('qualityTemplate.fields.frequencyDaysPlaceholder')}
|
||||
min="1"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Is Required */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.isRequired}
|
||||
checked={data.isRequired || false}
|
||||
onChange={(e) => handleFieldChange('isRequired', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.advancedFields.requiredCheck')}
|
||||
{t('qualityTemplate.fields.requiredCheck')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,15 +517,15 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.checkPointsJsonArray')}
|
||||
<Tooltip content={t('qualityTemplate.advancedFields.checkPointsTooltip')}>
|
||||
{t('qualityTemplate.fields.checkPointsJsonArray')}
|
||||
<Tooltip content={t('qualityTemplate.fields.checkPointsTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={data.checkPoints}
|
||||
value={data.checkPoints || ''}
|
||||
onChange={(e) => handleFieldChange('checkPoints', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')}
|
||||
placeholder={t('qualityTemplate.fields.checkPointsPlaceholder')}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||
/>
|
||||
@@ -242,12 +533,12 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.acceptanceCriteria')}
|
||||
{t('qualityTemplate.fields.acceptanceCriteria')}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.acceptanceCriteria}
|
||||
value={data.acceptanceCriteria || ''}
|
||||
onChange={(e) => handleFieldChange('acceptanceCriteria', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')}
|
||||
placeholder={t('qualityTemplate.fields.acceptanceCriteriaPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
@@ -260,54 +551,58 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
||||
{t('qualityTemplate.sections.advancedConfiguration')}
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.parametersJson')}
|
||||
<Tooltip content={t('qualityTemplate.advancedFields.parametersTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={data.parameters}
|
||||
onChange={(e) => handleFieldChange('parameters', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Parameters */}
|
||||
<KeyValueEditor
|
||||
label={t('qualityTemplate.fields.parametersJson')}
|
||||
tooltip={t('qualityTemplate.fields.parametersTooltip')}
|
||||
value={data.parameters}
|
||||
onChange={(value) => handleFieldChange('parameters', value)}
|
||||
placeholder="{}"
|
||||
suggestions={
|
||||
data.checkType === 'temperature'
|
||||
? [
|
||||
{ key: 'temp_min', value: '75', type: 'number' },
|
||||
{ key: 'temp_max', value: '85', type: 'number' },
|
||||
{ key: 'humidity', value: '65', type: 'number' }
|
||||
]
|
||||
: data.checkType === 'weight'
|
||||
? [
|
||||
{ key: 'weight_min', value: '450', type: 'number' },
|
||||
{ key: 'weight_max', value: '550', type: 'number' },
|
||||
{ key: 'unit', value: 'g', type: 'string' }
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.thresholdsJson')}
|
||||
<Tooltip content={t('qualityTemplate.advancedFields.thresholdsTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={data.thresholds}
|
||||
onChange={(e) => handleFieldChange('thresholds', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
{/* Thresholds */}
|
||||
<KeyValueEditor
|
||||
label={t('qualityTemplate.fields.thresholdsJson')}
|
||||
tooltip={t('qualityTemplate.fields.thresholdsTooltip')}
|
||||
value={data.thresholds}
|
||||
onChange={(value) => handleFieldChange('thresholds', value)}
|
||||
placeholder="{}"
|
||||
suggestions={[
|
||||
{ key: 'critical', value: '90', type: 'number' },
|
||||
{ key: 'warning', value: '70', type: 'number' },
|
||||
{ key: 'acceptable', value: '50', type: 'number' }
|
||||
]}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.scoringCriteriaJson')}
|
||||
<Tooltip content={t('qualityTemplate.advancedFields.scoringCriteriaTooltip')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={data.scoringCriteria}
|
||||
onChange={(e) => handleFieldChange('scoringCriteria', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
{/* Scoring Criteria */}
|
||||
<KeyValueEditor
|
||||
label={t('qualityTemplate.fields.scoringCriteriaJson')}
|
||||
tooltip={t('qualityTemplate.fields.scoringCriteriaTooltip')}
|
||||
value={data.scoringCriteria}
|
||||
onChange={(value) => handleFieldChange('scoringCriteria', value)}
|
||||
placeholder="{}"
|
||||
suggestions={[
|
||||
{ key: 'appearance', value: '30', type: 'number' },
|
||||
{ key: 'texture', value: '30', type: 'number' },
|
||||
{ key: 'taste', value: '40', type: 'number' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,38 +614,38 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.responsibleRole')}
|
||||
{t('qualityTemplate.fields.responsibleRole')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.responsibleRole}
|
||||
value={data.responsibleRole || ''}
|
||||
onChange={(e) => handleFieldChange('responsibleRole', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.responsibleRolePlaceholder')}
|
||||
placeholder={t('qualityTemplate.fields.responsibleRolePlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.requiredEquipment')}
|
||||
{t('qualityTemplate.fields.requiredEquipment')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.requiredEquipment}
|
||||
value={data.requiredEquipment || ''}
|
||||
onChange={(e) => handleFieldChange('requiredEquipment', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.requiredEquipmentPlaceholder')}
|
||||
placeholder={t('qualityTemplate.fields.requiredEquipmentPlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('qualityTemplate.advancedFields.specificConditions')}
|
||||
{t('qualityTemplate.fields.specificConditions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.specificConditions}
|
||||
value={data.specificConditions || ''}
|
||||
onChange={(e) => handleFieldChange('specificConditions', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')}
|
||||
placeholder={t('qualityTemplate.fields.specificConditionsPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
@@ -367,48 +662,48 @@ const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onData
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.isActive}
|
||||
checked={data.isActive !== false}
|
||||
onChange={(e) => handleFieldChange('isActive', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.advancedFields.activeTemplate')}
|
||||
{t('qualityTemplate.fields.activeTemplate')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.requiresPhoto}
|
||||
checked={data.requiresPhoto || false}
|
||||
onChange={(e) => handleFieldChange('requiresPhoto', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.advancedFields.requiresPhotoEvidence')}
|
||||
{t('qualityTemplate.fields.requiresPhotoEvidence')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.criticalControlPoint}
|
||||
onChange={(e) => handleFieldChange('criticalControlPoint', e.target.checked)}
|
||||
checked={data.isCritical || false}
|
||||
onChange={(e) => handleFieldChange('isCritical', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.advancedFields.criticalControlPoint')}
|
||||
{t('qualityTemplate.fields.criticalControlPoint')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.notifyOnFail}
|
||||
checked={data.notifyOnFail || false}
|
||||
onChange={(e) => handleFieldChange('notifyOnFail', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
{t('qualityTemplate.advancedFields.notifyOnFailure')}
|
||||
{t('qualityTemplate.fields.notifyOnFailure')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,13 +717,39 @@ export const QualityTemplateWizardSteps = (
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
setData: (data: Record<string, any>) => void
|
||||
): WizardStep[] => {
|
||||
// New architecture: return direct component references instead of arrow functions
|
||||
// dataRef and onDataChange are now passed through WizardModal props
|
||||
return [
|
||||
{
|
||||
id: 'template-details',
|
||||
title: 'qualityTemplate.advancedFields.templateDetailsTitle',
|
||||
component: QualityTemplateDetailsStep,
|
||||
id: 'check-type',
|
||||
title: 'qualityTemplate.steps.checkType',
|
||||
component: QualityCheckTypeStep,
|
||||
validate: () => {
|
||||
return !!dataRef.current.checkType;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'essential-configuration',
|
||||
title: 'qualityTemplate.steps.essentialConfiguration',
|
||||
component: EssentialConfigurationStep,
|
||||
validate: () => {
|
||||
const name = dataRef.current.name;
|
||||
const weight = dataRef.current.weight;
|
||||
return !!(
|
||||
name &&
|
||||
name.trim().length >= 1 &&
|
||||
weight !== undefined &&
|
||||
weight >= 0 &&
|
||||
weight <= 10
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'criteria-settings',
|
||||
title: 'qualityTemplate.steps.criteriaSettings',
|
||||
component: QualityCriteriaSettingsStep,
|
||||
validate: () => {
|
||||
// Optional step - all fields are optional
|
||||
return true;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||
import { ChefHat, Package, ClipboardCheck, CheckCircle2, Loader2, Plus, X, Search } from 'lucide-react';
|
||||
import { ChefHat, Package, ClipboardCheck, CheckCircle2, Loader2, Plus, X, Search, FileText } from 'lucide-react';
|
||||
import { useTenant } from '../../../../stores/tenant.store';
|
||||
import { recipesService } from '../../../../api/services/recipes';
|
||||
import { inventoryService } from '../../../../api/services/inventory';
|
||||
import { qualityTemplateService } from '../../../../api/services/qualityTemplates';
|
||||
import { IngredientResponse } from '../../../../api/types/inventory';
|
||||
import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit, RecipeQualityConfiguration, RecipeStatus } from '../../../../api/types/recipes';
|
||||
import { QualityCheckTemplateResponse } from '../../../../api/types/qualityTemplates';
|
||||
import { QualityCheckTemplate } from '../../../../api/types/qualityTemplates';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
import { RecipeInstructionsEditor } from '../../recipes/RecipeInstructionsEditor';
|
||||
import { RecipeQualityControlEditor } from '../../recipes/RecipeQualityControlEditor';
|
||||
|
||||
const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const { t } = useTranslation('wizards');
|
||||
@@ -51,7 +53,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
<p className="text-sm text-[var(--text-secondary)]">{t('recipe.recipeDetailsDescription')}</p>
|
||||
</div>
|
||||
|
||||
{/* Required Fields */}
|
||||
{/* Essential Fields Only */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
@@ -59,7 +61,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
value={data.name || ''}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
placeholder={t('recipe.fields.namePlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
@@ -72,7 +74,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
{t('recipe.fields.category')} *
|
||||
</label>
|
||||
<select
|
||||
value={data.category}
|
||||
value={data.category || 'bread'}
|
||||
onChange={(e) => handleFieldChange('category', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
@@ -95,7 +97,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={data.finishedProductId}
|
||||
value={data.finishedProductId || ''}
|
||||
onChange={(e) => handleFieldChange('finishedProductId', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
disabled={loading}
|
||||
@@ -110,12 +112,15 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||
{t('recipe.fields.yieldQuantity')} *
|
||||
<Tooltip content="How many units this recipe produces (e.g., 12 loaves)">
|
||||
<span />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.yieldQuantity}
|
||||
value={data.yieldQuantity || ''}
|
||||
onChange={(e) => handleFieldChange('yieldQuantity', e.target.value)}
|
||||
placeholder="12"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
@@ -129,7 +134,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
{t('recipe.fields.yieldUnit')} *
|
||||
</label>
|
||||
<select
|
||||
value={data.yieldUnit}
|
||||
value={data.yieldUnit || 'units'}
|
||||
onChange={(e) => handleFieldChange('yieldUnit', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
@@ -144,31 +149,21 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('recipe.fields.prepTime')}
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||
{t('recipe.fields.prepTime')} (minutes)
|
||||
<Tooltip content="Total preparation time in minutes">
|
||||
<span />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.prepTime}
|
||||
value={data.prepTime || ''}
|
||||
onChange={(e) => handleFieldChange('prepTime', e.target.value)}
|
||||
placeholder="60"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('recipe.fields.instructions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.instructions}
|
||||
onChange={(e) => handleFieldChange('instructions', e.target.value)}
|
||||
placeholder={t('recipe.fields.instructionsPlaceholder')}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Options */}
|
||||
@@ -456,13 +451,16 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
Preparation Notes
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||
Recipe Notes & Tips
|
||||
<Tooltip content="General notes, tips, or context about this recipe (not step-by-step instructions)">
|
||||
<span />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={data.preparationNotes}
|
||||
onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
|
||||
placeholder="Tips and notes for preparation..."
|
||||
placeholder="e.g., 'Works best in humid conditions', 'Can be prepared a day ahead', 'Traditional family recipe'..."
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
rows={3}
|
||||
/>
|
||||
@@ -511,12 +509,47 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
||||
);
|
||||
};
|
||||
|
||||
// New Step 2: Recipe Instructions
|
||||
const RecipeInstructionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const { t } = useTranslation('wizards');
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const handleInstructionsChange = (instructions: any) => {
|
||||
onDataChange?.({ ...data, instructions });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Recipe Instructions
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Add step-by-step instructions for preparing {data.name || 'this recipe'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RecipeInstructionsEditor
|
||||
value={data.instructions || null}
|
||||
onChange={handleInstructionsChange}
|
||||
/>
|
||||
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700">
|
||||
<strong>Tip:</strong> Break down the recipe into clear, manageable steps. Include durations to help with production planning.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SelectedIngredient {
|
||||
id: string;
|
||||
ingredientId: string;
|
||||
quantity: number;
|
||||
unit: MeasurementUnit;
|
||||
notes: string;
|
||||
preparationMethod?: string;
|
||||
isOptional?: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
@@ -555,6 +588,8 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
||||
quantity: 0,
|
||||
unit: MeasurementUnit.GRAMS,
|
||||
notes: '',
|
||||
preparationMethod: '',
|
||||
isOptional: false,
|
||||
order: (data.ingredients || []).length + 1,
|
||||
};
|
||||
onDataChange?.({ ...data, ingredients: [...(data.ingredients || []), newIngredient] });
|
||||
@@ -576,7 +611,7 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
||||
});
|
||||
};
|
||||
|
||||
const filteredIngredients = ingredients.filter(ing =>
|
||||
const filteredIngredients = ingredients.filter((ing: IngredientResponse) =>
|
||||
ing.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
@@ -584,8 +619,8 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
||||
<div className="space-y-6">
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('recipe.ingredients')}</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{data.name}</p>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Recipe Ingredients</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Add ingredients for {data.name || 'this recipe'}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -609,76 +644,109 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
||||
<p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
|
||||
</div>
|
||||
) : (
|
||||
(data.ingredients || []).map((selectedIng) => (
|
||||
(data.ingredients || []).map((selectedIng: SelectedIngredient) => (
|
||||
<div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
||||
<div className="md:col-span-5">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Ingredient *</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<div className="space-y-3">
|
||||
{/* Row 1: Ingredient, Quantity, Unit */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
||||
<div className="md:col-span-6">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Ingredient *</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<select
|
||||
value={selectedIng.ingredientId}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'ingredientId', e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{filteredIngredients.map((ing: IngredientResponse) => (
|
||||
<option key={ing.id} value={ing.id}>
|
||||
{ing.name} {ing.category ? `(${ing.category})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Quantity *</label>
|
||||
<input
|
||||
type="number"
|
||||
value={selectedIng.quantity || ''}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'quantity', parseFloat(e.target.value) || 0)}
|
||||
placeholder="0"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Unit *</label>
|
||||
<select
|
||||
value={selectedIng.ingredientId}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'ingredientId', e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
value={selectedIng.unit}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'unit', e.target.value as MeasurementUnit)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{filteredIngredients.map(ing => (
|
||||
<option key={ing.id} value={ing.id}>
|
||||
{ing.name} {ing.category ? `(${ing.category})` : ''}
|
||||
</option>
|
||||
))}
|
||||
<option value={MeasurementUnit.GRAMS}>Grams (g)</option>
|
||||
<option value={MeasurementUnit.KILOGRAMS}>Kilograms (kg)</option>
|
||||
<option value={MeasurementUnit.MILLILITERS}>Milliliters (ml)</option>
|
||||
<option value={MeasurementUnit.LITERS}>Liters (l)</option>
|
||||
<option value={MeasurementUnit.UNITS}>Units</option>
|
||||
<option value={MeasurementUnit.PIECES}>Pieces</option>
|
||||
<option value={MeasurementUnit.CUPS}>Cups</option>
|
||||
<option value={MeasurementUnit.TABLESPOONS}>Tablespoons</option>
|
||||
<option value={MeasurementUnit.TEASPOONS}>Teaspoons</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="md:col-span-1 flex items-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveIngredient(selectedIng.id)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Remove ingredient"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Quantity *</label>
|
||||
|
||||
{/* Row 2: Preparation Method and Optional Checkbox */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
||||
<div className="md:col-span-8">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Preparation Method
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedIng.preparationMethod || ''}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'preparationMethod', e.target.value)}
|
||||
placeholder="e.g., sifted, melted, chopped"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-4">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Notes</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedIng.notes}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'notes', e.target.value)}
|
||||
placeholder="Optional"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Optional Checkbox */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={selectedIng.quantity || ''}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'quantity', parseFloat(e.target.value) || 0)}
|
||||
placeholder="0"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
min="0"
|
||||
step="0.01"
|
||||
type="checkbox"
|
||||
id={`optional-${selectedIng.id}`}
|
||||
checked={selectedIng.isOptional || false}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'isOptional', e.target.checked)}
|
||||
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Unit *</label>
|
||||
<select
|
||||
value={selectedIng.unit}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'unit', e.target.value as MeasurementUnit)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
>
|
||||
<option value={MeasurementUnit.GRAMS}>Grams (g)</option>
|
||||
<option value={MeasurementUnit.KILOGRAMS}>Kilograms (kg)</option>
|
||||
<option value={MeasurementUnit.MILLILITERS}>Milliliters (ml)</option>
|
||||
<option value={MeasurementUnit.LITERS}>Liters (l)</option>
|
||||
<option value={MeasurementUnit.UNITS}>Units</option>
|
||||
<option value={MeasurementUnit.PIECES}>Pieces</option>
|
||||
<option value={MeasurementUnit.CUPS}>Cups</option>
|
||||
<option value={MeasurementUnit.TABLESPOONS}>Tablespoons</option>
|
||||
<option value={MeasurementUnit.TEASPOONS}>Teaspoons</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Notes</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedIng.notes}
|
||||
onChange={(e) => handleUpdateIngredient(selectedIng.id, 'notes', e.target.value)}
|
||||
placeholder="Optional"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-1 flex items-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveIngredient(selectedIng.id)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Remove ingredient"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<label htmlFor={`optional-${selectedIng.id}`} className="text-xs text-[var(--text-secondary)]">
|
||||
This ingredient is optional
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -700,11 +768,12 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
||||
);
|
||||
};
|
||||
|
||||
const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
||||
// New Step 4: Enhanced Quality Control Configuration
|
||||
const QualityControlStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
||||
const { t } = useTranslation('wizards');
|
||||
const data = dataRef?.current || {};
|
||||
const { currentTenant } = useTenant();
|
||||
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
|
||||
const [templates, setTemplates] = useState<QualityCheckTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -727,12 +796,8 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTemplate = (templateId: string) => {
|
||||
const currentTemplates = data.selectedTemplates || [];
|
||||
const newTemplates = currentTemplates.includes(templateId)
|
||||
? currentTemplates.filter(id => id !== templateId)
|
||||
: [...currentTemplates, templateId];
|
||||
onDataChange?.({ ...data, selectedTemplates: newTemplates });
|
||||
const handleQualityConfigChange = (config: any) => {
|
||||
onDataChange?.({ ...data, qualityConfiguration: config });
|
||||
};
|
||||
|
||||
const handleCreateRecipe = async () => {
|
||||
@@ -750,28 +815,13 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
||||
quantity: ing.quantity,
|
||||
unit: ing.unit,
|
||||
ingredient_notes: ing.notes || null,
|
||||
is_optional: false,
|
||||
preparation_method: ing.preparationMethod || null,
|
||||
is_optional: ing.isOptional || false,
|
||||
ingredient_order: index + 1,
|
||||
}));
|
||||
|
||||
let qualityConfig: RecipeQualityConfiguration | undefined;
|
||||
if ((data.selectedTemplates || []).length > 0) {
|
||||
qualityConfig = {
|
||||
stages: {
|
||||
production: {
|
||||
template_ids: data.selectedTemplates || [],
|
||||
required_checks: [],
|
||||
optional_checks: [],
|
||||
blocking_on_failure: true,
|
||||
min_quality_score: 7.0,
|
||||
}
|
||||
},
|
||||
overall_quality_threshold: 7.0,
|
||||
critical_stage_blocking: true,
|
||||
auto_create_quality_checks: true,
|
||||
quality_manager_approval_required: false,
|
||||
};
|
||||
}
|
||||
// Use the quality configuration from the editor if available
|
||||
const qualityConfig: RecipeQualityConfiguration | undefined = data.qualityConfiguration;
|
||||
|
||||
const recipeData: RecipeCreate = {
|
||||
name: data.name,
|
||||
@@ -800,7 +850,7 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
||||
is_signature_item: data.isSignatureItem || false,
|
||||
season_start_month: data.seasonStartMonth ? parseInt(data.seasonStartMonth) : null,
|
||||
season_end_month: data.seasonEndMonth ? parseInt(data.seasonEndMonth) : null,
|
||||
instructions: data.instructions ? { steps: data.instructions } : null,
|
||||
instructions: data.instructions || null,
|
||||
allergen_info: data.allergens ? data.allergens.split(',').map((a: string) => a.trim()) : null,
|
||||
dietary_tags: data.dietaryTags ? data.dietaryTags.split(',').map((t: string) => t.trim()) : null,
|
||||
ingredients: recipeIngredients,
|
||||
@@ -820,15 +870,22 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
||||
}
|
||||
};
|
||||
|
||||
// Format templates for the editor
|
||||
const formattedTemplates = templates.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
category: t.check_type || 'general'
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Quality Templates (Optional)
|
||||
Quality Control Configuration (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Select quality control templates to apply to this recipe
|
||||
Configure quality control checks for each production stage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -843,67 +900,25 @@ const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
||||
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
||||
<span className="ml-3 text-[var(--text-secondary)]">Loading templates...</span>
|
||||
</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
||||
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
|
||||
<p className="text-[var(--text-secondary)] mb-2">No quality templates available</p>
|
||||
<p className="text-sm text-[var(--text-tertiary)]">
|
||||
You can create templates from the Database menu
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{templates.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
||||
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
|
||||
<p className="text-[var(--text-secondary)] mb-2">No quality templates available</p>
|
||||
<p className="text-sm text-[var(--text-tertiary)]">
|
||||
You can create templates from the main wizard
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{templates.map((template) => (
|
||||
<button
|
||||
key={template.id}
|
||||
type="button"
|
||||
onClick={() => toggleTemplate(template.id)}
|
||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
||||
(data.selectedTemplates || []).includes(template.id)
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-semibold text-[var(--text-primary)]">{template.name}</h4>
|
||||
{template.is_required && (
|
||||
<span className="px-2 py-0.5 text-xs bg-orange-100 text-orange-700 rounded-full">
|
||||
Required
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{template.description && (
|
||||
<p className="text-sm text-[var(--text-secondary)] line-clamp-2">
|
||||
{template.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-[var(--text-tertiary)]">
|
||||
<span>Type: {template.check_type}</span>
|
||||
{template.frequency_days && (
|
||||
<span>• Every {template.frequency_days} days</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(data.selectedTemplates || []).includes(template.id) && (
|
||||
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 ml-3" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<RecipeQualityControlEditor
|
||||
value={data.qualityConfiguration || null}
|
||||
onChange={handleQualityConfigChange}
|
||||
allTemplates={formattedTemplates}
|
||||
/>
|
||||
|
||||
{(data.selectedTemplates || []).length > 0 && (
|
||||
<div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
|
||||
<p className="text-sm text-[var(--text-primary)]">
|
||||
<strong>{(data.selectedTemplates || []).length}</strong> template(s) selected
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700">
|
||||
<strong>Tip:</strong> Configure which quality checks are required at each production stage. You can make certain stages blocking to halt production if quality checks fail.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -940,18 +955,62 @@ export const RecipeWizardSteps = (dataRef: React.MutableRefObject<Record<string,
|
||||
title: 'wizards:recipe.steps.recipeDetails',
|
||||
description: 'wizards:recipe.steps.recipeDetailsDescription',
|
||||
component: RecipeDetailsStep,
|
||||
validate: () => {
|
||||
const data = dataRef.current;
|
||||
// Validate required fields
|
||||
if (!data.name || data.name.trim().length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (!data.category) {
|
||||
return false;
|
||||
}
|
||||
if (!data.finishedProductId) {
|
||||
return false;
|
||||
}
|
||||
if (!data.yieldQuantity || parseFloat(data.yieldQuantity) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!data.yieldUnit) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recipe-instructions',
|
||||
title: 'Recipe Instructions',
|
||||
description: 'Add step-by-step preparation instructions',
|
||||
component: RecipeInstructionsStep,
|
||||
isOptional: true,
|
||||
},
|
||||
{
|
||||
id: 'recipe-ingredients',
|
||||
title: 'wizards:recipe.steps.ingredients',
|
||||
description: 'wizards:recipe.steps.ingredientsDescription',
|
||||
component: IngredientsStep,
|
||||
validate: () => {
|
||||
const data = dataRef.current;
|
||||
const ingredients = data.ingredients || [];
|
||||
|
||||
// Must have at least one ingredient
|
||||
if (ingredients.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Each ingredient must have required fields
|
||||
return ingredients.every((ing: any) =>
|
||||
ing.ingredientId &&
|
||||
ing.quantity &&
|
||||
parseFloat(ing.quantity) > 0 &&
|
||||
ing.unit
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recipe-quality-templates',
|
||||
title: 'wizards:recipe.steps.qualityTemplates',
|
||||
description: 'wizards:recipe.steps.qualityTemplatesDescription',
|
||||
component: QualityTemplatesStep,
|
||||
id: 'recipe-quality-control',
|
||||
title: 'Quality Control',
|
||||
description: 'Configure quality checks for production stages',
|
||||
component: QualityControlStep,
|
||||
isOptional: true,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user