@@ -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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+