Add improved production UI 3

This commit is contained in:
Urtzi Alfaro
2025-09-23 19:24:22 +02:00
parent 7f871fc933
commit 7892c5a739
47 changed files with 6211 additions and 267 deletions

View File

@@ -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;