Add improved production UI 3
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
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,
|
||||
Input,
|
||||
Card,
|
||||
Badge,
|
||||
Select,
|
||||
StatusCard,
|
||||
Modal,
|
||||
StatsGrid
|
||||
} from '../../ui';
|
||||
import { LoadingSpinner } from '../../shared';
|
||||
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';
|
||||
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
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 [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 [selectedTemplate, setSelectedTemplate] = useState<QualityCheckTemplate | null>(null);
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
// 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}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-tertiary)]" />
|
||||
<Input
|
||||
placeholder="Buscar plantillas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={selectedCheckType}
|
||||
onChange={(e) => setSelectedCheckType(e.target.value as QualityCheckType | '')}
|
||||
placeholder="Tipo de control"
|
||||
>
|
||||
<option value="">Todos los tipos</option>
|
||||
{Object.entries(QUALITY_CHECK_TYPE_CONFIG).map(([type, config]) => (
|
||||
<option key={type} value={type}>
|
||||
{config.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={selectedStage}
|
||||
onChange={(e) => setSelectedStage(e.target.value as ProcessStage | '')}
|
||||
placeholder="Etapa del proceso"
|
||||
>
|
||||
<option value="">Todas las etapas</option>
|
||||
{Object.entries(PROCESS_STAGE_LABELS).map(([stage, label]) => (
|
||||
<option key={stage} value={stage}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="activeOnly"
|
||||
checked={showActiveOnly}
|
||||
onChange={(e) => setShowActiveOnly(e.target.checked)}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="activeOnly" className="text-sm text-[var(--text-secondary)]">
|
||||
Solo activas
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 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={template.category || 'Sin categoría'}
|
||||
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);
|
||||
// Could open a view modal here
|
||||
}
|
||||
},
|
||||
{
|
||||
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,
|
||||
variant: 'danger',
|
||||
priority: 'secondary',
|
||||
onClick: () => handleDeleteTemplate(template.id)
|
||||
}
|
||||
]}
|
||||
>
|
||||
{/* Additional badges */}
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{template.is_active && (
|
||||
<Badge variant="success" size="sm">Activa</Badge>
|
||||
)}
|
||||
{template.is_required && (
|
||||
<Badge variant="warning" size="sm">Requerida</Badge>
|
||||
)}
|
||||
{template.is_critical && (
|
||||
<Badge variant="error" size="sm">Crítica</Badge>
|
||||
)}
|
||||
</div>
|
||||
</StatusCard>
|
||||
);
|
||||
})}
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QualityTemplateManager;
|
||||
Reference in New Issue
Block a user