diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 29b39aa..e9c62e7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { Toaster } from 'sonner' import { NavLink, Outlet, Route, Routes } from 'react-router-dom' +import { SiteSelectionProvider, useSiteSelection } from './context/SiteSelectionContext' import { useWsLogErrorCount } from './hooks/useWsLogErrorCount' import { Dashboard } from './pages/Dashboard' import Economics from './pages/Economics' @@ -8,6 +9,47 @@ import { Logs } from './pages/Logs' import Planning from './pages/Planning' import { Settings } from './pages/Settings' +function SiteCombo() { + const { sites, selectedSiteId, setSelectedSiteId, ready, error } = useSiteSelection() + + if (!ready) { + return ( + + Lokality… + + ) + } + + if (error != null || sites.length === 0) { + return ( + + {error ?? 'Žádná lokalita'} + + ) + } + + return ( + + ) +} + function AppLayout() { const logErrors = useWsLogErrorCount(true) @@ -19,7 +61,7 @@ function AppLayout() { return (
@@ -55,14 +98,16 @@ function AppLayout() { export default function App() { return ( - - }> - } /> - } /> - } /> - } /> - - } /> - + + + }> + } /> + } /> + } /> + } /> + + } /> + + ) } diff --git a/frontend/src/api/backend.ts b/frontend/src/api/backend.ts index 5444823..8e0a3f1 100644 --- a/frontend/src/api/backend.ts +++ b/frontend/src/api/backend.ts @@ -30,6 +30,24 @@ export async function getBackendHealthDetailed(): Promise { + const { data } = await client.get('/me/sites') + return Array.isArray(data) ? data : [] +} + export async function getSiteStatusFull(siteId: number): Promise { const { data } = await client.get(`/sites/${siteId}/status/full`) return data diff --git a/frontend/src/context/SiteSelectionContext.tsx b/frontend/src/context/SiteSelectionContext.tsx new file mode 100644 index 0000000..3973aea --- /dev/null +++ b/frontend/src/context/SiteSelectionContext.tsx @@ -0,0 +1,88 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from 'react' + +import { getMySites, type MeSiteRow } from '../api/backend' + +export const SITE_STORAGE_KEY = 'ems.selected_site_id' + +export type SiteSelectionContextValue = { + sites: MeSiteRow[] + selectedSiteId: number | null + setSelectedSiteId: (id: number) => void + ready: boolean + error: string | null +} + +const SiteSelectionContext = createContext(null) + +export function SiteSelectionProvider({ children }: { children: ReactNode }) { + const [sites, setSites] = useState([]) + const [selectedSiteId, setSelectedSiteIdState] = useState(null) + const [ready, setReady] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const list = await getMySites() + if (cancelled) return + setSites(list) + if (list.length === 0) { + setError('Žádná aktivní lokalita') + setSelectedSiteIdState(null) + } else { + setError(null) + const raw = localStorage.getItem(SITE_STORAGE_KEY) + const parsed = raw != null ? Number.parseInt(raw, 10) : Number.NaN + const valid = Number.isFinite(parsed) && list.some((s) => s.id === parsed) + setSelectedSiteIdState(valid ? parsed : list[0]!.id) + } + } catch { + if (!cancelled) { + setSites([]) + setSelectedSiteIdState(null) + setError('Lokality se nepodařilo načíst') + } + } finally { + if (!cancelled) setReady(true) + } + })() + return () => { + cancelled = true + } + }, []) + + const setSelectedSiteId = useCallback((id: number) => { + setSelectedSiteIdState(id) + localStorage.setItem(SITE_STORAGE_KEY, String(id)) + }, []) + + const value = useMemo( + (): SiteSelectionContextValue => ({ + sites, + selectedSiteId, + setSelectedSiteId, + ready, + error, + }), + [sites, selectedSiteId, setSelectedSiteId, ready, error], + ) + + return {children} +} + +export function useSiteSelection(): SiteSelectionContextValue { + const ctx = useContext(SiteSelectionContext) + if (ctx == null) { + throw new Error('useSiteSelection must be used within SiteSelectionProvider') + } + return ctx +} diff --git a/frontend/src/hooks/useSiteStatus.ts b/frontend/src/hooks/useSiteStatus.ts index 4813391..4ac8fab 100644 --- a/frontend/src/hooks/useSiteStatus.ts +++ b/frontend/src/hooks/useSiteStatus.ts @@ -1,26 +1,52 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' + +import { useSiteSelection } from '../context/SiteSelectionContext' import { getJson } from '../api/postgrest' import type { SiteStatusRow } from '../types/ems' const POLL_MS = 30_000 export function useSiteStatus() { + const { selectedSiteId, ready: selectionReady, error: selectionError } = useSiteSelection() const [row, setRow] = useState(null) - const [ready, setReady] = useState(false) - const [error, setError] = useState(null) + const [statusReady, setStatusReady] = useState(false) + const [fetchError, setFetchError] = useState(null) + const selectedSiteIdRef = useRef(selectedSiteId) + selectedSiteIdRef.current = selectedSiteId const load = useCallback(async () => { - try { - const rows = await getJson('/vw_site_status') - setRow(Array.isArray(rows) && rows.length > 0 ? rows[0]! : null) - setError(null) - } catch { - setRow(null) - setError('Stav lokality se nepodařilo načíst') - } finally { - setReady(true) + if (!selectionReady) { + return } - }, []) + if (selectedSiteId == null) { + setRow(null) + setFetchError(null) + setStatusReady(true) + return + } + const sid = selectedSiteId + try { + const rows = await getJson('/vw_site_status', { + site_id: `eq.${sid}`, + }) + if (selectedSiteIdRef.current !== sid) return + setRow(Array.isArray(rows) && rows.length > 0 ? rows[0]! : null) + setFetchError(null) + } catch { + if (selectedSiteIdRef.current !== sid) return + setRow(null) + setFetchError('Stav lokality se nepodařilo načíst') + } finally { + if (selectedSiteIdRef.current === sid) { + setStatusReady(true) + } + } + }, [selectionReady, selectedSiteId]) + + useEffect(() => { + if (!selectionReady) return + setStatusReady(false) + }, [selectionReady, selectedSiteId]) useEffect(() => { void load() @@ -28,6 +54,9 @@ export function useSiteStatus() { return () => window.clearInterval(id) }, [load]) + const ready = selectionReady && statusReady + const error = selectionError ?? fetchError + const hasTelemetry = row != null && (row.pv_power_w != null ||