import { ChevronDown, ChevronRight, ExternalLink } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { NavLink } from 'react-router-dom'
import { getSiteConfiguration } from '../api/backend'
import { useSiteSelection } from '../context/SiteSelectionContext'
import type { SiteConfigurationResponse } from '../types/siteConfiguration'
export function osmMapUrl(lat: number, lon: number): string {
return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`
}
export function googleMapUrl(lat: number, lon: number): string {
return `https://www.google.com/maps?q=${lat},${lon}`
}
/** OSM embed: bbox = minLon, minLat, maxLon, maxLat */
export function osmEmbedUrl(lat: number, lon: number, delta = 0.012): string {
const left = lon - delta
const right = lon + delta
const bottom = lat - delta
const top = lat + delta
const bbox = `${left},${bottom},${right},${top}`
return `https://www.openstreetmap.org/export/embed.html?bbox=${encodeURIComponent(bbox)}&layer=mapnik&marker=${encodeURIComponent(`${lat},${lon}`)}`
}
function fmtVal(v: unknown): string {
if (v == null) return '—'
if (typeof v === 'boolean') return v ? 'ano' : 'ne'
if (typeof v === 'number') return Number.isFinite(v) ? String(v) : '—'
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
}
function DefRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
{label}
{children}
)
}
function Section({
kicker,
title,
children,
}: {
kicker: string
title: string
children: React.ReactNode
}) {
return (
{kicker}
{title}
{children}
)
}
function ObjectRows({
obj,
labels,
skip = [],
}: {
obj: Record
labels?: Record
skip?: string[]
}) {
const keys = Object.keys(obj)
.filter((k) => !skip.includes(k))
.filter((k) => obj[k] !== undefined)
.sort()
if (keys.length === 0) {
return Žádná data
}
return (
{keys.map((k) => (
{fmtVal(obj[k])}
))}
)
}
const GRID_LABELS: Record = {
max_import_power_w: 'Max. import (W)',
max_export_power_w: 'Max. export (W)',
no_export: 'Zákaz exportu',
block_export_on_negative_sell:
'LP: při záporném výkupu zákaz vývozu (site_grid_connection; viz planning.md)',
reserved_capacity_w: 'Rezervovaný výkon (W)',
notes: 'Poznámky',
}
const MARKET_LABELS: Record = {
purchase_pricing_mode: 'Režim nákupu',
sale_pricing_mode: 'Režim prodeje',
buy_margin_fixed_czk: 'Marže nákup fix (Kč/kWh)',
buy_margin_percent: 'Marže nákup %',
sell_margin_fixed_czk: 'Marže prodej fix (Kč/kWh)',
sell_margin_percent: 'Marže prodej %',
currency: 'Měna',
valid_from: 'Platnost od',
valid_to: 'Platnost do',
notes: 'Poznámky',
}
export default function SiteConfiguration() {
const { selectedSiteId, ready: selectionReady, error: selectionError } = useSiteSelection()
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [deyeOpen, setDeyeOpen] = useState>({})
const load = useCallback(async () => {
if (selectedSiteId == null) {
setData(null)
setError(null)
return
}
setLoading(true)
setError(null)
try {
const res = await getSiteConfiguration(selectedSiteId)
setData(res)
} catch {
setData(null)
setError('Konfiguraci se nepodařilo načíst')
} finally {
setLoading(false)
}
}, [selectedSiteId])
useEffect(() => {
void load()
}, [load])
const toggleDeye = (id: number) => {
setDeyeOpen((prev) => ({ ...prev, [id]: !prev[id] }))
}
if (!selectionReady) {
return (
)
}
if (selectionError != null || selectedSiteId == null) {
return (
{selectionError ?? 'Vyberte lokalitu v horní liště.'}
)
}
const site = data?.site
const lat = site?.latitude
const lon = site?.longitude
const hasCoords = lat != null && lon != null && Number.isFinite(lat) && Number.isFinite(lon)
return (
Konfigurace lokality
Souhrn statického nastavení z databáze pro vybranou lokalitu (pouze čtení).
{site ? (
{site.name} ({site.code}) · ID {site.id}
) : null}
{loading ?
Načítám konfiguraci…
: null}
{error ?
{error}
: null}
{data ? (
{site?.timezone}
{fmtVal(site?.active)}
{site?.notes?.trim() ? site.notes : '—'}
{site?.created_at ?? '—'}
{lat != null ? String(lat) : '—'}
{lon != null ? String(lon) : '—'}
{hasCoords ? (
) : (
Souřadnice nejsou vyplněné – mapa se nezobrazí.
)}
{data.operational.heartbeat_last_seen ?? '—'}
{data.operational.heartbeat_status ?? '—'}
{fmtVal(data.operational.has_active_plan)}
{data.operational.active_plan_created_at ?? '—'}
{data.grid_connection ? (
) : (
Záznam site_grid_connection chybí.
)}
{data.market_config_note}
{data.market_config ? (
) : (
Aktuální platný záznam site_market_config nenalezen.
)}
{data.endpoints.length === 0 ? (
Žádné endpointy.
) : (
{data.endpoints.map((ep) => (
-
{ep.endpoint_type}
{ep.enabled ? '' : ' (vypnuto)'}
{ep.host}
{ep.port != null ? `:${ep.port}` : ''} {ep.protocol ? `· ${ep.protocol}` : ''}
{ep.unit_id != null ? Unit ID: {ep.unit_id}
: null}
{ep.auth_reference ? (
Auth reference: {ep.auth_reference}
) : null}
{ep.notes ? {ep.notes}
: null}
))}
)}
{data.operating_mode ? (
) : (
Chybí záznam site_operating_mode.
)}
{data.active_overrides.length === 0 ? (
Žádné aktivní přepisy.
) : (
{data.active_overrides.map((o, i) => (
-
{fmtVal(o.override_type)}
{fmtVal(o.valid_from)} → {o.valid_to != null ? fmtVal(o.valid_to) : 'bez konce'}
{o.reason ? {String(o.reason)}
: null}
{o.value_json != null ? (
{typeof o.value_json === 'string' ? o.value_json : JSON.stringify(o.value_json, null, 2)}
) : null}
))}
)}
{data.inverters.length === 0 ? (
Žádné střídače.
) : (
{data.inverters.map((inv) => {
const row = inv as Record
const { deye_meta: dmRaw, ...rest } = row
const dm = dmRaw as Record | null | undefined
const id = Number(rest.id)
const hasDeye = dm != null && Object.keys(dm).length > 0
const open = deyeOpen[id] ?? false
return (
-
{fmtVal(rest.code)}
{hasDeye ? (
{open ? (
) : null}
) : null}
)
})}
)}
{data.batteries.length === 0 ? (
Žádné baterie.
) : (
{data.batteries.map((b, i) => (
-
))}
)}
{data.pv_arrays.length === 0 ? (
Žádná pole.
) : (
{data.pv_arrays.map((p, i) => (
-
))}
)}
Nabíječky
{data.ev_chargers.length === 0 ? (
Žádné nabíječky.
) : (
{data.ev_chargers.map((c, i) => (
-
))}
)}
Vozidla
{data.vehicles.length === 0 ? (
Žádná vozidla.
) : (
{data.vehicles.map((v, i) => (
-
))}
)}
{data.heat_pumps.length === 0 ? (
Žádná tepelná čerpadla.
) : (
{data.heat_pumps.map((h, i) => (
-
))}
)}
) : null}
)
}