193 lines
No EOL
7.5 KiB
TypeScript
193 lines
No EOL
7.5 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { AlertTriangle, VolumeX, Clock, CheckCircle } from "lucide-react"
|
|
import { useBedPressureStore } from "@/stores/bedPressureStore"
|
|
import { useEffect } from "react"
|
|
|
|
export function AlertsPanel() {
|
|
const {
|
|
alerts,
|
|
activeAlarms,
|
|
alarmManager,
|
|
acknowledgeAlarm,
|
|
silenceAlarm,
|
|
silenceAllAlarms
|
|
} = useBedPressureStore()
|
|
|
|
// Update active alarms periodically
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
// The store will handle updating alarms automatically
|
|
}, 1000)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [alarmManager])
|
|
|
|
const handleAcknowledge = (alarmId: string) => {
|
|
acknowledgeAlarm(alarmId)
|
|
}
|
|
|
|
const handleSilence = (alarmId: string) => {
|
|
silenceAlarm(alarmId, 300000) // Silence for 5 minutes
|
|
}
|
|
|
|
const handleSilenceAll = () => {
|
|
silenceAllAlarms(300000) // Silence all for 5 minutes
|
|
}
|
|
|
|
const getAlarmIcon = (type: 'warning' | 'alarm') => {
|
|
return type === 'alarm' ? (
|
|
<AlertTriangle className="w-4 h-4 text-red-600 animate-pulse" />
|
|
) : (
|
|
<AlertTriangle className="w-4 h-4 text-yellow-600" />
|
|
)
|
|
}
|
|
|
|
const getAlarmBgColor = (type: 'warning' | 'alarm', silenced: boolean) => {
|
|
if (silenced) return "bg-gray-100 border-gray-300"
|
|
return type === 'alarm' ? "bg-red-50 border-red-200" : "bg-yellow-50 border-yellow-200"
|
|
}
|
|
// Filter out warnings when there's an alarm with the same sensor name
|
|
const filteredAlarms = activeAlarms.filter(alarm => {
|
|
if (alarm.type === 'alarm') return true
|
|
// For warnings, only keep if there's no alarm with the same sensor name
|
|
return !activeAlarms.some(other =>
|
|
other.type === 'alarm' && other.sensorLabel === alarm.sensorLabel
|
|
)
|
|
})
|
|
|
|
const hasActiveAlarms = filteredAlarms.some(alarm => !alarm.silenced)
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertTriangle className={`w-5 h-5 ${hasActiveAlarms ? "text-red-600 animate-pulse" : "text-gray-400"}`} />
|
|
Active Alarms ({filteredAlarms.length})
|
|
</CardTitle>
|
|
{activeAlarms.length > 0 && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleSilenceAll}
|
|
className="text-orange-600 hover:text-orange-700"
|
|
>
|
|
<VolumeX className="w-4 h-4 mr-1" />
|
|
Silence All
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader> <CardContent>
|
|
<div className="space-y-3 max-h-80 overflow-y-auto">
|
|
{filteredAlarms.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-2" />
|
|
<p className="text-sm text-gray-500">No active alarms</p>
|
|
<p className="text-xs text-gray-400">System monitoring normally</p>
|
|
</div>
|
|
) : (
|
|
filteredAlarms.map((alarm) => (
|
|
<div
|
|
key={alarm.id}
|
|
className={`p-3 rounded-lg border ${getAlarmBgColor(alarm.type, alarm.silenced)}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
{getAlarmIcon(alarm.type)}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<p className={`text-sm font-medium ${
|
|
alarm.type === 'alarm' ? 'text-red-800' : 'text-yellow-800'
|
|
}`}>
|
|
{alarm.sensorLabel}
|
|
</p>
|
|
<span className={`text-xs px-2 py-1 rounded ${
|
|
alarm.type === 'alarm'
|
|
? 'bg-red-100 text-red-700'
|
|
: 'bg-yellow-100 text-yellow-700'
|
|
}`}>
|
|
{alarm.type.toUpperCase()}
|
|
</span>
|
|
{alarm.silenced && (
|
|
<span className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-600 flex items-center gap-1">
|
|
<VolumeX className="w-3 h-3" />
|
|
SILENCED
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className={`text-xs mb-2 ${
|
|
alarm.type === 'alarm' ? 'text-red-700' : 'text-yellow-700'
|
|
}`}>
|
|
Value: {alarm.value.toFixed(0)} (Threshold: {alarm.threshold})
|
|
</p>
|
|
<div className="flex items-center gap-2 text-xs text-gray-600">
|
|
<Clock className="w-3 h-3" />
|
|
<span>{alarm.time}</span>
|
|
{alarm.acknowledged && (
|
|
<span className="text-green-600 flex items-center gap-1">
|
|
<CheckCircle className="w-3 h-3" />
|
|
ACK
|
|
</span>
|
|
)}
|
|
{alarm.silenced && alarm.silencedUntil && (
|
|
<span className="text-gray-500">
|
|
Until {new Date(alarm.silencedUntil).toLocaleTimeString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1 ml-2">
|
|
{!alarm.acknowledged && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleAcknowledge(alarm.id)}
|
|
className="text-xs h-6 px-2"
|
|
>
|
|
ACK
|
|
</Button>
|
|
)}
|
|
{!alarm.silenced && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleSilence(alarm.id)}
|
|
className="text-xs h-6 px-2 text-orange-600 hover:text-orange-700"
|
|
>
|
|
<VolumeX className="w-3 h-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Legacy Alerts Section */}
|
|
{alerts.length > 0 && (
|
|
<div className="mt-6 pt-4 border-t">
|
|
<h4 className="text-sm font-medium text-gray-700 mb-3">Recent Alerts</h4>
|
|
<div className="space-y-2 max-h-32 overflow-y-auto">
|
|
{alerts.slice(0, 3).map((alert) => (
|
|
<div
|
|
key={alert.id}
|
|
className="flex items-start gap-2 p-2 bg-blue-50 rounded border border-blue-200"
|
|
>
|
|
<AlertTriangle className="w-3 h-3 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs font-medium text-blue-800">{alert.message}</p>
|
|
<p className="text-xs text-blue-600">{alert.time}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
} |