230 lines
No EOL
8.9 KiB
TypeScript
230 lines
No EOL
8.9 KiB
TypeScript
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>
|
|
)
|
|
} |