feat: update lake water data and optimize visual components for real-time monitoring

This commit is contained in:
David Fencl
2026-06-08 22:32:10 +02:00
parent 8fe39b7ab0
commit 4939d1c5dc
60 changed files with 3255 additions and 380 deletions
+120 -40
View File
@@ -12,9 +12,12 @@ interface KpiData {
inflow: number;
outflow: number;
volume: number;
currentVolume?: number;
fullness: number;
storageDiff?: number;
minDiff?: number;
minDiffLabelCs?: string;
minDiffLabelEn?: string;
avgInflow24h?: number;
avgOutflow24h?: number;
}
@@ -28,9 +31,28 @@ interface Props {
const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Props) => {
const [showTooltip, setShowTooltip] = useState(false);
const [showMinTooltip, setShowMinTooltip] = useState(false);
const dict = t[language].kpi;
const flowDiff = data.inflow - data.outflow;
// Graf: pokud přibývá → přítok vs průměr přítoku; pokud ubývá → odtok vs průměr odtoku
let visualFlowValue = 0;
if (flowDiff >= 0) {
// Voda přibývá → jak velký je přítok vůči průměru
if (data.avgInflow24h && data.avgInflow24h > 0) {
visualFlowValue = Math.min(100, (data.inflow / data.avgInflow24h) * 100);
} else if (data.inflow > 0) {
visualFlowValue = 50;
}
} else {
// Voda ubývá → jak velký je odtok vůči průměru
if (data.avgOutflow24h && data.avgOutflow24h > 0) {
visualFlowValue = Math.min(100, (data.outflow / data.avgOutflow24h) * 100);
} else if (data.outflow > 0) {
visualFlowValue = 50;
}
}
useEffect(() => {
if (showTooltip) {
const timer = setTimeout(() => {
@@ -40,6 +62,15 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
}
}, [showTooltip]);
useEffect(() => {
if (showMinTooltip) {
const timer = setTimeout(() => {
setShowMinTooltip(false);
}, 3500);
return () => clearTimeout(timer);
}
}, [showMinTooltip]);
if (isRiver) {
return (
<>
@@ -90,8 +121,8 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
)}
</div>
<div style={{
width: '70px', height: '70px', borderRadius: '50%',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
width: '70px', height: '70px', borderRadius: '50%',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
border: '2px dashed var(--color-cyan)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--color-cyan)', flexShrink: 0
@@ -163,41 +194,38 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
</div>
)}
</div>
{/* Flow Circle */}
<div style={{
width: '70px',
height: '70px',
borderRadius: '50%',
border: `4px solid ${flowDiff >= 0 ? 'rgba(74, 222, 128, 0.2)' : 'rgba(248, 113, 113, 0.2)'}`,
borderTopColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
borderRightColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'rotate(-45deg)',
flexShrink: 0
}}>
<span style={{ transform: 'rotate(45deg)', color: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', textAlign: 'center', lineHeight: 1.2 }}>
<div style={{ fontSize: '0.8rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</div>
<div style={{ fontSize: '0.6rem', opacity: 0.8 }}>m³/s</div>
</span>
{/* Flow Gauge using CircularProgress */}
<div style={{ position: 'relative', width: '70px', height: '70px', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ position: 'absolute', top: 0, left: 0 }}>
<CircularProgress
value={visualFlowValue || 0.1}
size={70}
strokeWidth={6}
hideText={true}
color={flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)'}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', color: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', lineHeight: 1.2 }}>
<span style={{ fontSize: '0.85rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</span>
<span style={{ fontSize: '0.6rem', opacity: 0.8 }}>m³/s</span>
</div>
</div>
</div>
</div>
{/* CARD 3: CAPACITY */}
<div className="kpi-card">
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
{dict.fullness}
<span
<span
onClick={() => setShowTooltip(!showTooltip)}
style={{ cursor: 'pointer', fontSize: '0.85rem', opacity: 0.6, padding: '0 4px' }}
>
</span>
{showTooltip && (
<div
<div
onClick={() => setShowTooltip(false)}
style={{
position: 'absolute',
@@ -221,25 +249,77 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
</div>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', flex: 1, minWidth: 0, paddingRight: '0.5rem' }}>
<div style={{ fontSize: '1.7rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', whiteSpace: 'nowrap', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
{dict.volume}: {data.volume.toFixed(1)} <span style={{ fontSize: '0.7rem' }}>mil. m³</span>
</div>
{data.minDiff !== undefined && (
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
{language === 'cs' ? 'K minimu:' : 'To min:'} <span style={{ color: data.minDiff < 0.5 ? 'var(--color-red)' : 'var(--color-green)' }}>{data.minDiff.toFixed(2)} m</span>
</div>
)}
<div style={{ position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center', height: '130px', marginTop: '-1rem' }}>
{/* Circular Progress Ring */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '130px', height: '130px', zIndex: 1 }}>
<CircularProgress value={data.fullness} size={130} strokeWidth={10} hideText={true} />
</div>
<div style={{ flexShrink: 0 }}>
<CircularProgress value={data.fullness} size={80} strokeWidth={8} />
{/* Percentage Text */}
<div style={{ position: 'absolute', top: '24px', left: '50%', transform: 'translateX(-50%)', zIndex: 10, fontSize: '0.95rem', fontWeight: 'bold', color: 'var(--text-main)', textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>
{data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A'}
</div>
{/* Center Data: Main Level Difference */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', marginTop: '-4px', zIndex: 10, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<span style={{ fontSize: '1.9rem', fontWeight: 'bold', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)', lineHeight: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)}` : `${data.storageDiff.toFixed(2)}`) : ''}
</span>
<span style={{ position: 'absolute', left: '100%', bottom: '0.15rem', marginLeft: '0.2rem', fontSize: '0.8rem', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)', whiteSpace: 'nowrap' }}>m</span>
</div>
{/* Bottom Inside Data: Min Diff */}
{data.minDiff !== undefined && (
<div
style={{ position: 'absolute', bottom: '26px', left: '50%', transform: 'translateX(-50%)', zIndex: 20, fontSize: '0.9rem', fontWeight: 'bold', color: data.minDiff < 0.5 ? 'var(--color-red)' : 'var(--color-green)', cursor: 'pointer', textShadow: '0 2px 10px rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', gap: '0.25rem' }}
onClick={() => setShowMinTooltip(!showMinTooltip)}
>
<span>{data.minDiff.toFixed(2)} m</span>
<span style={{ fontSize: '0.75rem', opacity: 0.7, fontWeight: 'normal' }}></span>
{showMinTooltip && (
<div
onClick={(e) => { e.stopPropagation(); setShowMinTooltip(false); }}
style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginTop: '8px',
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '0.75rem',
borderRadius: '8px',
width: '220px',
zIndex: 100,
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
color: 'var(--text-main)',
fontSize: '0.85rem',
lineHeight: 1.4,
cursor: 'pointer',
whiteSpace: 'normal',
textAlign: 'center',
fontWeight: 'normal',
textShadow: 'none'
}}>
{language === 'cs' ? (data.minDiffLabelCs || 'K minimu') : (data.minDiffLabelEn || 'To min')}
</div>
)}
</div>
)}
</div>
{/* Bottom Elements */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', marginTop: '0.2rem' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.4rem', fontSize: '0.85rem', whiteSpace: 'nowrap' }}>
<span style={{ color: 'var(--text-muted)' }}>{dict.volume}:</span>
<span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>
{data.currentVolume !== undefined && data.volume > 0 ? `${data.currentVolume.toFixed(1)} / ` : ''}{data.volume.toFixed(1)} mil. m³
</span>
</div>
</div>
</div>
</>
);