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