nova stranka flow a obsluha
This commit is contained in:
@@ -5,6 +5,7 @@ import { SiteSelectionProvider, useSiteSelection } from './context/SiteSelection
|
||||
import { useWsLogErrorCount } from './hooks/useWsLogErrorCount'
|
||||
import { Dashboard } from './pages/Dashboard'
|
||||
import Economics from './pages/Economics'
|
||||
import EnergyFlows from './pages/EnergyFlows'
|
||||
import { Logs } from './pages/Logs'
|
||||
import Planning from './pages/Planning'
|
||||
import { Settings } from './pages/Settings'
|
||||
@@ -71,6 +72,9 @@ function AppLayout() {
|
||||
<NavLink to="/economics" className={tabClass}>
|
||||
Ekonomika
|
||||
</NavLink>
|
||||
<NavLink to="/energy-flows" className={tabClass}>
|
||||
Toky
|
||||
</NavLink>
|
||||
<NavLink to="/settings" className={tabClass}>
|
||||
Nastavení
|
||||
</NavLink>
|
||||
@@ -104,6 +108,7 @@ export default function App() {
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="planning" element={<Planning />} />
|
||||
<Route path="economics" element={<Economics />} />
|
||||
<Route path="energy-flows" element={<EnergyFlows />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
<Route path="logs" element={<Logs />} />
|
||||
|
||||
101
frontend/src/components/EnergyFlowSankey.tsx
Normal file
101
frontend/src/components/EnergyFlowSankey.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ResponsiveSankey } from '@nivo/sankey'
|
||||
|
||||
export type FlowTotals = {
|
||||
pv_to_load_kwh: number
|
||||
pv_to_batt_kwh: number
|
||||
pv_to_grid_kwh: number
|
||||
batt_to_load_kwh: number
|
||||
batt_to_grid_kwh: number
|
||||
grid_to_load_kwh: number
|
||||
grid_to_batt_kwh: number
|
||||
}
|
||||
|
||||
const NODES = [
|
||||
{ id: 'FVE' },
|
||||
{ id: 'Síť' },
|
||||
{ id: 'Baterie' },
|
||||
{ id: 'Spotřeba' },
|
||||
] as const
|
||||
|
||||
function buildLinks(t: FlowTotals): { source: string; target: string; value: number }[] {
|
||||
const out: { source: string; target: string; value: number }[] = []
|
||||
const add = (source: string, target: string, v: number) => {
|
||||
if (v > 0.0005) out.push({ source, target, value: v })
|
||||
}
|
||||
add('FVE', 'Spotřeba', t.pv_to_load_kwh)
|
||||
add('FVE', 'Baterie', t.pv_to_batt_kwh)
|
||||
add('FVE', 'Síť', t.pv_to_grid_kwh)
|
||||
add('Baterie', 'Spotřeba', t.batt_to_load_kwh)
|
||||
add('Baterie', 'Síť', t.batt_to_grid_kwh)
|
||||
add('Síť', 'Spotřeba', t.grid_to_load_kwh)
|
||||
add('Síť', 'Baterie', t.grid_to_batt_kwh)
|
||||
return out
|
||||
}
|
||||
|
||||
type Props = {
|
||||
totals: FlowTotals | null
|
||||
}
|
||||
|
||||
export function EnergyFlowSankey({ totals }: Props) {
|
||||
if (!totals) {
|
||||
return (
|
||||
<div className="flex h-[440px] items-center justify-center text-sm text-slate-500">
|
||||
Žádná data
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const links = buildLinks(totals)
|
||||
if (links.length === 0) {
|
||||
return (
|
||||
<div className="flex h-[440px] items-center justify-center text-sm text-slate-500">
|
||||
V tomto měsíci nejsou žádné modelované toky (chybí audit / telemetrie).
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[440px] w-full min-h-[320px]">
|
||||
<ResponsiveSankey
|
||||
data={{ nodes: [...NODES], links }}
|
||||
margin={{ top: 24, right: 180, bottom: 24, left: 24 }}
|
||||
align="justify"
|
||||
sort="input"
|
||||
colors={{ scheme: 'set2' }}
|
||||
nodeOpacity={1}
|
||||
nodeHoverOpacity={1}
|
||||
nodeThickness={20}
|
||||
nodeSpacing={28}
|
||||
nodeBorderWidth={0}
|
||||
linkOpacity={0.45}
|
||||
linkHoverOpacity={0.75}
|
||||
linkContract={2}
|
||||
enableLinkGradient
|
||||
labelPosition="outside"
|
||||
labelOrientation="horizontal"
|
||||
labelPadding={12}
|
||||
labelTextColor={{ from: 'color', modifiers: [['darker', 1.2]] }}
|
||||
theme={{
|
||||
background: 'transparent',
|
||||
labels: {
|
||||
text: {
|
||||
fill: '#e2e8f0',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
container: {
|
||||
background: '#1e293b',
|
||||
color: '#f8fafc',
|
||||
fontSize: 12,
|
||||
borderRadius: 8,
|
||||
border: '1px solid #334155',
|
||||
},
|
||||
},
|
||||
}}
|
||||
valueFormat={(v) => `${Number(v).toFixed(2)} kWh`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
frontend/src/hooks/useEnergyFlows.ts
Normal file
67
frontend/src/hooks/useEnergyFlows.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { backendClient } from '../api/backend'
|
||||
import type {
|
||||
DailyEnergyFlows,
|
||||
DailyEnergyFlowsResponse,
|
||||
IntervalEnergyFlows,
|
||||
} from '../types/energy-flows'
|
||||
|
||||
export function useEnergyFlowsDaily(siteId: number | null, month: string) {
|
||||
const [days, setDays] = useState<DailyEnergyFlows[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (siteId == null || !month) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { data } = await backendClient.get<DailyEnergyFlowsResponse>(
|
||||
`/sites/${siteId}/energy-flows/daily`,
|
||||
{ params: { month }, timeout: 30_000 },
|
||||
)
|
||||
setDays(data.days ?? [])
|
||||
} catch {
|
||||
setDays([])
|
||||
setError('Nepodařilo se načíst toky energie')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [siteId, month])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
return { days, loading, error, reload: load }
|
||||
}
|
||||
|
||||
export function useEnergyFlowsIntervals(siteId: number | null, day: string | null) {
|
||||
const [intervals, setIntervals] = useState<IntervalEnergyFlows[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (siteId == null || !day) {
|
||||
setIntervals([])
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await backendClient.get<IntervalEnergyFlows[]>(
|
||||
`/sites/${siteId}/energy-flows/daily/${day}/intervals`,
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
setIntervals(Array.isArray(data) ? data : [])
|
||||
} catch {
|
||||
setIntervals([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [siteId, day])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
return { intervals, loading }
|
||||
}
|
||||
321
frontend/src/pages/EnergyFlows.tsx
Normal file
321
frontend/src/pages/EnergyFlows.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
|
||||
import { EnergyFlowSankey, type FlowTotals } from '../components/EnergyFlowSankey'
|
||||
import { useEnergyFlowsDaily, useEnergyFlowsIntervals } from '../hooks/useEnergyFlows'
|
||||
import { useSiteStatus } from '../hooks/useSiteStatus'
|
||||
import { pragueCalendarDay } from '../lib/pragueDate'
|
||||
import type { DailyEnergyFlows } from '../types/energy-flows'
|
||||
|
||||
function currentMonth(): string {
|
||||
return pragueCalendarDay().slice(0, 7)
|
||||
}
|
||||
|
||||
function monthLabel(ym: string): string {
|
||||
const [y, m] = ym.split('-').map(Number)
|
||||
const names = [
|
||||
'Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen',
|
||||
'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec',
|
||||
]
|
||||
return `${names[m - 1]} ${y}`
|
||||
}
|
||||
|
||||
function shiftMonth(ym: string, delta: number): string {
|
||||
const [y, m] = ym.split('-').map(Number)
|
||||
const d = new Date(y, m - 1 + delta, 1)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function fmtDay(iso: string): string {
|
||||
const d = new Date(iso + 'T00:00:00')
|
||||
return d.toLocaleDateString('cs-CZ', { weekday: 'short', day: 'numeric', month: 'numeric' })
|
||||
}
|
||||
|
||||
function fmtTime(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Prague' })
|
||||
}
|
||||
|
||||
function kwh(v: number | null | undefined, d = 2): string {
|
||||
if (v == null) return '–'
|
||||
return v.toFixed(d)
|
||||
}
|
||||
|
||||
function aggregateFlows(days: DailyEnergyFlows[]): FlowTotals & {
|
||||
pv_production_kwh: number
|
||||
grid_import_kwh: number
|
||||
grid_export_kwh: number
|
||||
batt_charge_kwh: number
|
||||
batt_discharge_kwh: number
|
||||
load_kwh: number
|
||||
} {
|
||||
const z = {
|
||||
pv_production_kwh: 0,
|
||||
grid_import_kwh: 0,
|
||||
grid_export_kwh: 0,
|
||||
batt_charge_kwh: 0,
|
||||
batt_discharge_kwh: 0,
|
||||
load_kwh: 0,
|
||||
pv_to_load_kwh: 0,
|
||||
pv_to_batt_kwh: 0,
|
||||
pv_to_grid_kwh: 0,
|
||||
batt_to_load_kwh: 0,
|
||||
batt_to_grid_kwh: 0,
|
||||
grid_to_load_kwh: 0,
|
||||
grid_to_batt_kwh: 0,
|
||||
}
|
||||
for (const d of days) {
|
||||
z.pv_production_kwh += d.pv_production_kwh
|
||||
z.grid_import_kwh += d.grid_import_kwh
|
||||
z.grid_export_kwh += d.grid_export_kwh
|
||||
z.batt_charge_kwh += d.batt_charge_kwh
|
||||
z.batt_discharge_kwh += d.batt_discharge_kwh
|
||||
z.load_kwh += d.load_kwh
|
||||
z.pv_to_load_kwh += d.pv_to_load_kwh
|
||||
z.pv_to_batt_kwh += d.pv_to_batt_kwh
|
||||
z.pv_to_grid_kwh += d.pv_to_grid_kwh
|
||||
z.batt_to_load_kwh += d.batt_to_load_kwh
|
||||
z.batt_to_grid_kwh += d.batt_to_grid_kwh
|
||||
z.grid_to_load_kwh += d.grid_to_load_kwh
|
||||
z.grid_to_batt_kwh += d.grid_to_batt_kwh
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
function IntervalDetail({ siteId, day }: { siteId: number; day: string }) {
|
||||
const { intervals, loading } = useEnergyFlowsIntervals(siteId, day)
|
||||
|
||||
if (loading) {
|
||||
return <div className="py-3 text-center text-xs text-slate-500">Načítání intervalů…</div>
|
||||
}
|
||||
if (intervals.length === 0) {
|
||||
return <div className="py-3 text-center text-xs text-slate-500">Žádné intervaly</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700 text-slate-400">
|
||||
<th className="px-2 py-1 text-left">Čas</th>
|
||||
<th className="px-2 py-1 text-right">PV→Load</th>
|
||||
<th className="px-2 py-1 text-right">PV→Batt</th>
|
||||
<th className="px-2 py-1 text-right">PV→Grid</th>
|
||||
<th className="px-2 py-1 text-right">Batt→Load</th>
|
||||
<th className="px-2 py-1 text-right">Batt→Grid</th>
|
||||
<th className="px-2 py-1 text-right">Grid→Load</th>
|
||||
<th className="px-2 py-1 text-right">Grid→Batt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{intervals.map((iv) => (
|
||||
<tr key={iv.interval_start} className="border-b border-slate-800 hover:bg-slate-800/40">
|
||||
<td className="px-2 py-1 text-slate-300">{fmtTime(iv.interval_start)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.pv_to_load_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.pv_to_batt_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.pv_to_grid_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.batt_to_load_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.batt_to_grid_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.grid_to_load_kwh)}</td>
|
||||
<td className="px-2 py-1 text-right">{kwh(iv.grid_to_batt_kwh)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function EnergyFlows() {
|
||||
const { site: siteRow, ready: siteReady, error: siteError } = useSiteStatus()
|
||||
const siteId = siteRow?.site_id ?? null
|
||||
|
||||
const [month, setMonth] = useState(currentMonth)
|
||||
const [expandedDay, setExpandedDay] = useState<string | null>(null)
|
||||
|
||||
const { days, loading, error, reload } = useEnergyFlowsDaily(siteId, month)
|
||||
|
||||
const totals = useMemo(() => (days.length > 0 ? aggregateFlows(days) : null), [days])
|
||||
|
||||
const flowOnly: FlowTotals | null = totals
|
||||
? {
|
||||
pv_to_load_kwh: totals.pv_to_load_kwh,
|
||||
pv_to_batt_kwh: totals.pv_to_batt_kwh,
|
||||
pv_to_grid_kwh: totals.pv_to_grid_kwh,
|
||||
batt_to_load_kwh: totals.batt_to_load_kwh,
|
||||
batt_to_grid_kwh: totals.batt_to_grid_kwh,
|
||||
grid_to_load_kwh: totals.grid_to_load_kwh,
|
||||
grid_to_batt_kwh: totals.grid_to_batt_kwh,
|
||||
}
|
||||
: null
|
||||
|
||||
const battEff =
|
||||
totals && totals.batt_charge_kwh > 0.01
|
||||
? Math.min(100, (totals.batt_discharge_kwh / totals.batt_charge_kwh) * 100)
|
||||
: null
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-7xl space-y-6 px-4 py-6 md:px-8">
|
||||
{siteReady && siteRow && (
|
||||
<p className="text-xs text-slate-500">
|
||||
Lokalita: <span className="text-slate-400">{siteRow.site_code}</span> — toky jsou{' '}
|
||||
<strong className="text-slate-400">modelované</strong> prioritní alokací z minutové telemetrie (
|
||||
<code className="rounded bg-slate-800 px-1">fn_fill_audit_interval</code>), ne přímé měření větví.
|
||||
</p>
|
||||
)}
|
||||
{siteError && (
|
||||
<div className="rounded-lg border border-amber-900/50 bg-amber-950/30 px-3 py-2 text-sm text-amber-200">
|
||||
{siteError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth((m) => shiftMonth(m, -1))}
|
||||
className="rounded-lg p-2 text-slate-400 transition hover:bg-slate-800 hover:text-white"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<h1 className="min-w-[200px] text-center text-lg font-semibold text-white">
|
||||
Toky energie — {monthLabel(month)}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth((m) => shiftMonth(m, 1))}
|
||||
className="rounded-lg p-2 text-slate-400 transition hover:bg-slate-800 hover:text-white"
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!siteReady ? (
|
||||
<div className="py-12 text-center text-sm text-slate-500">Načítání lokality…</div>
|
||||
) : siteId == null ? (
|
||||
<div className="py-12 text-center text-sm text-slate-500">Vyberte lokalitu v horní liště.</div>
|
||||
) : loading ? (
|
||||
<div className="py-12 text-center text-sm text-slate-500">Načítání…</div>
|
||||
) : error ? (
|
||||
<div className="rounded-lg border border-red-900/50 bg-red-950/30 px-4 py-3 text-sm text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{totals && (
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-amber-400">Perspektiva FVE</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{totals.pv_production_kwh.toFixed(1)} kWh</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-slate-400">
|
||||
<li>→ spotřeba: {totals.pv_to_load_kwh.toFixed(2)} kWh</li>
|
||||
<li>→ baterie: {totals.pv_to_batt_kwh.toFixed(2)} kWh</li>
|
||||
<li>→ síť (export): {totals.pv_to_grid_kwh.toFixed(2)} kWh</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-sky-400">Perspektiva síť</p>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Import: <span className="font-semibold text-red-300">{totals.grid_import_kwh.toFixed(2)}</span> kWh
|
||||
{' · '}
|
||||
Export:{' '}
|
||||
<span className="font-semibold text-green-300">{totals.grid_export_kwh.toFixed(2)}</span> kWh
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-slate-400">
|
||||
<li>Import → spotřeba: {totals.grid_to_load_kwh.toFixed(2)} kWh</li>
|
||||
<li>Import → baterie: {totals.grid_to_batt_kwh.toFixed(2)} kWh</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-emerald-400">Perspektiva baterie</p>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Nabito: <span className="text-white">{totals.batt_charge_kwh.toFixed(2)}</span> kWh · Vybito:{' '}
|
||||
<span className="text-white">{totals.batt_discharge_kwh.toFixed(2)}</span> kWh
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-slate-400">
|
||||
<li>Z FVE: {totals.pv_to_batt_kwh.toFixed(2)} kWh · Ze sítě: {totals.grid_to_batt_kwh.toFixed(2)} kWh</li>
|
||||
<li>Do spotřeby: {totals.batt_to_load_kwh.toFixed(2)} kWh · Do sítě: {totals.batt_to_grid_kwh.toFixed(2)} kWh</li>
|
||||
{battEff != null && (
|
||||
<li className="text-slate-500">Poměr vybití/nabití: {battEff.toFixed(0)} % (zjednodušeně)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<h2 className="mb-2 text-sm font-medium text-slate-300">Sankey — součet za měsíc</h2>
|
||||
<EnergyFlowSankey totals={flowOnly} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-xl border border-slate-800 bg-slate-900">
|
||||
<div className="border-b border-slate-800 px-4 py-3">
|
||||
<h2 className="text-sm font-medium text-slate-300">Denní přehled</h2>
|
||||
</div>
|
||||
{days.length === 0 ? (
|
||||
<div className="px-4 py-10 text-center text-sm text-slate-500">
|
||||
Žádná data — zkuste jiný měsíc nebo backfill auditu (
|
||||
<code className="rounded bg-slate-800 px-1">fn_fill_audit_range</code>).
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700 text-xs text-slate-400">
|
||||
<th className="px-3 py-2 text-left">Den</th>
|
||||
<th className="px-3 py-2 text-right">PV kWh</th>
|
||||
<th className="px-3 py-2 text-right">Import</th>
|
||||
<th className="px-3 py-2 text-right">Export</th>
|
||||
<th className="px-3 py-2 text-right">Nabití</th>
|
||||
<th className="px-3 py-2 text-right">Vybití</th>
|
||||
<th className="px-3 py-2 text-right">Spotřeba</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{days.map((row) => (
|
||||
<Fragment key={row.day}>
|
||||
<tr
|
||||
className="cursor-pointer border-b border-slate-800 hover:bg-slate-800/50"
|
||||
onClick={() => setExpandedDay((p) => (p === row.day ? null : row.day))}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm text-slate-200">
|
||||
<span className="mr-1 inline-block w-4">
|
||||
{expandedDay === row.day ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
</span>
|
||||
{fmtDay(row.day)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.pv_production_kwh.toFixed(1)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.grid_import_kwh.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.grid_export_kwh.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.batt_charge_kwh.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.batt_discharge_kwh.toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{row.load_kwh.toFixed(1)}</td>
|
||||
</tr>
|
||||
{expandedDay === row.day && siteId != null ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="bg-slate-900/50 px-4 py-2">
|
||||
<IntervalDetail siteId={siteId} day={row.day} />
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-slate-600">
|
||||
<button
|
||||
type="button"
|
||||
className="underline hover:text-slate-400"
|
||||
onClick={() => void reload()}
|
||||
>
|
||||
Znovu načíst
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
38
frontend/src/types/energy-flows.ts
Normal file
38
frontend/src/types/energy-flows.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export type DailyEnergyFlows = {
|
||||
day: string
|
||||
interval_count: number
|
||||
pv_production_kwh: number
|
||||
grid_import_kwh: number
|
||||
grid_export_kwh: number
|
||||
batt_charge_kwh: number
|
||||
batt_discharge_kwh: number
|
||||
load_kwh: number
|
||||
pv_to_load_kwh: number
|
||||
pv_to_batt_kwh: number
|
||||
pv_to_grid_kwh: number
|
||||
batt_to_load_kwh: number
|
||||
batt_to_grid_kwh: number
|
||||
grid_to_load_kwh: number
|
||||
grid_to_batt_kwh: number
|
||||
}
|
||||
|
||||
export type DailyEnergyFlowsResponse = {
|
||||
days: DailyEnergyFlows[]
|
||||
}
|
||||
|
||||
export type IntervalEnergyFlows = {
|
||||
interval_start: string
|
||||
pv_production_kwh: number | null
|
||||
grid_import_kwh: number | null
|
||||
grid_export_kwh: number | null
|
||||
batt_charge_kwh: number | null
|
||||
batt_discharge_kwh: number | null
|
||||
load_kwh: number | null
|
||||
pv_to_load_kwh: number | null
|
||||
pv_to_batt_kwh: number | null
|
||||
pv_to_grid_kwh: number | null
|
||||
batt_to_load_kwh: number | null
|
||||
batt_to_grid_kwh: number | null
|
||||
grid_to_load_kwh: number | null
|
||||
grid_to_batt_kwh: number | null
|
||||
}
|
||||
Reference in New Issue
Block a user