474 lines
14 KiB
TypeScript
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; |