Add improved production UI 3
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user