Improve the frontend and fix TODOs

This commit is contained in:
Urtzi Alfaro
2025-10-24 13:05:04 +02:00
parent 07c33fa578
commit 61376b7a9f
100 changed files with 8284 additions and 3419 deletions

View File

@@ -1,20 +1,14 @@
import React, { useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import {
Modal,
Button,
Input,
Textarea,
Select,
Badge,
Card
} from '../../ui';
import { Edit, ClipboardCheck, Target, Settings, Cog } from 'lucide-react';
import { EditViewModal, EditViewModalSection } from '../../ui/EditViewModal/EditViewModal';
import {
QualityCheckType,
ProcessStage,
type QualityCheckTemplate,
type QualityCheckTemplateUpdate
} from '../../../api/types/qualityTemplates';
import { statusColors } from '../../../styles/colors';
import { useTranslation } from 'react-i18next';
interface EditQualityTemplateModalProps {
isOpen: boolean;
@@ -43,18 +37,58 @@ const PROCESS_STAGE_OPTIONS = [
{ 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' }
const CATEGORY_OPTIONS_KEYS = [
{ value: '', key: '' },
{ value: 'appearance', key: 'appearance' },
{ value: 'structure', key: 'structure' },
{ value: 'texture', key: 'texture' },
{ value: 'flavor', key: 'flavor' },
{ value: 'safety', key: 'safety' },
{ value: 'packaging', key: 'packaging' },
{ value: 'temperature', key: 'temperature' },
{ value: 'weight', key: 'weight' },
{ value: 'dimensions', key: 'dimensions' },
{ value: 'weight_check', key: 'weight_check' },
{ value: 'temperature_check', key: 'temperature_check' },
{ value: 'moisture_check', key: 'moisture_check' },
{ value: 'volume_check', key: 'volume_check' },
{ value: 'time_check', key: 'time_check' },
{ value: 'chemical', key: 'chemical' },
{ value: 'hygiene', key: 'hygiene' }
];
// Component for managing process stages selection (multiselect)
const ProcessStagesSelector: React.FC<{
value: ProcessStage[];
onChange: (stages: ProcessStage[]) => void;
}> = ({ value, onChange }) => {
const handleToggle = (stage: ProcessStage) => {
const newStages = value.includes(stage)
? value.filter(s => s !== stage)
: [...value, stage];
onChange(newStages);
};
return (
<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)] transition-colors"
>
<input
type="checkbox"
checked={value.includes(stage.value)}
onChange={() => handleToggle(stage.value)}
className="rounded border-[var(--border-primary)]"
/>
<span className="text-sm">{stage.label}</span>
</label>
))}
</div>
);
};
export const EditQualityTemplateModal: React.FC<EditQualityTemplateModalProps> = ({
isOpen,
onClose,
@@ -62,84 +96,99 @@ export const EditQualityTemplateModal: React.FC<EditQualityTemplateModalProps> =
onUpdateTemplate,
isLoading = false
}) => {
const { t } = useTranslation();
const [mode, setMode] = useState<'view' | 'edit'>('edit');
const [editedTemplate, setEditedTemplate] = useState<QualityCheckTemplate>(template);
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
}
});
// Helper function to get translated category label
const getCategoryLabel = (category: string | null | undefined): string => {
if (!category) return 'Sin categoría';
const translationKey = `production.quality.categories.${category}`;
const translated = t(translationKey);
// If translation is same as key, it means no translation exists, return the original
return translated === translationKey ? category : translated;
};
const checkType = watch('check_type');
// Build category options with translations
const getCategoryOptions = () => {
return CATEGORY_OPTIONS_KEYS.map(option => ({
value: option.value,
label: option.value === '' ? 'Seleccionar categoría' : getCategoryLabel(option.key)
}));
};
// Update local state when template changes
useEffect(() => {
if (template) {
setEditedTemplate(template);
setSelectedStages(template.applicable_stages || []);
}
}, [template]);
const checkType = editedTemplate.check_type;
const showMeasurementFields = [
QualityCheckType.MEASUREMENT,
QualityCheckType.TEMPERATURE,
QualityCheckType.WEIGHT
].includes(checkType || template.check_type);
].includes(checkType);
// 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 || []);
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {
const sections = getSections();
const field = sections[sectionIndex].fields[fieldIndex];
const fieldLabel = field.label;
// Map field labels to template properties
const fieldMap: Record<string, keyof QualityCheckTemplate | 'stages'> = {
'Nombre': 'name',
'Código de Plantilla': 'template_code',
'Tipo de Control': 'check_type',
'Categoría': 'category',
'Descripción': 'description',
'Instrucciones para el Personal': 'instructions',
'Valor Mínimo': 'min_value',
'Valor Máximo': 'max_value',
'Valor Objetivo': 'target_value',
'Unidad': 'unit',
'Tolerancia (%)': 'tolerance_percentage',
'Etapas Aplicables': 'stages',
'Peso en Puntuación General': 'weight',
'Plantilla Activa': 'is_active',
'Control Requerido': 'is_required',
'Control Crítico': 'is_critical'
};
const propertyName = fieldMap[fieldLabel];
if (propertyName) {
if (propertyName === 'stages') {
// Handle stages separately through the custom component
return;
}
// Convert string booleans to actual booleans
let processedValue: any = value;
if (propertyName === 'is_active' || propertyName === 'is_required' || propertyName === 'is_critical') {
processedValue = String(value) === 'true';
}
setEditedTemplate(prev => ({
...prev,
[propertyName]: processedValue
}));
}
}, [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) => {
const handleSave = async () => {
try {
// Only include changed fields
// Build update object with only changed fields
const updates: QualityCheckTemplateUpdate = {};
Object.entries(data).forEach(([key, value]) => {
// Check each field for changes
Object.entries(editedTemplate).forEach(([key, value]) => {
const originalValue = (template as any)[key];
if (value !== originalValue) {
if (value !== originalValue && key !== 'id' && key !== 'created_at' && key !== 'updated_at' && key !== 'created_by') {
(updates as any)[key] = value;
}
});
@@ -155,309 +204,275 @@ export const EditQualityTemplateModal: React.FC<EditQualityTemplateModalProps> =
// Only submit if there are actual changes
if (Object.keys(updates).length > 0) {
await onUpdateTemplate(updates);
} else {
onClose();
}
setMode('view');
} catch (error) {
console.error('Error updating template:', error);
throw error;
}
};
const handleClose = () => {
reset();
const handleCancel = () => {
// Reset to original values
setEditedTemplate(template);
setSelectedStages(template.applicable_stages || []);
onClose();
setMode('view');
};
const getSections = (): EditViewModalSection[] => {
const sections: EditViewModalSection[] = [
{
title: 'Información Básica',
icon: ClipboardCheck,
fields: [
{
label: 'Nombre',
value: editedTemplate.name,
type: 'text',
highlight: true,
editable: true,
required: true,
placeholder: 'Ej: Control Visual de Pan'
},
{
label: 'Código de Plantilla',
value: editedTemplate.template_code || '',
type: 'text',
editable: true,
placeholder: 'Ej: CV_PAN_01'
},
{
label: 'Tipo de Control',
value: editedTemplate.check_type,
type: 'select',
editable: true,
required: true,
options: QUALITY_CHECK_TYPE_OPTIONS
},
{
label: 'Categoría',
value: editedTemplate.category || '',
type: 'select',
editable: true,
options: getCategoryOptions()
},
{
label: 'Descripción',
value: editedTemplate.description || '',
type: 'textarea',
editable: true,
placeholder: 'Describe qué evalúa esta plantilla de calidad',
span: 2
},
{
label: 'Instrucciones para el Personal',
value: editedTemplate.instructions || '',
type: 'textarea',
editable: true,
placeholder: 'Instrucciones detalladas para realizar este control de calidad',
span: 2,
helpText: 'Pasos específicos que debe seguir el operario'
}
]
}
];
// Add measurement configuration section if applicable
if (showMeasurementFields) {
sections.push({
title: 'Configuración de Medición',
icon: Target,
fields: [
{
label: 'Valor Mínimo',
value: editedTemplate.min_value || 0,
type: 'number',
editable: true,
placeholder: '0',
helpText: 'Valor mínimo aceptable para la medición'
},
{
label: 'Valor Máximo',
value: editedTemplate.max_value || 0,
type: 'number',
editable: true,
placeholder: '100',
helpText: 'Valor máximo aceptable para la medición'
},
{
label: 'Valor Objetivo',
value: editedTemplate.target_value || 0,
type: 'number',
editable: true,
placeholder: '50',
helpText: 'Valor ideal que se busca alcanzar'
},
{
label: 'Unidad',
value: editedTemplate.unit || '',
type: 'text',
editable: true,
placeholder: '°C / g / cm',
helpText: 'Unidad de medida (ej: °C para temperatura)'
},
{
label: 'Tolerancia (%)',
value: editedTemplate.tolerance_percentage || 0,
type: 'number',
editable: true,
placeholder: '5',
helpText: 'Porcentaje de tolerancia permitido'
}
]
});
}
// Process stages section with custom component
sections.push({
title: 'Etapas del Proceso',
icon: Settings,
description: 'Selecciona las etapas donde se debe aplicar este control. Si no seleccionas ninguna, se aplicará a todas las etapas.',
fields: [
{
label: 'Etapas Aplicables',
value: selectedStages.length > 0
? selectedStages.map(s => PROCESS_STAGE_OPTIONS.find(opt => opt.value === s)?.label || s).join(', ')
: 'Todas las etapas',
type: 'component',
editable: true,
component: ProcessStagesSelector,
componentProps: {
value: selectedStages,
onChange: setSelectedStages
},
span: 2
}
]
});
// Configuration section
sections.push({
title: 'Configuración Avanzada',
icon: Cog,
fields: [
{
label: 'Peso en Puntuación General',
value: editedTemplate.weight,
type: 'number',
editable: true,
placeholder: '1.0',
helpText: 'Mayor peso = mayor importancia en la puntuación final (0-10)',
validation: (value: string | number) => {
const num = Number(value);
return num < 0 || num > 10 ? 'El peso debe estar entre 0 y 10' : null;
}
},
{
label: 'Plantilla Activa',
value: editedTemplate.is_active,
type: 'select',
editable: true,
options: [
{ value: 'true', label: 'Sí' },
{ value: 'false', label: 'No' }
]
},
{
label: 'Control Requerido',
value: editedTemplate.is_required,
type: 'select',
editable: true,
options: [
{ value: 'true', label: 'Sí' },
{ value: 'false', label: 'No' }
],
helpText: 'Si es requerido, debe completarse obligatoriamente'
},
{
label: 'Control Crítico',
value: editedTemplate.is_critical,
type: 'select',
editable: true,
options: [
{ value: 'true', label: 'Sí' },
{ value: 'false', label: 'No' }
],
helpText: 'Si es crítico, bloquea la producción si falla'
}
]
});
// Template metadata section
sections.push({
title: 'Información de la Plantilla',
icon: ClipboardCheck,
collapsible: true,
collapsed: true,
fields: [
{
label: 'ID',
value: editedTemplate.id,
type: 'text',
editable: false
},
{
label: 'Creado',
value: editedTemplate.created_at,
type: 'datetime',
editable: false
},
{
label: 'Última Actualización',
value: editedTemplate.updated_at,
type: 'datetime',
editable: false
}
]
});
return sections;
};
const getStatusIndicator = () => {
const typeColors: Record<QualityCheckType, string> = {
[QualityCheckType.VISUAL]: '#3B82F6',
[QualityCheckType.MEASUREMENT]: '#10B981',
[QualityCheckType.TEMPERATURE]: '#EF4444',
[QualityCheckType.WEIGHT]: '#A855F7',
[QualityCheckType.BOOLEAN]: '#6B7280',
[QualityCheckType.TIMING]: '#F59E0B',
[QualityCheckType.CHECKLIST]: '#6366F1'
};
return {
color: editedTemplate.is_active ? typeColors[editedTemplate.check_type] : '#6B7280',
text: editedTemplate.is_active ? 'Activa' : 'Inactiva',
icon: Edit,
isCritical: editedTemplate.is_critical,
isHighlight: editedTemplate.is_required
};
};
return (
<Modal
<EditViewModal
isOpen={isOpen}
onClose={handleClose}
title={`Editar Plantilla: ${template.name}`}
onClose={onClose}
mode={mode}
onModeChange={setMode}
title={editedTemplate.name}
subtitle={`${getCategoryLabel(editedTemplate.category)}${editedTemplate.template_code || 'Sin código'}`}
statusIndicator={getStatusIndicator()}
sections={getSections()}
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>
loading={isLoading}
showDefaultActions={true}
onSave={handleSave}
onCancel={handleCancel}
onFieldChange={handleFieldChange}
mobileOptimized={true}
/>
);
};
};
export default EditQualityTemplateModal;