diff --git a/README.md b/README.md index 98fe7d9..99c72e2 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen. | 🚨 **Raid-Schutz** | Erkennt Beitritts-Wellen und sperrt den Server vorübergehend (Prüfstufe hoch, Einladungen aus). `/raid an\|aus\|status` auch von Hand | | 📋 **Protokoll** | Gelöschte und bearbeitete Nachrichten, Joins/Leaves, Namens- und Rollen-Änderungen in einem privaten Kanal | | 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück | -| 🎫 **Tickets** | Button öffnet einen privaten Thread (ein offenes Ticket pro User). Schließen = **Transcript per DM** an den Ersteller + Kopie ins Mod-Log | +| 🎫 **Tickets** | Button öffnet einen privaten Thread (ein offenes Ticket pro User). **Anliegen** als Auswahlmenü — je eigener Name, Einstiegstext und Rolle, die benachrichtigt wird. Das Team kann ein Ticket **übernehmen**, damit klar ist, wer sich kümmert. Schließen = **Transcript per DM** an den Ersteller + Kopie ins Mod-Log, inklusive Anliegen und Bearbeiter | | 👋 **Willkommens-Karten** | Begrüßung als gerendertes Bild (Avatar mit Neon-Ring, Member-Nummer, Brand-Look), konfigurierbar; Fallback aufs Text-Embed | | 🪪 **Auto- & Sticky-Roles** | Start-Rolle für neue Member; Rollen werden bei Leave gesichert und bei Rejoin wiederhergestellt | | 🐛 **`/bug`** | Eingabefenster (was ist passiert, was war erwartet, wie nachstellen) → Gitea-Issue inkl. Screenshot-Upload; Issue geschlossen → DM an den Reporter | diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 992660a..55a5726 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -344,6 +344,8 @@ export default function Settings({ me }) { const [serverStats, setServerStats] = useState(null); const [watchdog, setWatchdog] = useState(null); const [automod, setAutomod] = useState(null); + const [anliegen, setAnliegen] = useState([]); + const [anliegenDraft, setAnliegenDraft] = useState({ id: null, name: '', emoji: '🎫', beschreibung: '', ping_role_id: '', intro: '' }); const [dienstDraft, setDienstDraft] = useState({ id: null, name: '', url: '', gruppe: 'Dienste', oeffentlich: false }); const [owner, setOwner] = useState(null); const [auditAlle, setAuditAlle] = useState(false); @@ -473,6 +475,7 @@ export default function Settings({ me }) { apiGet('/api/serverstats').then(setServerStats).catch(() => {}); apiGet('/api/watchdog').then(setWatchdog).catch(() => {}); apiGet('/api/automod').then(setAutomod).catch(() => {}); + apiGet('/api/ticket-categories').then((d) => setAnliegen(d.kategorien ?? [])).catch(() => {}); apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {}); apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {}); apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {}); @@ -1835,6 +1838,33 @@ export default function Settings({ me }) { ); + async function anliegenSpeichern() { + const body = { ...anliegenDraft }; + try { + const res = body.id + ? await apiPut(`/api/ticket-categories/${body.id}`, body) + : await apiPost('/api/ticket-categories', body); + setAnliegen(res.kategorien ?? []); + setAnliegenDraft({ id: null, name: '', emoji: '🎫', beschreibung: '', ping_role_id: '', intro: '' }); + setFeedback(body.id ? 'Anliegen gespeichert.' : 'Anliegen angelegt.'); + } catch (e) { + setFeedback(e.body?.error ?? 'Speichern fehlgeschlagen.'); + } + setTimeout(() => setFeedback(null), 3000); + } + + async function anliegenLoeschen(id, name) { + if (!window.confirm(`„${name}" wirklich entfernen? Offene Tickets bleiben bestehen.`)) return; + try { + const res = await apiDelete(`/api/ticket-categories/${id}`); + setAnliegen(res.kategorien ?? []); + if (anliegenDraft.id === id) setAnliegenDraft({ id: null, name: '', emoji: '🎫', beschreibung: '', ping_role_id: '', intro: '' }); + } catch { + setFeedback('Entfernen fehlgeschlagen.'); + setTimeout(() => setFeedback(null), 3000); + } + } + /** AutoMod-Regel an- oder abschalten */ async function automodSchalten(id, aktiv) { try { @@ -1849,6 +1879,97 @@ export default function Settings({ me }) { const tabSupport = ( <> +
+

// Ticket-Anliegen

+

+ Ohne Anliegen hat der Ticket-Post einen Knopf und jedes Ticket sieht gleich aus. + Mit Anliegen wird daraus ein Auswahlmenü: der Titel steht im Thread-Namen, + der Einstiegstext gleich im ersten Beitrag, und die hinterlegte Rolle erfährt + sofort, dass etwas anliegt. Nach dem Ändern /ticket-setup neu + ausführen — der alte Post behält sonst sein altes Menü. +

+ + {anliegen.length === 0 && ( +

Noch keine Anliegen — es bleibt beim einfachen Knopf.

+ )} + + {anliegen.map((k) => ( +
+
+ {k.emoji} + {k.name} + {k.ping_role_id && ( + + pingt {roleItems.find((r) => r.id === k.ping_role_id)?.label ?? 'Rolle'} + + )} + + +
+ {(k.beschreibung || k.intro) && ( +
+ {k.beschreibung && {k.beschreibung}} + {k.intro && Einstiegstext hinterlegt} +
+ )} +
+ ))} + +

+ {anliegenDraft.id ? 'Anliegen bearbeiten' : 'Anliegen hinzufügen'} +

+
+ + setAnliegenDraft({ ...anliegenDraft, name: e.target.value })} /> +
+
+ + setAnliegenDraft({ ...anliegenDraft, emoji: e.target.value })} /> +
+
+ + setAnliegenDraft({ ...anliegenDraft, beschreibung: e.target.value })} /> +
+
+ + setAnliegenDraft({ ...anliegenDraft, ping_role_id: v })} + empty="— kein Ping —" + /> +
+
+ +