import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Lock, Unlock } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { toast } from 'sonner' import { EconomicsChart } from '../components/charts/EconomicsChart' import { lockDay, unlockDay, useEconomicsChart, useEconomicsDaily, useEconomicsIntervals, } from '../hooks/useEconomics' import { useSiteStatus } from '../hooks/useSiteStatus' import { pragueCalendarDay } from '../lib/pragueDate' import type { DailyEconomics } from '../types/economics' function currentMonth(): string { const today = pragueCalendarDay() return today.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 czk(v: number | null | undefined, decimals = 2): string { if (v == null) return '–' const sign = v > 0 ? '+' : '' return `${sign}${v.toFixed(decimals)}` } function kwh(v: number | null | undefined): string { if (v == null) return '–' return v.toFixed(1) } function balanceColor(v: number): string { if (v > 0) return 'text-green-400' if (v < 0) return 'text-red-400' return 'text-slate-400' } function IntervalDetail({ siteId, day, hasGreenBonus }: { siteId: number; day: string; hasGreenBonus: boolean }) { const { intervals, loading } = useEconomicsIntervals(siteId, day) if (loading) { return
Načítání intervalů…
} if (intervals.length === 0) { return
Žádné intervaly
} return (
{hasGreenBonus && } {intervals.map((iv) => ( {hasGreenBonus && ( )} ))}
Čas Import kWh Export kWh Náklad Kč Cena nákup Cena prodejBonus KčPlán grid W Skuteč. grid W Plán náklad
{fmtTime(iv.interval_start)} {kwh(iv.import_kwh)} {kwh(iv.export_kwh)} {iv.dynamic_cost_czk != null ? iv.dynamic_cost_czk.toFixed(2) : '–'} {iv.effective_buy_price != null ? iv.effective_buy_price.toFixed(2) : '–'} {iv.effective_sell_price != null ? iv.effective_sell_price.toFixed(2) : '–'} {iv.green_bonus_czk != null && iv.green_bonus_czk > 0 ? iv.green_bonus_czk.toFixed(2) : '–'} {iv.planned_grid_w ?? '–'} {iv.actual_grid_power_w ?? '–'} {iv.planned_cost_czk != null ? iv.planned_cost_czk.toFixed(2) : '–'}
) } function DailyRow({ row, hasGreenBonus, siteId, expanded, onToggle, onLockToggle, }: { row: DailyEconomics hasGreenBonus: boolean siteId: number expanded: boolean onToggle: () => void onLockToggle: () => void }) { const colCount = hasGreenBonus ? 13 : 12 return ( <> {expanded ? : } {fmtDay(row.day)} {kwh(row.import_kwh)} {kwh(row.export_kwh)} {kwh(row.pv_self_consumption_kwh)} {row.grid_import_cashflow_czk.toFixed(2)} {row.grid_export_revenue_czk.toFixed(2)} {row.import_cost_czk.toFixed(2)} {row.export_revenue_czk.toFixed(2)} {row.planned_balance_czk != null ? czk(row.planned_balance_czk) : '–'} {row.deviation_cost_czk != null ? czk(row.deviation_cost_czk) : '–'} {hasGreenBonus && ( {row.green_bonus_czk > 0 ? row.green_bonus_czk.toFixed(2) : '–'} )} {czk(row.total_balance_czk)} {expanded && ( )} ) } export default function Economics() { const { site: siteRow, ready: siteReady, error: siteError } = useSiteStatus() const siteId = siteRow?.site_id ?? null const [month, setMonth] = useState(currentMonth) const [expandedDay, setExpandedDay] = useState(null) const { days, hasGreenBonus, loading, error, reload } = useEconomicsDaily(siteId, month) const { points } = useEconomicsChart(siteId, month) const summary = useMemo(() => { if (days.length === 0) return null return { import_cost: days.reduce((s, d) => s + d.import_cost_czk, 0), export_revenue: days.reduce((s, d) => s + d.export_revenue_czk, 0), grid_import_cashflow: days.reduce((s, d) => s + d.grid_import_cashflow_czk, 0), grid_export_revenue: days.reduce((s, d) => s + d.grid_export_revenue_czk, 0), green_bonus: days.reduce((s, d) => s + d.green_bonus_czk, 0), total_balance: days.reduce((s, d) => s + d.total_balance_czk, 0), } }, [days]) const handleLockToggle = useCallback( async (row: DailyEconomics) => { if (siteId == null) return try { if (row.is_locked) { await unlockDay(siteId, row.day) toast.success(`Den ${row.day} odemčen`) } else { await lockDay(siteId, row.day) toast.success(`Den ${row.day} zamčen`) } reload() } catch { toast.error('Operace se nezdařila') } }, [reload, siteId], ) return (
{siteReady && siteRow && (

Lokalita: {siteRow.site_code} (id {siteRow.site_id}) — ekonomika vychází z{' '} audit_interval, ne z raw telemetrie.

)} {siteError && (
{siteError} — API ekonomiky potřebuje stejnou lokalitu jako Přehled (PostgREST{' '} vw_site_status).
)} {/* Month selector */}

{monthLabel(month)}

{/* Summary cards */} {summary && (

Nákup ze sítě

{summary.grid_import_cashflow.toFixed(2)} Kč

Prodej do sítě

{summary.grid_export_revenue.toFixed(2)} Kč

Náklad celkem

{summary.import_cost.toFixed(2)} Kč

Příjem celkem

{summary.export_revenue.toFixed(2)} Kč

{hasGreenBonus && (

Zelený bonus

{summary.green_bonus.toFixed(2)} Kč

)}

Bilance měsíce

{czk(summary.total_balance)} Kč

)} {/* Chart */}

Denní bilance (síť + bonus) a kumulativ

{/* Daily table */}
{error && (
{error}
)} {!siteReady ? (
Načítání lokality…
) : siteId == null ? (
Není dostupná lokalita z přehledu. Zkontrolujte PostgREST a tabulku{' '} ems.vw_site_status.
) : loading ? (
Načítání…
) : days.length === 0 ? (

Žádná data pro tento měsíc

Záložka čte jen to, co je v ems.audit_interval (15min agregace po jobu audit filler). Telemetrie a ceny samy o sobě tabulku audit nenaplní.

Zkuste jiný měsíc (šipky), pokud máte historii jinde než v aktuálním měsíci.

Backfill na DB (psql):{' '} SELECT ems.fn_fill_audit_range(<site_id>, '2024-01-01'::timestamptz, now());

) : (
{hasGreenBonus && } {days.map((row) => ( setExpandedDay((prev) => (prev === row.day ? null : row.day))} onLockToggle={() => handleLockToggle(row)} /> ))}
Den Import kWh Export kWh FVE vl. spot. Nákup ze sítě Prodej do sítě Náklad Kč Příjem Kč Plán Kč Odchylka KčBonus KčBilance Kč
)}
) }