Add improved production UI 3

This commit is contained in:
Urtzi Alfaro
2025-09-23 19:24:22 +02:00
parent 7f871fc933
commit 7892c5a739
47 changed files with 6211 additions and 267 deletions

View File

@@ -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>
);
};