Cena nákup: {buy != null ? `${buy.toFixed(3)} Kč/kWh` : '—'} · prodej:{' '}
@@ -203,6 +409,59 @@ function PlanTooltip({ active, payload }: { active?: boolean; payload?: Array<{
)
}
+function CenaCell({ i, nowMs }: { i: PlanningIntervalDto; nowMs: number }) {
+ const pred = isPredictedPriceSlot(i, nowMs)
+ return (
+
+
+ {pred && (
+
+ odhad
+
+ )}
+
+ {i.effective_buy_price != null ? i.effective_buy_price.toFixed(3) : '—'}
+ /
+ {i.effective_sell_price != null ? i.effective_sell_price.toFixed(3) : '—'}
+
+
+ |
+ )
+}
+
+function HorizonToggle({
+ value,
+ onChange,
+ disabled,
+}: {
+ value: HorizonHours
+ onChange: (h: HorizonHours) => void
+ disabled?: boolean
+}) {
+ const opts: HorizonHours[] = [24, 48, 96]
+ return (
+
+
Horizont:
+
+ {opts.map((h) => (
+
+ ))}
+
+
+ )
+}
+
export default function Planning() {
const { site, ready: siteReady } = useSiteStatus()
const siteId = site?.site_id ?? null
@@ -212,7 +471,10 @@ export default function Planning() {
const [error, setError] = useState
(null)
const [replanning, setReplanning] = useState(false)
const [prepAction, setPrepAction] = useState(null)
+ const [importDate, setImportDate] = useState<'today' | 'tomorrow'>('tomorrow')
const [selectedStart, setSelectedStart] = useState(null)
+ const [tableHorizonH, setTableHorizonH] = useState(48)
+ const [chartHorizonH, setChartHorizonH] = useState(48)
const load = useCallback(async () => {
if (siteId == null) return
@@ -239,36 +501,46 @@ export default function Planning() {
}, [siteId, load])
const nowMs = Date.now()
- const dayMs = 24 * 60 * 60 * 1000
+ const slotFloorMs = floorSlotUtcMs(nowMs)
- const intervals24h = useMemo(() => {
+ const futureSlots = useMemo(() => {
if (!data?.intervals?.length) return []
- const end = nowMs + dayMs
return data.intervals
- .filter((i) => {
- const t = slotStartUtcMs(i.interval_start)
- return t >= nowMs && t < end
- })
- .slice(0, 96)
- }, [data?.intervals, nowMs])
+ .filter((i) => slotStartUtcMs(i.interval_start) >= slotFloorMs)
+ .sort((a, b) => slotStartUtcMs(a.interval_start) - slotStartUtcMs(b.interval_start))
+ .slice(0, MAX_FUTURE_SLOTS)
+ }, [data?.intervals, slotFloorMs])
+
+ const visibleSlots = useMemo(() => {
+ const endMs = nowMs + tableHorizonH * 60 * 60 * 1000
+ return futureSlots.filter((s) => slotStartUtcMs(s.interval_start) <= endMs)
+ }, [futureSlots, nowMs, tableHorizonH])
+
+ const chartIntervals = useMemo(() => {
+ const endMs = nowMs + chartHorizonH * 60 * 60 * 1000
+ return futureSlots.filter((s) => slotStartUtcMs(s.interval_start) <= endMs)
+ }, [futureSlots, nowMs, chartHorizonH])
+
+ const planTableRows = useMemo(() => buildPlanTableRows(visibleSlots), [visibleSlots])
const xTicks = useMemo(() => {
- if (!intervals24h.length) return undefined
- const stepMs = 2 * 60 * 60 * 1000
- const first = slotStartUtcMs(intervals24h[0].interval_start)
- const last = slotStartUtcMs(intervals24h[intervals24h.length - 1].interval_start)
+ if (!chartIntervals.length) return undefined
+ const stepH = chartHorizonH <= 24 ? 2 : chartHorizonH <= 48 ? 4 : 6
+ const stepMs = stepH * 60 * 60 * 1000
+ const first = slotStartUtcMs(chartIntervals[0].interval_start)
+ const last = slotStartUtcMs(chartIntervals[chartIntervals.length - 1].interval_start)
const ticks: string[] = []
let t = Math.ceil(first / stepMs) * stepMs
while (t <= last) {
- const hit = intervals24h.find((i) => Math.abs(slotStartUtcMs(i.interval_start) - t) < 30 * 60 * 1000)
+ const hit = chartIntervals.find((i) => Math.abs(slotStartUtcMs(i.interval_start) - t) < 30 * 60 * 1000)
if (hit) ticks.push(hit.interval_start)
t += stepMs
}
return ticks.length ? ticks.map((iso) => formatLocalTime(iso)) : undefined
- }, [intervals24h])
+ }, [chartIntervals, chartHorizonH])
const chartRows: ChartRow[] = useMemo(() => {
- return intervals24h.map((i) => ({
+ return chartIntervals.map((i) => ({
label: formatLocalTime(i.interval_start),
ts: slotStartUtcMs(i.interval_start),
pv_a_w: pvAProxyW(i),
@@ -277,7 +549,7 @@ export default function Planning() {
effective_buy_price: i.effective_buy_price,
raw: i,
}))
- }, [intervals24h])
+ }, [chartIntervals])
async function onReplan() {
if (siteId == null) return
@@ -304,7 +576,11 @@ export default function Planning() {
setPrepAction('import')
setError(null)
try {
- const r = await postImportSitePrices(siteId)
+ const selectedDate = new Date()
+ if (importDate === 'tomorrow') {
+ selectedDate.setDate(selectedDate.getDate() + 1)
+ }
+ const r = await postImportSitePrices(siteId, pragueYmd(selectedDate))
toast.success(
`Ceny: ${r.slots_imported} slotů (${r.date}), první ${r.first_price_czk_kwh.toFixed(3)} Kč/kWh`,
)
@@ -336,7 +612,11 @@ export default function Planning() {
setPrepAction('init')
setError(null)
try {
- const imp = await postImportSitePrices(siteId)
+ const selectedDate = new Date()
+ if (importDate === 'tomorrow') {
+ selectedDate.setDate(selectedDate.getDate() + 1)
+ }
+ const imp = await postImportSitePrices(siteId, pragueYmd(selectedDate))
toast.success(
`Ceny: ${imp.slots_imported} slotů (${imp.date}), první ${imp.first_price_czk_kwh.toFixed(3)} Kč/kWh`,
)
@@ -370,9 +650,7 @@ export default function Planning() {
const run = data?.run
const summary = data?.summary
- const planStale =
- run != null && Date.now() - new Date(run.created_at).getTime() > 2 * 60 * 60 * 1000
- const showPrepActions = !loading && (run == null || planStale)
+ const showPrepActions = !loading
const prepBusy = prepAction !== null
const correctionPct =
@@ -384,7 +662,8 @@ export default function Planning() {
@@ -410,6 +689,8 @@ export default function Planning() {
void handleImportPrices()}
onForecast={() => void handleRunForecast()}
onInit={() => void handleInitializePlan()}
@@ -462,6 +743,17 @@ export default function Planning() {
{run.solver_duration_ms != null ? `${run.solver_duration_ms} ms` : '—'}