Files
bakery-ia/frontend/src/components/domain/production/EquipmentManager.tsx
2025-09-23 12:49:35 +02:00

632 lines
24 KiB
TypeScript
Raw Blame History

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;