// 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
)}
{/* 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 && (
)}
);
})}
)}
{/* Werte dieser Funktion */}
{detail.werte.length > 0 && (
Werte
{detail.werte.map((w) => {
const wert = werteEntwurf[w.id] ?? w.value;
const geaendert = werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value);
return (
);
})}
)}
{/* Werkzeuge liegen weiter in ihrem Bereich — hier nur der Weg dorthin */}
{detail.tool && (
Werkzeug
Das Eigentliche wird in einem eigenen Bereich gepflegt — dort ist mehr Platz dafür.
)}
);
}