604 lines
24 KiB
TypeScript
604 lines
24 KiB
TypeScript
|
|
import React, { useState, useMemo } from 'react';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import { Plus, AlertTriangle, Settings, CheckCircle, Eye, Clock, Zap, Wrench, Thermometer, Activity, Search, Filter, Download, TrendingUp, Bell, History, Calendar, Edit, Trash2 } from 'lucide-react';
|
||
|
|
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
|
||
|
|
import { LoadingSpinner } from '../../../../components/shared';
|
||
|
|
import { PageHeader } from '../../../../components/layout';
|
||
|
|
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||
|
|
import { Equipment } from '../../../../types/equipment';
|
||
|
|
|
||
|
|
const MOCK_EQUIPMENT: Equipment[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
name: 'Horno Principal #1',
|
||
|
|
type: 'oven',
|
||
|
|
model: 'Miwe Condo CO 4.1212',
|
||
|
|
serialNumber: 'MCO-2021-001',
|
||
|
|
location: 'Área de Horneado - Zona A',
|
||
|
|
status: 'operational',
|
||
|
|
installDate: '2021-03-15',
|
||
|
|
lastMaintenance: '2024-01-15',
|
||
|
|
nextMaintenance: '2024-04-15',
|
||
|
|
maintenanceInterval: 90,
|
||
|
|
temperature: 220,
|
||
|
|
targetTemperature: 220,
|
||
|
|
efficiency: 92,
|
||
|
|
uptime: 98.5,
|
||
|
|
energyUsage: 45.2,
|
||
|
|
utilizationToday: 87,
|
||
|
|
alerts: [],
|
||
|
|
maintenanceHistory: [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
date: '2024-01-15',
|
||
|
|
type: 'preventive',
|
||
|
|
description: 'Limpieza general y calibración de termostatos',
|
||
|
|
technician: 'Juan Pérez',
|
||
|
|
cost: 150,
|
||
|
|
downtime: 2,
|
||
|
|
partsUsed: ['Filtros de aire', 'Sellos de puerta']
|
||
|
|
}
|
||
|
|
],
|
||
|
|
specifications: {
|
||
|
|
power: 45,
|
||
|
|
capacity: 24,
|
||
|
|
dimensions: { width: 200, height: 180, depth: 120 },
|
||
|
|
weight: 850
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
name: 'Batidora Industrial #2',
|
||
|
|
type: 'mixer',
|
||
|
|
model: 'Hobart HL800',
|
||
|
|
serialNumber: 'HHL-2020-002',
|
||
|
|
location: 'Área de Preparación - Zona B',
|
||
|
|
status: 'warning',
|
||
|
|
installDate: '2020-08-10',
|
||
|
|
lastMaintenance: '2024-01-20',
|
||
|
|
nextMaintenance: '2024-02-20',
|
||
|
|
maintenanceInterval: 30,
|
||
|
|
efficiency: 88,
|
||
|
|
uptime: 94.2,
|
||
|
|
energyUsage: 12.8,
|
||
|
|
utilizationToday: 76,
|
||
|
|
alerts: [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
type: 'warning',
|
||
|
|
message: 'Vibración inusual detectada en el motor',
|
||
|
|
timestamp: '2024-01-23T10:30:00Z',
|
||
|
|
acknowledged: false
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
type: 'info',
|
||
|
|
message: 'Mantenimiento programado en 5 días',
|
||
|
|
timestamp: '2024-01-23T08:00:00Z',
|
||
|
|
acknowledged: true
|
||
|
|
}
|
||
|
|
],
|
||
|
|
maintenanceHistory: [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
date: '2024-01-20',
|
||
|
|
type: 'corrective',
|
||
|
|
description: 'Reemplazo de correas de transmisión',
|
||
|
|
technician: 'María González',
|
||
|
|
cost: 85,
|
||
|
|
downtime: 4,
|
||
|
|
partsUsed: ['Correa tipo V', 'Rodamientos']
|
||
|
|
}
|
||
|
|
],
|
||
|
|
specifications: {
|
||
|
|
power: 15,
|
||
|
|
capacity: 80,
|
||
|
|
dimensions: { width: 120, height: 150, depth: 80 },
|
||
|
|
weight: 320
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
name: 'Cámara de Fermentación #1',
|
||
|
|
type: 'proofer',
|
||
|
|
model: 'Bongard EUROPA 16.18',
|
||
|
|
serialNumber: 'BEU-2022-001',
|
||
|
|
location: 'Área de Fermentación',
|
||
|
|
status: 'maintenance',
|
||
|
|
installDate: '2022-06-20',
|
||
|
|
lastMaintenance: '2024-01-23',
|
||
|
|
nextMaintenance: '2024-01-24',
|
||
|
|
maintenanceInterval: 60,
|
||
|
|
temperature: 32,
|
||
|
|
targetTemperature: 35,
|
||
|
|
efficiency: 0,
|
||
|
|
uptime: 85.1,
|
||
|
|
energyUsage: 0,
|
||
|
|
utilizationToday: 0,
|
||
|
|
alerts: [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
type: 'info',
|
||
|
|
message: 'En mantenimiento programado',
|
||
|
|
timestamp: '2024-01-23T06:00:00Z',
|
||
|
|
acknowledged: true
|
||
|
|
}
|
||
|
|
],
|
||
|
|
maintenanceHistory: [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
date: '2024-01-23',
|
||
|
|
type: 'preventive',
|
||
|
|
description: 'Mantenimiento programado - sistema de humidificación',
|
||
|
|
technician: 'Carlos Rodríguez',
|
||
|
|
cost: 200,
|
||
|
|
downtime: 8,
|
||
|
|
partsUsed: ['Sensor de humedad', 'Válvulas']
|
||
|
|
}
|
||
|
|
],
|
||
|
|
specifications: {
|
||
|
|
power: 8,
|
||
|
|
capacity: 16,
|
||
|
|
dimensions: { width: 180, height: 200, depth: 100 },
|
||
|
|
weight: 450
|
||
|
|
}
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const MaquinariaPage: React.FC = () => {
|
||
|
|
const { t } = useTranslation(['equipment', 'common']);
|
||
|
|
const [searchTerm, setSearchTerm] = useState('');
|
||
|
|
const [statusFilter, setStatusFilter] = useState<Equipment['status'] | 'all'>('all');
|
||
|
|
const [selectedItem, setSelectedItem] = useState<Equipment | null>(null);
|
||
|
|
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
|
||
|
|
|
||
|
|
const currentTenant = useCurrentTenant();
|
||
|
|
const tenantId = currentTenant?.id || '';
|
||
|
|
|
||
|
|
// Mock functions for equipment actions - these would be replaced with actual API calls
|
||
|
|
const handleCreateEquipment = () => {
|
||
|
|
console.log('Create new equipment');
|
||
|
|
// Implementation would go here
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleEditEquipment = (equipmentId: string) => {
|
||
|
|
console.log('Edit equipment:', equipmentId);
|
||
|
|
// Implementation would go here
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleScheduleMaintenance = (equipmentId: string) => {
|
||
|
|
console.log('Schedule maintenance for equipment:', equipmentId);
|
||
|
|
// Implementation would go here
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAcknowledgeAlert = (equipmentId: string, alertId: string) => {
|
||
|
|
console.log('Acknowledge alert:', alertId, 'for equipment:', equipmentId);
|
||
|
|
// Implementation would go here
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleViewMaintenanceHistory = (equipmentId: string) => {
|
||
|
|
console.log('View maintenance history for equipment:', equipmentId);
|
||
|
|
// Implementation would go here
|
||
|
|
};
|
||
|
|
|
||
|
|
const filteredEquipment = useMemo(() => {
|
||
|
|
return MOCK_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 === 'all' || eq.status === statusFilter;
|
||
|
|
|
||
|
|
return matchesSearch && matchesStatus;
|
||
|
|
});
|
||
|
|
}, [MOCK_EQUIPMENT, searchTerm, statusFilter]);
|
||
|
|
|
||
|
|
const equipmentStats = useMemo(() => {
|
||
|
|
const total = MOCK_EQUIPMENT.length;
|
||
|
|
const operational = MOCK_EQUIPMENT.filter(e => e.status === 'operational').length;
|
||
|
|
const warning = MOCK_EQUIPMENT.filter(e => e.status === 'warning').length;
|
||
|
|
const maintenance = MOCK_EQUIPMENT.filter(e => e.status === 'maintenance').length;
|
||
|
|
const down = MOCK_EQUIPMENT.filter(e => e.status === 'down').length;
|
||
|
|
const avgEfficiency = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.efficiency, 0) / total;
|
||
|
|
const avgUptime = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.uptime, 0) / total;
|
||
|
|
const totalAlerts = MOCK_EQUIPMENT.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
|
||
|
|
|
||
|
|
return {
|
||
|
|
total,
|
||
|
|
operational,
|
||
|
|
warning,
|
||
|
|
maintenance,
|
||
|
|
down,
|
||
|
|
avgEfficiency,
|
||
|
|
avgUptime,
|
||
|
|
totalAlerts
|
||
|
|
};
|
||
|
|
}, [MOCK_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];
|
||
|
|
};
|
||
|
|
|
||
|
|
const getTypeIcon = (type: Equipment['type']) => {
|
||
|
|
const icons = {
|
||
|
|
oven: Thermometer,
|
||
|
|
mixer: Activity,
|
||
|
|
proofer: Settings,
|
||
|
|
freezer: Zap,
|
||
|
|
packaging: Settings,
|
||
|
|
other: Settings
|
||
|
|
};
|
||
|
|
return icons[type];
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStatusColor = (status: string): string => {
|
||
|
|
const statusColors: { [key: string]: string } = {
|
||
|
|
operational: '#10B981', // green-500
|
||
|
|
warning: '#F59E0B', // amber-500
|
||
|
|
maintenance: '#3B82F6', // blue-500
|
||
|
|
down: '#EF4444' // red-500
|
||
|
|
};
|
||
|
|
return statusColors[status] || '#6B7280'; // gray-500 as default
|
||
|
|
};
|
||
|
|
|
||
|
|
const stats = [
|
||
|
|
{
|
||
|
|
title: t('labels.total_equipment'),
|
||
|
|
value: equipmentStats.total,
|
||
|
|
icon: Settings,
|
||
|
|
variant: 'default' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: t('labels.operational'),
|
||
|
|
value: equipmentStats.operational,
|
||
|
|
icon: CheckCircle,
|
||
|
|
variant: 'success' as const,
|
||
|
|
subtitle: `${((equipmentStats.operational / equipmentStats.total) * 100).toFixed(1)}%`
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: t('labels.avg_efficiency'),
|
||
|
|
value: `${equipmentStats.avgEfficiency.toFixed(1)}%`,
|
||
|
|
icon: TrendingUp,
|
||
|
|
variant: equipmentStats.avgEfficiency >= 90 ? 'success' as const : 'warning' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: t('labels.active_alerts'),
|
||
|
|
value: equipmentStats.totalAlerts,
|
||
|
|
icon: Bell,
|
||
|
|
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const handleShowMaintenanceDetails = (equipment: Equipment) => {
|
||
|
|
setSelectedItem(equipment);
|
||
|
|
setShowMaintenanceModal(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCloseMaintenanceModal = () => {
|
||
|
|
setShowMaintenanceModal(false);
|
||
|
|
setSelectedItem(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Loading state
|
||
|
|
if (!tenantId) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center min-h-64">
|
||
|
|
<LoadingSpinner text="Cargando datos..." />
|
||
|
|
</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}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Controls */}
|
||
|
|
<Card className="p-4">
|
||
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
||
|
|
<div className="flex-1">
|
||
|
|
<div className="relative">
|
||
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||
|
|
<Input
|
||
|
|
placeholder={t('common:forms.search_placeholder')}
|
||
|
|
value={searchTerm}
|
||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||
|
|
className="pl-10"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Filter className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||
|
|
<select
|
||
|
|
value={statusFilter}
|
||
|
|
onChange={(e) => setStatusFilter(e.target.value as Equipment['status'] | 'all')}
|
||
|
|
className="px-3 py-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||
|
|
>
|
||
|
|
<option value="all">{t('common:forms.select_option')}</option>
|
||
|
|
<option value="operational">{t('equipment_status.operational')}</option>
|
||
|
|
<option value="warning">{t('equipment_status.warning')}</option>
|
||
|
|
<option value="maintenance">{t('equipment_status.maintenance')}</option>
|
||
|
|
<option value="down">{t('equipment_status.down')}</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 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)}%`
|
||
|
|
}}
|
||
|
|
actions={[
|
||
|
|
{
|
||
|
|
label: t('actions.view_details'),
|
||
|
|
icon: Eye,
|
||
|
|
variant: 'primary',
|
||
|
|
priority: 'primary',
|
||
|
|
onClick: () => handleShowMaintenanceDetails(equipment)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: t('actions.view_history'),
|
||
|
|
icon: History,
|
||
|
|
priority: 'secondary',
|
||
|
|
onClick: () => handleViewMaintenanceHistory(equipment.id)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: t('actions.schedule_maintenance'),
|
||
|
|
icon: Wrench,
|
||
|
|
priority: 'secondary',
|
||
|
|
onClick: () => handleScheduleMaintenance(equipment.id)
|
||
|
|
}
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Empty State */}
|
||
|
|
{filteredEquipment.length === 0 && (
|
||
|
|
<div className="text-center py-12">
|
||
|
|
<Settings 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">
|
||
|
|
{t('common:forms.no_results')}
|
||
|
|
</h3>
|
||
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
||
|
|
{t('common:forms.empty_state')}
|
||
|
|
</p>
|
||
|
|
<Button
|
||
|
|
onClick={handleCreateEquipment}
|
||
|
|
variant="primary"
|
||
|
|
size="md"
|
||
|
|
className="font-medium px-4 sm:px-6 py-2 sm:py-3 shadow-sm hover:shadow-md transition-all duration-200"
|
||
|
|
>
|
||
|
|
<Plus className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2 flex-shrink-0" />
|
||
|
|
<span className="text-sm sm:text-base">{t('actions.add_equipment')}</span>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Maintenance Details Modal */}
|
||
|
|
{selectedItem && showMaintenanceModal && (
|
||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
||
|
|
<div className="bg-[var(--bg-primary)] rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto my-8">
|
||
|
|
<div className="p-4 sm:p-6">
|
||
|
|
<div className="flex items-center justify-between mb-4 sm:mb-6">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-lg sm:text-xl font-semibold text-[var(--text-primary)]">
|
||
|
|
{selectedItem.name}
|
||
|
|
</h2>
|
||
|
|
<p className="text-[var(--text-secondary)] text-sm">
|
||
|
|
{selectedItem.model} - {selectedItem.serialNumber}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={handleCloseMaintenanceModal}
|
||
|
|
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] p-1"
|
||
|
|
>
|
||
|
|
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-4 sm:space-y-6">
|
||
|
|
{/* Equipment Status */}
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||
|
|
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||
|
|
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.status')}</h3>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<div
|
||
|
|
className="w-2 h-2 sm:w-3 sm:h-3 rounded-full"
|
||
|
|
style={{ backgroundColor: getStatusConfig(selectedItem.status).color }}
|
||
|
|
/>
|
||
|
|
<span className="text-[var(--text-primary)] text-sm sm:text-base">
|
||
|
|
{t(`equipment_status.${selectedItem.status}`)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||
|
|
<h3 className="font-medium text-[var(--text-primary)] mb-2 text-sm sm:text-base">{t('fields.efficiency')}</h3>
|
||
|
|
<div className="text-lg sm:text-2xl font-bold text-[var(--text-primary)]">
|
||
|
|
{selectedItem.efficiency}%
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Maintenance Information */}
|
||
|
|
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||
|
|
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.title')}</h3>
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||
|
|
<div>
|
||
|
|
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.last')}</p>
|
||
|
|
<p className="text-[var(--text-primary)] text-sm sm:text-base">
|
||
|
|
{new Date(selectedItem.lastMaintenance).toLocaleDateString('es-ES')}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.next')}</p>
|
||
|
|
<p className={`font-medium ${(new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime()) ? 'text-red-500' : 'text-[var(--text-primary)]'} text-sm sm:text-base`}>
|
||
|
|
{new Date(selectedItem.nextMaintenance).toLocaleDateString('es-ES')}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-xs sm:text-sm text-[var(--text-secondary)]">{t('maintenance.interval')}</p>
|
||
|
|
<p className="text-[var(--text-primary)] text-sm sm:text-base">
|
||
|
|
{selectedItem.maintenanceInterval} {t('common:units.days')}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{new Date(selectedItem.nextMaintenance).getTime() < new Date().getTime() && (
|
||
|
|
<div className="mt-3 p-2 bg-red-50 dark:bg-red-900/20 rounded border-l-2 border-red-500">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<AlertTriangle className="w-4 h-4 text-red-500" />
|
||
|
|
<span className="text-xs sm:text-sm font-medium text-red-700 dark:text-red-300">
|
||
|
|
{t('maintenance.overdue')}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Active Alerts */}
|
||
|
|
{selectedItem.alerts.filter(a => !a.acknowledged).length > 0 && (
|
||
|
|
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||
|
|
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('alerts.title')}</h3>
|
||
|
|
<div className="space-y-2 sm:space-y-3">
|
||
|
|
{selectedItem.alerts.filter(a => !a.acknowledged).map((alert) => (
|
||
|
|
<div
|
||
|
|
key={alert.id}
|
||
|
|
className={`p-2 sm:p-3 rounded border-l-2 ${
|
||
|
|
alert.type === 'critical' ? 'bg-red-50 border-red-500 dark:bg-red-900/20' :
|
||
|
|
alert.type === 'warning' ? 'bg-orange-50 border-orange-500 dark:bg-orange-900/20' :
|
||
|
|
'bg-blue-50 border-blue-500 dark:bg-blue-900/20'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<AlertTriangle className={`w-3 h-3 sm:w-4 sm:h-4 ${
|
||
|
|
alert.type === 'critical' ? 'text-red-500' :
|
||
|
|
alert.type === 'warning' ? 'text-orange-500' : 'text-blue-500'
|
||
|
|
}`} />
|
||
|
|
<span className="font-medium text-[var(--text-primary)] text-xs sm:text-sm">
|
||
|
|
{alert.message}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<span className="text-xs text-[var(--text-secondary)] hidden sm:block">
|
||
|
|
{new Date(alert.timestamp).toLocaleString('es-ES')}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Maintenance History */}
|
||
|
|
<div className="p-3 sm:p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||
|
|
<h3 className="font-medium text-[var(--text-primary)] mb-3 sm:mb-4 text-sm sm:text-base">{t('maintenance.history')}</h3>
|
||
|
|
<div className="space-y-3 sm:space-y-4">
|
||
|
|
{selectedItem.maintenanceHistory.map((history) => (
|
||
|
|
<div key={history.id} className="border-b border-[var(--border-primary)] pb-2 sm:pb-3 last:border-0 last:pb-0">
|
||
|
|
<div className="flex justify-between items-start">
|
||
|
|
<div>
|
||
|
|
<h4 className="font-medium text-[var(--text-primary)] text-sm">{history.description}</h4>
|
||
|
|
<p className="text-xs text-[var(--text-secondary)]">
|
||
|
|
{new Date(history.date).toLocaleDateString('es-ES')} - {history.technician}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<span className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
|
||
|
|
{t(`maintenance.type.${history.type}`)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="mt-1 sm:mt-2 flex flex-wrap gap-2">
|
||
|
|
<span className="text-xs">
|
||
|
|
<span className="text-[var(--text-secondary)]">{t('common:actions.cost')}:</span>
|
||
|
|
<span className="font-medium text-[var(--text-primary)]"> €{history.cost}</span>
|
||
|
|
</span>
|
||
|
|
<span className="text-xs">
|
||
|
|
<span className="text-[var(--text-secondary)]">{t('fields.uptime')}:</span>
|
||
|
|
<span className="font-medium text-[var(--text-primary)]"> {history.downtime}h</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
{history.partsUsed.length > 0 && (
|
||
|
|
<div className="mt-1 sm:mt-2">
|
||
|
|
<span className="text-xs text-[var(--text-secondary)]">{t('fields.parts')}:</span>
|
||
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
||
|
|
{history.partsUsed.map((part, index) => (
|
||
|
|
<span key={index} className="px-1.5 py-0.5 sm:px-2 sm:py-1 bg-[var(--bg-tertiary)] text-xs rounded">
|
||
|
|
{part}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex justify-end space-x-2 sm:space-x-3 mt-4 sm:mt-6">
|
||
|
|
<Button variant="outline" size="sm" onClick={handleCloseMaintenanceModal}>
|
||
|
|
{t('common:actions.close')}
|
||
|
|
</Button>
|
||
|
|
<Button variant="primary" size="sm" onClick={() => selectedItem && handleScheduleMaintenance(selectedItem.id)}>
|
||
|
|
{t('actions.schedule_maintenance')}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default MaquinariaPage;
|