Initial commit

Made-with: Cursor
This commit is contained in:
Dusan Vojacek
2026-03-20 13:27:37 +01:00
commit 8b4af663d8
77 changed files with 13337 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { useCallback, useEffect, useState } from 'react'
import { getJson } from '../api/postgrest'
import type { ModeLogRecentRow } from '../types/ems'
function modeBadgeClass(code: string): string {
const c = code.toUpperCase()
if (c === 'AUTO') return 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/35'
if (c === 'SELF_SUSTAIN') return 'bg-cyan-500/15 text-cyan-300 ring-1 ring-cyan-500/35'
if (c === 'CHARGE_CHEAP') return 'bg-violet-500/15 text-violet-200 ring-1 ring-violet-500/35'
if (c === 'PRESERVE') return 'bg-amber-500/15 text-amber-200 ring-1 ring-amber-500/35'
if (c === 'MANUAL') return 'bg-slate-600/50 text-slate-200 ring-1 ring-slate-500/40'
return 'bg-slate-700/60 text-slate-200 ring-1 ring-slate-600/50'
}
function num(v: string | number | null | undefined): number {
if (v == null) return NaN
const n = typeof v === 'number' ? v : Number(v)
return n
}
function formatDuration(sec: number): string {
if (!Number.isFinite(sec) || sec < 0) return '—'
const h = Math.floor(sec / 3600)
const m = Math.floor((sec % 3600) / 60)
const s = Math.floor(sec % 60)
if (h > 0) return `${h} h ${m} min`
if (m > 0) return `${m} min ${s > 0 ? `${s} s` : ''}`.trim()
return `${s} s`
}
function fmtTime(iso: string): string {
try {
const d = new Date(iso)
return d.toLocaleString('cs-CZ', { dateStyle: 'short', timeStyle: 'medium' })
} catch {
return iso
}
}
type Props = {
siteId: number | null
}
export function ModeLog({ siteId }: Props) {
const [rows, setRows] = useState<ModeLogRecentRow[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const load = useCallback(async () => {
if (siteId == null) {
setRows([])
setLoading(false)
return
}
setLoading(true)
setError(null)
try {
const data = await getJson<ModeLogRecentRow[]>('/vw_mode_log_recent', {
site_id: `eq.${siteId}`,
order: 'activated_at.desc',
limit: '20',
})
setRows(Array.isArray(data) ? data : [])
} catch (e) {
setError(String(e))
setRows([])
} finally {
setLoading(false)
}
}, [siteId])
useEffect(() => {
void load()
}, [load])
if (siteId == null) {
return <p className="text-sm text-slate-500">Vyberte nebo načtěte lokalitu.</p>
}
if (loading) {
return <div className="h-40 animate-pulse rounded-xl border border-slate-800 bg-slate-900/40" />
}
if (error) {
return <p className="text-sm text-red-400">Nelze načíst log: {error}</p>
}
return (
<div className="overflow-x-auto rounded-xl border border-slate-800">
<table className="w-full min-w-[640px] border-collapse text-left text-sm">
<thead>
<tr className="border-b border-slate-800 bg-slate-900/80 text-xs font-semibold uppercase tracking-wide text-slate-500">
<th className="px-4 py-3">Čas</th>
<th className="px-4 py-3">Režim</th>
<th className="px-4 py-3">Trvání</th>
<th className="px-4 py-3">Kdo</th>
<th className="px-4 py-3">Poznámka</th>
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">
Žádné záznamy za posledních 7 dní.
</td>
</tr>
) : (
rows.map((r) => (
<tr key={r.id} className="border-b border-slate-800/80 hover:bg-slate-900/40">
<td className="whitespace-nowrap px-4 py-3 tabular-nums text-slate-300">{fmtTime(r.activated_at)}</td>
<td className="px-4 py-3">
<span
className={`inline-flex rounded-md px-2 py-0.5 text-xs font-semibold uppercase tracking-wide ${modeBadgeClass(r.mode_code)}`}
>
{r.mode_code}
</span>
</td>
<td className="whitespace-nowrap px-4 py-3 tabular-nums text-slate-400">{formatDuration(num(r.duration_sec))}</td>
<td className="max-w-[140px] truncate px-4 py-3 text-slate-400" title={r.activated_by ?? ''}>
{r.activated_by ?? '—'}
</td>
<td className="max-w-[280px] truncate px-4 py-3 text-slate-400" title={r.notes ?? ''}>
{r.notes?.trim() ? r.notes : '—'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)
}