Values now update live without manual page reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { api } from '../api/client';
|
|
import StatusBadge from '../components/StatusBadge';
|
|
|
|
interface Datapoint {
|
|
id: number;
|
|
name: string;
|
|
label: string | null;
|
|
value: string | null;
|
|
unit: string | null;
|
|
source_type: string;
|
|
status: string;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export default function Datapoints() {
|
|
const [datapoints, setDatapoints] = useState<Datapoint[]>([]);
|
|
const [edits, setEdits] = useState<Record<number, { label: string; unit: string }>>({});
|
|
const [error, setError] = useState('');
|
|
|
|
async function load() {
|
|
try {
|
|
const data = await api.get<Datapoint[]>('/api/datapoints');
|
|
setDatapoints(data);
|
|
const e: Record<number, { label: string; unit: string }> = {};
|
|
data.forEach((dp) => { e[dp.id] = { label: dp.label ?? '', unit: dp.unit ?? '' }; });
|
|
setEdits(e);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load();
|
|
const interval = setInterval(load, 10000);
|
|
return () => clearInterval(interval);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
async function handleBlur(id: number, field: 'label' | 'unit') {
|
|
try {
|
|
await api.put(`/api/datapoints/${id}`, { [field]: edits[id]?.[field] || null });
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
}
|
|
}
|
|
|
|
async function handleDelete(id: number) {
|
|
if (!confirm('Datenpunkt wirklich loeschen?')) return;
|
|
try {
|
|
await api.del(`/api/datapoints/${id}`);
|
|
load();
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-gray-800 mb-6">Datenpunkte</h2>
|
|
{error && <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">{error}</div>}
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Interner Name</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Bezeichnung</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Wert</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Einheit</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Quelle</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{datapoints.length === 0 && (
|
|
<tr><td colSpan={7} className="px-4 py-6 text-center text-gray-400">Keine Datenpunkte vorhanden</td></tr>
|
|
)}
|
|
{datapoints.map((dp) => (
|
|
<tr key={dp.id} className="hover:bg-gray-50">
|
|
<td className="px-4 py-2 font-mono text-xs text-gray-600">{dp.name}</td>
|
|
<td className="px-4 py-2">
|
|
<input
|
|
className="border border-transparent hover:border-gray-300 focus:border-sky-400 rounded px-2 py-0.5 text-sm w-full focus:outline-none"
|
|
value={edits[dp.id]?.label ?? ''}
|
|
onChange={(e) => setEdits({ ...edits, [dp.id]: { ...edits[dp.id], label: e.target.value } })}
|
|
onBlur={() => handleBlur(dp.id, 'label')}
|
|
/>
|
|
</td>
|
|
<td className="px-4 py-2 font-semibold text-gray-900">{dp.value ?? '-'}</td>
|
|
<td className="px-4 py-2">
|
|
<input
|
|
className="border border-transparent hover:border-gray-300 focus:border-sky-400 rounded px-2 py-0.5 text-sm w-20 focus:outline-none"
|
|
value={edits[dp.id]?.unit ?? ''}
|
|
onChange={(e) => setEdits({ ...edits, [dp.id]: { ...edits[dp.id], unit: e.target.value } })}
|
|
onBlur={() => handleBlur(dp.id, 'unit')}
|
|
/>
|
|
</td>
|
|
<td className="px-4 py-2 text-gray-500 text-xs">{dp.source_type}</td>
|
|
<td className="px-4 py-2"><StatusBadge status={dp.status} /></td>
|
|
<td className="px-4 py-2">
|
|
<button onClick={() => handleDelete(dp.id)} className="text-red-400 hover:text-red-600 text-xs">Loeschen</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|