Initial commit
Made-with: Cursor
This commit is contained in:
132
frontend/src/components/ModeLog.tsx
Normal file
132
frontend/src/components/ModeLog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user