Files
bakery-ia/frontend/src/components/domain/production/QualityTemplateManager.tsx
2025-10-24 13:05:04 +02:00

474 lines
14 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import {
Plus,
Search,
Filter,
Edit,
Copy,
Trash2,
Eye,
CheckCircle,
AlertTriangle,
Settings,
Tag,
Thermometer,
Scale,
Timer,
FileCheck
} from 'lucide-react';
import {
Button,
Badge,
StatusCard,
Modal,
StatsGrid,
SearchAndFilter,
type FilterConfig
} from '../../ui';
import { LoadingSpinner } from '../../ui';
import { PageHeader } from '../../layout';
import { useCurrentTenant } from '../../../stores/tenant.store';
import {
useQualityTemplates,
useCreateQualityTemplate,
useUpdateQualityTemplate,
useDeleteQualityTemplate,
useDuplicateQualityTemplate
} from '../../../api/hooks/qualityTemplates';
import {
QualityCheckType,
ProcessStage,
type QualityCheckTemplate,
type QualityCheckTemplateCreate,
type QualityCheckTemplateUpdate
} from '../../../api/types/qualityTemplates';
import { CreateQualityTemplateModal } from './CreateQualityTemplateModal';
import { EditQualityTemplateModal } from './EditQualityTemplateModal';
import { ViewQualityTemplateModal } from './ViewQualityTemplateModal';
import { useTranslation } from 'react-i18next';
interface QualityTemplateManagerProps {
className?: string;
}
const QUALITY_CHECK_TYPE_CONFIG = {
[QualityCheckType.VISUAL]: {
icon: Eye,
label: 'Visual',
color: 'bg-blue-500',
description: 'Inspección visual'
},
[QualityCheckType.MEASUREMENT]: {
icon: Settings,
label: 'Medición',
color: 'bg-green-500',
description: 'Mediciones precisas'
},
[QualityCheckType.TEMPERATURE]: {
icon: Thermometer,
label: 'Temperatura',
color: 'bg-red-500',
description: 'Control de temperatura'
},
[QualityCheckType.WEIGHT]: {
icon: Scale,
label: 'Peso',
color: 'bg-purple-500',
description: 'Control de peso'
},
[QualityCheckType.BOOLEAN]: {
icon: CheckCircle,
label: 'Sí/No',
color: 'bg-gray-500',
description: 'Verificación binaria'
},
[QualityCheckType.TIMING]: {
icon: Timer,
label: 'Tiempo',
color: 'bg-orange-500',
description: 'Control de tiempo'
},
[QualityCheckType.CHECKLIST]: {
icon: FileCheck,
label: 'Lista de verificación',
color: 'bg-indigo-500',
description: 'Checklist de verificación'
}
};
const PROCESS_STAGE_LABELS = {
[ProcessStage.MIXING]: 'Mezclado',
[ProcessStage.PROOFING]: 'Fermentación',
[ProcessStage.SHAPING]: 'Formado',
[ProcessStage.BAKING]: 'Horneado',
[ProcessStage.COOLING]: 'Enfriado',
[ProcessStage.PACKAGING]: 'Empaquetado',
[ProcessStage.FINISHING]: 'Acabado'
};
export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
className = ''
}) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
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 [selectedTemplate, setSelectedTemplate] = useState<QualityCheckTemplate | null>(null);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// 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;
};
// API hooks
const {
data: templatesData,
isLoading,
error
} = useQualityTemplates(tenantId, {
check_type: selectedCheckType || undefined,
stage: selectedStage || undefined,
is_active: showActiveOnly,
search: searchTerm || undefined
});
const createTemplateMutation = useCreateQualityTemplate(tenantId);
const updateTemplateMutation = useUpdateQualityTemplate(tenantId);
const deleteTemplateMutation = useDeleteQualityTemplate(tenantId);
const duplicateTemplateMutation = useDuplicateQualityTemplate(tenantId);
// Filtered templates
const filteredTemplates = useMemo(() => {
if (!templatesData?.templates) return [];
return templatesData.templates.filter(template => {
const matchesSearch = !searchTerm ||
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
template.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
template.category?.toLowerCase().includes(searchTerm.toLowerCase());
return matchesSearch;
});
}, [templatesData?.templates, searchTerm]);
// Statistics
const templateStats = useMemo(() => {
const templates = templatesData?.templates || [];
return {
total: templates.length,
active: templates.filter(t => t.is_active).length,
critical: templates.filter(t => t.is_critical).length,
required: templates.filter(t => t.is_required).length,
byType: Object.values(QualityCheckType).map(type => ({
type,
count: templates.filter(t => t.check_type === type).length
}))
};
}, [templatesData?.templates]);
// Event handlers
const handleCreateTemplate = async (templateData: QualityCheckTemplateCreate) => {
try {
await createTemplateMutation.mutateAsync(templateData);
setShowCreateModal(false);
} catch (error) {
console.error('Error creating template:', error);
}
};
const handleUpdateTemplate = async (templateData: QualityCheckTemplateUpdate) => {
if (!selectedTemplate) return;
try {
await updateTemplateMutation.mutateAsync({
templateId: selectedTemplate.id,
templateData
});
setShowEditModal(false);
setSelectedTemplate(null);
} catch (error) {
console.error('Error updating template:', error);
}
};
const handleDeleteTemplate = async (templateId: string) => {
if (!confirm('¿Estás seguro de que quieres eliminar esta plantilla?')) return;
try {
await deleteTemplateMutation.mutateAsync(templateId);
} catch (error) {
console.error('Error deleting template:', error);
}
};
const handleDuplicateTemplate = async (templateId: string) => {
try {
await duplicateTemplateMutation.mutateAsync(templateId);
} catch (error) {
console.error('Error duplicating template:', error);
}
};
const getTemplateStatusConfig = (template: QualityCheckTemplate) => {
const typeConfig = QUALITY_CHECK_TYPE_CONFIG[template.check_type];
return {
color: template.is_active ? typeConfig.color : '#6b7280',
text: typeConfig.label,
icon: typeConfig.icon
};
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-64">
<LoadingSpinner text="Cargando plantillas de calidad..." />
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Error al cargar las plantillas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{error?.message || 'Ha ocurrido un error inesperado'}
</p>
<Button onClick={() => window.location.reload()}>
Reintentar
</Button>
</div>
);
}
return (
<div className={`space-y-6 ${className}`}>
<PageHeader
title="Plantillas de Control de Calidad"
description="Gestiona las plantillas de control de calidad para tus procesos de producción"
actions={[
{
id: "new",
label: "Nueva Plantilla",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowCreateModal(true)
}
]}
/>
{/* Statistics */}
<StatsGrid
stats={[
{
title: 'Total Plantillas',
value: templateStats.total,
variant: 'default',
icon: FileCheck
},
{
title: 'Activas',
value: templateStats.active,
variant: 'success',
icon: CheckCircle
},
{
title: 'Críticas',
value: templateStats.critical,
variant: 'error',
icon: AlertTriangle
},
{
title: 'Requeridas',
value: templateStats.required,
variant: 'warning',
icon: Tag
}
]}
columns={4}
/>
{/* Search and Filter Controls */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="Buscar plantillas..."
filters={[
{
key: 'checkType',
label: 'Tipo de control',
type: 'dropdown',
value: selectedCheckType,
onChange: (value) => setSelectedCheckType(value as QualityCheckType | ''),
placeholder: 'Todos los tipos',
options: Object.entries(QUALITY_CHECK_TYPE_CONFIG).map(([type, config]) => ({
value: type,
label: config.label
}))
},
{
key: 'stage',
label: 'Etapa del proceso',
type: 'dropdown',
value: selectedStage,
onChange: (value) => setSelectedStage(value as ProcessStage | ''),
placeholder: 'Todas las etapas',
options: Object.entries(PROCESS_STAGE_LABELS).map(([stage, label]) => ({
value: stage,
label: label
}))
},
{
key: 'activeOnly',
label: 'Solo activas',
type: 'checkbox',
value: showActiveOnly,
onChange: (value) => setShowActiveOnly(value as boolean)
}
] as FilterConfig[]}
/>
{/* Templates Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredTemplates.map((template) => {
const statusConfig = getTemplateStatusConfig(template);
const stageLabels = template.applicable_stages?.map(stage =>
PROCESS_STAGE_LABELS[stage]
) || ['Todas las etapas'];
return (
<StatusCard
key={template.id}
id={template.id}
statusIndicator={statusConfig}
title={template.name}
subtitle={getCategoryLabel(template.category)}
primaryValue={template.weight}
primaryValueLabel="peso"
secondaryInfo={{
label: 'Etapas',
value: stageLabels.length > 2
? `${stageLabels.slice(0, 2).join(', ')}...`
: stageLabels.join(', ')
}}
metadata={[
template.description || 'Sin descripción',
`${template.is_required ? 'Requerido' : 'Opcional'}`,
`${template.is_critical ? 'Crítico' : 'Normal'}`
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => {
setSelectedTemplate(template);
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)
}
]}
/>
);
})}
</div>
{/* Empty State */}
{filteredTemplates.length === 0 && (
<div className="text-center py-12">
<FileCheck className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No se encontraron plantillas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
{templatesData?.templates?.length === 0
? 'No hay plantillas de calidad creadas. Crea la primera plantilla para comenzar.'
: 'Intenta ajustar los filtros de búsqueda o crear una nueva plantilla'
}
</p>
<Button onClick={() => setShowCreateModal(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Plantilla
</Button>
</div>
)}
{/* Create Template Modal */}
<CreateQualityTemplateModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onCreateTemplate={handleCreateTemplate}
isLoading={createTemplateMutation.isPending}
/>
{/* Edit Template Modal */}
{selectedTemplate && (
<EditQualityTemplateModal
isOpen={showEditModal}
onClose={() => {
setShowEditModal(false);
setSelectedTemplate(null);
}}
template={selectedTemplate}
onUpdateTemplate={handleUpdateTemplate}
isLoading={updateTemplateMutation.isPending}
/>
)}
{/* View Template Modal */}
{selectedTemplate && (
<ViewQualityTemplateModal
isOpen={showViewModal}
onClose={() => {
setShowViewModal(false);
setSelectedTemplate(null);
}}
template={selectedTemplate}
onEdit={() => {
setShowViewModal(false);
setShowEditModal(true);
}}
/>
)}
</div>
);
};
export default QualityTemplateManager;