Files
bakery-ia/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx
2026-01-11 17:03:46 +01:00

603 lines
20 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, AlertTriangle, Settings, CheckCircle, Eye, Wrench, Thermometer, Activity, Search, Filter, Bell, History, Calendar, Edit, Trash2 } from 'lucide-react';
import { Button, StatsGrid, StatusCard, getStatusColor, SearchAndFilter, type FilterConfig, EmptyState } from '../../../../components/ui';
import { Badge } from '../../../../components/ui/Badge';
import { LoadingSpinner } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { Equipment } from '../../../../api/types/equipment';
import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal';
import { DeleteEquipmentModal } from '../../../../components/domain/equipment/DeleteEquipmentModal';
import { MaintenanceHistoryModal } from '../../../../components/domain/equipment/MaintenanceHistoryModal';
import { ScheduleMaintenanceModal, type MaintenanceScheduleData } from '../../../../components/domain/equipment/ScheduleMaintenanceModal';
import { ReportFailureModal } from '../../../../components/domain/equipment/ReportFailureModal';
import { MarkAsRepairedModal } from '../../../../components/domain/equipment/MarkAsRepairedModal';
import { useEquipment, useCreateEquipment, useUpdateEquipment, useDeleteEquipment, useHardDeleteEquipment } from '../../../../api/hooks/equipment';
import { equipmentService } from '../../../../api/services/equipment';
import { useQueryClient } from '@tanstack/react-query';
const MaquinariaPage: React.FC = () => {
const queryClient = useQueryClient();
const { t } = useTranslation(['equipment', 'common']);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create');
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
// New modal states
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showReportFailureModal, setShowReportFailureModal] = useState(false);
const [showMarkRepairedModal, setShowMarkRepairedModal] = useState(false);
const [equipmentForAction, setEquipmentForAction] = useState<Equipment | null>(null);
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Fetch equipment data from API
const { data: equipment = [], isLoading, error } = useEquipment(tenantId, {
is_active: true
});
// Mutations for create, update, and delete
const createEquipmentMutation = useCreateEquipment(tenantId);
const updateEquipmentMutation = useUpdateEquipment(tenantId);
const deleteEquipmentMutation = useDeleteEquipment(tenantId);
const hardDeleteEquipmentMutation = useHardDeleteEquipment(tenantId);
const handleCreateEquipment = () => {
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);
};
const handleEditEquipment = (equipmentId: string) => {
// Find the equipment to edit from real data
const equipmentToEdit = equipment.find(eq => eq.id === equipmentId);
if (equipmentToEdit) {
setSelectedEquipment(equipmentToEdit);
setEquipmentModalMode('edit');
setShowEquipmentModal(true);
}
};
const handleReportFailure = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowReportFailureModal(true);
};
const handleMarkAsRepaired = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowMarkRepairedModal(true);
};
const handleScheduleMaintenance = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowScheduleModal(true);
};
const handleReportFailureSubmit = async (equipmentId: string, failureData: {
failureType: string;
severity: string;
description: string;
photos?: File[];
estimatedImpact: boolean;
}) => {
try {
// Use the new API endpoint
await equipmentService.reportEquipmentFailure(
tenantId,
equipmentId,
failureData
);
// Refresh equipment data
await queryClient.invalidateQueries({ queryKey: ['equipment', tenantId] });
setShowReportFailureModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error reporting failure:', error);
throw error;
}
};
const handleMarkAsRepairedSubmit = async (equipmentId: string, repairData: {
repairDate: string;
technicianName: string;
repairDescription: string;
partsReplaced: string[];
cost: number;
photos?: File[];
testResults: boolean;
}) => {
try {
// Use the new API endpoint
await equipmentService.markEquipmentAsRepaired(
tenantId,
equipmentId,
repairData
);
// Refresh equipment data
await queryClient.invalidateQueries({ queryKey: ['equipment', tenantId] });
setShowMarkRepairedModal(false);
setEquipmentForAction(null);
} catch (error) {
console.error('Error marking as repaired:', error);
throw error;
}
};
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;
}
};
const handleViewMaintenanceHistory = (equipment: Equipment) => {
setEquipmentForAction(equipment);
setShowHistoryModal(true);
};
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;
}
};
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
}
};
const filteredEquipment = useMemo(() => {
return equipment.filter(eq => {
const matchesSearch = !searchTerm ||
eq.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
eq.type.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = !statusFilter || eq.status === statusFilter;
const matchesType = !typeFilter || eq.type === typeFilter;
return matchesSearch && matchesStatus && matchesType;
});
}, [equipment, searchTerm, statusFilter, typeFilter]);
const equipmentStats = useMemo(() => {
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);
return {
total,
operational,
warning,
maintenance,
down,
totalAlerts
};
}, [equipment]);
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 }
};
return configs[status] || { color: getStatusColor('other'), text: status, icon: Settings };
};
const getTypeIcon = (type: Equipment['type']) => {
const icons = {
oven: Thermometer,
mixer: Activity,
proofer: Settings,
freezer: Settings,
packaging: Settings,
other: Settings
};
return icons[type];
};
const stats = [
{
title: t('labels.total_equipment'),
value: equipmentStats.total,
variant: 'default' as const,
icon: Settings,
},
{
title: t('labels.operational'),
value: equipmentStats.operational,
variant: 'success' as const,
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,
},
{
title: t('labels.active_alerts'),
value: equipmentStats.totalAlerts,
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const,
icon: Bell,
},
];
const handleShowMaintenanceDetails = (equipment: Equipment) => {
setSelectedEquipment(equipment);
setEquipmentModalMode('view');
setShowEquipmentModal(true);
};
// Loading state
if (!tenantId) {
return (
<div className="flex items-center justify-center min-h-64">
<LoadingSpinner text="Cargando datos..." />
</div>
);
}
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>
);
}
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}
/>
{/* 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[]}
/>
{/* 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)}%`
}}
onClick={() => handleShowMaintenanceDetails(equipment)}
actions={[
{
label: t('actions.view_details'),
icon: Eye,
variant: 'primary',
priority: 'primary',
onClick: () => handleShowMaintenanceDetails(equipment)
},
// Context-aware actions based on equipment status
...(equipment.status === 'operational' || equipment.status === 'warning' ? [
{
label: t('actions.report_failure'),
icon: AlertTriangle,
variant: 'outline' as const,
priority: 'secondary' as const,
highlighted: true,
destructive: true,
onClick: () => handleReportFailure(equipment)
}
] : []),
...(equipment.status === 'down' ? [
{
label: t('actions.mark_repaired'),
icon: CheckCircle,
variant: 'primary' as const,
priority: 'secondary' as const,
highlighted: true,
onClick: () => handleMarkAsRepaired(equipment)
}
] : []),
{
label: t('actions.view_history'),
icon: History,
priority: 'secondary',
onClick: () => handleViewMaintenanceHistory(equipment)
},
{
label: t('actions.schedule_maintenance'),
icon: Wrench,
priority: 'secondary',
highlighted: true,
onClick: () => handleScheduleMaintenance(equipment)
},
{
label: t('actions.delete'),
icon: Trash2,
priority: 'secondary',
destructive: true,
onClick: () => handleDeleteEquipment(equipment)
}
]}
/>
);
})}
</div>
{/* Empty State */}
{filteredEquipment.length === 0 && (
<EmptyState
icon={Settings}
title={t('common:forms.no_results')}
description={t('common:forms.empty_state')}
actionLabel={t('actions.add_equipment')}
actionIcon={Plus}
onAction={handleCreateEquipment}
/>
)}
{/* Equipment Modal - Used for View Details, Edit, and Create */}
{showEquipmentModal && (
<EquipmentModal
isOpen={showEquipmentModal}
onClose={() => {
setShowEquipmentModal(false);
setSelectedEquipment(null);
}}
equipment={selectedEquipment}
onSave={handleSaveEquipment}
mode={equipmentModalMode}
/>
)}
{/* 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}
/>
)}
{/* Report Failure Modal */}
{showReportFailureModal && equipmentForAction && (
<ReportFailureModal
isOpen={showReportFailureModal}
onClose={() => {
setShowReportFailureModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onReportFailure={(failureData) => handleReportFailureSubmit(equipmentForAction.id, failureData)}
isLoading={updateEquipmentMutation.isPending}
/>
)}
{/* Mark as Repaired Modal */}
{showMarkRepairedModal && equipmentForAction && (
<MarkAsRepairedModal
isOpen={showMarkRepairedModal}
onClose={() => {
setShowMarkRepairedModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onMarkAsRepaired={(repairData) => handleMarkAsRepairedSubmit(equipmentForAction.id, repairData)}
isLoading={updateEquipmentMutation.isPending}
/>
)}
{/* Schedule Maintenance Modal */}
{showScheduleModal && equipmentForAction && (
<ScheduleMaintenanceModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setEquipmentForAction(null);
}}
equipment={equipmentForAction}
onSchedule={handleScheduleMaintenanceSubmit}
isLoading={updateEquipmentMutation.isPending}
/>
)}
</div>
);
};
export default MaquinariaPage;