import { useEffect, useState } from 'react'; import type { FormEvent } from 'react'; import { api } from '../api/client'; import EpaperPreview from '../components/EpaperPreview'; interface Layout { id: number; name: string; type: string; width: number; height: number; } interface LayoutElement { id: number; layout_id: number; type: string; x: number; y: number; width: number | null; height: number | null; font_size: number; font_weight: string; text_align: string; style: string; static_text: string | null; datapoint_id: number | null; icon: string | null; sort_order: number; } interface LayoutWithElements extends Layout { elements: LayoutElement[]; } interface Datapoint { id: number; name: string; label: string | null; value: string | null; unit: string | null; } const ICONS = ['', 'thermometer', 'droplet', 'wind', 'pressure', 'sun', 'bolt', 'gauge', 'wifi', 'battery', 'check', 'warning', 'clock']; export default function Layouts() { const [layouts, setLayouts] = useState([]); const [selected, setSelected] = useState(null); const [datapoints, setDatapoints] = useState([]); const [newName, setNewName] = useState(''); const [error, setError] = useState(''); async function loadLayouts() { try { setLayouts(await api.get('/api/layouts')); } catch (e) { setError((e as Error).message); } } async function loadDatapoints() { try { setDatapoints(await api.get('/api/datapoints')); } catch (_) {} } async function selectLayout(id: number) { try { setSelected(await api.get(`/api/layouts/${id}`)); } catch (e) { setError((e as Error).message); } } useEffect(() => { loadLayouts(); loadDatapoints(); }, []); async function handleAddLayout(e: FormEvent) { e.preventDefault(); if (!newName.trim()) return; try { const res = await api.post<{ id: number }>('/api/layouts', { name: newName.trim() }); setNewName(''); loadLayouts(); selectLayout(res.id); } catch (e) { setError((e as Error).message); } } async function handleDeleteLayout(id: number) { if (!confirm('Layout wirklich loeschen?')) return; try { await api.del(`/api/layouts/${id}`); if (selected?.id === id) setSelected(null); loadLayouts(); } catch (e) { setError((e as Error).message); } } async function handleAddElement(type: string) { if (!selected) return; const sortOrder = selected.elements.length; const defaults: Record = { value: { type: 'value', x: 20, y: 70 + sortOrder * 100, width: 370, height: 90, font_size: 32, font_weight: 'bold', style: 'card', sort_order: sortOrder }, label: { type: 'label', x: 400, y: 15, font_size: 28, font_weight: 'bold', text_align: 'center', static_text: 'Titel', sort_order: sortOrder }, line: { type: 'line', x: 10, y: 55, width: 780, height: 2, sort_order: sortOrder }, rect: { type: 'rect', x: 10, y: 10, width: 100, height: 50, sort_order: sortOrder }, }; try { await api.post(`/api/layouts/${selected.id}/elements`, defaults[type] || defaults.value); selectLayout(selected.id); } catch (e) { setError((e as Error).message); } } async function updateElement(elId: number, field: string, value: any) { if (!selected) return; try { await api.put(`/api/layouts/${selected.id}/elements/${elId}`, { [field]: value }); selectLayout(selected.id); } catch (e) { setError((e as Error).message); } } async function updateDatapointLabel(dpId: number, label: string) { try { await api.put(`/api/datapoints/${dpId}`, { label }); loadDatapoints(); if (selected) selectLayout(selected.id); } catch (e) { setError((e as Error).message); } } async function updateDatapointUnit(dpId: number, unit: string) { try { await api.put(`/api/datapoints/${dpId}`, { unit }); loadDatapoints(); if (selected) selectLayout(selected.id); } catch (e) { setError((e as Error).message); } } async function handleDeleteElement(elId: number) { if (!selected) return; try { await api.del(`/api/layouts/${selected.id}/elements/${elId}`); selectLayout(selected.id); } catch (e) { setError((e as Error).message); } } function handleExport() { if (!selected) return; window.open(`/api/layouts/${selected.id}/export`, '_blank'); } async function handleImport() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); const result = await api.post('/api/layouts/import', data); loadLayouts(); selectLayout(result.id); } catch (e) { setError((e as Error).message); } }; input.click(); } function NumInput({ value, onChange, label, w }: { value: number; onChange: (v: number) => void; label: string; w?: string }) { return (
{label}
{ const v = parseInt(e.target.value); if (!isNaN(v) && v !== value) onChange(v); }} />
); } return (

Layouts

{error &&
{error}
}
{/* Left: layout list */}
{layouts.length === 0 &&

Keine Layouts

} {layouts.map((l) => (
selectLayout(l.id)} className={`flex items-center justify-between px-3 py-2.5 cursor-pointer text-sm border-b border-gray-100 last:border-0 ${selected?.id === l.id ? 'bg-sky-50 text-sky-700 font-medium' : 'hover:bg-gray-50'}`}> {l.name}
))}
setNewName(e.target.value)} />
{/* Right: element editor */} {selected && (

{selected.name} ({selected.width}x{selected.height})

{/* Element list — editable */}
{selected.elements.length === 0 && (
Noch keine Elemente. Klicke oben auf "+ Wert" oder "+ Label" um ein Element hinzuzufuegen.
)} {selected.elements.map((el) => { const dp = datapoints.find((d) => d.id === el.datapoint_id); return (
{el.type.toUpperCase()} {el.type === 'value' && dp && ( {dp.label || dp.name} = {dp.value ?? '—'} {dp.unit || ''} )} {el.type === 'label' && ( "{el.static_text}" )}
{/* Position */} updateElement(el.id, 'x', v)} /> updateElement(el.id, 'y', v)} /> {/* Size (for value, line, rect) */} {(el.type === 'value' || el.type === 'line' || el.type === 'rect') && ( <> updateElement(el.id, 'width', v)} /> updateElement(el.id, 'height', v)} /> )} {/* Font size (for value, label) */} {(el.type === 'value' || el.type === 'label') && ( updateElement(el.id, 'font_size', v)} /> )} {/* Font weight */} {(el.type === 'value' || el.type === 'label') && (
Gewicht
)} {/* Text align (for label) */} {el.type === 'label' && (
Ausrichtung
)} {/* Style (for value) */} {el.type === 'value' && (
Stil
)} {/* Icon (for value) */} {el.type === 'value' && (
Icon
)} {/* Datapoint (for value) */} {el.type === 'value' && (
Datenpunkt
)} {/* Editable datapoint label + unit (for value with assigned datapoint) */} {el.type === 'value' && dp && ( <>
Anzeigename
{ if (e.target.value !== (dp.label || dp.name)) updateDatapointLabel(dp.id, e.target.value); }} placeholder="Anzeigename" />
Einheit
{ if (e.target.value !== (dp.unit || '')) updateDatapointUnit(dp.id, e.target.value); }} placeholder="z.B. W" />
)} {/* Static text (for label) */} {el.type === 'label' && (
Text
updateElement(el.id, 'static_text', e.target.value)} />
)}
); })}
{/* E-paper preview */}

Vorschau ({selected.width}x{selected.height})

)}
); }