import axios from 'axios' import { Car } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' import { patchEvSession, type ActiveEvSessionRow } from '../api/backend' import { ModeLog } from '../components/ModeLog' import { ModeSelector } from '../components/ModeSelector' import { useEVSessions } from '../hooks/useEVSessions' import { useSiteStatus } from '../hooks/useSiteStatus' function SectionTitle({ kicker, title }: { kicker: string; title: string }) { return (

{kicker}

{title}

) } function toDatetimeLocalValue(d: Date): string { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const h = String(d.getHours()).padStart(2, '0') const min = String(d.getMinutes()).padStart(2, '0') return `${y}-${m}-${day}T${h}:${min}` } /** Dnešní HH:00 nebo zítřejší, pokud už je po té hodině (včetně celé hodiny). */ function nextDeadlineAtHour(hour: number): Date { const now = new Date() const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, 0, 0, 0) if (d.getTime() <= now.getTime()) { d.setDate(d.getDate() + 1) } return d } function isoToDatetimeLocal(iso: string): string { return toDatetimeLocalValue(new Date(iso)) } function datetimeLocalToIsoUtc(local: string): string { const d = new Date(local) if (Number.isNaN(d.getTime())) { return new Date().toISOString() } return d.toISOString() } function vehicleTitle(s: ActiveEvSessionRow): string { const m = (s.make ?? '').trim() const mo = (s.model ?? '').trim() if (!m && !mo) return 'Neznámé vozidlo' return `${m} ${mo}`.trim() } /** Popisek do toastu – preferuje model (např. Model Y). */ function toastVehicleLabel(s: ActiveEvSessionRow): string { const mo = (s.model ?? '').trim() if (mo) return mo return vehicleTitle(s) } const CHARGER_SLOTS: { code: string; label: string }[] = [ { code: 'ev-charger-1', label: 'Tesla' }, { code: 'ev-charger-2', label: 'Zoe' }, ] function EvChargerCard({ siteId, chargerLabel, session, onSaved, }: { siteId: number chargerLabel: string session: ActiveEvSessionRow | undefined onSaved: () => void }) { const [soc, setSoc] = useState('') const [deadlineLocal, setDeadlineLocal] = useState('') const [saving, setSaving] = useState(false) useEffect(() => { if (!session) { setSoc('') setDeadlineLocal('') return } const defSoc = session.target_soc_pct ?? session.default_target_soc_pct ?? 80 setSoc(Math.round(Number(defSoc))) if (session.target_deadline) { setDeadlineLocal(isoToDatetimeLocal(session.target_deadline)) } else { const h = session.default_deadline_hour ?? 7 setDeadlineLocal(toDatetimeLocalValue(nextDeadlineAtHour(h))) } }, [ session?.id, session?.target_soc_pct, session?.target_deadline, session?.default_deadline_hour, session?.default_target_soc_pct, ]) if (!session) { return (

Nepřipojeno

{chargerLabel}

) } const kwh = ((session.energy_delivered_wh ?? 0) / 1000).toFixed(1) const onSubmit = async (e: React.FormEvent) => { e.preventDefault() if (soc === '' || !deadlineLocal) return const clamped = Math.min(100, Math.max(10, Math.round(Number(soc)))) setSaving(true) try { await patchEvSession(siteId, session.id, { target_soc_pct: clamped, target_deadline: datetimeLocalToIsoUtc(deadlineLocal), }) toast.success(`Deadline nastaven pro ${toastVehicleLabel(session)}`) onSaved() } catch (err) { const msg = axios.isAxiosError(err) && err.response?.data && typeof err.response.data === 'object' ? JSON.stringify(err.response.data) : err instanceof Error ? err.message : 'Neznámá chyba' toast.error('Uložení se nezdařilo', { description: msg }) } finally { setSaving(false) } } return (

Připojeno

{vehicleTitle(session)}

{chargerLabel}

void onSubmit(e)} className="mt-4 space-y-3">

Energie v session:{' '} {kwh} kWh

) } export function Settings() { const { site, ready, reload } = useSiteStatus() const siteId = site?.site_id ?? null const { sessions, ready: evReady, error: evError, reload: reloadEv } = useEVSessions(siteId) return (

Nastavení

Provozní režim a plánování flexibilní zátěže

{ready && site ? (

Lokalita: {site.site_name} ({site.site_code})

) : null}

Přepnutí zapíše stav do databáze a notifikuje Loxone. U dočasného režimu lze nastavit čas návratu; po vypršení systém obnoví předchozí režim.

void reload()} />

Poslední přepnutí

Při připojení vozidla na wallbox se zobrazí aktivní session (dotaz každých 30 s). Cílový SoC a deadline se ukládají do ev_session pro plánovač.

{siteId === null ? (

Načítám lokalitu…

) : !evReady ? (

Načítám EV session…

) : ( <> {evError ?

{evError}

: null}
{CHARGER_SLOTS.map((slot) => ( s.charger_code === slot.code)} onSaved={() => void reloadEv()} /> ))}
)}
) }