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 (
)
}
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}
)
}
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 (
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()}
/>
))}
>
)}
)
}