// Die Seite einer einzelnen Funktion: Schalter, ihre Kanäle und Felder, ihre // Texte, ihre Werte — alles an einem Ort statt über vier Bereiche verteilt. // // Vorbild ist die Plugin-Seite von MEE6. Die Felder werden aus den Registern // gerendert (modules.js, templates.js, tuning.js), nicht von Hand gebaut — // eine neue Funktion bekommt ihre Seite dadurch geschenkt. import { useEffect, useState } from 'react'; import { apiGet, apiPut, apiPost } from '../api.js'; import { moduleIcon } from '../module-icons.jsx'; import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown } from '../icons.jsx'; /** Ein Feld aus dem Register — Art entscheidet über die Darstellung */ function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) { const gemeinsam = { id: `feld-${feld.setting}` }; if (feld.art === 'kanal' || feld.art === 'rolle') { return ( ); } if (feld.art === 'schalter') { return ( ); } if (feld.art === 'farbe') { return ( ); } return ( onChange(e.target.value)} /> ); } /** * @param {{ * id: string, onBack: () => void, onToggle: (id, on) => void, * form: object, setForm: (f: object) => void, * PickSelect: Function, channelItems: Array, roleItems: Array, * welcomePreviewUrl: string, flash: (msg: string) => void, * goTab: (tab: string) => void, * }} props */ export default function ModulePage(props) { const { id, onBack, onToggle, form, setForm, PickSelect, channelItems, roleItems, welcomePreviewUrl, flash, goTab } = props; const [detail, setDetail] = useState(null); const [fehler, setFehler] = useState(false); const [offenerText, setOffenerText] = useState(null); const [textEntwurf, setTextEntwurf] = useState({}); const [werteEntwurf, setWerteEntwurf] = useState({}); const laden = () => { setFehler(false); apiGet(`/api/modules/${id}`).then(setDetail).catch(() => setFehler(true)); }; useEffect(laden, [id]); if (fehler) return

Diese Funktion konnte nicht geladen werden.

; if (!detail) return

Lade …

; const Icon = moduleIcon(detail.id); async function textSpeichern(textId, wert) { try { const res = await apiPut(`/api/texts/${encodeURIComponent(textId)}`, { text: wert }); setDetail((d) => ({ ...d, texte: res.templates.filter((t) => t.module === d.id) })); setTextEntwurf(({ [textId]: _weg, ...rest }) => rest); flash(wert.trim() ? '✓ Text gespeichert' : '✓ Auf den Standard zurückgesetzt'); } catch { flash('✗ Speichern fehlgeschlagen'); } } async function werteSpeichern() { const geaendert = Object.fromEntries( Object.entries(werteEntwurf).filter(([wid, v]) => { const jetzt = detail.werte.find((w) => w.id === wid); return jetzt && String(v) !== String(jetzt.value); }) ); if (Object.keys(geaendert).length === 0) return; try { const res = await apiPut('/api/tuning', geaendert); setDetail((d) => ({ ...d, werte: res.values.filter((w) => w.module === d.id) })); setWerteEntwurf({}); flash('✓ Werte gespeichert'); } catch (error) { flash(`✗ ${error.body?.error ?? 'Speichern fehlgeschlagen'}`); } } async function anlegen(setting) { try { const res = await apiPost('/api/setup/create', { setting }); setForm(res.settings); laden(); flash(res.wiederverwendet ? `✓ „${res.name}" verknüpft` : `✓ „${res.name}" angelegt`); } catch (error) { flash(`✗ ${error.body?.error ?? 'Anlegen fehlgeschlagen'}`); } } const werteOffen = detail.werte.some( (w) => werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value) ); return (

{detail.name}

{detail.desc}

{detail.enabled && detail.missing.length > 0 && (

Fehlt noch: {detail.missing.map((m) => m.label).join(', ')}

)} {detail.enabled && detail.missing.length === 0 && (

Einsatzbereit

)} {/* Kanäle, Rollen und alles Weitere */} {detail.felder.length > 0 && (

Einstellungen

{detail.felder.filter((f) => f.art !== 'farbe').map((f) => (
{f.art !== 'schalter' && ( )}
setForm({ ...form, [f.setting]: v })} PickSelect={PickSelect} channelItems={channelItems} roleItems={roleItems} /> {/* Fehlt eine Voraussetzung, kann der Bot sie anlegen */} {f.pflicht && detail.missing.some((m) => m.setting === f.setting) && detail.missing.find((m) => m.setting === f.setting)?.create && ( )}
))} {detail.felder.some((f) => f.art === 'farbe') && (
{detail.felder.filter((f) => f.art === 'farbe').map((f) => ( setForm({ ...form, [f.setting]: v })} PickSelect={PickSelect} channelItems={channelItems} roleItems={roleItems} /> ))}
)}
)} {/* Vorschau, wo es eine gibt */} {detail.vorschau === 'welcome' && form.welcome_card_enabled && (

Vorschau

Vorschau der Willkommens-Karte
)} {/* Texte dieser Funktion */} {detail.texte.length > 0 && (

Texte

{detail.texte.map((t) => { const entwurf = textEntwurf[t.id]; const wert = entwurf ?? t.text; const geaendert = entwurf != null && entwurf !== t.text; const offen = offenerText === t.id; return (
{offen && (