dynamic graph
This commit is contained in:
parent
a606796d9e
commit
5e029ff99c
17 changed files with 1707 additions and 569 deletions
|
@ -1,524 +1,39 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||||
import { Activity, AlertTriangle, Download, Pause, Play, Settings, User } from "lucide-react"
|
||||
|
||||
// Mock data generator
|
||||
const generateTimeSeriesData = (hours = 24) => {
|
||||
const data = []
|
||||
const now = new Date()
|
||||
|
||||
for (let i = hours * 60; i >= 0; i -= 5) {
|
||||
const time = new Date(now.getTime() - i * 60 * 1000)
|
||||
data.push({
|
||||
time: time.toLocaleTimeString("en-US", { hour12: false }),
|
||||
timestamp: time.getTime(),
|
||||
pressure: Math.random() * 100 + Math.sin(i / 60) * 20 + 40,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// Sensor configuration interface
|
||||
interface SensorConfig {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
zone: string;
|
||||
label: string;
|
||||
pin?: number;
|
||||
}
|
||||
|
||||
const getPressureColor = (pressure: number) => {
|
||||
if (pressure < 30) return "#22c55e" // Green - Low pressure
|
||||
if (pressure < 50) return "#eab308" // Yellow - Medium pressure
|
||||
if (pressure < 70) return "#f97316" // Orange - High pressure
|
||||
return "#ef4444" // Red - Very high pressure
|
||||
}
|
||||
|
||||
const getPressureLevel = (pressure: number) => {
|
||||
if (pressure < 30) return "Low"
|
||||
if (pressure < 50) return "Medium"
|
||||
if (pressure < 70) return "High"
|
||||
return "Critical"
|
||||
}
|
||||
|
||||
interface SensorData {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
zone: string;
|
||||
label: string;
|
||||
currentPressure: number;
|
||||
data: Array<{ time: string; timestamp: number; pressure: number }>;
|
||||
status: string;
|
||||
source?: 'hardware' | 'mock';
|
||||
pin?: number;
|
||||
digitalState?: number;
|
||||
}
|
||||
import { BedPressureHeader } from "./bed-pressure/BedPressureHeader"
|
||||
import { StatsCards } from "./bed-pressure/StatsCards"
|
||||
import { BedVisualization } from "./bed-pressure/BedVisualization"
|
||||
import { AlertsPanel } from "./bed-pressure/AlertsPanel"
|
||||
import { SensorDetailModal } from "./bed-pressure/SensorDetailModal"
|
||||
import { useBedPressureData } from "@/hooks/useBedPressureData"
|
||||
|
||||
export default function Component() {
|
||||
const [sensorData, setSensorData] = useState<Record<string, SensorData>>({})
|
||||
const [sensorConfig, setSensorConfig] = useState<SensorConfig[]>([])
|
||||
const [selectedSensor, setSelectedSensor] = useState<string | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isMonitoring, setIsMonitoring] = useState(true)
|
||||
const [alerts, setAlerts] = useState<Array<{ id: string; message: string; time: string }>>([])
|
||||
|
||||
// Initialize sensor configuration
|
||||
useEffect(() => {
|
||||
const fetchSensorConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/sensors/config')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success && data.sensors) {
|
||||
setSensorConfig(data.sensors)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sensor config:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchSensorConfig()
|
||||
}, [])
|
||||
|
||||
// Initialize sensor data
|
||||
useEffect(() => {
|
||||
if (sensorConfig.length === 0) return
|
||||
|
||||
const initialData: Record<string, SensorData> = {}
|
||||
sensorConfig.forEach((sensor) => {
|
||||
initialData[sensor.id] = {
|
||||
...sensor,
|
||||
currentPressure: Math.random() * 100,
|
||||
data: generateTimeSeriesData(),
|
||||
status: "normal",
|
||||
}
|
||||
})
|
||||
setSensorData(initialData)
|
||||
}, [sensorConfig])
|
||||
|
||||
// Fetch sensor data from API
|
||||
useEffect(() => {
|
||||
if (!isMonitoring) return
|
||||
|
||||
const fetchSensorData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/sensors')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success && data.sensors) {
|
||||
setSensorData((prev) => {
|
||||
const updated = { ...prev }
|
||||
const newAlerts: Array<{ id: string; message: string; time: string }> = []
|
||||
|
||||
data.sensors.forEach((sensor: {
|
||||
id: string;
|
||||
label: string;
|
||||
zone: string;
|
||||
pressure: number;
|
||||
source: 'hardware' | 'mock';
|
||||
pin?: number;
|
||||
digitalState?: number;
|
||||
}) => {
|
||||
const currentSensor = updated[sensor.id]
|
||||
const newPressure = sensor.pressure
|
||||
|
||||
// Check for alerts
|
||||
if (newPressure > 80 && currentSensor && currentSensor.currentPressure <= 80) {
|
||||
newAlerts.push({
|
||||
id: `${sensor.id}-${Date.now()}`,
|
||||
message: `High pressure detected at ${sensor.label}`,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
})
|
||||
}
|
||||
|
||||
// Update sensor data
|
||||
updated[sensor.id] = {
|
||||
...sensorConfig.find(s => s.id === sensor.id),
|
||||
id: sensor.id,
|
||||
x: sensorConfig.find(s => s.id === sensor.id)?.x || 50,
|
||||
y: sensorConfig.find(s => s.id === sensor.id)?.y || 50,
|
||||
zone: sensor.zone,
|
||||
label: sensor.label,
|
||||
currentPressure: newPressure,
|
||||
data: currentSensor ? [
|
||||
...currentSensor.data.slice(1),
|
||||
{
|
||||
time: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
||||
timestamp: Date.now(),
|
||||
pressure: newPressure,
|
||||
},
|
||||
] : [{
|
||||
time: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
||||
timestamp: Date.now(),
|
||||
pressure: newPressure,
|
||||
}],
|
||||
status: newPressure > 80 ? "critical" : newPressure > 60 ? "warning" : "normal",
|
||||
source: sensor.source,
|
||||
pin: sensor.pin,
|
||||
digitalState: sensor.digitalState
|
||||
}
|
||||
})
|
||||
|
||||
if (newAlerts.length > 0) {
|
||||
setAlerts((prev) => [...newAlerts, ...prev].slice(0, 10))
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sensor data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
fetchSensorData()
|
||||
|
||||
// Set up polling
|
||||
const interval = setInterval(fetchSensorData, 2000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isMonitoring, sensorConfig])
|
||||
|
||||
const averagePressure =
|
||||
Object.values(sensorData).reduce((sum: number, sensor: SensorData) => sum + (sensor.currentPressure || 0), 0) /
|
||||
Object.keys(sensorData).length
|
||||
|
||||
const criticalSensors = Object.values(sensorData).filter((sensor: SensorData) => sensor.currentPressure > 80).length
|
||||
// Initialize data fetching
|
||||
useBedPressureData()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<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>
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="w-4 h-4" />
|
||||
Export
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<BedPressureHeader />
|
||||
|
||||
{/* Stats Cards */}
|
||||
<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 Pressure</p>
|
||||
<p className="text-2xl font-bold" style={{ color: getPressureColor(averagePressure) }}>
|
||||
{averagePressure.toFixed(1)} mmHg
|
||||
</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">{Object.keys(sensorData).length}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className={`w-5 h-5 ${criticalSensors > 0 ? "text-red-600" : "text-gray-400"}`} />
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Critical Alerts</p>
|
||||
<p className={`text-2xl font-bold ${criticalSensors > 0 ? "text-red-600" : "text-gray-600"}`}>
|
||||
{criticalSensors}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<StatsCards />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Bed Visualization */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Pressure Distribution Map</CardTitle>
|
||||
<p className="text-sm text-gray-600">Click on any sensor point to view detailed pressure 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={getPressureColor(sensorInfo.currentPressure)}
|
||||
stroke="white"
|
||||
strokeWidth="1"
|
||||
className="cursor-pointer transition-all duration-200 hover:r-4 hover:opacity-80"
|
||||
onClick={() => {
|
||||
setSelectedSensor(sensor.id)
|
||||
setIsModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Pressure 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 ({"<"}30)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<span>Medium (30-50)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-orange-500"></div>
|
||||
<span>High (50-70)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<span>Critical ({">"}70)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<BedVisualization />
|
||||
</div>
|
||||
|
||||
{/* Sensor Details & Alerts */}
|
||||
{/* Alerts Panel */}
|
||||
<div className="space-y-6">
|
||||
{/* Pressure Graph Modal */}
|
||||
{isModalOpen && selectedSensor && sensorData[selectedSensor] && (
|
||||
<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">{sensorData[selectedSensor].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 Pressure</p>
|
||||
<p
|
||||
className="text-3xl font-bold"
|
||||
style={{ color: getPressureColor(sensorData[selectedSensor].currentPressure) }}
|
||||
>
|
||||
{sensorData[selectedSensor].currentPressure.toFixed(1)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">mmHg</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={
|
||||
sensorData[selectedSensor].status === "critical"
|
||||
? "destructive"
|
||||
: sensorData[selectedSensor].status === "warning"
|
||||
? "secondary"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{getPressureLevel(sensorData[selectedSensor].currentPressure)}
|
||||
</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">{sensorData[selectedSensor].zone}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Large Pressure Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>24-Hour Pressure Trend</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={sensorData[selectedSensor].data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" tick={{ fontSize: 12 }} interval="preserveStartEnd" />
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
label={{ value: "Pressure (mmHg)", angle: -90, position: "insideLeft" }}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number) => [`${value.toFixed(1)} mmHg`, "Pressure"]}
|
||||
labelFormatter={(label) => `Time: ${label}`}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="pressure"
|
||||
stroke={getPressureColor(sensorData[selectedSensor].currentPressure)}
|
||||
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 Pressure</p>
|
||||
<p className="text-lg font-bold text-red-600">
|
||||
{Math.max(...sensorData[selectedSensor].data.map((d: { time: string; timestamp: number; pressure: number }) => d.pressure)).toFixed(1)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4 text-center">
|
||||
<p className="text-sm text-gray-600">Min Pressure</p>
|
||||
<p className="text-lg font-bold text-green-600">
|
||||
{Math.min(...sensorData[selectedSensor].data.map((d: { time: string; timestamp: number; pressure: number }) => d.pressure)).toFixed(1)}
|
||||
</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">
|
||||
{(
|
||||
sensorData[selectedSensor].data.reduce((sum: number, d: { time: string; timestamp: number; pressure: number }) => sum + d.pressure, 0) /
|
||||
sensorData[selectedSensor].data.length
|
||||
).toFixed(1)}
|
||||
</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">{sensorData[selectedSensor].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>
|
||||
)}
|
||||
|
||||
{/* Alerts */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
Recent Alerts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-64 overflow-y-auto">
|
||||
{alerts.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">No recent alerts</p>
|
||||
) : (
|
||||
alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className="flex items-start gap-3 p-3 bg-red-50 rounded-lg border border-red-200"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-red-800">{alert.message}</p>
|
||||
<p className="text-xs text-red-600">{alert.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AlertsPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sensor Detail Modal */}
|
||||
<SensorDetailModal />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
186
components/bed-pressure/AlertsPanel.tsx
Normal file
186
components/bed-pressure/AlertsPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
29
components/bed-pressure/BedPressureHeader.tsx
Normal file
29
components/bed-pressure/BedPressureHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
79
components/bed-pressure/BedVisualization.tsx
Normal file
79
components/bed-pressure/BedVisualization.tsx
Normal 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>
|
||||
)
|
||||
}
|
230
components/bed-pressure/SensorDetailModal.tsx
Normal file
230
components/bed-pressure/SensorDetailModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
68
components/bed-pressure/StatsCards.tsx
Normal file
68
components/bed-pressure/StatsCards.tsx
Normal 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>
|
||||
)
|
||||
}
|
185
components/ui/select.tsx
Normal file
185
components/ui/select.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue