Files
bakery-ia/frontend/src/components/domain/production/EditQualityTemplateModal.tsx
2025-10-27 16:33:26 +01:00

479 lines
15 KiB
TypeScript

import React, { useState, useEffect } from 'react';
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;
onClose: () => void;
template: QualityCheckTemplate;
onUpdateTemplate: (templateData: QualityCheckTemplateUpdate) => Promise<void>;
isLoading?: boolean;
}
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 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 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,
template,
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 || []
);
// Helper function to get translated category label
const getCategoryLabel = (category: string | null | undefined): string => {
if (!category) return t('production.quality.categories.appearance', '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;
};
// 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);
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
}));
}
};
const handleSave = async () => {
try {
// Build update object with only changed fields
const updates: QualityCheckTemplateUpdate = {};
// Check each field for changes
Object.entries(editedTemplate).forEach(([key, value]) => {
const originalValue = (template as any)[key];
if (value !== originalValue && key !== 'id' && key !== 'created_at' && key !== 'updated_at' && key !== 'created_by') {
(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);
}
setMode('view');
} catch (error) {
console.error('Error updating template:', error);
throw error;
}
};
const handleCancel = () => {
// Reset to original values
setEditedTemplate(template);
setSelectedStages(template.applicable_stages || []);
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 (
<EditViewModal
isOpen={isOpen}
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"
loading={isLoading}
showDefaultActions={true}
onSave={handleSave}
onCancel={handleCancel}
onFieldChange={handleFieldChange}
mobileOptimized={true}
/>
);
};
export default EditQualityTemplateModal;