479 lines
15 KiB
TypeScript
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;
|