114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import {
|
|
CartesianGrid,
|
|
Legend,
|
|
Line,
|
|
LineChart,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts'
|
|
import { getJson } from '../api/postgrest'
|
|
import { instantPragueDay, pragueCalendarDay } from '../lib/pragueDate'
|
|
import type { SiteEffectivePriceRow } from '../types/ems'
|
|
|
|
function parseNum(v: string | number | null | undefined): number | null {
|
|
if (v == null) return null
|
|
if (typeof v === 'number' && !Number.isNaN(v)) return v
|
|
const n = Number(v)
|
|
return Number.isFinite(n) ? n : null
|
|
}
|
|
|
|
export type PricePoint = {
|
|
label: string
|
|
buy: number | null
|
|
sell: number | null
|
|
}
|
|
|
|
type Props = {
|
|
siteId: number | null
|
|
pollMs?: number
|
|
}
|
|
|
|
/** Efektivní nákup / prodej (Kč/kWh) pro dnešní den v Europe/Prague. */
|
|
export function PriceChart({ siteId, pollMs = 120_000 }: Props) {
|
|
const [points, setPoints] = useState<PricePoint[]>([])
|
|
const [ready, setReady] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
if (siteId == null) {
|
|
setPoints([])
|
|
setReady(true)
|
|
return
|
|
}
|
|
try {
|
|
const rows = await getJson<SiteEffectivePriceRow[]>('/vw_site_effective_price', {
|
|
site_id: `eq.${siteId}`,
|
|
order: 'interval_start.desc',
|
|
limit: '200',
|
|
})
|
|
const today = pragueCalendarDay()
|
|
const todayRows = Array.isArray(rows)
|
|
? rows.filter((r) => instantPragueDay(r.interval_start) === today)
|
|
: []
|
|
todayRows.sort((a, b) => new Date(a.interval_start).getTime() - new Date(b.interval_start).getTime())
|
|
|
|
const mapped: PricePoint[] = todayRows.map((r) => {
|
|
const t = new Date(r.interval_start)
|
|
return {
|
|
label: t.toLocaleTimeString('cs-CZ', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
timeZone: 'Europe/Prague',
|
|
}),
|
|
buy: parseNum(r.effective_buy_price_czk_kwh),
|
|
sell: parseNum(r.effective_sell_price_czk_kwh),
|
|
}
|
|
})
|
|
setPoints(mapped)
|
|
} catch {
|
|
setPoints([])
|
|
} finally {
|
|
setReady(true)
|
|
}
|
|
}, [siteId])
|
|
|
|
useEffect(() => {
|
|
void load()
|
|
const id = window.setInterval(() => void load(), pollMs)
|
|
return () => window.clearInterval(id)
|
|
}, [load, pollMs])
|
|
|
|
if (!ready || points.length === 0) {
|
|
return (
|
|
<div className="h-chart-sm w-full animate-pulse rounded-xl border border-slate-800 bg-slate-900/40 sm:h-chart-md lg:h-chart-xl" />
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="h-chart-sm w-full rounded-xl border border-slate-800 bg-slate-900/40 p-2 pt-4 sm:h-chart-md lg:h-chart-xl">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={points} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.6} />
|
|
<XAxis dataKey="label" tick={{ fill: '#94a3b8', fontSize: 10 }} interval="preserveStartEnd" />
|
|
<YAxis
|
|
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
|
label={{ value: 'Kč/kWh', angle: -90, position: 'insideLeft', fill: '#64748b', fontSize: 11 }}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: '#0f172a',
|
|
border: '1px solid #1e293b',
|
|
borderRadius: '8px',
|
|
}}
|
|
/>
|
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
<Line type="stepAfter" dataKey="buy" name="Nákup" stroke="#f97316" strokeWidth={2} dot={false} connectNulls />
|
|
<Line type="stepAfter" dataKey="sell" name="Prodej" stroke="#38bdf8" strokeWidth={2} dot={false} connectNulls />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
)
|
|
}
|