329 lines
18 KiB
TypeScript
329 lines
18 KiB
TypeScript
import { FiArrowUp, FiArrowDown } from 'react-icons/fi';
|
|
import { TbRipple } from 'react-icons/tb';
|
|
import { type Language, t } from '../translations';
|
|
import { useState, useEffect } from 'react';
|
|
import { CircularProgress } from './CircularProgress';
|
|
|
|
interface KpiData {
|
|
level: number;
|
|
levelDiff24h?: number;
|
|
levelDiff7d?: number;
|
|
levelDiff30d?: number;
|
|
inflow: number;
|
|
outflow: number;
|
|
volume: number;
|
|
currentVolume?: number;
|
|
fullness: number;
|
|
storageDiff?: number;
|
|
minDiff?: number;
|
|
minDiffLabelCs?: string;
|
|
minDiffLabelEn?: string;
|
|
avgInflow24h?: number;
|
|
avgOutflow24h?: number;
|
|
}
|
|
|
|
interface Props {
|
|
data: KpiData;
|
|
language: Language;
|
|
lakeName?: string;
|
|
isRiver?: boolean;
|
|
}
|
|
|
|
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(() => {
|
|
setShowTooltip(false);
|
|
}, 3500);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [showTooltip]);
|
|
|
|
useEffect(() => {
|
|
if (showMinTooltip) {
|
|
const timer = setTimeout(() => {
|
|
setShowMinTooltip(false);
|
|
}, 3500);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [showMinTooltip]);
|
|
|
|
if (isRiver) {
|
|
return (
|
|
<>
|
|
{/* CARD 1: WATER LEVEL */}
|
|
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
|
{dict.waterLevel} {lakeName}
|
|
</div>
|
|
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, whiteSpace: 'nowrap' }}>
|
|
{data.level.toFixed(0)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>cm</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', justifyContent: 'center', marginTop: '0.5rem' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>1D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff24h ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{(data.levelDiff24h ?? 0).toFixed(0)} cm
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>7D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff7d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{(data.levelDiff7d ?? 0).toFixed(0)} cm
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>30D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff30d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{(data.levelDiff30d ?? 0).toFixed(0)} cm
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CARD 2: FLOW */}
|
|
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
|
{dict.currentFlow}
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1.5rem', width: '100%' }}>
|
|
<div style={{ textAlign: 'left' }}>
|
|
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, whiteSpace: 'nowrap', marginBottom: '0.5rem' }}>
|
|
{data.outflow.toFixed(1)} <span style={{ fontSize: '1.25rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m³/s</span>
|
|
</div>
|
|
{data.avgOutflow24h !== undefined && (
|
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
|
Ø 24h: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.avgOutflow24h.toFixed(1)} m³/s</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div style={{
|
|
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
|
|
}}>
|
|
<TbRipple size={36} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* CARD 1: WATER LEVEL */}
|
|
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
|
{dict.level} {lakeName}
|
|
</div>
|
|
<div style={{ fontSize: '2.5rem', fontWeight: 'bold', color: 'var(--color-cyan)', lineHeight: 1, whiteSpace: 'nowrap' }}>
|
|
{data.level.toFixed(2)} <span style={{ fontSize: '1rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>m n. m.</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', justifyContent: 'center' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>1D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff24h ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff24h ?? 0) > 0 ? '+' : ''}{((data.levelDiff24h ?? 0) * 100).toFixed(1)} cm
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>7D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff7d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff7d ?? 0) > 0 ? '+' : ''}{((data.levelDiff7d ?? 0) * 100).toFixed(1)} cm
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', background: 'rgba(0,0,0,0.15)', padding: '0.2rem 0.4rem', borderRadius: '6px' }}>
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 'bold' }}>30D</span>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 'bold', color: (data.levelDiff30d ?? 0) >= 0 ? 'var(--color-green)' : 'var(--color-red)' }}>
|
|
{(data.levelDiff30d ?? 0) > 0 ? '+' : ''}{((data.levelDiff30d ?? 0) * 100).toFixed(1)} cm
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CARD 2: FLOW */}
|
|
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
|
{dict.flow}
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1.5rem', width: '100%' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', fontSize: '0.85rem', textAlign: 'left' }}>
|
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
|
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-green)', marginRight: '6px', flexShrink: 0 }}></span>
|
|
{dict.inflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)', marginLeft: '4px' }}>{data.inflow.toFixed(1)} m³/s</span>
|
|
</div>
|
|
{data.avgInflow24h !== undefined && (
|
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '14px', marginTop: '-2px' }}>
|
|
Ø 24h: {data.avgInflow24h.toFixed(1)} m³/s
|
|
</div>
|
|
)}
|
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.25rem', whiteSpace: 'nowrap' }}>
|
|
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', backgroundColor: 'var(--color-red)', marginRight: '2px', flexShrink: 0 }}></span>
|
|
{dict.outflow}: <span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{data.outflow.toFixed(1)} m³/s</span>
|
|
{flowDiff > 0 ? <FiArrowUp color="var(--color-green)" /> : flowDiff < 0 ? <FiArrowDown color="var(--color-red)" /> : null}
|
|
</div>
|
|
{data.avgOutflow24h !== undefined && (
|
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', paddingLeft: '14px', marginTop: '-2px' }}>
|
|
Ø 24h: {data.avgOutflow24h.toFixed(1)} m³/s
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Flow Gauge using CircularProgress */}
|
|
<div style={{ position: 'relative', width: '90px', height: '90px', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
|
<CircularProgress
|
|
value={visualFlowValue || 0.1}
|
|
size={90}
|
|
strokeWidth={7}
|
|
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: '1rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</span>
|
|
<span style={{ fontSize: '0.65rem', opacity: 0.8 }}>m³/s</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CARD 3: CAPACITY */}
|
|
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.4rem', justifyContent: 'center', position: 'relative', width: '100%' }}>
|
|
{dict.fullness}
|
|
<span
|
|
onClick={() => setShowTooltip(!showTooltip)}
|
|
style={{ cursor: 'pointer', fontSize: '0.85rem', opacity: 0.6, padding: '0 4px' }}
|
|
>
|
|
ⓘ
|
|
</span>
|
|
{showTooltip && (
|
|
<div
|
|
onClick={() => setShowTooltip(false)}
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: '100%',
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
marginBottom: '8px',
|
|
backgroundColor: 'var(--bg-card)',
|
|
border: '1px solid var(--border-color)',
|
|
padding: '0.75rem',
|
|
borderRadius: '8px',
|
|
width: '250px',
|
|
zIndex: 100,
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
|
color: 'var(--text-main)',
|
|
fontSize: '0.85rem',
|
|
lineHeight: 1.4,
|
|
cursor: 'pointer'
|
|
}}>
|
|
{language === 'cs' ? "Rozdíl mezi aktuální hladinou a hladinou zásobního prostoru (důležité pro jachtaře a rekreaci)." : "Difference between current water level and storage space level (important for sailing and recreation)."}
|
|
</div>
|
|
)}
|
|
</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>
|
|
|
|
{/* Percentage Text */}
|
|
<div style={{ position: 'absolute', top: '18px', 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', whiteSpace: 'nowrap' }}
|
|
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>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default KpiCards;
|