🔧 Build Fixes:
- Created FloorToggle component
- Created HealthLegend component
- Added name field to User interface
Components complete for heatmap feature
160 lines
4.8 KiB
TypeScript
160 lines
4.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { Bed } from './GrowRoomHeatmap';
|
|
import BedTooltip from './BedTooltip';
|
|
|
|
/**
|
|
* BedCell - Individual bed cell in the grid
|
|
*
|
|
* Renders a single bed with color-coded health score.
|
|
* Shows tooltip on hover with detailed bed information.
|
|
*
|
|
* Color scale:
|
|
* - 90-100: Dark green (excellent)
|
|
* - 70-89: Light green (good)
|
|
* - 50-69: Yellow (fair)
|
|
* - 30-49: Orange (needs attention)
|
|
* - 0-29: Red (critical)
|
|
* - Empty: Gray outline (no plant)
|
|
*/
|
|
|
|
interface BedCellProps {
|
|
bed?: Bed;
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
row: number;
|
|
column: number;
|
|
}
|
|
|
|
export default function BedCell({ bed, x, y, size, row, column }: BedCellProps) {
|
|
const [showTooltip, setShowTooltip] = useState(false);
|
|
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
|
|
|
// Get color based on health score
|
|
const getHealthColor = (score: number): string => {
|
|
if (score >= 90) return '#059669'; // emerald-600
|
|
if (score >= 70) return '#10b981'; // emerald-500
|
|
if (score >= 50) return '#eab308'; // yellow-500
|
|
if (score >= 30) return '#f97316'; // orange-500
|
|
return '#dc2626'; // red-600
|
|
};
|
|
|
|
const handleMouseEnter = (e: React.MouseEvent) => {
|
|
if (bed && bed.status !== 'empty') {
|
|
setTooltipPosition({ x: e.clientX, y: e.clientY });
|
|
setShowTooltip(true);
|
|
}
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setShowTooltip(false);
|
|
};
|
|
|
|
const handleClick = () => {
|
|
if (bed && bed.status !== 'empty') {
|
|
// TODO: Navigate to bed detail page
|
|
console.log('Navigate to bed:', bed.bed_id);
|
|
}
|
|
};
|
|
|
|
// Empty bed
|
|
if (!bed || bed.status === 'empty') {
|
|
return (
|
|
<g>
|
|
<rect
|
|
x={x}
|
|
y={y}
|
|
width={size}
|
|
height={size}
|
|
fill="transparent"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1"
|
|
strokeDasharray="4 4"
|
|
rx="4"
|
|
className="opacity-30"
|
|
/>
|
|
<text
|
|
x={x + size / 2}
|
|
y={y + size / 2}
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
className="text-xs fill-slate-400 dark:fill-slate-600"
|
|
>
|
|
Empty
|
|
</text>
|
|
</g>
|
|
);
|
|
}
|
|
|
|
const healthColor = getHealthColor(bed.health_score);
|
|
const hasAlert = !!bed.last_alert;
|
|
|
|
return (
|
|
<>
|
|
<g
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onClick={handleClick}
|
|
className="cursor-pointer transition-opacity hover:opacity-90"
|
|
>
|
|
{/* Main cell */}
|
|
<rect
|
|
x={x}
|
|
y={y}
|
|
width={size}
|
|
height={size}
|
|
fill={healthColor}
|
|
stroke={hasAlert ? '#dc2626' : '#64748b'}
|
|
strokeWidth={hasAlert ? '3' : '1'}
|
|
rx="4"
|
|
/>
|
|
|
|
{/* Health score text */}
|
|
<text
|
|
x={x + size / 2}
|
|
y={y + size / 2}
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
className="text-sm font-bold fill-white"
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
{bed.health_score}
|
|
</text>
|
|
|
|
{/* Alert indicator */}
|
|
{hasAlert && (
|
|
<circle
|
|
cx={x + size - 10}
|
|
cy={y + 10}
|
|
r="6"
|
|
fill="#dc2626"
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
/>
|
|
)}
|
|
|
|
{/* Batch ID (small text) */}
|
|
{bed.plant_batch_id && (
|
|
<text
|
|
x={x + size / 2}
|
|
y={y + size - 8}
|
|
textAnchor="middle"
|
|
className="text-xs fill-white opacity-70"
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
{bed.plant_batch_id.substring(0, 8)}
|
|
</text>
|
|
)}
|
|
</g>
|
|
|
|
{/* Tooltip */}
|
|
{showTooltip && bed && (
|
|
<BedTooltip
|
|
bed={bed}
|
|
position={tooltipPosition}
|
|
onClose={() => setShowTooltip(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|