Files
bakery-ia/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx

480 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-23 19:24:22 +02:00
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
2025-09-23 22:11:34 +02:00
import { Plus, AlertTriangle, Settings, CheckCircle, Eye, Wrench, Thermometer, Activity, Search, Filter, Bell, History, Calendar, Edit, Trash2 } from 'lucide-react';
2025-10-27 16:33:26 +01:00
import { Button, StatsGrid, StatusCard, getStatusColor, SearchAndFilter, type FilterConfig, EmptyState } from '../../../../components/ui';
2025-09-23 22:11:34 +02:00
import { Badge } from '../../../../components/ui/Badge';
2025-09-26 07:46:25 +02:00
import { LoadingSpinner } from '../../../../components/ui';
2025-09-23 19:24:22 +02:00
import { PageHeader } from '../../../../components/layout';
import { useCurrentTenant } from '../../../../stores/tenant.store';
2025-09-26 07:46:25 +02:00
import { Equipment } from '../../../../api/types/equipment';
2025-09-24 15:58:18 +02:00
import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal';
2025-10-29 06:58:05 +01:00
import { DeleteEquipmentModal } from '../../../../components/domain/equipment/DeleteEquipmentModal';
import { MaintenanceHistoryModal } from '../../../../components/domain/equipment/MaintenanceHistoryModal';
import { ScheduleMaintenanceModal, type MaintenanceScheduleData } from '../../../../components/domain/equipment/ScheduleMaintenanceModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment, useDeleteEquipment, useHardDeleteEquipment } from '../../../../api/hooks/equipment';
2025-09-23 19:24:22 +02:00
const MaquinariaPage: React.FC = () => {
const { t } = useTranslation(['equipment', 'common']);
const [searchTerm, setSearchTerm] = useState('');
2025-09-26 12:12:17 +02:00
const [statusFilter, setStatusFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
2025-09-24 15:58:18 +02:00
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create');
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
2025-10-19 19:22:37 +02:00
2025-10-29 06:58:05 +01:00
// New modal states
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [equipmentForAction, setEquipmentForAction] = useState<Equipment | null>(null);
2025-09-23 19:24:22 +02:00
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
2025-10-19 19:22:37 +02:00
// Fetch equipment data from API
const { data: equipment = [], isLoading, error } = useEquipment(tenantId, {
is_active: true
});
2025-10-29 06:58:05 +01:00
// Mutations for create, update, and delete
2025-10-19 19:22:37 +02:00
const createEquipmentMutation = useCreateEquipment(tenantId);
const updateEquipmentMutation = useUpdateEquipment(tenantId);
2025-10-29 06:58:05 +01:00
const deleteEquipmentMutation = useDeleteEquipment(tenantId);
const hardDeleteEquipmentMutation = useHardDeleteEquipment(tenantId);
2025-10-19 19:22:37 +02:00
2025-09-23 19:24:22 +02:00
const handleCreateEquipment = () => {
2025-09-24 15:58:18 +02:00
setSelectedEquipment({
id: '',
name: '',
type: 'other',
model: '',
serialNumber: '',
location: '',
status: 'operational',
installDate: new Date().toISOString().split('T')[0],
lastMaintenance: new Date().toISOString().split('T')[0],
nextMaintenance: new Date().toISOString().split('T')[0],
maintenanceInterval: 30,
efficiency: 100,
uptime: 100,
energyUsage: 0,
utilizationToday: 0,
alerts: [],
maintenanceHistory: [],
specifications: {
power: 0,
capacity: 0,
dimensions: { width: 0, height: 0, depth: 0 },
weight: 0
}
} as Equipment);
setEquipmentModalMode('create');
setShowEquipmentModal(true);
2025-09-23 19:24:22 +02:00
};
const handleEditEquipment = (equipmentId: string) => {
2025-10-19 19:22:37 +02:00
// Find the equipment to edit from real data
const equipmentToEdit = equipment.find(eq => eq.id === equipmentId);
2025-09-24 15:58:18 +02:00
if (equipmentToEdit) {
setSelectedEquipment(equipmentToEdit);
setEquipmentModalMode('edit');
setShowEquipmentModal(true);
}
2025-09-23 19:24:22 +02:00
};
2025-10-29 06:58:05 +01:00
const handleScheduleMaintenance = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowScheduleModal(true);
};
const handleScheduleMaintenanceSubmit = async (equipmentId: string, maintenanceData: MaintenanceScheduleData) => {
try {
// Update next maintenance date based on scheduled date
await updateEquipmentMutation.mutateAsync({
equipmentId: equipmentId,
equipmentData: {
nextMaintenance: maintenanceData.scheduledDate
} as Partial<Equipment>
});
setShowScheduleModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error scheduling maintenance:', error);
throw error;
}
2025-09-23 19:24:22 +02:00
};
2025-10-29 06:58:05 +01:00
const handleViewMaintenanceHistory = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowHistoryModal(true);
2025-09-23 19:24:22 +02:00
};
2025-10-29 06:58:05 +01:00
const handleDeleteEquipment = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowDeleteModal(true);
};
const handleSoftDelete = async (equipmentId: string) => {
try {
await deleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error deleting equipment:', error);
throw error;
}
};
const handleHardDelete = async (equipmentId: string) => {
try {
await hardDeleteEquipmentMutation.mutateAsync(equipmentId);
setShowDeleteModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error hard deleting equipment:', error);
throw error;
}
2025-09-23 19:24:22 +02:00
};
2025-10-19 19:22:37 +02:00
const handleSaveEquipment = async (equipmentData: Equipment) => {
try {
if (equipmentModalMode === 'create') {
await createEquipmentMutation.mutateAsync(equipmentData);
} else if (equipmentModalMode === 'edit' && equipmentData.id) {
await updateEquipmentMutation.mutateAsync({
equipmentId: equipmentData.id,
equipmentData: equipmentData
});
}
setShowEquipmentModal(false);
setSelectedEquipment(null);
} catch (error) {
console.error('Error saving equipment:', error);
// Error is already handled by mutation with toast
}
2025-09-24 15:58:18 +02:00
};
2025-09-23 19:24:22 +02:00
const filteredEquipment = useMemo(() => {
2025-10-19 19:22:37 +02:00
return equipment.filter(eq => {
2025-09-23 19:24:22 +02:00
const matchesSearch = !searchTerm ||
eq.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.type.toLowerCase().includes(searchTerm.toLowerCase());
2025-09-26 12:12:17 +02:00
const matchesStatus = !statusFilter || eq.status === statusFilter;
const matchesType = !typeFilter || eq.type === typeFilter;
2025-09-23 19:24:22 +02:00
2025-09-26 12:12:17 +02:00
return matchesSearch && matchesStatus && matchesType;
2025-09-23 19:24:22 +02:00
});
2025-10-19 19:22:37 +02:00
}, [equipment, searchTerm, statusFilter, typeFilter]);
2025-09-23 19:24:22 +02:00
const equipmentStats = useMemo(() => {
2025-10-19 19:22:37 +02:00
const total = equipment.length;
const operational = equipment.filter(e => e.status === 'operational').length;
const warning = equipment.filter(e => e.status === 'warning').length;
const maintenance = equipment.filter(e => e.status === 'maintenance').length;
const down = equipment.filter(e => e.status === 'down').length;
const totalAlerts = equipment.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
2025-09-23 19:24:22 +02:00
return {
total,
operational,
warning,
maintenance,
down,
totalAlerts
};
2025-10-19 19:22:37 +02:00
}, [equipment]);
2025-09-23 19:24:22 +02:00
const getStatusConfig = (status: Equipment['status']) => {
const configs = {
operational: { color: getStatusColor('completed'), text: t('equipment_status.operational'), icon: CheckCircle },
warning: { color: getStatusColor('warning'), text: t('equipment_status.warning'), icon: AlertTriangle },
maintenance: { color: getStatusColor('info'), text: t('equipment_status.maintenance'), icon: Wrench },
down: { color: getStatusColor('error'), text: t('equipment_status.down'), icon: AlertTriangle }
};
2025-12-13 23:57:54 +01:00
return configs[status] || { color: getStatusColor('other'), text: status, icon: Settings };
2025-09-23 19:24:22 +02:00
};
const getTypeIcon = (type: Equipment['type']) => {
const icons = {
oven: Thermometer,
mixer: Activity,
proofer: Settings,
2025-09-23 22:11:34 +02:00
freezer: Settings,
2025-09-23 19:24:22 +02:00
packaging: Settings,
other: Settings
};
return icons[type];
};
const stats = [
{
title: t('labels.total_equipment'),
value: equipmentStats.total,
2025-10-27 16:33:26 +01:00
variant: 'default' as const,
2025-09-23 19:24:22 +02:00
icon: Settings,
},
{
title: t('labels.operational'),
value: equipmentStats.operational,
variant: 'success' as const,
2025-10-27 16:33:26 +01:00
icon: CheckCircle,
},
{
title: t('labels.warning'),
value: equipmentStats.warning,
variant: 'warning' as const,
icon: AlertTriangle,
},
{
title: t('labels.maintenance_required'),
value: equipmentStats.maintenance,
variant: 'info' as const,
icon: Wrench,
},
{
title: t('labels.down'),
value: equipmentStats.down,
variant: 'error' as const,
icon: AlertTriangle,
2025-09-23 19:24:22 +02:00
},
{
title: t('labels.active_alerts'),
value: equipmentStats.totalAlerts,
2025-10-27 16:33:26 +01:00
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const,
2025-09-23 19:24:22 +02:00
icon: Bell,
2025-10-27 16:33:26 +01:00
},
2025-09-23 19:24:22 +02:00
];
const handleShowMaintenanceDetails = (equipment: Equipment) => {
2025-10-29 06:58:05 +01:00
setSelectedEquipment(equipment);
setEquipmentModalMode('view');
setShowEquipmentModal(true);
2025-09-23 19:24:22 +02:00
};
// Loading state
if (!tenantId) {
return (
<div className="flex items-center justify-center min-h-64">
<LoadingSpinner text="Cargando datos..." />
</div>
);
}
2025-10-19 19:22:37 +02:00
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-64">
<LoadingSpinner text={t('common:loading')} />
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center min-h-64">
<AlertTriangle className="w-12 h-12 text-red-500 mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
{t('common:errors.load_error')}
</h3>
<p className="text-[var(--text-secondary)]">
{t('common:errors.try_again')}
</p>
</div>
);
}
2025-09-23 19:24:22 +02:00
return (
<div className="space-y-6">
<PageHeader
title={t('title')}
description={t('subtitle')}
actions={[
{
id: "add-new-equipment",
label: t('actions.add_equipment'),
variant: "primary" as const,
icon: Plus,
onClick: handleCreateEquipment,
tooltip: t('actions.add_equipment'),
size: "md"
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={3}
/>
2025-09-26 12:12:17 +02:00
{/* Search and Filter Controls */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder={t('common:forms.search_placeholder')}
filters={[
{
key: 'status',
label: t('fields.status'),
type: 'dropdown',
value: statusFilter,
onChange: (value) => setStatusFilter(value as string),
placeholder: t('common:forms.select_option'),
options: [
{ value: 'operational', label: t('equipment_status.operational') },
{ value: 'warning', label: t('equipment_status.warning') },
{ value: 'maintenance', label: t('equipment_status.maintenance') },
{ value: 'down', label: t('equipment_status.down') }
]
},
{
key: 'type',
label: 'Tipo',
type: 'dropdown',
value: typeFilter,
onChange: (value) => setTypeFilter(value as string),
placeholder: 'Todos los tipos',
options: [
{ value: 'oven', label: 'Horno' },
{ value: 'mixer', label: 'Batidora' },
{ value: 'proofer', label: 'Fermentadora' },
{ value: 'freezer', label: 'Congelador' },
{ value: 'packaging', label: 'Empaquetado' },
{ value: 'other', label: 'Otro' }
]
}
] as FilterConfig[]}
/>
2025-09-23 19:24:22 +02:00
{/* Equipment Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredEquipment.map((equipment) => {
const statusConfig = getStatusConfig(equipment.status);
const TypeIcon = getTypeIcon(equipment.type);
// Calculate maintenance status
const nextMaintenanceDate = new Date(equipment.nextMaintenance);
const daysUntilMaintenance = Math.ceil((nextMaintenanceDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
const isOverdue = daysUntilMaintenance < 0;
return (
<StatusCard
key={equipment.id}
id={equipment.id}
statusIndicator={statusConfig}
title={equipment.name}
subtitle={equipment.location}
primaryValue={`${equipment.efficiency}%`}
primaryValueLabel={t('fields.efficiency')}
secondaryInfo={{
label: t('fields.uptime'),
value: `${equipment.uptime.toFixed(1)}%`
}}
2025-09-26 12:12:17 +02:00
onClick={() => handleShowMaintenanceDetails(equipment)}
2025-09-23 19:24:22 +02:00
actions={[
{
label: t('actions.view_details'),
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => handleShowMaintenanceDetails(equipment)
},
{
label: t('actions.view_history'),
icon: History,
priority: 'secondary',
2025-10-29 06:58:05 +01:00
onClick: () => handleViewMaintenanceHistory(equipment)
2025-09-23 19:24:22 +02:00
},
{
label: t('actions.schedule_maintenance'),
icon: Wrench,
priority: 'secondary',
2025-10-29 06:58:05 +01:00
highlighted: true,
onClick: () => handleScheduleMaintenance(equipment)
},
{
label: t('actions.delete'),
icon: Trash2,
priority: 'secondary',
destructive: true,
onClick: () => handleDeleteEquipment(equipment)
2025-09-23 19:24:22 +02:00
}
]}
/>
);
})}
</div>
{/* Empty State */}
{filteredEquipment.length === 0 && (
2025-10-27 16:33:26 +01:00
<EmptyState
icon={Settings}
title={t('common:forms.no_results')}
description={t('common:forms.empty_state')}
actionLabel={t('actions.add_equipment')}
actionIcon={Plus}
onAction={handleCreateEquipment}
/>
2025-09-23 19:24:22 +02:00
)}
2025-10-29 06:58:05 +01:00
{/* Equipment Modal - Used for View Details, Edit, and Create */}
2025-09-24 15:58:18 +02:00
{showEquipmentModal && (
<EquipmentModal
isOpen={showEquipmentModal}
onClose={() => {
setShowEquipmentModal(false);
setSelectedEquipment(null);
}}
equipment={selectedEquipment}
onSave={handleSaveEquipment}
mode={equipmentModalMode}
/>
)}
2025-10-29 06:58:05 +01:00
{/* Delete Equipment Modal */}
{showDeleteModal && equipmentForAction && (
<DeleteEquipmentModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSoftDelete={handleSoftDelete}
onHardDelete={handleHardDelete}
isLoading={deleteEquipmentMutation.isPending || hardDeleteEquipmentMutation.isPending}
/>
)}
{/* Maintenance History Modal */}
{showHistoryModal && equipmentForAction && (
<MaintenanceHistoryModal
isOpen={showHistoryModal}
onClose={() => {
setShowHistoryModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
/>
)}
{/* Schedule Maintenance Modal */}
{showScheduleModal && equipmentForAction && (
<ScheduleMaintenanceModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSchedule={handleScheduleMaintenanceSubmit}
isLoading={updateEquipmentMutation.isPending}
/>
)}
2025-09-23 19:24:22 +02:00
</div>
);
};
2025-10-27 16:33:26 +01:00
export default MaquinariaPage;