diff --git a/display/frontend/src/pages/Layouts.tsx b/display/frontend/src/pages/Layouts.tsx index 2c8b2be..f6d6c11 100644 --- a/display/frontend/src/pages/Layouts.tsx +++ b/display/frontend/src/pages/Layouts.tsx @@ -20,10 +20,13 @@ interface LayoutElement { 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 { @@ -34,50 +37,48 @@ interface Datapoint { id: number; name: string; label: string | null; + value: string | null; + unit: string | null; } -const ELEMENT_TYPES = ['label', 'value', 'line', 'rect', 'static']; -const emptyEl = { type: 'value', x: '0', y: '0', font_size: '24', style: 'plain', datapoint_id: '', icon: '', static_text: '' }; +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 [elForm, setElForm] = useState(emptyEl); const [error, setError] = useState(''); async function loadLayouts() { try { setLayouts(await api.get('/api/layouts')); - } catch (e) { - setError((e as Error).message); - } + } 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); - } + } catch (e) { setError((e as Error).message); } } - useEffect(() => { - loadLayouts(); - api.get('/api/datapoints').then(setDatapoints).catch(() => {}); - }, []); + useEffect(() => { loadLayouts(); loadDatapoints(); }, []); async function handleAddLayout(e: FormEvent) { e.preventDefault(); if (!newName.trim()) return; try { - await api.post('/api/layouts', { name: newName.trim() }); + const res = await api.post<{ id: number }>('/api/layouts', { name: newName.trim() }); setNewName(''); loadLayouts(); - } catch (e) { - setError((e as Error).message); - } + selectLayout(res.id); + } catch (e) { setError((e as Error).message); } } async function handleDeleteLayout(id: number) { @@ -86,30 +87,30 @@ export default function Layouts() { await api.del(`/api/layouts/${id}`); if (selected?.id === id) setSelected(null); loadLayouts(); - } catch (e) { - setError((e as Error).message); - } + } catch (e) { setError((e as Error).message); } } - async function handleAddElement(e: FormEvent) { - e.preventDefault(); + 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.post(`/api/layouts/${selected.id}/elements`, { - type: elForm.type, - x: parseInt(elForm.x), - y: parseInt(elForm.y), - font_size: parseInt(elForm.font_size), - style: elForm.style, - datapoint_id: elForm.datapoint_id ? parseInt(elForm.datapoint_id) : null, - icon: elForm.icon || null, - static_text: elForm.static_text || null, - }); - setElForm(emptyEl); + await api.put(`/api/layouts/elements/${elId}`, { [field]: value }); selectLayout(selected.id); - } catch (e) { - setError((e as Error).message); - } + } catch (e) { setError((e as Error).message); } } async function handleDeleteElement(elId: number) { @@ -117,47 +118,39 @@ export default function Layouts() { try { await api.del(`/api/layouts/${selected.id}/elements/${elId}`); selectLayout(selected.id); - } catch (e) { - setError((e as Error).message); - } + } catch (e) { setError((e as Error).message); } + } + + 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}
} + {error &&
{error}
}
{/* Left: layout list */} -
+
- {layouts.length === 0 && ( -

Keine Layouts vorhanden

- )} + {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'}`} - > +
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)} - /> + setNewName(e.target.value)} />
@@ -165,100 +158,142 @@ export default function Layouts() { {/* Right: element editor */} {selected && (
-

- Elemente in {selected.name}{' '} - ({selected.width}x{selected.height}) -

- - {/* Element list */} -
- - - - - - - - - - - - - {selected.elements.length === 0 && ( - - )} - {selected.elements.map((el) => { - const dp = datapoints.find((d) => d.id === el.datapoint_id); - return ( - - - - - - - - - ); - })} - -
TypX / YSchriftgroesseDatenpunktStatischer TextAktion
Noch keine Elemente
{el.type}{el.x} / {el.y}{el.font_size}{dp ? (dp.label || dp.name) : '-'}{el.static_text ?? '-'} - -
+
+

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

+
+ + + +
- {/* Add element form */} -
-

Element hinzufuegen

-
-
- - + {/* Element list — editable */} +
+ {selected.elements.length === 0 && ( +
+ Noch keine Elemente. Klicke oben auf "+ Wert" oder "+ Label" um ein Element hinzuzufuegen.
-
- - setElForm({ ...elForm, x: e.target.value })} /> -
-
- - setElForm({ ...elForm, y: e.target.value })} /> -
-
- - setElForm({ ...elForm, font_size: e.target.value })} /> -
-
- - -
-
- - -
-
- - setElForm({ ...elForm, icon: e.target.value })} placeholder="z.B. thermometer" /> -
-
- - setElForm({ ...elForm, static_text: e.target.value })} placeholder="Fester Text" /> -
-
- -
- + )} + {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
+ +
+ )} + + {/* Static text (for label) */} + {el.type === 'label' && ( +
+
Text
+ updateElement(el.id, 'static_text', e.target.value)} /> +
+ )} +
+
+ ); + })}
{/* E-paper preview */} +

Vorschau (800x480)