632 lines
24 KiB
TypeScript
632 lines
24 KiB
TypeScript
|
|
import React, { useState, useMemo } from 'react';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { Card, CardHeader, CardBody } from '../../ui/Card';
|
|||
|
|
import { Button } from '../../ui/Button';
|
|||
|
|
import { Input } from '../../ui/Input';
|
|||
|
|
import { Badge } from '../../ui/Badge';
|
|||
|
|
import { Modal } from '../../ui/Modal';
|
|||
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../ui/Tabs';
|
|||
|
|
import { StatsGrid } from '../../ui/Stats';
|
|||
|
|
import {
|
|||
|
|
Settings,
|
|||
|
|
AlertTriangle,
|
|||
|
|
CheckCircle,
|
|||
|
|
Wrench,
|
|||
|
|
Calendar,
|
|||
|
|
Clock,
|
|||
|
|
Thermometer,
|
|||
|
|
Activity,
|
|||
|
|
Zap,
|
|||
|
|
TrendingUp,
|
|||
|
|
Search,
|
|||
|
|
Plus,
|
|||
|
|
Filter,
|
|||
|
|
Download,
|
|||
|
|
BarChart3,
|
|||
|
|
Bell,
|
|||
|
|
MapPin,
|
|||
|
|
User
|
|||
|
|
} from 'lucide-react';
|
|||
|
|
import { useCurrentTenant } from '../../../stores/tenant.store';
|
|||
|
|
|
|||
|
|
export interface Equipment {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
type: 'oven' | 'mixer' | 'proofer' | 'freezer' | 'packaging' | 'other';
|
|||
|
|
model: string;
|
|||
|
|
serialNumber: string;
|
|||
|
|
location: string;
|
|||
|
|
status: 'operational' | 'maintenance' | 'down' | 'warning';
|
|||
|
|
installDate: string;
|
|||
|
|
lastMaintenance: string;
|
|||
|
|
nextMaintenance: string;
|
|||
|
|
maintenanceInterval: number; // days
|
|||
|
|
temperature?: number;
|
|||
|
|
targetTemperature?: number;
|
|||
|
|
efficiency: number;
|
|||
|
|
uptime: number;
|
|||
|
|
energyUsage: number;
|
|||
|
|
utilizationToday: number;
|
|||
|
|
alerts: Array<{
|
|||
|
|
id: string;
|
|||
|
|
type: 'warning' | 'critical' | 'info';
|
|||
|
|
message: string;
|
|||
|
|
timestamp: string;
|
|||
|
|
acknowledged: boolean;
|
|||
|
|
}>;
|
|||
|
|
maintenanceHistory: Array<{
|
|||
|
|
id: string;
|
|||
|
|
date: string;
|
|||
|
|
type: 'preventive' | 'corrective' | 'emergency';
|
|||
|
|
description: string;
|
|||
|
|
technician: string;
|
|||
|
|
cost: number;
|
|||
|
|
downtime: number; // hours
|
|||
|
|
partsUsed: string[];
|
|||
|
|
}>;
|
|||
|
|
specifications: {
|
|||
|
|
power: number; // kW
|
|||
|
|
capacity: number;
|
|||
|
|
dimensions: {
|
|||
|
|
width: number;
|
|||
|
|
height: number;
|
|||
|
|
depth: number;
|
|||
|
|
};
|
|||
|
|
weight: number;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface EquipmentManagerProps {
|
|||
|
|
className?: string;
|
|||
|
|
equipment?: Equipment[];
|
|||
|
|
onCreateEquipment?: () => void;
|
|||
|
|
onEditEquipment?: (equipmentId: string) => void;
|
|||
|
|
onScheduleMaintenance?: (equipmentId: string) => void;
|
|||
|
|
onAcknowledgeAlert?: (equipmentId: string, alertId: string) => void;
|
|||
|
|
onViewMaintenanceHistory?: (equipmentId: string) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const MOCK_EQUIPMENT: Equipment[] = [
|
|||
|
|
{
|
|||
|
|
id: '1',
|
|||
|
|
name: 'Horno Principal #1',
|
|||
|
|
type: 'oven',
|
|||
|
|
model: 'Miwe Condo CO 4.1212',
|
|||
|
|
serialNumber: 'MCO-2021-001',
|
|||
|
|
location: '<27>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<63>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: '<27>rea de Preparaci<63>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<63>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<73>n',
|
|||
|
|
technician: 'Mar<61>a Gonz<6E>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<63>n #1',
|
|||
|
|
type: 'proofer',
|
|||
|
|
model: 'Bongard EUROPA 16.18',
|
|||
|
|
serialNumber: 'BEU-2022-001',
|
|||
|
|
location: '<27>rea de Fermentaci<63>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<63>n',
|
|||
|
|
technician: 'Carlos Rodr<64>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 EquipmentManager: React.FC<EquipmentManagerProps> = ({
|
|||
|
|
className,
|
|||
|
|
equipment = MOCK_EQUIPMENT,
|
|||
|
|
onCreateEquipment,
|
|||
|
|
onEditEquipment,
|
|||
|
|
onScheduleMaintenance,
|
|||
|
|
onAcknowledgeAlert,
|
|||
|
|
onViewMaintenanceHistory
|
|||
|
|
}) => {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [activeTab, setActiveTab] = useState('overview');
|
|||
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|||
|
|
const [statusFilter, setStatusFilter] = useState<Equipment['status'] | 'all'>('all');
|
|||
|
|
const [selectedEquipment, setSelectedEquipment] = useState<Equipment | null>(null);
|
|||
|
|
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
|
|||
|
|
|
|||
|
|
const filteredEquipment = useMemo(() => {
|
|||
|
|
return equipment.filter(eq => {
|
|||
|
|
const matchesSearch = !searchQuery ||
|
|||
|
|
eq.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|||
|
|
eq.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|||
|
|
eq.type.toLowerCase().includes(searchQuery.toLowerCase());
|
|||
|
|
|
|||
|
|
const matchesStatus = statusFilter === 'all' || eq.status === statusFilter;
|
|||
|
|
|
|||
|
|
return matchesSearch && matchesStatus;
|
|||
|
|
});
|
|||
|
|
}, [equipment, searchQuery, statusFilter]);
|
|||
|
|
|
|||
|
|
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 avgEfficiency = equipment.reduce((sum, e) => sum + e.efficiency, 0) / total;
|
|||
|
|
const avgUptime = equipment.reduce((sum, e) => sum + e.uptime, 0) / total;
|
|||
|
|
const totalAlerts = equipment.reduce((sum, e) => sum + e.alerts.filter(a => !a.acknowledged).length, 0);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
total,
|
|||
|
|
operational,
|
|||
|
|
warning,
|
|||
|
|
maintenance,
|
|||
|
|
down,
|
|||
|
|
avgEfficiency,
|
|||
|
|
avgUptime,
|
|||
|
|
totalAlerts
|
|||
|
|
};
|
|||
|
|
}, [equipment]);
|
|||
|
|
|
|||
|
|
const getStatusConfig = (status: Equipment['status']) => {
|
|||
|
|
const configs = {
|
|||
|
|
operational: { color: 'success' as const, icon: CheckCircle, label: t('equipment.status.operational', 'Operational') },
|
|||
|
|
warning: { color: 'warning' as const, icon: AlertTriangle, label: t('equipment.status.warning', 'Warning') },
|
|||
|
|
maintenance: { color: 'info' as const, icon: Wrench, label: t('equipment.status.maintenance', 'Maintenance') },
|
|||
|
|
down: { color: 'error' as const, icon: AlertTriangle, label: t('equipment.status.down', 'Down') }
|
|||
|
|
};
|
|||
|
|
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 formatDateTime = (dateString: string) => {
|
|||
|
|
return new Date(dateString).toLocaleDateString('es-ES', {
|
|||
|
|
day: '2-digit',
|
|||
|
|
month: '2-digit',
|
|||
|
|
year: 'numeric'
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const stats = [
|
|||
|
|
{
|
|||
|
|
title: t('equipment.stats.total', 'Total Equipment'),
|
|||
|
|
value: equipmentStats.total,
|
|||
|
|
icon: Settings,
|
|||
|
|
variant: 'default' as const
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: t('equipment.stats.operational', 'Operational'),
|
|||
|
|
value: equipmentStats.operational,
|
|||
|
|
icon: CheckCircle,
|
|||
|
|
variant: 'success' as const,
|
|||
|
|
subtitle: `${((equipmentStats.operational / equipmentStats.total) * 100).toFixed(1)}%`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: t('equipment.stats.avg_efficiency', 'Avg Efficiency'),
|
|||
|
|
value: `${equipmentStats.avgEfficiency.toFixed(1)}%`,
|
|||
|
|
icon: TrendingUp,
|
|||
|
|
variant: equipmentStats.avgEfficiency >= 90 ? 'success' as const : 'warning' as const
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: t('equipment.stats.alerts', 'Active Alerts'),
|
|||
|
|
value: equipmentStats.totalAlerts,
|
|||
|
|
icon: Bell,
|
|||
|
|
variant: equipmentStats.totalAlerts === 0 ? 'success' as const : 'error' as const
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card className={className}>
|
|||
|
|
<CardHeader>
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
|||
|
|
{t('equipment.manager.title', 'Equipment Management')}
|
|||
|
|
</h3>
|
|||
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|||
|
|
{t('equipment.manager.subtitle', 'Monitor and manage production equipment')}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex space-x-2">
|
|||
|
|
<Button variant="outline" size="sm">
|
|||
|
|
<Download className="w-4 h-4 mr-2" />
|
|||
|
|
{t('equipment.actions.export', 'Export')}
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="primary" size="sm" onClick={onCreateEquipment}>
|
|||
|
|
<Plus className="w-4 h-4 mr-2" />
|
|||
|
|
{t('equipment.actions.add', 'Add Equipment')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardHeader>
|
|||
|
|
|
|||
|
|
<CardBody className="space-y-6">
|
|||
|
|
{/* Stats */}
|
|||
|
|
<StatsGrid stats={stats} columns={4} gap="md" />
|
|||
|
|
|
|||
|
|
{/* Filters */}
|
|||
|
|
<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('equipment.search.placeholder', 'Search equipment...')}
|
|||
|
|
value={searchQuery}
|
|||
|
|
onChange={(e) => setSearchQuery(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('equipment.filter.all', 'All Status')}</option>
|
|||
|
|
<option value="operational">{t('equipment.status.operational', 'Operational')}</option>
|
|||
|
|
<option value="warning">{t('equipment.status.warning', 'Warning')}</option>
|
|||
|
|
<option value="maintenance">{t('equipment.status.maintenance', 'Maintenance')}</option>
|
|||
|
|
<option value="down">{t('equipment.status.down', 'Down')}</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Equipment List */}
|
|||
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|||
|
|
<TabsList className="grid w-full grid-cols-3">
|
|||
|
|
<TabsTrigger value="overview">
|
|||
|
|
{t('equipment.tabs.overview', 'Overview')}
|
|||
|
|
</TabsTrigger>
|
|||
|
|
<TabsTrigger value="maintenance">
|
|||
|
|
{t('equipment.tabs.maintenance', 'Maintenance')}
|
|||
|
|
</TabsTrigger>
|
|||
|
|
<TabsTrigger value="alerts">
|
|||
|
|
{t('equipment.tabs.alerts', 'Alerts')}
|
|||
|
|
</TabsTrigger>
|
|||
|
|
</TabsList>
|
|||
|
|
|
|||
|
|
<TabsContent value="overview" className="space-y-4">
|
|||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|||
|
|
{filteredEquipment.map((eq) => {
|
|||
|
|
const statusConfig = getStatusConfig(eq.status);
|
|||
|
|
const TypeIcon = getTypeIcon(eq.type);
|
|||
|
|
const StatusIcon = statusConfig.icon;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={eq.id}
|
|||
|
|
className="p-4 bg-[var(--bg-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors cursor-pointer"
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedEquipment(eq);
|
|||
|
|
setShowEquipmentModal(true);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center justify-between mb-3">
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<TypeIcon className="w-5 h-5 text-[var(--color-primary)]" />
|
|||
|
|
<h4 className="font-semibold text-[var(--text-primary)]">{eq.name}</h4>
|
|||
|
|
</div>
|
|||
|
|
<Badge variant={statusConfig.color}>
|
|||
|
|
{statusConfig.label}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2 text-sm">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.efficiency', 'Efficiency')}:</span>
|
|||
|
|
<span className="font-medium">{eq.efficiency}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.uptime', 'Uptime')}:</span>
|
|||
|
|
<span className="font-medium">{eq.uptime.toFixed(1)}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.location', 'Location')}:</span>
|
|||
|
|
<span className="font-medium text-xs">{eq.location}</span>
|
|||
|
|
</div>
|
|||
|
|
{eq.temperature && (
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.temperature', 'Temperature')}:</span>
|
|||
|
|
<span className="font-medium">{eq.temperature}<EFBFBD>C</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{eq.alerts.filter(a => !a.acknowledged).length > 0 && (
|
|||
|
|
<div className="mt-3 p-2 bg-orange-50 dark:bg-orange-900/20 rounded border-l-2 border-orange-500">
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<AlertTriangle className="w-4 h-4 text-orange-500" />
|
|||
|
|
<span className="text-sm font-medium text-orange-700 dark:text-orange-300">
|
|||
|
|
{eq.alerts.filter(a => !a.acknowledged).length} {t('equipment.unread_alerts', 'unread alerts')}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="flex justify-between mt-4 pt-3 border-t border-[var(--border-primary)]">
|
|||
|
|
<Button
|
|||
|
|
variant="outline"
|
|||
|
|
size="xs"
|
|||
|
|
onClick={(e) => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
onEditEquipment?.(eq.id);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{t('common.edit', 'Edit')}
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
variant="primary"
|
|||
|
|
size="xs"
|
|||
|
|
onClick={(e) => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
onScheduleMaintenance?.(eq.id);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{t('equipment.actions.maintenance', 'Maintenance')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</TabsContent>
|
|||
|
|
|
|||
|
|
<TabsContent value="maintenance" className="space-y-4">
|
|||
|
|
{equipment.map((eq) => (
|
|||
|
|
<div key={eq.id} className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|||
|
|
<div className="flex items-center justify-between mb-3">
|
|||
|
|
<h4 className="font-semibold text-[var(--text-primary)]">{eq.name}</h4>
|
|||
|
|
<Badge variant={new Date(eq.nextMaintenance) <= new Date() ? 'error' : 'success'}>
|
|||
|
|
{new Date(eq.nextMaintenance) <= new Date() ? t('equipment.maintenance.overdue', 'Overdue') : t('equipment.maintenance.scheduled', 'Scheduled')}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.maintenance.last', 'Last')}:</span>
|
|||
|
|
<div className="font-medium">{formatDateTime(eq.lastMaintenance)}</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.maintenance.next', 'Next')}:</span>
|
|||
|
|
<div className="font-medium">{formatDateTime(eq.nextMaintenance)}</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.maintenance.interval', 'Interval')}:</span>
|
|||
|
|
<div className="font-medium">{eq.maintenanceInterval} {t('common.days', 'days')}</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-[var(--text-secondary)]">{t('equipment.maintenance.history', 'History')}:</span>
|
|||
|
|
<div className="font-medium">{eq.maintenanceHistory.length} {t('equipment.maintenance.records', 'records')}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</TabsContent>
|
|||
|
|
|
|||
|
|
<TabsContent value="alerts" className="space-y-4">
|
|||
|
|
{equipment.flatMap(eq =>
|
|||
|
|
eq.alerts.map(alert => (
|
|||
|
|
<div key={`${eq.id}-${alert.id}`} className={`p-4 rounded-lg border-l-4 ${
|
|||
|
|
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 mb-2">
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<AlertTriangle className={`w-5 h-5 ${
|
|||
|
|
alert.type === 'critical' ? 'text-red-500' :
|
|||
|
|
alert.type === 'warning' ? 'text-orange-500' : 'text-blue-500'
|
|||
|
|
}`} />
|
|||
|
|
<h4 className="font-semibold text-[var(--text-primary)]">{eq.name}</h4>
|
|||
|
|
<Badge variant={alert.acknowledged ? 'success' : 'warning'}>
|
|||
|
|
{alert.acknowledged ? t('equipment.alerts.acknowledged', 'Acknowledged') : t('equipment.alerts.new', 'New')}
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
<span className="text-sm text-[var(--text-secondary)]">
|
|||
|
|
{new Date(alert.timestamp).toLocaleString('es-ES')}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-[var(--text-secondary)] mb-3">{alert.message}</p>
|
|||
|
|
{!alert.acknowledged && (
|
|||
|
|
<Button
|
|||
|
|
variant="outline"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => onAcknowledgeAlert?.(eq.id, alert.id)}
|
|||
|
|
>
|
|||
|
|
{t('equipment.alerts.acknowledge', 'Acknowledge')}
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</TabsContent>
|
|||
|
|
</Tabs>
|
|||
|
|
|
|||
|
|
{/* Equipment Details Modal */}
|
|||
|
|
{selectedEquipment && (
|
|||
|
|
<Modal
|
|||
|
|
isOpen={showEquipmentModal}
|
|||
|
|
onClose={() => {
|
|||
|
|
setShowEquipmentModal(false);
|
|||
|
|
setSelectedEquipment(null);
|
|||
|
|
}}
|
|||
|
|
title={selectedEquipment.name}
|
|||
|
|
size="lg"
|
|||
|
|
>
|
|||
|
|
<div className="p-6 space-y-6">
|
|||
|
|
{/* Basic Info */}
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">{t('equipment.model', 'Model')}</label>
|
|||
|
|
<p className="text-[var(--text-primary)]">{selectedEquipment.model}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">{t('equipment.serial', 'Serial Number')}</label>
|
|||
|
|
<p className="text-[var(--text-primary)]">{selectedEquipment.serialNumber}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">{t('equipment.location', 'Location')}</label>
|
|||
|
|
<p className="text-[var(--text-primary)]">{selectedEquipment.location}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="text-sm font-medium text-[var(--text-secondary)]">{t('equipment.install_date', 'Install Date')}</label>
|
|||
|
|
<p className="text-[var(--text-primary)]">{formatDateTime(selectedEquipment.installDate)}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Current Status */}
|
|||
|
|
<div className="grid grid-cols-3 gap-4 p-4 bg-[var(--bg-secondary)] rounded-lg">
|
|||
|
|
<div className="text-center">
|
|||
|
|
<div className="text-2xl font-bold text-[var(--text-primary)]">{selectedEquipment.efficiency}%</div>
|
|||
|
|
<div className="text-sm text-[var(--text-secondary)]">{t('equipment.efficiency', 'Efficiency')}</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-center">
|
|||
|
|
<div className="text-2xl font-bold text-[var(--text-primary)]">{selectedEquipment.uptime.toFixed(1)}%</div>
|
|||
|
|
<div className="text-sm text-[var(--text-secondary)]">{t('equipment.uptime', 'Uptime')}</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-center">
|
|||
|
|
<div className="text-2xl font-bold text-[var(--text-primary)]">{selectedEquipment.energyUsage} kW</div>
|
|||
|
|
<div className="text-sm text-[var(--text-secondary)]">{t('equipment.energy_usage', 'Energy Usage')}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Actions */}
|
|||
|
|
<div className="flex justify-end space-x-3">
|
|||
|
|
<Button variant="outline" onClick={() => onViewMaintenanceHistory?.(selectedEquipment.id)}>
|
|||
|
|
{t('equipment.actions.view_history', 'View History')}
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="secondary" onClick={() => onEditEquipment?.(selectedEquipment.id)}>
|
|||
|
|
{t('common.edit', 'Edit')}
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="primary" onClick={() => onScheduleMaintenance?.(selectedEquipment.id)}>
|
|||
|
|
{t('equipment.actions.schedule_maintenance', 'Schedule Maintenance')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Modal>
|
|||
|
|
)}
|
|||
|
|
</CardBody>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default EquipmentManager;
|