This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -214,6 +214,11 @@ router.get('/', (req, res) => {
|
|||||||
.module-meta { display:flex; flex-direction:column; gap:4px; }
|
.module-meta { display:flex; flex-direction:column; gap:4px; }
|
||||||
.module-title { font-weight:700; color:var(--text); }
|
.module-title { font-weight:700; color:var(--text); }
|
||||||
.module-desc { color:var(--muted); font-size:13px; }
|
.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 { 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.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); }
|
.toast.show { opacity:1; transform:translateY(0); }
|
||||||
@@ -330,10 +335,10 @@ router.get('/', (req, res) => {
|
|||||||
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Tickets</p>
|
<p class="section-title">Tickets</p>
|
||||||
<p class="section-sub"><3E>bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
<p class="section-sub"><3E>bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||||
<button class="secondary-btn ticket-tab-btn active" data-tab="overview"><3E>bersicht</button>
|
<button class="secondary-btn ticket-tab-btn active" data-tab="overview"><3E>bersicht</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
|
||||||
@@ -346,7 +351,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Ticketliste</p>
|
<p class="section-title">Ticketliste</p>
|
||||||
<p class="section-sub">Links ausw<73>hlen, Details im Modal. Plus <20>ffnet Panel-Erstellung.</p>
|
<p class="section-sub">Links ausw<73>hlen, Details im Modal. Plus <20>ffnet Panel-Erstellung.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap:10px;">
|
<div class="row" style="gap:10px;">
|
||||||
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
||||||
@@ -381,7 +386,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Status-Pipeline</p>
|
<p class="section-title">Status-Pipeline</p>
|
||||||
<p class="section-sub">Tickets nach Phase. Status per Dropdown <20>ndern.</p>
|
<p class="section-sub">Tickets nach Phase. Status per Dropdown <20>ndern.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
||||||
@@ -430,7 +435,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Automationen</p>
|
<p class="section-title">Automationen</p>
|
||||||
<p class="section-sub">Regeln f<>r Ticket-Aktionen.</p>
|
<p class="section-sub">Regeln f<>r Ticket-Aktionen.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,7 +472,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Knowledge-Base</p>
|
<p class="section-title">Knowledge-Base</p>
|
||||||
<p class="section-sub">Artikel f<>r Self-Service.</p>
|
<p class="section-sub">Artikel f<>r Self-Service.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -670,6 +675,79 @@ router.get('/', (req, res) => {
|
|||||||
<div id="moduleList" class="module-list"></div>
|
<div id="moduleList" class="module-list"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section" data-section="register">
|
||||||
|
<section class="card">
|
||||||
|
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Register Modul</p>
|
||||||
|
<p class="section-sub">Formulare verwalten und Antraege einsehen.</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge" id="registerStatus">Aktiv</span>
|
||||||
|
</div>
|
||||||
|
<div class="register-tabs">
|
||||||
|
<button class="ticket-tab-btn register-tab-btn active" data-tab="forms">Formulare</button>
|
||||||
|
<button class="ticket-tab-btn register-tab-btn" data-tab="apps">Registrierungen</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="register-tab ticket-tab active" data-tab="forms">
|
||||||
|
<section class="card">
|
||||||
|
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Formulare</p>
|
||||||
|
<p class="section-sub">Formulare anlegen, bearbeiten und Panels senden.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||||
|
<button class="secondary-btn" id="registerFormsReload" type="button">Reload</button>
|
||||||
|
<button class="secondary-btn" id="registerFormNew" type="button">Neues Formular</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="registerFormList" class="module-list" style="margin-top:12px;"></div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<form id="registerFormForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:12px;">
|
||||||
|
<input type="hidden" id="registerFormId" />
|
||||||
|
<div class="form-field"><label class="form-label">Name</label><input id="registerFormName" placeholder="Bewerbung" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Review Channel ID</label><input id="registerFormChannel" placeholder="123456789012345678" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Notify Rollen (kommagetrennt)</label><input id="registerFormRoles" placeholder="123,456" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Aktiv</label><div id="registerFormActive" class="switch on"></div></div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Beschreibung</label><textarea id="registerFormDescription" rows="2" placeholder="Kurze Beschreibung"></textarea></div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Felder (Label | Typ | Pflicht)</label><textarea id="registerFormFields" rows="4" placeholder="Name | shortText | true Begruendung | longText | true"></textarea><p class="muted">Typ: shortText oder longText. Eine Zeile pro Feld.</p></div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1; display:flex; gap:8px;">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
<button type="button" class="secondary-btn" id="registerFormReset">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="muted" id="registerFormStatus"></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="register-tab ticket-tab" data-tab="apps">
|
||||||
|
<section class="card">
|
||||||
|
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Registrierungen</p>
|
||||||
|
<p class="section-sub">Antraege filtern und einsehen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="gap:8px; flex-wrap:wrap; align-items:center;">
|
||||||
|
<select id="registerAppsFilter" class="muted" style="min-width:140px;">
|
||||||
|
<option value="">Alle Status</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="accepted">Accepted</option>
|
||||||
|
<option value="invited">Invited</option>
|
||||||
|
<option value="rejected">Rejected</option>
|
||||||
|
</select>
|
||||||
|
<select id="registerAppsFormFilter" class="muted" style="min-width:160px;">
|
||||||
|
<option value="">Alle Formulare</option>
|
||||||
|
</select>
|
||||||
|
<button class="secondary-btn" id="registerAppsReload" type="button">Reload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="registerAppsList" class="ticket-list-pane"></div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<div id="registerAppDetail" class="muted">Waehle einen Antrag aus der Liste.</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section" data-section="birthday">
|
<div class="section" data-section="birthday">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
|
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
|
||||||
@@ -1211,6 +1289,10 @@ router.get('/', (req, res) => {
|
|||||||
let editingReactionRole = null;
|
let editingReactionRole = null;
|
||||||
let supportLoginCache = {};
|
let supportLoginCache = {};
|
||||||
let eventsCache = [];
|
let eventsCache = [];
|
||||||
|
let registerFormsCache = [];
|
||||||
|
let registerAppsCache = [];
|
||||||
|
let registerSelectedApp = null;
|
||||||
|
let registerConfigCache = {};
|
||||||
|
|
||||||
function activateSection(key) {
|
function activateSection(key) {
|
||||||
sections.forEach((s) => s.classList.toggle('active', s.dataset.section === 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 birthdayEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'birthdayEnabled') ? modulesCache['birthdayEnabled'] : true;
|
||||||
const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
|
const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
|
||||||
const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : 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 (automodNav) automodNav.classList.toggle('hidden', !autoEnabled);
|
||||||
if (welcomeNav) welcomeNav.classList.toggle('hidden', !welcomeEnabled);
|
if (welcomeNav) welcomeNav.classList.toggle('hidden', !welcomeEnabled);
|
||||||
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
|
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
|
||||||
@@ -1254,6 +1337,12 @@ router.get('/', (req, res) => {
|
|||||||
if (reactionRolesNav) reactionRolesNav.classList.toggle('hidden', !reactionRolesEnabled);
|
if (reactionRolesNav) reactionRolesNav.classList.toggle('hidden', !reactionRolesEnabled);
|
||||||
const eventsNav = document.querySelector('.nav .events-link');
|
const eventsNav = document.querySelector('.nav .events-link');
|
||||||
if (eventsNav) eventsNav.classList.toggle('hidden', !eventsEnabled);
|
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');
|
const adminNav = document.querySelector('.nav .admin-link');
|
||||||
if (adminNav) adminNav.classList.toggle('hidden', !isAdmin);
|
if (adminNav) adminNav.classList.toggle('hidden', !isAdmin);
|
||||||
const current = location.hash.replace('#','') || 'overview';
|
const current = location.hash.replace('#','') || 'overview';
|
||||||
@@ -1265,6 +1354,7 @@ router.get('/', (req, res) => {
|
|||||||
(current === 'birthday' && !birthdayEnabled) ||
|
(current === 'birthday' && !birthdayEnabled) ||
|
||||||
(current === 'reactionroles' && !reactionRolesEnabled) ||
|
(current === 'reactionroles' && !reactionRolesEnabled) ||
|
||||||
(current === 'events' && !eventsEnabled) ||
|
(current === 'events' && !eventsEnabled) ||
|
||||||
|
(current === 'register' && !registerEnabled) ||
|
||||||
(current === 'admin' && !isAdmin)
|
(current === 'admin' && !isAdmin)
|
||||||
) {
|
) {
|
||||||
activateSection('overview');
|
activateSection('overview');
|
||||||
@@ -1755,7 +1845,7 @@ router.get('/', (req, res) => {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class=\"ticket-meta\">User: ' +
|
'<div class=\"ticket-meta\">User: ' +
|
||||||
(t.userId || '-') +
|
(t.userId || '-') +
|
||||||
(t.claimedBy ? ' <20> Supporter: ' + t.claimedBy : '') +
|
(t.claimedBy ? ' <20> Supporter: ' + t.claimedBy : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.innerHTML =
|
select.innerHTML =
|
||||||
@@ -1869,14 +1959,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillAutomationForm(r));
|
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'L<>schen';
|
del.textContent = 'L<>schen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/automations/' + r.id, {
|
const res = await fetch('/api/automations/' + r.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
body: JSON.stringify({ guildId: currentGuild })
|
||||||
});
|
});
|
||||||
showToast(res.ok ? 'Regel gel<65>scht' : 'L<>schen fehlgeschlagen', !res.ok);
|
showToast(res.ok ? 'Regel gel<65>scht' : 'L<>schen fehlgeschlagen', !res.ok);
|
||||||
if (res.ok) loadAutomations();
|
if (res.ok) loadAutomations();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -1936,14 +2026,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillKbForm(a));
|
edit.addEventListener('click', () => fillKbForm(a));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'L<>schen';
|
del.textContent = 'L<>schen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/kb/' + a.id, {
|
const res = await fetch('/api/kb/' + a.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
body: JSON.stringify({ guildId: currentGuild })
|
||||||
});
|
});
|
||||||
showToast(res.ok ? 'Artikel gel<65>scht' : 'L<>schen fehlgeschlagen', !res.ok);
|
showToast(res.ok ? 'Artikel gel<65>scht' : 'L<>schen fehlgeschlagen', !res.ok);
|
||||||
if (res.ok) loadKb();
|
if (res.ok) loadKb();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -2389,7 +2479,7 @@ router.get('/', (req, res) => {
|
|||||||
meta.className = 'module-meta';
|
meta.className = 'module-meta';
|
||||||
const descParts = ['Channel: ' + (set.channelId || '-')];
|
const descParts = ['Channel: ' + (set.channelId || '-')];
|
||||||
if (set.messageId) descParts.push('Message: ' + set.messageId);
|
if (set.messageId) descParts.push('Message: ' + set.messageId);
|
||||||
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' • ') + '</div>';
|
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' - ') + '</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'row';
|
actions.className = 'row';
|
||||||
const editBtn = document.createElement('button');
|
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 = '<div class="muted">Modul deaktiviert.</div>';
|
||||||
|
if (registerAppsList) registerAppsList.innerHTML = '<div class="muted">Modul deaktiviert.</div>';
|
||||||
|
if (registerAppDetail) registerAppDetail.innerHTML = '<div class="muted">Register deaktiviert.</div>';
|
||||||
|
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 = '<div class="muted">Keine Formulare.</div>';
|
||||||
|
} 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 = '<div class="module-title">' + (form.name || 'Formular') + '</div><div class="module-desc">' + descParts.join(' - ') + '</div>';
|
||||||
|
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 = '<option value="">Alle Formulare</option>';
|
||||||
|
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 = '<div class="ticket-empty">Keine Antraege.</div>';
|
||||||
|
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 = '<div class="ticket-item-top"><div class="ticket-title">' + (app.form?.name || 'Formular') + '</div><span class="ticket-status-badge ' + registerStatusClass(app.status) + '">' + (app.status || 'pending') + '</span></div>' +
|
||||||
|
'<div class="ticket-meta">User: ' + (app.userId || '-') + ' - Erstellt: ' + formatDate(app.createdAt) + '</div>';
|
||||||
|
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 = '<div class="muted">Waehle einen Antrag.</div>';
|
||||||
|
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(/</g, '<');
|
||||||
|
return '<div class="register-answer"><div class="form-label">' + label + '</div><div class="module-desc">' + value + '</div></div>';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
registerAppDetail.innerHTML =
|
||||||
|
'<div class="row" style="justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap;">' +
|
||||||
|
'<div><p class="section-title">' + (app.form?.name || 'Formular') + '</p><p class="section-sub">User: ' + (app.userId || '-') + '</p></div>' +
|
||||||
|
'<span class="ticket-status-badge ' + registerStatusClass(app.status) + '">' + (app.status || 'pending') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="module-list" style="margin-top:12px;">' + (answersHtml || '<div class="muted">Keine Antworten vorhanden.</div>') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: MODULE: Liste um Musik/Forum/Automod-Konfiguration ergaenzen.
|
// TODO: MODULE: Liste um Musik/Forum/Automod-Konfiguration ergaenzen.
|
||||||
// - Module-Status inkl. Direktlinks zu Detailseiten (Automod/Welcome/Musik) rendern.
|
// - Module-Status inkl. Direktlinks zu Detailseiten (Automod/Welcome/Musik) rendern.
|
||||||
// - Module-Flags aus BotModuleService spiegeln statt doppeltem Fetch.
|
// - Module-Flags aus BotModuleService spiegeln statt doppeltem Fetch.
|
||||||
@@ -2460,6 +2806,7 @@ router.get('/', (req, res) => {
|
|||||||
let birthdayActive = false;
|
let birthdayActive = false;
|
||||||
let reactionRolesActive = false;
|
let reactionRolesActive = false;
|
||||||
let eventsActive = false;
|
let eventsActive = false;
|
||||||
|
let registerActive = false;
|
||||||
(data.modules || []).forEach((m) => {
|
(data.modules || []).forEach((m) => {
|
||||||
modulesCache[m.key] = !!m.enabled;
|
modulesCache[m.key] = !!m.enabled;
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
@@ -2484,7 +2831,10 @@ router.get('/', (req, res) => {
|
|||||||
if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
|
if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
|
||||||
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
|
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
|
||||||
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
|
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
|
||||||
|
if (m.key === 'registerEnabled') modulesCache['registerEnabled'] = willEnable;
|
||||||
applyNavVisibility();
|
applyNavVisibility();
|
||||||
|
if (m.key === 'registerEnabled' && willEnable) { await loadRegisterForms(); await loadRegisterApps(); }
|
||||||
|
if (m.key === 'registerEnabled' && !willEnable) { clearRegisterUi(); }
|
||||||
} else {
|
} else {
|
||||||
showToast('Speichern fehlgeschlagen', true);
|
showToast('Speichern fehlgeschlagen', true);
|
||||||
}
|
}
|
||||||
@@ -2500,6 +2850,7 @@ router.get('/', (req, res) => {
|
|||||||
if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled;
|
if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled;
|
||||||
if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
|
if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
|
||||||
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
|
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
|
||||||
|
if (m.key === 'registerEnabled') registerActive = !!m.enabled;
|
||||||
});
|
});
|
||||||
applyNavVisibility();
|
applyNavVisibility();
|
||||||
applyTicketsVisibility(ticketsActive);
|
applyTicketsVisibility(ticketsActive);
|
||||||
@@ -2507,12 +2858,13 @@ router.get('/', (req, res) => {
|
|||||||
if (birthdayActive) loadBirthday();
|
if (birthdayActive) loadBirthday();
|
||||||
if (reactionRolesActive) loadReactionRoles();
|
if (reactionRolesActive) loadReactionRoles();
|
||||||
if (eventsActive) loadEvents();
|
if (eventsActive) loadEvents();
|
||||||
|
if (registerActive) { loadRegisterForms(); loadRegisterApps(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveModuleToggle(key, enabled) {
|
async function saveModuleToggle(key, enabled) {
|
||||||
if (!currentGuild) return false;
|
if (!currentGuild) return false;
|
||||||
const payload = { guildId: currentGuild };
|
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];
|
if (modulesCache[k] !== undefined) payload[k] = modulesCache[k];
|
||||||
});
|
});
|
||||||
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
|
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');
|
const slaRange = document.getElementById('slaRange');
|
||||||
if (slaRange) slaRange.addEventListener('change', loadSla);
|
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');
|
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 (el) el.addEventListener('click', () => el.classList.toggle('on'));
|
||||||
});
|
});
|
||||||
if (logSystem) logSystem.addEventListener('click', () => logSystem.classList.toggle('on'));
|
if (logSystem) logSystem.addEventListener('click', () => logSystem.classList.toggle('on'));
|
||||||
@@ -2883,3 +3255,23 @@ router.get('/settings', (_req, res) => {
|
|||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user