diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts index fd9769a..348b483 100644 --- a/src/web/routes/dashboard.ts +++ b/src/web/routes/dashboard.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +import { Router } from 'express'; const router = Router(); @@ -214,6 +214,11 @@ router.get('/', (req, res) => { .module-meta { display:flex; flex-direction:column; gap:4px; } .module-title { font-weight:700; color:var(--text); } .module-desc { color:var(--muted); font-size:13px; } + .register-tabs { display:flex; gap:8px; flex-wrap:wrap; margin-top:6px; } + .register-tab { display:none; } + .register-tab.active { display:block; } + .register-meta { display:flex; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:12px; } + .register-answer { padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.04); } .toast { position:fixed; top:20px; right:24px; padding:12px 14px; border-radius:12px; background:rgba(249,115,22,0.18); border:1px solid rgba(249,115,22,0.45); color:#ffe6d0; font-weight:700; box-shadow:0 12px 32px rgba(0,0,0,0.34); opacity:0; transform:translateY(-10px); transition:opacity 150ms ease, transform 150ms ease; z-index:1100; backdrop-filter:blur(10px); } .toast.error { background:rgba(239,68,68,0.18); border-color:rgba(239,68,68,0.4); color:#ffe4e6; } .toast.show { opacity:1; transform:translateY(0); } @@ -330,10 +335,10 @@ router.get('/', (req, res) => {

Tickets

-

Übersicht, Pipeline, SLA, Automationen, Knowledge-Base.

+

�bersicht, Pipeline, SLA, Automationen, Knowledge-Base.

- + @@ -346,7 +351,7 @@ router.get('/', (req, res) => {

Ticketliste

-

Links auswählen, Details im Modal. Plus öffnet Panel-Erstellung.

+

Links ausw�hlen, Details im Modal. Plus �ffnet Panel-Erstellung.

@@ -381,7 +386,7 @@ router.get('/', (req, res) => {

Status-Pipeline

-

Tickets nach Phase. Status per Dropdown ändern.

+

Tickets nach Phase. Status per Dropdown �ndern.

@@ -430,7 +435,7 @@ router.get('/', (req, res) => {

Automationen

-

Regeln für Ticket-Aktionen.

+

Regeln f�r Ticket-Aktionen.

@@ -467,7 +472,7 @@ router.get('/', (req, res) => {

Knowledge-Base

-

Artikel für Self-Service.

+

Artikel f�r Self-Service.

@@ -670,6 +675,79 @@ router.get('/', (req, res) => {
+
+
+
+
+

Register Modul

+

Formulare verwalten und Antraege einsehen.

+
+ Aktiv +
+
+ + +
+
+
+
+
+
+

Formulare

+

Formulare anlegen, bearbeiten und Panels senden.

+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+

Typ: shortText oder longText. Eine Zeile pro Feld.

+
+ + +
+
+

+
+
+
+
+
+
+

Registrierungen

+

Antraege filtern und einsehen.

+
+
+ + + +
+
+
+
+
+
Waehle einen Antrag aus der Liste.
+
+
+
@@ -1211,6 +1289,10 @@ router.get('/', (req, res) => { let editingReactionRole = null; let supportLoginCache = {}; let eventsCache = []; + let registerFormsCache = []; + let registerAppsCache = []; + let registerSelectedApp = null; + let registerConfigCache = {}; function activateSection(key) { sections.forEach((s) => s.classList.toggle('active', s.dataset.section === key)); @@ -1243,6 +1325,7 @@ router.get('/', (req, res) => { const birthdayEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'birthdayEnabled') ? modulesCache['birthdayEnabled'] : true; const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true; const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : true; + const registerEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'registerEnabled') ? modulesCache['registerEnabled'] : true; if (automodNav) automodNav.classList.toggle('hidden', !autoEnabled); if (welcomeNav) welcomeNav.classList.toggle('hidden', !welcomeEnabled); if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled); @@ -1254,6 +1337,12 @@ router.get('/', (req, res) => { if (reactionRolesNav) reactionRolesNav.classList.toggle('hidden', !reactionRolesEnabled); const eventsNav = document.querySelector('.nav .events-link'); if (eventsNav) eventsNav.classList.toggle('hidden', !eventsEnabled); + if (registerNav) registerNav.classList.toggle('hidden', !registerEnabled); + if (registerSection) registerSection.classList.toggle('hidden', !registerEnabled); + if (registerStatus) { + registerStatus.textContent = registerEnabled ? 'Aktiv' : 'Deaktiviert'; + registerStatus.className = 'badge' + (registerEnabled ? ' active' : ''); + } const adminNav = document.querySelector('.nav .admin-link'); if (adminNav) adminNav.classList.toggle('hidden', !isAdmin); const current = location.hash.replace('#','') || 'overview'; @@ -1265,6 +1354,7 @@ router.get('/', (req, res) => { (current === 'birthday' && !birthdayEnabled) || (current === 'reactionroles' && !reactionRolesEnabled) || (current === 'events' && !eventsEnabled) || + (current === 'register' && !registerEnabled) || (current === 'admin' && !isAdmin) ) { activateSection('overview'); @@ -1755,7 +1845,7 @@ router.get('/', (req, res) => { '
' + '
User: ' + (t.userId || '-') + - (t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') + + (t.claimedBy ? ' � Supporter: ' + t.claimedBy : '') + '
'; const select = document.createElement('select'); select.innerHTML = @@ -1869,14 +1959,14 @@ router.get('/', (req, res) => { edit.addEventListener('click', () => fillAutomationForm(r)); const del = document.createElement('button'); del.className = 'danger-btn'; - del.textContent = 'Löschen'; + del.textContent = 'L�schen'; del.addEventListener('click', async () => { const res = await fetch('/api/automations/' + r.id, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guildId: currentGuild }) }); - showToast(res.ok ? 'Regel gelöscht' : 'Löschen fehlgeschlagen', !res.ok); + showToast(res.ok ? 'Regel gel�scht' : 'L�schen fehlgeschlagen', !res.ok); if (res.ok) loadAutomations(); }); actions.appendChild(edit); @@ -1936,14 +2026,14 @@ router.get('/', (req, res) => { edit.addEventListener('click', () => fillKbForm(a)); const del = document.createElement('button'); del.className = 'danger-btn'; - del.textContent = 'Löschen'; + del.textContent = 'L�schen'; del.addEventListener('click', async () => { const res = await fetch('/api/kb/' + a.id, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guildId: currentGuild }) }); - showToast(res.ok ? 'Artikel gelöscht' : 'Löschen fehlgeschlagen', !res.ok); + showToast(res.ok ? 'Artikel gel�scht' : 'L�schen fehlgeschlagen', !res.ok); if (res.ok) loadKb(); }); actions.appendChild(edit); @@ -2389,7 +2479,7 @@ router.get('/', (req, res) => { meta.className = 'module-meta'; const descParts = ['Channel: ' + (set.channelId || '-')]; if (set.messageId) descParts.push('Message: ' + set.messageId); - meta.innerHTML = '
' + (set.title || 'Reaction Role') + '
' + descParts.join(' • ') + '
'; + meta.innerHTML = '
' + (set.title || 'Reaction Role') + '
' + descParts.join(' - ') + '
'; const actions = document.createElement('div'); actions.className = 'row'; const editBtn = document.createElement('button'); @@ -2445,6 +2535,262 @@ router.get('/', (req, res) => { } } + function parseRegisterFields(raw) { + return (raw || '') + .split('\n') + .map((l, idx) => { + const parts = l.split('|').map((p) => p.trim()); + const label = parts[0]; + if (!label) return null; + const typeRaw = (parts[1] || 'shortText').toLowerCase(); + const type = typeRaw.includes('long') ? 'longText' : 'shortText'; + const requiredRaw = (parts[2] || '').toLowerCase(); + const required = ['true', '1', 'yes', 'ja'].includes(requiredRaw); + return { label, type, required, order: idx }; + }) + .filter(Boolean); + } + + function formatRegisterFields(fields) { + return (fields || []) + .slice() + .sort((a, b) => (a.sortOrder ?? a.order ?? 0) - (b.sortOrder ?? b.order ?? 0)) + .map((f) => [f.label || 'Feld', f.type || 'shortText', f.required ? 'true' : 'false'].join(' | ')) + .join('\n'); + } + + function setRegisterFormDefaults() { + if (registerFormId) registerFormId.value = ''; + if (registerFormName) registerFormName.value = ''; + if (registerFormDescription) registerFormDescription.value = ''; + if (registerFormChannel) registerFormChannel.value = registerConfigCache.reviewChannelId || ''; + if (registerFormRoles) registerFormRoles.value = (registerConfigCache.notifyRoleIds || []).join(', '); + if (registerFormFields) registerFormFields.value = ''; + setSwitch(registerFormActive, true); + if (registerFormStatus) registerFormStatus.textContent = ''; + } + + function fillRegisterForm(form) { + if (!form) return; + if (registerFormId) registerFormId.value = form.id || ''; + if (registerFormName) registerFormName.value = form.name || ''; + if (registerFormDescription) registerFormDescription.value = form.description || ''; + if (registerFormChannel) registerFormChannel.value = form.reviewChannelId || registerConfigCache.reviewChannelId || ''; + if (registerFormRoles) registerFormRoles.value = (form.notifyRoleIds || []).join(', '); + setSwitch(registerFormActive, form.isActive !== false); + if (registerFormFields) registerFormFields.value = formatRegisterFields(form.fields || []); + if (registerFormStatus) registerFormStatus.textContent = 'Bearbeitung aktiv'; + } + + function clearRegisterUi() { + if (registerFormList) registerFormList.innerHTML = '
Modul deaktiviert.
'; + if (registerAppsList) registerAppsList.innerHTML = '
Modul deaktiviert.
'; + if (registerAppDetail) registerAppDetail.innerHTML = '
Register deaktiviert.
'; + setRegisterFormDefaults(); + } + + async function loadRegisterForms() { + if (!currentGuild) return; + if (modulesCache['registerEnabled'] === false) { clearRegisterUi(); return; } + const res = await fetch('/api/register/forms?guildId=' + encodeURIComponent(currentGuild)); + if (!res.ok) return; + const data = await res.json(); + registerFormsCache = data.forms || []; + renderRegisterForms(); + } + + function renderRegisterForms() { + if (!registerFormList) return; + registerFormList.innerHTML = ''; + if (!registerFormsCache.length) { + registerFormList.innerHTML = '
Keine Formulare.
'; + } else { + registerFormsCache.forEach((form) => { + const row = document.createElement('div'); + row.className = 'module-item'; + const meta = document.createElement('div'); + meta.className = 'module-meta'; + const descParts = []; + descParts.push('Felder: ' + ((form.fields || []).length)); + if (form.reviewChannelId) descParts.push('Review: ' + form.reviewChannelId); + if (form.notifyRoleIds?.length) descParts.push('Notify: ' + form.notifyRoleIds.join(', ')); + meta.innerHTML = '
' + (form.name || 'Formular') + '
' + descParts.join(' - ') + '
'; + const actions = document.createElement('div'); + actions.className = 'row'; + const activeBadge = document.createElement('span'); + activeBadge.className = 'badge' + (form.isActive !== false ? ' active' : ''); + activeBadge.textContent = form.isActive !== false ? 'Aktiv' : 'Inaktiv'; + actions.appendChild(activeBadge); + const editBtn = document.createElement('button'); + editBtn.className = 'secondary-btn'; + editBtn.style.padding = '8px 10px'; + editBtn.style.fontSize = '12px'; + editBtn.textContent = 'Bearbeiten'; + editBtn.addEventListener('click', () => fillRegisterForm(form)); + const panelBtn = document.createElement('button'); + panelBtn.className = 'secondary-btn'; + panelBtn.style.padding = '8px 10px'; + panelBtn.style.fontSize = '12px'; + panelBtn.textContent = 'Panel senden'; + panelBtn.addEventListener('click', () => sendRegisterPanel(form)); + const delBtn = document.createElement('button'); + delBtn.className = 'danger-btn'; + delBtn.style.padding = '8px 10px'; + delBtn.style.fontSize = '12px'; + delBtn.textContent = 'Loeschen'; + delBtn.addEventListener('click', () => deleteRegisterForm(form.id)); + actions.appendChild(editBtn); + actions.appendChild(panelBtn); + actions.appendChild(delBtn); + row.appendChild(meta); + row.appendChild(actions); + registerFormList.appendChild(row); + }); + } + populateRegisterFormFilter(); + } + + async function sendRegisterPanel(form) { + if (!currentGuild || !form?.id) return; + const channelId = prompt('Channel ID fuer Panel', form.reviewChannelId || registerConfigCache.reviewChannelId || '') || ''; + if (!channelId.trim()) return; + const message = prompt('Nachricht im Panel (optional)', 'Klicke auf Registrieren, um das Formular zu oeffnen.'); + const res = await fetch('/api/register/forms/' + form.id + '/panel', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId: currentGuild, channelId, message: message || undefined }) + }); + showToast(res.ok ? 'Panel gesendet' : 'Panel fehlgeschlagen', !res.ok); + } + + async function deleteRegisterForm(id) { + if (!currentGuild || !id) return; + const res = await fetch('/api/register/forms/' + id, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guildId: currentGuild }) }); + showToast(res.ok ? 'Formular geloescht' : 'Loeschen fehlgeschlagen', !res.ok); + if (res.ok) await loadRegisterForms(); + } + + function parseRegisterRoles(raw) { + return (raw || '') + .split(/[,\n]/) + .map((s) => s.trim()) + .filter(Boolean); + } + + function populateRegisterFormFilter() { + if (!registerAppsFormFilter) return; + const current = registerAppsFormFilter.value; + registerAppsFormFilter.innerHTML = ''; + registerFormsCache.forEach((f) => { + const opt = document.createElement('option'); + opt.value = f.id; + opt.textContent = f.name || 'Formular'; + registerAppsFormFilter.appendChild(opt); + }); + if (current) registerAppsFormFilter.value = current; + } + + async function saveRegisterForm(e) { + if (e) e.preventDefault(); + if (!currentGuild) return; + const fields = parseRegisterFields(registerFormFields?.value || ''); + const payload = { + guildId: currentGuild, + name: registerFormName?.value || 'Formular', + description: registerFormDescription?.value || '', + reviewChannelId: registerFormChannel?.value || undefined, + notifyRoleIds: parseRegisterRoles(registerFormRoles?.value || ''), + isActive: getSwitch(registerFormActive), + fields + }; + const id = registerFormId?.value; + const url = id ? '/api/register/forms/' + id : '/api/register/forms'; + const method = id ? 'PUT' : 'POST'; + const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (registerFormStatus) registerFormStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler'; + showToast(res.ok ? 'Formular gespeichert' : 'Speichern fehlgeschlagen', !res.ok); + if (res.ok) { + setRegisterFormDefaults(); + await loadRegisterForms(); + } + } + + async function loadRegisterApps() { + if (!currentGuild) return; + if (modulesCache['registerEnabled'] === false) { clearRegisterUi(); return; } + registerSelectedApp = null; + if (registerAppDetail) registerAppDetail.innerHTML = 'Waehle einen Antrag aus der Liste.'; + const qs = new URLSearchParams({ guildId: currentGuild }); + if (registerAppsFilter?.value) qs.set('status', registerAppsFilter.value); + if (registerAppsFormFilter?.value) qs.set('formId', registerAppsFormFilter.value); + const res = await fetch('/api/register/apps?' + qs.toString()); + if (!res.ok) return; + const data = await res.json(); + registerAppsCache = data.applications || []; + renderRegisterApps(); + } + + function registerStatusClass(status) { + const val = (status || '').toLowerCase(); + if (val === 'accepted') return 'status-open'; + if (val === 'invited') return 'status-in-progress'; + if (val === 'rejected') return 'status-closed'; + return 'status-open'; + } + + function renderRegisterApps() { + if (!registerAppsList) return; + registerAppsList.innerHTML = ''; + if (!registerAppsCache.length) { + registerSelectedApp = null; + registerAppsList.innerHTML = '
Keine Antraege.
'; + if (registerAppDetail) registerAppDetail.innerHTML = 'Waehle einen Antrag aus der Liste.'; + return; + } + registerAppsCache.forEach((app) => { + const row = document.createElement('div'); + row.className = 'ticket-list-item'; + row.innerHTML = '
' + (app.form?.name || 'Formular') + '
' + (app.status || 'pending') + '
' + + '
User: ' + (app.userId || '-') + ' - Erstellt: ' + formatDate(app.createdAt) + '
'; + row.addEventListener('click', () => loadRegisterApplication(app.id)); + registerAppsList.appendChild(row); + }); + } + + async function loadRegisterApplication(id) { + if (!id) return; + const res = await fetch('/api/register/apps/' + id); + if (!res.ok) return; + const data = await res.json(); + registerSelectedApp = data.application || null; + renderRegisterDetail(); + } + + function renderRegisterDetail() { + if (!registerAppDetail) return; + if (!registerSelectedApp) { + registerAppDetail.innerHTML = '
Waehle einen Antrag.
'; + return; + } + const app = registerSelectedApp; + const formFields = registerFormsCache.find((f) => f.id === app.formId)?.fields || []; + const answers = app.answers || []; + const answersHtml = answers + .map((a) => { + const field = formFields.find((f) => f.id === a.fieldId); + const label = field?.label || 'Feld'; + const value = (a.value || '').replace(/
' + label + '
' + value + '
'; + }) + .join(''); + registerAppDetail.innerHTML = + '
' + + '

' + (app.form?.name || 'Formular') + '

User: ' + (app.userId || '-') + '

' + + '' + (app.status || 'pending') + '' + + '
' + + '
' + (answersHtml || '
Keine Antworten vorhanden.
') + '
'; + } + // TODO: MODULE: Liste um Musik/Forum/Automod-Konfiguration ergaenzen. // - Module-Status inkl. Direktlinks zu Detailseiten (Automod/Welcome/Musik) rendern. // - Module-Flags aus BotModuleService spiegeln statt doppeltem Fetch. @@ -2460,6 +2806,7 @@ router.get('/', (req, res) => { let birthdayActive = false; let reactionRolesActive = false; let eventsActive = false; + let registerActive = false; (data.modules || []).forEach((m) => { modulesCache[m.key] = !!m.enabled; const row = document.createElement('div'); @@ -2484,7 +2831,10 @@ router.get('/', (req, res) => { if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable; if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable; if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable; + if (m.key === 'registerEnabled') modulesCache['registerEnabled'] = willEnable; applyNavVisibility(); + if (m.key === 'registerEnabled' && willEnable) { await loadRegisterForms(); await loadRegisterApps(); } + if (m.key === 'registerEnabled' && !willEnable) { clearRegisterUi(); } } else { showToast('Speichern fehlgeschlagen', true); } @@ -2500,6 +2850,7 @@ router.get('/', (req, res) => { if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled; if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled; if (m.key === 'eventsEnabled') eventsActive = !!m.enabled; + if (m.key === 'registerEnabled') registerActive = !!m.enabled; }); applyNavVisibility(); applyTicketsVisibility(ticketsActive); @@ -2507,12 +2858,13 @@ router.get('/', (req, res) => { if (birthdayActive) loadBirthday(); if (reactionRolesActive) loadReactionRoles(); if (eventsActive) loadEvents(); + if (registerActive) { loadRegisterForms(); loadRegisterApps(); } } async function saveModuleToggle(key, enabled) { if (!currentGuild) return false; const payload = { guildId: currentGuild }; - ['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled'].forEach((k) => { + ['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled', 'registerEnabled'].forEach((k) => { if (modulesCache[k] !== undefined) payload[k] = modulesCache[k]; }); payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled']; @@ -2623,6 +2975,26 @@ router.get('/', (req, res) => { }); }); + const registerTabs = Array.from(document.querySelectorAll('.register-tab')); + document.querySelectorAll('.register-tab-btn').forEach((btn) => { + btn.addEventListener('click', async () => { + document.querySelectorAll('.register-tab-btn').forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + const tab = btn.dataset.tab || 'forms'; + registerTabs.forEach((t) => t.classList.toggle('active', t.dataset.tab === tab)); + if (tab === 'forms') await loadRegisterForms(); + if (tab === 'apps') await loadRegisterApps(); + }); + }); + + if (registerFormForm) registerFormForm.addEventListener('submit', saveRegisterForm); + if (registerFormReset) registerFormReset.addEventListener('click', (e) => { e.preventDefault(); setRegisterFormDefaults(); }); + if (registerFormNew) registerFormNew.addEventListener('click', () => setRegisterFormDefaults()); + if (registerFormsReload) registerFormsReload.addEventListener('click', loadRegisterForms); + if (registerAppsReload) registerAppsReload.addEventListener('click', loadRegisterApps); + if (registerAppsFilter) registerAppsFilter.addEventListener('change', loadRegisterApps); + if (registerAppsFormFilter) registerAppsFormFilter.addEventListener('change', loadRegisterApps); + const slaRange = document.getElementById('slaRange'); if (slaRange) slaRange.addEventListener('change', loadSla); @@ -2706,7 +3078,7 @@ router.get('/', (req, res) => { document.getElementById('logoutBtn').addEventListener('click', () => window.location.href = BASE_AUTH + '/logout'); - [automodToggle, badWordToggle, linkFilterToggle, spamFilterToggle, capsFilterToggle, logJoinLeave, logMsgEdit, logMsgDelete, logAutomod, logTickets, logMusic, dynamicVoiceToggle, supportLoginAuto].forEach((el) => { + [automodToggle, badWordToggle, linkFilterToggle, spamFilterToggle, capsFilterToggle, logJoinLeave, logMsgEdit, logMsgDelete, logAutomod, logTickets, logMusic, dynamicVoiceToggle, supportLoginAuto, registerFormActive].forEach((el) => { if (el) el.addEventListener('click', () => el.classList.toggle('on')); }); if (logSystem) logSystem.addEventListener('click', () => logSystem.classList.toggle('on')); @@ -2883,3 +3255,23 @@ router.get('/settings', (_req, res) => { export default router; + + + + + + + + + + + + + + + + + + + +