import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react' import { Fragment, useEffect, 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 czk(v: number | null | undefined): string { if (v == null) return '–' return v.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } 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 grid_import_cashflow_czk: number grid_export_revenue_czk: number grid_to_load_cost_czk: number grid_to_batt_cost_czk: 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, grid_import_cashflow_czk: 0, grid_export_revenue_czk: 0, grid_to_load_cost_czk: 0, grid_to_batt_cost_czk: 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 z.grid_import_cashflow_czk += d.grid_import_cashflow_czk ?? 0 z.grid_export_revenue_czk += d.grid_export_revenue_czk ?? 0 z.grid_to_load_cost_czk += d.grid_to_load_cost_czk ?? 0 z.grid_to_batt_cost_czk += d.grid_to_batt_cost_czk ?? 0 } return z } function IntervalDetail({ siteId, day }: { siteId: number; day: string }) { const { intervals, loading } = useEnergyFlowsIntervals(siteId, day) if (loading) { return
Načítání intervalů…
} if (intervals.length === 0) { return
Žádné intervaly
} return (
{intervals.map((iv) => ( ))}
Čas PV→Load PV→Batt PV→Grid Batt→Load Batt→Grid Grid→Load Grid→Batt
{fmtTime(iv.interval_start)} {kwh(iv.pv_to_load_kwh)} {kwh(iv.pv_to_batt_kwh)} {kwh(iv.pv_to_grid_kwh)} {kwh(iv.batt_to_load_kwh)} {kwh(iv.batt_to_grid_kwh)} {kwh(iv.grid_to_load_kwh)} {kwh(iv.grid_to_batt_kwh)}
) } 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(null) /** null = součet za celý měsíc; jinak ISO den pro Sankey + perspektivní karty */ const [scopeDay, setScopeDay] = useState(null) const { days, loading, error, reload } = useEnergyFlowsDaily(siteId, month) useEffect(() => { setScopeDay(null) }, [month]) const totals = useMemo(() => { if (days.length === 0) return null if (scopeDay) { const row = days.find((d) => d.day === scopeDay) if (row) return aggregateFlows([row]) } return aggregateFlows(days) }, [days, scopeDay]) 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 const avgImportKcPerKwh = totals && totals.grid_import_kwh > 0.001 ? totals.grid_import_cashflow_czk / totals.grid_import_kwh : null const avgExportKcPerKwh = totals && totals.grid_export_kwh > 0.001 ? totals.grid_export_revenue_czk / totals.grid_export_kwh : null const gridNetCzk = totals != null ? totals.grid_import_cashflow_czk - totals.grid_export_revenue_czk : null return (
{siteReady && siteRow && (

Lokalita: {siteRow.site_code} — toky jsou{' '} modelované prioritní alokací z minutové telemetrie ( fn_fill_audit_interval), ne přímé měření větví.

)} {siteError && (
{siteError}
)}

Toky energie — {monthLabel(month)}

{!siteReady ? (
Načítání lokality…
) : siteId == null ? (
Vyberte lokalitu v horní liště.
) : loading ? (
Načítání…
) : error ? (
{error}
) : ( <> {days.length > 0 && (
)} {totals && (

Perspektiva FVE

{totals.pv_production_kwh.toFixed(1)} kWh

  • → spotřeba: {totals.pv_to_load_kwh.toFixed(2)} kWh
  • → baterie: {totals.pv_to_batt_kwh.toFixed(2)} kWh
  • → síť (export): {totals.pv_to_grid_kwh.toFixed(2)} kWh

Perspektiva síť

Import: {totals.grid_import_kwh.toFixed(2)} kWh {' · '} Export:{' '} {totals.grid_export_kwh.toFixed(2)} kWh

  • Import → spotřeba: {totals.grid_to_load_kwh.toFixed(2)} kWh
  • Import → baterie: {totals.grid_to_batt_kwh.toFixed(2)} kWh

Perspektiva baterie

Nabito: {totals.batt_charge_kwh.toFixed(2)} kWh · Vybito:{' '} {totals.batt_discharge_kwh.toFixed(2)} kWh

  • Z FVE: {totals.pv_to_batt_kwh.toFixed(2)} kWh · Ze sítě: {totals.grid_to_batt_kwh.toFixed(2)} kWh
  • Do spotřeby: {totals.batt_to_load_kwh.toFixed(2)} kWh · Do sítě: {totals.batt_to_grid_kwh.toFixed(2)} kWh
  • {battEff != null && (
  • Poměr vybití/nabití: {battEff.toFixed(0)} % (zjednodušeně)
  • )}

Perspektiva spotřeby

{totals.load_kwh.toFixed(1)} kWh

Celkový odběr (model z telemetrie)

  • Z FVE: {totals.pv_to_load_kwh.toFixed(2)} kWh
  • Z baterie: {totals.batt_to_load_kwh.toFixed(2)} kWh
  • Ze sítě (import): {totals.grid_to_load_kwh.toFixed(2)} kWh

Součet tří toků odpovídá modelu prioritní alokace; může se mírně lišit od sloupce „Spotřeba“ kvůli zaokrouhlení po 15min intervalech.

Perspektiva financí

Nákup ze sítě:{' '} {czk(totals.grid_import_cashflow_czk)}

Prodej do sítě:{' '} {czk(totals.grid_export_revenue_czk)}

Bilance sítě (nákup − prodej):{' '} {czk(gridNetCzk)}

  • Prům. cena nákupu:{' '} {avgImportKcPerKwh != null ? ( {avgImportKcPerKwh.toFixed(3)} Kč/kWh ) : ( )}
  • Prům. cena prodeje:{' '} {avgExportKcPerKwh != null ? ( {avgExportKcPerKwh.toFixed(3)} Kč/kWh ) : ( )}

Rozpad nákladů importu (efektivní cena × modelovaný tok)

  • Do spotřeby: {czk(totals.grid_to_load_cost_czk)} Kč
  • Do baterie: {czk(totals.grid_to_batt_cost_czk)} Kč

Stejná jednotková cena v každém 15min slotu; součet rozpadu se může mírně lišit od celkového nákupu kvůli zaokrouhlení a odchylce modelu toků od měřeného importu.

)}

Sankey — {scopeDay ? fmtDay(scopeDay) : 'celý měsíc (součet)'}

Denní přehled

{days.length === 0 ? (
Žádná data — zkuste jiný měsíc nebo backfill auditu ( fn_fill_audit_range).
) : (
{days.map((row) => ( setExpandedDay((p) => (p === row.day ? null : row.day))} > {expandedDay === row.day && siteId != null ? ( ) : null} ))}
Den PV kWh Import Export Nabití Vybití Spotřeba
{expandedDay === row.day ? : } {row.pv_production_kwh.toFixed(1)} {row.grid_import_kwh.toFixed(2)} {row.grid_export_kwh.toFixed(2)} {row.batt_charge_kwh.toFixed(2)} {row.batt_discharge_kwh.toFixed(2)} {row.load_kwh.toFixed(1)}
)}

)}
) }