Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -1,12 +1,14 @@
import React, { useState, useEffect } from 'react';
import { Plus, Settings, ClipboardCheck, Target, Cog } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
import { Select } from '../../ui/Select';
import {
QualityCheckType,
ProcessStage,
type QualityCheckTemplateCreate
} from '../../../api/types/qualityTemplates';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useAuthUser } from '../../../stores/auth.store';
import { useQuery } from '@tanstack/react-query';
import { recipesService } from '../../../api/services/recipes';
import type { RecipeResponse } from '../../../api/types/recipes';
@@ -33,7 +35,8 @@ const QUALITY_CHECK_TYPE_OPTIONS = [
{ 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' }
{ value: QualityCheckType.TIMING, label: 'Tiempo - Control temporal' },
{ value: QualityCheckType.CHECKLIST, label: 'Checklist - Lista de verificación' }
];
const PROCESS_STAGE_OPTIONS = [
@@ -72,17 +75,19 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
isLoading: externalLoading = false,
initialRecipe
}) => {
const { t } = useTranslation();
const { t } = useTranslation(['production', 'common']);
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const [loading, setLoading] = useState(false);
const [selectedStages, setSelectedStages] = useState<ProcessStage[]>([]);
const [selectedCheckType, setSelectedCheckType] = useState<QualityCheckType | null>(null);
// 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);
return translated === translationKey ? category : translated;
if (!category) return t('production:quality.categories.no_category', 'Sin categoría');
const translationKey = `quality.categories.${category}`;
const translated = t(`production:${translationKey}`);
return translated === `production:${translationKey}` ? category : translated;
};
// Build category options with translations
@@ -109,6 +114,22 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
setLoading(true);
try {
// Type-specific validation
if (formData.check_type === QualityCheckType.VISUAL) {
const hasAnyScoring = formData.scoring_excellent_min || formData.scoring_excellent_max ||
formData.scoring_good_min || formData.scoring_good_max ||
formData.scoring_acceptable_min || formData.scoring_acceptable_max;
if (!hasAnyScoring) {
throw new Error('Los criterios de puntuación son requeridos para controles visuales');
}
}
if ([QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT].includes(formData.check_type)) {
if (!formData.unit?.trim()) {
throw new Error('La unidad es requerida para controles de medición, temperatura y peso');
}
}
// Process applicable stages - convert string back to array
const applicableStages = formData.applicable_stages
? (typeof formData.applicable_stages === 'string'
@@ -116,6 +137,29 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
: formData.applicable_stages)
: [];
// Build scoring criteria for visual checks
let scoringCriteria: Record<string, any> | undefined;
if (formData.check_type === QualityCheckType.VISUAL) {
scoringCriteria = {
excellent: {
min: formData.scoring_excellent_min ? Number(formData.scoring_excellent_min) : undefined,
max: formData.scoring_excellent_max ? Number(formData.scoring_excellent_max) : undefined
},
good: {
min: formData.scoring_good_min ? Number(formData.scoring_good_min) : undefined,
max: formData.scoring_good_max ? Number(formData.scoring_good_max) : undefined
},
acceptable: {
min: formData.scoring_acceptable_min ? Number(formData.scoring_acceptable_min) : undefined,
max: formData.scoring_acceptable_max ? Number(formData.scoring_acceptable_max) : undefined
},
fail: {
below: formData.scoring_fail_below ? Number(formData.scoring_fail_below) : undefined,
above: formData.scoring_fail_above ? Number(formData.scoring_fail_above) : undefined
}
};
}
const templateData: QualityCheckTemplateCreate = {
name: formData.name,
template_code: formData.template_code || '',
@@ -128,13 +172,15 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
is_critical: formData.is_critical || false,
weight: Number(formData.weight) || 1.0,
applicable_stages: applicableStages.length > 0 ? applicableStages as ProcessStage[] : undefined,
created_by: currentTenant?.id || '',
created_by: user?.id || '',
// Measurement fields
min_value: formData.min_value ? Number(formData.min_value) : undefined,
max_value: formData.max_value ? Number(formData.max_value) : undefined,
target_value: formData.target_value ? Number(formData.target_value) : undefined,
unit: formData.unit || undefined,
tolerance_percentage: formData.tolerance_percentage ? Number(formData.tolerance_percentage) : undefined
unit: formData.unit && formData.unit.trim() ? formData.unit.trim() : undefined,
tolerance_percentage: formData.tolerance_percentage ? Number(formData.tolerance_percentage) : undefined,
// Scoring criteria (for visual checks)
scoring_criteria: scoringCriteria
};
// Handle recipe associations if provided
@@ -170,15 +216,16 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
isHighlight: true
};
// Determine if measurement fields should be shown based on check type
const showMeasurementFields = (checkType: string) => [
QualityCheckType.MEASUREMENT,
QualityCheckType.TEMPERATURE,
QualityCheckType.WEIGHT
].includes(checkType as QualityCheckType);
// Handler for field changes to track check_type selection
const handleFieldChange = (fieldName: string, value: any) => {
if (fieldName === 'check_type') {
setSelectedCheckType(value as QualityCheckType);
}
};
const sections = [
{
// Function to build sections dynamically based on selected check type
const getSections = () => {
const basicInfoSection = {
title: 'Información Básica',
icon: ClipboardCheck,
fields: [
@@ -204,8 +251,9 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
name: 'check_type',
type: 'select' as const,
required: true,
defaultValue: QualityCheckType.VISUAL,
options: QUALITY_CHECK_TYPE_OPTIONS
placeholder: 'Selecciona un tipo de control...',
options: QUALITY_CHECK_TYPE_OPTIONS,
helpText: 'Los campos de configuración cambiarán según el tipo seleccionado'
},
{
label: 'Categoría',
@@ -230,8 +278,9 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
helpText: 'Pasos específicos que debe seguir el operario'
}
]
},
{
};
const measurementSection = {
title: 'Configuración de Medición',
icon: Target,
fields: [
@@ -257,11 +306,12 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
helpText: 'Valor ideal que se busca alcanzar'
},
{
label: 'Unidad',
label: 'Unidad *',
name: 'unit',
type: 'text' as const,
required: true,
placeholder: '°C / g / cm',
helpText: 'Unidad de medida (ej: °C para temperatura)'
helpText: 'REQUERIDO para este tipo de control (ej: °C, g, cm)'
},
{
label: 'Tolerancia (%)',
@@ -271,22 +321,93 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
helpText: 'Porcentaje de tolerancia permitido'
}
]
},
{
};
const scoringSection = {
title: 'Criterios de Puntuación (Controles Visuales)',
icon: Target,
fields: [
{
label: 'Excelente - Mínimo',
name: 'scoring_excellent_min',
type: 'number' as const,
placeholder: '9.0',
helpText: 'Puntuación mínima para nivel excelente'
},
{
label: 'Excelente - Máximo',
name: 'scoring_excellent_max',
type: 'number' as const,
placeholder: '10.0',
helpText: 'Puntuación máxima para nivel excelente'
},
{
label: 'Bueno - Mínimo',
name: 'scoring_good_min',
type: 'number' as const,
placeholder: '7.0',
helpText: 'Puntuación mínima para nivel bueno'
},
{
label: 'Bueno - Máximo',
name: 'scoring_good_max',
type: 'number' as const,
placeholder: '8.9',
helpText: 'Puntuación máxima para nivel bueno'
},
{
label: 'Aceptable - Mínimo',
name: 'scoring_acceptable_min',
type: 'number' as const,
placeholder: '5.0',
helpText: 'Puntuación mínima para nivel aceptable'
},
{
label: 'Aceptable - Máximo',
name: 'scoring_acceptable_max',
type: 'number' as const,
placeholder: '6.9',
helpText: 'Puntuación máxima para nivel aceptable'
},
{
label: 'Fallo - Por Debajo',
name: 'scoring_fail_below',
type: 'number' as const,
placeholder: '5.0',
helpText: 'Valor por debajo del cual se considera fallo'
},
{
label: 'Fallo - Por Encima',
name: 'scoring_fail_above',
type: 'number' as const,
placeholder: '10.0',
helpText: 'Valor por encima del cual se considera fallo (opcional)'
}
]
};
const stagesSection = {
title: 'Etapas del Proceso',
icon: Settings,
fields: [
{
label: 'Etapas Aplicables',
name: 'applicable_stages',
type: 'text' as const,
placeholder: 'Se seleccionarán las etapas donde aplicar',
helpText: 'Las etapas se configuran mediante la selección múltiple',
type: 'component' as const,
component: Select,
componentProps: {
options: PROCESS_STAGE_OPTIONS,
multiple: true,
placeholder: 'Seleccionar etapas del proceso',
searchable: true
},
helpText: 'Selecciona las etapas donde se aplicará este control de calidad',
span: 2 as const
}
]
},
{
};
const recipesSection = {
title: 'Asociación con Recetas',
icon: Plus,
fields: [
@@ -300,8 +421,9 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
span: 2 as const
}
]
},
{
};
const advancedSection = {
title: 'Configuración Avanzada',
icon: Cog,
fields: [
@@ -350,8 +472,28 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
helpText: 'Si es crítico, bloquea la producción si falla'
}
]
};
// Build sections array based on selected check type
const sections = [basicInfoSection];
// Add type-specific configuration sections
if (selectedCheckType === QualityCheckType.VISUAL) {
sections.push(scoringSection);
} else if ([QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT].includes(selectedCheckType as QualityCheckType)) {
sections.push(measurementSection);
}
];
// For BOOLEAN, TIMING, CHECKLIST - no special configuration sections yet
// Always add these sections
sections.push(stagesSection);
sections.push(recipesSection);
sections.push(advancedSection);
return sections;
};
const sections = getSections();
return (
<>
@@ -365,15 +507,8 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
size="xl"
loading={loading || externalLoading}
onSave={handleSave}
onFieldChange={handleFieldChange}
/>
{/* TODO: Stage selection would need a custom component or enhanced AddModal field types */}
{isOpen && (
<div style={{ display: 'none' }}>
<p>Nota: La selección de etapas del proceso requiere un componente personalizado no implementado en esta versión simplificada.</p>
<p>Las etapas actualmente se manejan mediante un campo de texto que debería ser reemplazado por un selector múltiple.</p>
</div>
)}
</>
);
};

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { BaseDeleteModal } from '../../ui';
import { QualityCheckTemplate } from '../../../api/types/qualityTemplates';
import { useTranslation } from 'react-i18next';
interface DeleteQualityTemplateModalProps {
isOpen: boolean;
onClose: () => void;
template: QualityCheckTemplate | null;
onSoftDelete: (templateId: string) => Promise<void>;
onHardDelete: (templateId: string) => Promise<void>;
isLoading?: boolean;
}
/**
* Modal for quality template deletion with soft/hard delete options
* - Soft delete: Mark as inactive (reversible)
* - Hard delete: Permanent deletion with dependency checking
*/
export const DeleteQualityTemplateModal: React.FC<DeleteQualityTemplateModalProps> = ({
isOpen,
onClose,
template,
onSoftDelete,
onHardDelete,
isLoading = false,
}) => {
const { t } = useTranslation(['production', 'common']);
if (!template) return null;
return (
<BaseDeleteModal<QualityCheckTemplate>
isOpen={isOpen}
onClose={onClose}
entity={template}
onSoftDelete={onSoftDelete}
onHardDelete={onHardDelete}
isLoading={isLoading}
title={t('production:quality.delete.title', 'Eliminar Plantilla de Calidad')}
getEntityId={(temp) => temp.id}
getEntityDisplay={(temp) => ({
primaryText: temp.name,
secondaryText: `${t('production:quality.delete.template_code', 'Código')}: ${temp.template_code || 'N/A'}${t('production:quality.delete.check_type', 'Tipo')}: ${temp.check_type}`,
})}
softDeleteOption={{
title: t('production:quality.delete.soft_delete', 'Desactivar (Recomendado)'),
description: t('production:quality.delete.soft_explanation', 'La plantilla se marca como inactiva pero conserva todo su historial. Ideal para plantillas temporalmente fuera de uso.'),
benefits: t('production:quality.delete.soft_benefits', '✓ Reversible • ✓ Conserva historial • ✓ Conserva datos'),
}}
hardDeleteOption={{
title: t('production:quality.delete.hard_delete', 'Eliminar Permanentemente'),
description: t('production:quality.delete.hard_explanation', 'Elimina completamente la plantilla y todos sus datos asociados. Use solo para datos erróneos o pruebas.'),
benefits: t('production:quality.delete.hard_risks', '⚠️ No reversible • ⚠️ Elimina historial • ⚠️ Elimina todos los datos'),
enabled: true,
}}
softDeleteWarning={{
title: t('production:quality.delete.soft_info_title', ' Esta acción desactivará la plantilla:'),
items: [
t('production:quality.delete.soft_info_1', 'La plantilla se marcará como inactiva'),
t('production:quality.delete.soft_info_2', 'No aparecerá en listas activas'),
t('production:quality.delete.soft_info_3', 'Se conserva todo el historial y datos'),
t('production:quality.delete.soft_info_4', 'Se puede reactivar posteriormente'),
],
}}
hardDeleteWarning={{
title: t('production:quality.delete.hard_warning_title', '⚠️ Esta acción eliminará permanentemente:'),
items: [
t('production:quality.delete.hard_warning_1', 'La plantilla y toda su información'),
t('production:quality.delete.hard_warning_2', 'Todas las configuraciones de calidad asociadas'),
t('production:quality.delete.hard_warning_3', 'Todo el historial de controles de calidad'),
t('production:quality.delete.hard_warning_4', 'Las alertas y métricas relacionadas'),
],
footer: t('production:quality.delete.irreversible', 'Esta acción NO se puede deshacer'),
}}
requireConfirmText={true}
confirmText="ELIMINAR"
showSuccessScreen={true}
successTitle={t('production:quality.delete.success_soft_title', 'Plantilla Desactivada')}
getSuccessMessage={(temp, mode) =>
mode === 'hard'
? t('production:quality.delete.template_deleted', { name: temp.name })
: t('production:quality.delete.template_deactivated', { name: temp.name })
}
autoCloseDelay={1500}
/>
);
};
export default DeleteQualityTemplateModal;

