133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
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>
|
|
)
|
|
}
|