463 lines
15 KiB
TypeScript
463 lines
15 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import { useForm, Controller } from 'react-hook-form';
|
||
|
|
import {
|
||
|
|
Modal,
|
||
|
|
Button,
|
||
|
|
Input,
|
||
|
|
Textarea,
|
||
|
|
Select,
|
||
|
|
Badge,
|
||
|
|
Card
|
||
|
|
} from '../../ui';
|
||
|
|
import {
|
||
|
|
QualityCheckType,
|
||
|
|
ProcessStage,
|
||
|
|
type QualityCheckTemplate,
|
||
|
|
type QualityCheckTemplateUpdate
|
||
|
|
} from '../../../api/types/qualityTemplates';
|
||
|
|
|
||
|
|
interface EditQualityTemplateModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
template: QualityCheckTemplate;
|
||
|
|
onUpdateTemplate: (templateData: QualityCheckTemplateUpdate) => Promise<void>;
|
||
|
|
isLoading?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QUALITY_CHECK_TYPE_OPTIONS = [
|
||
|
|
{ value: QualityCheckType.VISUAL, label: 'Visual - Inspección visual' },
|
||
|
|
{ value: QualityCheckType.MEASUREMENT, label: 'Medición - Medidas precisas' },
|
||
|
|
{ value: QualityCheckType.TEMPERATURE, label: 'Temperatura - Control térmico' },
|
||
|
|
{ value: QualityCheckType.WEIGHT, label: 'Peso - Control de peso' },
|
||
|
|
{ value: QualityCheckType.BOOLEAN, label: 'Sí/No - Verificación binaria' },
|
||
|
|
{ value: QualityCheckType.TIMING, label: 'Tiempo - Control temporal' }
|
||
|
|
];
|
||
|
|
|
||
|
|
const PROCESS_STAGE_OPTIONS = [
|
||
|
|
{ value: ProcessStage.MIXING, label: 'Mezclado' },
|
||
|
|
{ value: ProcessStage.PROOFING, label: 'Fermentación' },
|
||
|
|
{ value: ProcessStage.SHAPING, label: 'Formado' },
|
||
|
|
{ value: ProcessStage.BAKING, label: 'Horneado' },
|
||
|
|
{ value: ProcessStage.COOLING, label: 'Enfriado' },
|
||
|
|
{ value: ProcessStage.PACKAGING, label: 'Empaquetado' },
|
||
|
|
{ value: ProcessStage.FINISHING, label: 'Acabado' }
|
||
|
|
];
|
||
|
|
|
||
|
|
const CATEGORY_OPTIONS = [
|
||
|
|
{ value: 'appearance', label: 'Apariencia' },
|
||
|
|
{ value: 'structure', label: 'Estructura' },
|
||
|
|
{ value: 'texture', label: 'Textura' },
|
||
|
|
{ value: 'flavor', label: 'Sabor' },
|
||
|
|
{ value: 'safety', label: 'Seguridad' },
|
||
|
|
{ value: 'packaging', label: 'Empaque' },
|
||
|
|
{ value: 'temperature', label: 'Temperatura' },
|
||
|
|
{ value: 'weight', label: 'Peso' },
|
||
|
|
{ value: 'dimensions', label: 'Dimensiones' }
|
||
|
|
];
|
||
|
|
|
||
|
|
export const EditQualityTemplateModal: React.FC<EditQualityTemplateModalProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
template,
|
||
|
|
onUpdateTemplate,
|
||
|
|
isLoading = false
|
||
|
|
}) => {
|
||
|
|
const [selectedStages, setSelectedStages] = useState<ProcessStage[]>(
|
||
|
|
template.applicable_stages || []
|
||
|
|
);
|
||
|
|
|
||
|
|
const {
|
||
|
|
register,
|
||
|
|
control,
|
||
|
|
handleSubmit,
|
||
|
|
watch,
|
||
|
|
reset,
|
||
|
|
formState: { errors, isDirty }
|
||
|
|
} = useForm<QualityCheckTemplateUpdate>({
|
||
|
|
defaultValues: {
|
||
|
|
name: template.name,
|
||
|
|
template_code: template.template_code || '',
|
||
|
|
check_type: template.check_type,
|
||
|
|
category: template.category || '',
|
||
|
|
description: template.description || '',
|
||
|
|
instructions: template.instructions || '',
|
||
|
|
is_active: template.is_active,
|
||
|
|
is_required: template.is_required,
|
||
|
|
is_critical: template.is_critical,
|
||
|
|
weight: template.weight,
|
||
|
|
min_value: template.min_value,
|
||
|
|
max_value: template.max_value,
|
||
|
|
target_value: template.target_value,
|
||
|
|
unit: template.unit || '',
|
||
|
|
tolerance_percentage: template.tolerance_percentage
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const checkType = watch('check_type');
|
||
|
|
const showMeasurementFields = [
|
||
|
|
QualityCheckType.MEASUREMENT,
|
||
|
|
QualityCheckType.TEMPERATURE,
|
||
|
|
QualityCheckType.WEIGHT
|
||
|
|
].includes(checkType || template.check_type);
|
||
|
|
|
||
|
|
// Update form when template changes
|
||
|
|
useEffect(() => {
|
||
|
|
if (template) {
|
||
|
|
reset({
|
||
|
|
name: template.name,
|
||
|
|
template_code: template.template_code || '',
|
||
|
|
check_type: template.check_type,
|
||
|
|
category: template.category || '',
|
||
|
|
description: template.description || '',
|
||
|
|
instructions: template.instructions || '',
|
||
|
|
is_active: template.is_active,
|
||
|
|
is_required: template.is_required,
|
||
|
|
is_critical: template.is_critical,
|
||
|
|
weight: template.weight,
|
||
|
|
min_value: template.min_value,
|
||
|
|
max_value: template.max_value,
|
||
|
|
target_value: template.target_value,
|
||
|
|
unit: template.unit || '',
|
||
|
|
tolerance_percentage: template.tolerance_percentage
|
||
|
|
});
|
||
|
|
setSelectedStages(template.applicable_stages || []);
|
||
|
|
}
|
||
|
|
}, [template, reset]);
|
||
|
|
|
||
|
|
const handleStageToggle = (stage: ProcessStage) => {
|
||
|
|
const newStages = selectedStages.includes(stage)
|
||
|
|
? selectedStages.filter(s => s !== stage)
|
||
|
|
: [...selectedStages, stage];
|
||
|
|
|
||
|
|
setSelectedStages(newStages);
|
||
|
|
};
|
||
|
|
|
||
|
|
const onSubmit = async (data: QualityCheckTemplateUpdate) => {
|
||
|
|
try {
|
||
|
|
// Only include changed fields
|
||
|
|
const updates: QualityCheckTemplateUpdate = {};
|
||
|
|
|
||
|
|
Object.entries(data).forEach(([key, value]) => {
|
||
|
|
const originalValue = (template as any)[key];
|
||
|
|
if (value !== originalValue) {
|
||
|
|
(updates as any)[key] = value;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Handle applicable stages
|
||
|
|
const stagesChanged = JSON.stringify(selectedStages.sort()) !==
|
||
|
|
JSON.stringify((template.applicable_stages || []).sort());
|
||
|
|
|
||
|
|
if (stagesChanged) {
|
||
|
|
updates.applicable_stages = selectedStages.length > 0 ? selectedStages : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only submit if there are actual changes
|
||
|
|
if (Object.keys(updates).length > 0) {
|
||
|
|
await onUpdateTemplate(updates);
|
||
|
|
} else {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating template:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClose = () => {
|
||
|
|
reset();
|
||
|
|
setSelectedStages(template.applicable_stages || []);
|
||
|
|
onClose();
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Modal
|
||
|
|
isOpen={isOpen}
|
||
|
|
onClose={handleClose}
|
||
|
|
title={`Editar Plantilla: ${template.name}`}
|
||
|
|
size="xl"
|
||
|
|
>
|
||
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
|
|
{/* Basic Information */}
|
||
|
|
<Card className="p-4">
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] mb-4">
|
||
|
|
Información Básica
|
||
|
|
</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Nombre *
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
{...register('name', { required: 'El nombre es requerido' })}
|
||
|
|
placeholder="Ej: Control Visual de Pan"
|
||
|
|
error={errors.name?.message}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Código de Plantilla
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
{...register('template_code')}
|
||
|
|
placeholder="Ej: CV_PAN_01"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Tipo de Control *
|
||
|
|
</label>
|
||
|
|
<Controller
|
||
|
|
name="check_type"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<Select {...field} options={QUALITY_CHECK_TYPE_OPTIONS.map(opt => ({ value: opt.value, label: opt.label }))}>
|
||
|
|
{QUALITY_CHECK_TYPE_OPTIONS.map(option => (
|
||
|
|
<option key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Categoría
|
||
|
|
</label>
|
||
|
|
<Controller
|
||
|
|
name="category"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<Select {...field} options={[{ value: '', label: 'Seleccionar categoría' }, ...CATEGORY_OPTIONS]}>
|
||
|
|
<option value="">Seleccionar categoría</option>
|
||
|
|
{CATEGORY_OPTIONS.map(option => (
|
||
|
|
<option key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-4">
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Descripción
|
||
|
|
</label>
|
||
|
|
<Textarea
|
||
|
|
{...register('description')}
|
||
|
|
placeholder="Describe qué evalúa esta plantilla de calidad"
|
||
|
|
rows={2}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-4">
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Instrucciones para el Personal
|
||
|
|
</label>
|
||
|
|
<Textarea
|
||
|
|
{...register('instructions')}
|
||
|
|
placeholder="Instrucciones detalladas para realizar este control de calidad"
|
||
|
|
rows={3}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Measurement Configuration */}
|
||
|
|
{showMeasurementFields && (
|
||
|
|
<Card className="p-4">
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] mb-4">
|
||
|
|
Configuración de Medición
|
||
|
|
</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Valor Mínimo
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
{...register('min_value', { valueAsNumber: true })}
|
||
|
|
placeholder="0"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Valor Máximo
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
{...register('max_value', { valueAsNumber: true })}
|
||
|
|
placeholder="100"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Valor Objetivo
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
{...register('target_value', { valueAsNumber: true })}
|
||
|
|
placeholder="50"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Unidad
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
{...register('unit')}
|
||
|
|
placeholder={
|
||
|
|
checkType === QualityCheckType.TEMPERATURE ? '°C' :
|
||
|
|
checkType === QualityCheckType.WEIGHT ? 'g' : 'unidad'
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-4">
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Tolerancia (%)
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
step="0.1"
|
||
|
|
{...register('tolerance_percentage', { valueAsNumber: true })}
|
||
|
|
placeholder="5"
|
||
|
|
className="w-32"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Process Stages */}
|
||
|
|
<Card className="p-4">
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] mb-4">
|
||
|
|
Etapas del Proceso Aplicables
|
||
|
|
</h4>
|
||
|
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||
|
|
Selecciona las etapas donde se debe aplicar este control. Si no seleccionas ninguna, se aplicará a todas las etapas.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||
|
|
{PROCESS_STAGE_OPTIONS.map(stage => (
|
||
|
|
<label
|
||
|
|
key={stage.value}
|
||
|
|
className="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-[var(--bg-secondary)]"
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={selectedStages.includes(stage.value)}
|
||
|
|
onChange={() => handleStageToggle(stage.value)}
|
||
|
|
className="rounded border-[var(--border-primary)]"
|
||
|
|
/>
|
||
|
|
<span className="text-sm">{stage.label}</span>
|
||
|
|
</label>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Settings */}
|
||
|
|
<Card className="p-4">
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] mb-4">
|
||
|
|
Configuración
|
||
|
|
</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||
|
|
Peso en Puntuación General
|
||
|
|
</label>
|
||
|
|
<Input
|
||
|
|
type="number"
|
||
|
|
step="0.1"
|
||
|
|
min="0"
|
||
|
|
max="10"
|
||
|
|
{...register('weight', { valueAsNumber: true })}
|
||
|
|
placeholder="1.0"
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||
|
|
Mayor peso = mayor importancia en la puntuación final
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
<label className="flex items-center space-x-2">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
{...register('is_active')}
|
||
|
|
className="rounded border-[var(--border-primary)]"
|
||
|
|
/>
|
||
|
|
<span className="text-sm">Plantilla activa</span>
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<label className="flex items-center space-x-2">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
{...register('is_required')}
|
||
|
|
className="rounded border-[var(--border-primary)]"
|
||
|
|
/>
|
||
|
|
<span className="text-sm">Control requerido</span>
|
||
|
|
</label>
|
||
|
|
|
||
|
|
<label className="flex items-center space-x-2">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
{...register('is_critical')}
|
||
|
|
className="rounded border-[var(--border-primary)]"
|
||
|
|
/>
|
||
|
|
<span className="text-sm">Control crítico</span>
|
||
|
|
<Badge variant="error" size="sm">
|
||
|
|
Bloquea producción si falla
|
||
|
|
</Badge>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Template Info */}
|
||
|
|
<Card className="p-4 bg-[var(--bg-secondary)]">
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] mb-2">
|
||
|
|
Información de la Plantilla
|
||
|
|
</h4>
|
||
|
|
<div className="text-sm text-[var(--text-secondary)] space-y-1">
|
||
|
|
<p>ID: {template.id}</p>
|
||
|
|
<p>Creado: {new Date(template.created_at).toLocaleDateString('es-ES')}</p>
|
||
|
|
<p>Última actualización: {new Date(template.updated_at).toLocaleDateString('es-ES')}</p>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex justify-end space-x-3 pt-4 border-t border-[var(--border-primary)]">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={handleClose}
|
||
|
|
disabled={isLoading}
|
||
|
|
>
|
||
|
|
Cancelar
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
variant="primary"
|
||
|
|
disabled={!isDirty || isLoading}
|
||
|
|
isLoading={isLoading}
|
||
|
|
>
|
||
|
|
Guardar Cambios
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
};
|