dynamic graph

This commit is contained in:
Siwat Sirichai 2025-06-21 12:55:27 +07:00
parent a606796d9e
commit 5e029ff99c
17 changed files with 1707 additions and 569 deletions

View file

@ -0,0 +1,186 @@
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"
}
const hasActiveAlarms = activeAlarms.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 ({activeAlarms.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">
{activeAlarms.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>
) : (
activeAlarms.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>
)
}

View file

@ -0,0 +1,29 @@
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Activity, Pause, Play } from "lucide-react"
import { useBedPressureStore } from "@/stores/bedPressureStore"
export function BedPressureHeader() {
const { isMonitoring, setIsMonitoring } = useBedPressureStore()
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Activity className="w-8 h-8 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Bed Pressure Monitor</h1>
</div>
<Badge variant={isMonitoring ? "default" : "secondary"} className="px-3 py-1">
{isMonitoring ? "Live" : "Paused"}
</Badge>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setIsMonitoring(!isMonitoring)}>
{isMonitoring ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
{isMonitoring ? "Pause" : "Resume"}
</Button>
</div>
</div>
)
}

View file

@ -0,0 +1,79 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useBedPressureStore } from "@/stores/bedPressureStore"
const getValueColor = (value: number) => {
if (value < 1500) return "#22c55e" // Green - Low value
if (value < 2500) return "#eab308" // Yellow - Medium value
if (value < 3500) return "#f97316" // Orange - High value
return "#ef4444" // Red - Very high value
}
export function BedVisualization() {
const { sensorConfig, sensorData, setSelectedSensor, setIsModalOpen } = useBedPressureStore()
const handleSensorClick = (sensorId: string) => {
setSelectedSensor(sensorId)
setIsModalOpen(true)
}
return (
<Card>
<CardHeader>
<CardTitle>Sensor Value Distribution Map</CardTitle>
<p className="text-sm text-gray-600">Click on any sensor point to view detailed value graphs</p>
</CardHeader>
<CardContent>
<div className="relative">
{/* Bed outline */}
<svg viewBox="0 0 100 100" className="w-full h-96 border-2 border-gray-200 rounded-lg bg-white">
{/* Bed frame */}
<rect x="25" y="10" width="50" height="80" fill="none" stroke="#e5e7eb" strokeWidth="2" rx="8" />
{/* Pillow area */}
<rect x="30" y="12" width="40" height="15" fill="#f3f4f6" stroke="#d1d5db" strokeWidth="1" rx="4" />
{/* Pressure sensors */}
{sensorConfig.map((sensor) => {
const sensorInfo = sensorData[sensor.id]
if (!sensorInfo) return null
return (
<circle
key={sensor.id}
cx={sensor.x}
cy={sensor.y}
r="3"
fill={getValueColor(sensorInfo.currentValue)}
stroke="white"
strokeWidth="1"
className="cursor-pointer transition-all duration-200 hover:r-4 hover:opacity-80"
onClick={() => handleSensorClick(sensor.id)}
/>
)
})}
</svg>
{/* Value Legend */}
<div className="mt-4 flex items-center justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span>Low ({"<"}1500)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<span>Medium (1500-2500)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-orange-500"></div>
<span>High (2500-3500)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<span>Critical ({">"}3500)</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
}

View file

@ -0,0 +1,230 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
import { Download, Clock } from "lucide-react"
import { useBedPressureStore } from "@/stores/bedPressureStore"
import { useEffect } from "react"
const getValueColor = (value: number) => {
if (value < 1500) return "#22c55e" // Green - Low value
if (value < 2500) return "#eab308" // Yellow - Medium value
if (value < 3500) return "#f97316" // Orange - High value
return "#ef4444" // Red - Very high value
}
const getValueLevel = (value: number) => {
if (value < 1500) return "Low"
if (value < 2500) return "Medium"
if (value < 3500) return "High"
return "Critical"
}
export function SensorDetailModal() {
const {
isModalOpen,
setIsModalOpen,
selectedSensor,
sensorData,
selectedTimespan,
setSelectedTimespan,
fetchSensorHistory
} = useBedPressureStore()
const sensor = selectedSensor && sensorData[selectedSensor] ? sensorData[selectedSensor] : null
// Fetch historical data when modal opens or timespan changes
useEffect(() => {
if (isModalOpen && selectedSensor && sensor?.source === 'hardware') {
fetchSensorHistory(selectedSensor, selectedTimespan)
}
}, [isModalOpen, selectedSensor, selectedTimespan, fetchSensorHistory, sensor?.source])
const handleTimespanChange = (value: string) => {
const newTimespan = parseInt(value)
setSelectedTimespan(newTimespan)
}
const getTimespanLabel = (timespan: number) => {
if (timespan < 60000) return `${timespan / 1000}s`
if (timespan < 3600000) return `${timespan / 60000}m`
if (timespan < 86400000) return `${timespan / 3600000}h`
return `${timespan / 86400000}d`
}
if (!isModalOpen || !selectedSensor || !sensor) {
return null
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900">{sensor.label}</h2>
<p className="text-gray-600">Pressure Monitoring Details</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setIsModalOpen(false)}
className="text-gray-500 hover:text-gray-700"
>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Current Value</p>
<p
className="text-3xl font-bold"
style={{ color: getValueColor(sensor.currentValue) }}
>
{sensor.currentValue.toFixed(0)}
</p>
<p className="text-sm text-gray-500">ADC Units</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Status Level</p>
<Badge
className="text-lg px-3 py-1 mt-2"
variant={
sensor.status === "critical" || sensor.status === "alarm"
? "destructive"
: sensor.status === "warning"
? "secondary"
: "default"
}
>
{getValueLevel(sensor.currentValue)}
</Badge>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Body Zone</p>
<p className="text-xl font-semibold capitalize mt-2">{sensor.zone}</p>
</CardContent>
</Card>
</div>
{/* Large Value Chart */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Value Trend</CardTitle>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-500" />
<Select value={selectedTimespan.toString()} onValueChange={handleTimespanChange}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Select timespan" />
</SelectTrigger>
<SelectContent>
<SelectItem value="30000">30s</SelectItem>
<SelectItem value="60000">1m</SelectItem>
<SelectItem value="300000">5m</SelectItem>
<SelectItem value="600000">10m</SelectItem>
<SelectItem value="1800000">30m</SelectItem>
<SelectItem value="3600000">1h</SelectItem>
<SelectItem value="7200000">2h</SelectItem>
<SelectItem value="21600000">6h</SelectItem>
<SelectItem value="43200000">12h</SelectItem>
<SelectItem value="86400000">24h</SelectItem>
<SelectItem value="259200000">3d</SelectItem>
<SelectItem value="604800000">7d</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<p className="text-sm text-gray-600">
Showing data for the last {getTimespanLabel(selectedTimespan)}
{sensor.source === 'hardware' ? ' (Real sensor data)' : ' (Mock data)'}
</p>
</CardHeader>
<CardContent>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={sensor.data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" tick={{ fontSize: 12 }} interval="preserveStartEnd" />
<YAxis
tick={{ fontSize: 12 }}
label={{ value: "ADC Value", angle: -90, position: "insideLeft" }}
/>
<Tooltip
formatter={(value: number) => [`${value.toFixed(0)}`, "Value"]}
labelFormatter={(label) => `Time: ${label}`}
/>
<Line
type="monotone"
dataKey="value"
stroke={getValueColor(sensor.currentValue)}
strokeWidth={3}
dot={false}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Additional Statistics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Max Value</p>
<p className="text-lg font-bold text-red-600">
{Math.max(...sensor.data.map(d => d.value)).toFixed(0)}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Min Value</p>
<p className="text-lg font-bold text-green-600">
{Math.min(...sensor.data.map(d => d.value)).toFixed(0)}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Average</p>
<p className="text-lg font-bold text-blue-600">
{(sensor.data.reduce((sum, d) => sum + d.value, 0) / sensor.data.length).toFixed(0)}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-sm text-gray-600">Data Points</p>
<p className="text-lg font-bold text-gray-600">{sensor.data.length}</p>
</CardContent>
</Card>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
Close
</Button>
<Button>
<Download className="w-4 h-4 mr-2" />
Export Data
</Button>
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,68 @@
import { Card, CardContent } from "@/components/ui/card"
import { AlertTriangle, User } from "lucide-react"
import { useBedPressureStore } from "@/stores/bedPressureStore"
const getValueColor = (value: number) => {
if (value < 1500) return "#22c55e" // Green - Low value
if (value < 2500) return "#eab308" // Yellow - Medium value
if (value < 3500) return "#f97316" // Orange - High value
return "#ef4444" // Red - Very high value
}
export function StatsCards() {
const { sensorData, averageValue, criticalSensors } = useBedPressureStore()
const avgValue = averageValue()
const criticalCount = criticalSensors()
const activeSensors = Object.keys(sensorData).length
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2">
<User className="w-5 h-5 text-blue-600" />
<div>
<p className="text-sm text-gray-600">Patient</p>
<p className="font-semibold">John Doe - Room 204</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-sm text-gray-600">Average Value</p>
<p className="text-2xl font-bold" style={{ color: getValueColor(avgValue) }}>
{avgValue.toFixed(0)}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-sm text-gray-600">Active Sensors</p>
<p className="text-2xl font-bold text-green-600">{activeSensors}</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2">
<AlertTriangle className={`w-5 h-5 ${criticalCount > 0 ? "text-red-600" : "text-gray-400"}`} />
<div>
<p className="text-sm text-gray-600">Critical Alerts</p>
<p className={`text-2xl font-bold ${criticalCount > 0 ? "text-red-600" : "text-gray-600"}`}>
{criticalCount}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)
}