View File

@@ -3,8 +3,6 @@ import {
Plus,
Search,
Filter,
Edit,
Copy,
Trash2,
Eye,
CheckCircle,
@@ -32,8 +30,7 @@ import {
useQualityTemplates,
useCreateQualityTemplate,
useUpdateQualityTemplate,
useDeleteQualityTemplate,
useDuplicateQualityTemplate
useDeleteQualityTemplate
} from '../../../api/hooks/qualityTemplates';
import {
QualityCheckType,
@@ -45,6 +42,7 @@ import {
import { CreateQualityTemplateModal } from './CreateQualityTemplateModal';
import { EditQualityTemplateModal } from './EditQualityTemplateModal';
import { ViewQualityTemplateModal } from './ViewQualityTemplateModal';
import { DeleteQualityTemplateModal } from './DeleteQualityTemplateModal';
import { useTranslation } from 'react-i18next';
interface QualityTemplateManagerProps {
@@ -114,10 +112,12 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
const [selectedCheckType, setSelectedCheckType] = useState<QualityCheckType | ''>('');
const [selectedStage, setSelectedStage] = useState<ProcessStage | ''>('');
const [showActiveOnly, setShowActiveOnly] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showViewModal, setShowViewModal] = useState(false);
const [showCreateModal, setShowCreateModal] = useState<boolean>(false);
const [showEditModal, setShowEditModal] = useState<boolean>(false);
const [showViewModal, setShowViewModal] = useState<boolean>(false);
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const [selectedTemplate, setSelectedTemplate] = useState<QualityCheckTemplate | null>(null);
const [templateToDelete, setTemplateToDelete] = useState<QualityCheckTemplate | null>(null);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
@@ -146,7 +146,6 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
const createTemplateMutation = useCreateQualityTemplate(tenantId);
const updateTemplateMutation = useUpdateQualityTemplate(tenantId);
const deleteTemplateMutation = useDeleteQualityTemplate(tenantId);
const duplicateTemplateMutation = useDuplicateQualityTemplate(tenantId);
// Filtered templates
const filteredTemplates = useMemo(() => {
@@ -214,25 +213,25 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
}
};
const handleDeleteTemplate = async (templateId: string) => {
if (!confirm('¿Estás seguro de que quieres eliminar esta plantilla?')) return;
const handleSoftDelete = async (templateId: string) => {
try {
await deleteTemplateMutation.mutateAsync(templateId);
} catch (error) {
console.error('Error deleting template:', error);
throw error;
}
};
const handleDuplicateTemplate = async (templateId: string) => {
const handleHardDelete = async (templateId: string) => {
try {
await duplicateTemplateMutation.mutateAsync(templateId);
await deleteTemplateMutation.mutateAsync(templateId);
} catch (error) {
console.error('Error duplicating template:', error);
console.error('Error deleting template:', error);
throw error;
}
};
const getTemplateStatusConfig = (template: QualityCheckTemplate) => {
const getTemplateStatusConfig = (template: QualityCheckTemplate) => {
const typeConfigs = QUALITY_CHECK_TYPE_CONFIG(t);
const typeConfig = typeConfigs[template.check_type];
@@ -241,7 +240,7 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
text: typeConfig.label,
icon: typeConfig.icon
};
};
};
if (isLoading) {
return (
@@ -406,27 +405,15 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
setShowViewModal(true);
}
},
{
label: 'Editar',
icon: Edit,
priority: 'secondary',
onClick: () => {
setSelectedTemplate(template);
setShowEditModal(true);
}
},
{
label: 'Duplicar',
icon: Copy,
priority: 'secondary',
onClick: () => handleDuplicateTemplate(template.id)
},
{
label: 'Eliminar',
icon: Trash2,
destructive: true,
priority: 'secondary',
onClick: () => handleDeleteTemplate(template.id)
onClick: () => {
setTemplateToDelete(template);
setShowDeleteModal(true);
}
}
]}
/>
@@ -491,6 +478,21 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
}}
/>
)}
{/* Delete Template Modal */}
{templateToDelete && (
<DeleteQualityTemplateModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setTemplateToDelete(null);
}}
template={templateToDelete}
onSoftDelete={handleSoftDelete}
onHardDelete={handleHardDelete}
isLoading={deleteTemplateMutation.isPending}
/>
)}
</div>
);
};