[deploy] expand ticket dashboard ui
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Ticket" ADD COLUMN "firstClaimAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "firstResponseAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "kbSuggestionSentAt" TIMESTAMP(3),
|
||||||
|
ALTER COLUMN "status" SET DEFAULT 'neu';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TicketAutomationRule" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"condition" JSONB NOT NULL,
|
||||||
|
"action" JSONB NOT NULL,
|
||||||
|
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TicketAutomationRule_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "KnowledgeBaseArticle" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"keywords" TEXT[],
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "KnowledgeBaseArticle_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TicketAutomationRule_guildId_active_idx" ON "TicketAutomationRule"("guildId", "active");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "KnowledgeBaseArticle_guildId_idx" ON "KnowledgeBaseArticle"("guildId");
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -43,18 +43,18 @@ router.get('/', (req, res) => {
|
|||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">Papo Control</div>
|
<div class="brand">Papo Control</div>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a class="active" href="#overview" data-target="overview"><span class="icon">🏠</span> Uebersicht</a>
|
<a class="active" href="#overview" data-target="overview"><span class="icon">ðŸ </span> Uebersicht</a>
|
||||||
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
||||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡️</span> Automod</a>
|
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡ï¸</span> Automod</a>
|
||||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">✨</span> Willkommen</a>
|
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">✨</span> Willkommen</a>
|
||||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎧</span> Dynamic Voice</a>
|
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎧</span> Dynamic Voice</a>
|
||||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
||||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">😎</span> Reaction Roles</a>
|
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">😎</span> Reaction Roles</a>
|
||||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">🖥️</span> Statuspage</a>
|
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">🖥ï¸</span> Statuspage</a>
|
||||||
<a href="#settings" data-target="settings"><span class="icon">⚙️</span> Einstellungen</a>
|
<a href="#settings" data-target="settings"><span class="icon">âš™ï¸</span> Einstellungen</a>
|
||||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
||||||
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
||||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛡</span> Admin</a>
|
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛡</span> Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
||||||
<button id="logoutBtn" class="logout">Logout</button>
|
<button id="logoutBtn" class="logout">Logout</button>
|
||||||
@@ -186,6 +186,9 @@ router.get('/', (req, res) => {
|
|||||||
.status-in-progress { background:rgba(255,184,70,0.16); color:#fbbf24; border-color:rgba(255,184,70,0.42); }
|
.status-in-progress { background:rgba(255,184,70,0.16); color:#fbbf24; border-color:rgba(255,184,70,0.42); }
|
||||||
.status-closed { background:rgba(239,68,68,0.16); color:#f87171; border-color:rgba(239,68,68,0.42); }
|
.status-closed { background:rgba(239,68,68,0.16); color:#f87171; border-color:rgba(239,68,68,0.42); }
|
||||||
.ticket-empty { padding:20px; text-align:center; color:var(--muted); border:1px dashed rgba(255,255,255,0.16); border-radius:14px; background:rgba(255,255,255,0.03); }
|
.ticket-empty { padding:20px; text-align:center; color:var(--muted); border:1px dashed rgba(255,255,255,0.16); border-radius:14px; background:rgba(255,255,255,0.03); }
|
||||||
|
.ticket-tab { display:none; }
|
||||||
|
.ticket-tab.active { display:block; }
|
||||||
|
.ticket-tab-btn.active { background:rgba(249,115,22,0.18); border-color:rgba(249,115,22,0.45); color:var(--accent-strong); }
|
||||||
form { display:flex; flex-direction:column; gap:10px; margin-top:4px; }
|
form { display:flex; flex-direction:column; gap:10px; margin-top:4px; }
|
||||||
.form-field { display:flex; flex-direction:column; gap:6px; }
|
.form-field { display:flex; flex-direction:column; gap:6px; }
|
||||||
.form-label { font-size:13px; color:var(--muted); letter-spacing:0.2px; font-weight:600; }
|
.form-label { font-size:13px; color:var(--muted); letter-spacing:0.2px; font-weight:600; }
|
||||||
@@ -324,11 +327,26 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-section="tickets">
|
<div class="section" data-section="tickets">
|
||||||
|
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Tickets</p>
|
||||||
|
<p class="section-sub">Übersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||||
|
<button class="secondary-btn ticket-tab-btn active" data-tab="overview">Übersicht</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="automations">Automationen</button>
|
||||||
|
<button class="secondary-btn ticket-tab-btn" data-tab="kb">Knowledge-Base</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ticketTabOverview" class="ticket-tab active">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<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">Tickets</p>
|
<p class="section-title">Ticketliste</p>
|
||||||
<p class="section-sub">Links auswaehlen, Details im Modal. Plus oeffnet Panel-Erstellung.</p>
|
<p class="section-sub">Links auswählen, Details im Modal. Plus ö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>
|
||||||
@@ -343,7 +361,7 @@ router.get('/', (req, res) => {
|
|||||||
<p class="section-title">Support-Login Status</p>
|
<p class="section-title">Support-Login Status</p>
|
||||||
<p class="section-sub">Aktive Supporter und letzte Sessions.</p>
|
<p class="section-sub">Aktive Supporter und letzte Sessions.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="icon-button" id="refreshSupportStatus">↻</button>
|
<button class="icon-button" id="refreshSupportStatus">⟳</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;">
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
@@ -357,7 +375,118 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-section="automod">
|
|
||||||
|
<div id="ticketTabPipeline" class="ticket-tab">
|
||||||
|
<section class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Status-Pipeline</p>
|
||||||
|
<p class="section-sub">Tickets nach Phase. Status per Dropdown ändern.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
||||||
|
<div><p class="label">Neu</p><div class="ticket-list-pane" id="pipelineNeu"></div></div>
|
||||||
|
<div><p class="label">In Bearbeitung</p><div class="ticket-list-pane" id="pipelineIn"></div></div>
|
||||||
|
<div><p class="label">Warten auf User</p><div class="ticket-list-pane" id="pipelineWait"></div></div>
|
||||||
|
<div><p class="label">Erledigt</p><div class="ticket-list-pane" id="pipelineDone"></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ticketTabSla" class="ticket-tab">
|
||||||
|
<section class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">SLA / Response-Zeiten</p>
|
||||||
|
<p class="section-sub">Average Time to Claim / First Response.</p>
|
||||||
|
</div>
|
||||||
|
<select id="slaRange">
|
||||||
|
<option value="7">Letzte 7 Tage</option>
|
||||||
|
<option value="30" selected>Letzte 30 Tage</option>
|
||||||
|
<option value="90">Letzte 90 Tage</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap:12px;">
|
||||||
|
<div>
|
||||||
|
<p class="label">SLA pro Supporter</p>
|
||||||
|
<table style="width:100%; border-collapse:collapse;" id="slaSupporterTable">
|
||||||
|
<thead><tr><th style="text-align:left;">Supporter</th><th>Tickets</th><th>TTC</th><th>TTFR</th></tr></thead>
|
||||||
|
<tbody id="slaSupporterBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="label">SLA pro Tag</p>
|
||||||
|
<table style="width:100%; border-collapse:collapse;" id="slaDaysTable">
|
||||||
|
<thead><tr><th style="text-align:left;">Datum</th><th>Tickets</th><th>TTC</th><th>TTFR</th></tr></thead>
|
||||||
|
<tbody id="slaDaysBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ticketTabAutomations" class="ticket-tab">
|
||||||
|
<section class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Automationen</p>
|
||||||
|
<p class="section-sub">Regeln für Ticket-Aktionen.</p>
|
||||||
|
</div>
|
||||||
|
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
||||||
|
</div>
|
||||||
|
<div id="automationList" class="ticket-list-pane" style="margin-top:10px;"></div>
|
||||||
|
<div id="automationFormWrap" style="margin-top:12px;">
|
||||||
|
<p class="label">Regel bearbeiten</p>
|
||||||
|
<form id="automationForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap:10px;">
|
||||||
|
<input type="hidden" id="automationId" />
|
||||||
|
<div class="form-field"><label class="form-label">Name</label><input id="automationName" placeholder="Regelname" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Bedingung</label><select id="automationConditionType">
|
||||||
|
<option value="category">Kategorie</option>
|
||||||
|
<option value="status">Status</option>
|
||||||
|
<option value="age">Ticketalter (Stunden)</option>
|
||||||
|
</select></div>
|
||||||
|
<div class="form-field"><label class="form-label">Wert</label><input id="automationConditionValue" placeholder="z.B. Bug oder 24" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Aktion</label><select id="automationActionType">
|
||||||
|
<option value="pingRole">Rolle pingen</option>
|
||||||
|
<option value="reminder">Reminder posten</option>
|
||||||
|
<option value="flag">Status setzen</option>
|
||||||
|
</select></div>
|
||||||
|
<div class="form-field"><label class="form-label">Rolle/Status/Nachricht</label><input id="automationActionValue" placeholder="Rollen-ID, Status oder Nachricht" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Aktiv</label><div id="automationActive" class="switch on"></div></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="automationReset">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ticketTabKb" class="ticket-tab">
|
||||||
|
<section class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Knowledge-Base</p>
|
||||||
|
<p class="section-sub">Artikel für Self-Service.</p>
|
||||||
|
</div>
|
||||||
|
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
||||||
|
</div>
|
||||||
|
<div id="kbList" class="ticket-list-pane" style="margin-top:10px;"></div>
|
||||||
|
<div id="kbFormWrap" style="margin-top:12px;">
|
||||||
|
<form id="kbForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:10px;">
|
||||||
|
<input type="hidden" id="kbId" />
|
||||||
|
<div class="form-field"><label class="form-label">Titel</label><input id="kbTitle" placeholder="Titel" /></div>
|
||||||
|
<div class="form-field"><label class="form-label">Keywords (kommagetrennt)</label><input id="kbKeywords" placeholder="bug, musik, ticket" /></div>
|
||||||
|
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Inhalt/Link</label><textarea id="kbContent" rows="3" placeholder="Kurze Anleitung oder Link"></textarea></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="kbReset">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div><div class="section" data-section="automod">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
@@ -457,7 +586,7 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Embed Footer</label>
|
<label class="form-label">Embed Footer</label>
|
||||||
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
|
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@@ -480,7 +609,7 @@ router.get('/', (req, res) => {
|
|||||||
<div class="embed-color" id="welcomePreviewColor"></div>
|
<div class="embed-color" id="welcomePreviewColor"></div>
|
||||||
<div class="embed-body">
|
<div class="embed-body">
|
||||||
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
|
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
|
||||||
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
|
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
|
||||||
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
|
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
|
||||||
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
||||||
</div>
|
</div>
|
||||||
@@ -607,7 +736,7 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
|
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
|
||||||
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
||||||
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
@@ -681,7 +810,7 @@ router.get('/', (req, res) => {
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row" style="justify-content:space-between;">
|
<div class="row" style="justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Aktivität (letzte 24h)</p>
|
<p class="section-title">Aktivität (letzte 24h)</p>
|
||||||
<p class="section-sub">Events/Commands pro Stunde</p>
|
<p class="section-sub">Events/Commands pro Stunde</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -691,7 +820,7 @@ router.get('/', (req, res) => {
|
|||||||
<div class="row" style="justify-content:space-between;">
|
<div class="row" style="justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Logs</p>
|
<p class="section-title">Logs</p>
|
||||||
<p class="section-sub">Neueste Einträge</p>
|
<p class="section-sub">Neueste Einträge</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="log-list" id="adminLogs"></ul>
|
<ul class="log-list" id="adminLogs"></ul>
|
||||||
@@ -718,13 +847,13 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-grid">
|
<div class="option-grid">
|
||||||
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>✏️ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
<div class="option-card"><span>âœï¸ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🗑️ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
<div class="option-card"><span>ðŸ—‘ï¸ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🛡️ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
<div class="option-card"><span>ðŸ›¡ï¸ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>⚙️ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
<div class="option-card"><span>âš™ï¸ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
||||||
<button id="loggingSave" type="button">Logging speichern</button>
|
<button id="loggingSave" type="button">Logging speichern</button>
|
||||||
@@ -1065,6 +1194,11 @@ router.get('/', (req, res) => {
|
|||||||
};
|
};
|
||||||
let currentGuild = '${guildId}';
|
let currentGuild = '${guildId}';
|
||||||
let ticketCache = [];
|
let ticketCache = [];
|
||||||
|
let pipelineCache = { neu: [], in_bearbeitung: [], warten_auf_user: [], erledigt: [] };
|
||||||
|
let slaSupporters = [];
|
||||||
|
let slaDays = [];
|
||||||
|
let automationCache = [];
|
||||||
|
let kbCache = [];
|
||||||
let selectedTicket = null;
|
let selectedTicket = null;
|
||||||
let activeModal = null;
|
let activeModal = null;
|
||||||
let automodConfigCache = {};
|
let automodConfigCache = {};
|
||||||
@@ -1092,6 +1226,15 @@ router.get('/', (req, res) => {
|
|||||||
setTimeout(() => toastEl.classList.remove('show'), 2200);
|
setTimeout(() => toastEl.classList.remove('show'), 2200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms) {
|
||||||
|
if (ms === null || ms === undefined) return '-';
|
||||||
|
const minutes = Math.max(0, Math.round(ms / 60000));
|
||||||
|
if (minutes < 60) return minutes + 'm';
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const mins = minutes % 60;
|
||||||
|
return hours + 'h ' + (mins ? mins + 'm' : '');
|
||||||
|
}
|
||||||
|
|
||||||
function applyNavVisibility() {
|
function applyNavVisibility() {
|
||||||
const autoEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'automodEnabled') ? modulesCache['automodEnabled'] : true;
|
const autoEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'automodEnabled') ? modulesCache['automodEnabled'] : true;
|
||||||
const welcomeEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'welcomeEnabled') ? modulesCache['welcomeEnabled'] : true;
|
const welcomeEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'welcomeEnabled') ? modulesCache['welcomeEnabled'] : true;
|
||||||
@@ -1276,7 +1419,7 @@ router.get('/', (req, res) => {
|
|||||||
actions.className = 'row';
|
actions.className = 'row';
|
||||||
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 () => {
|
||||||
await deleteService(svc.id);
|
await deleteService(svc.id);
|
||||||
});
|
});
|
||||||
@@ -1334,7 +1477,7 @@ router.get('/', (req, res) => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await loadStatuspage();
|
await loadStatuspage();
|
||||||
} else {
|
} else {
|
||||||
showToast('Service löschen fehlgeschlagen', true);
|
showToast('Service löschen fehlgeschlagen', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1540,6 +1683,10 @@ router.get('/', (req, res) => {
|
|||||||
await loadModules();
|
await loadModules();
|
||||||
await loadOverview();
|
await loadOverview();
|
||||||
await loadTickets();
|
await loadTickets();
|
||||||
|
await loadPipeline();
|
||||||
|
await loadSla();
|
||||||
|
await loadAutomations();
|
||||||
|
await loadKb();
|
||||||
await loadAutomodSettings(currentGuild);
|
await loadAutomodSettings(currentGuild);
|
||||||
await loadLoggingSettings(currentGuild);
|
await loadLoggingSettings(currentGuild);
|
||||||
await loadWelcomeSettings(currentGuild);
|
await loadWelcomeSettings(currentGuild);
|
||||||
@@ -1570,6 +1717,249 @@ router.get('/', (req, res) => {
|
|||||||
renderTickets();
|
renderTickets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPipeline() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const res = await fetch('/api/tickets/pipeline?guildId=' + encodeURIComponent(currentGuild));
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
pipelineCache = data.pipeline || pipelineCache;
|
||||||
|
renderPipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPipeline() {
|
||||||
|
const states = [
|
||||||
|
{ key: 'neu', el: document.getElementById('pipelineNeu') },
|
||||||
|
{ key: 'in_bearbeitung', el: document.getElementById('pipelineIn') },
|
||||||
|
{ key: 'warten_auf_user', el: document.getElementById('pipelineWait') },
|
||||||
|
{ key: 'erledigt', el: document.getElementById('pipelineDone') }
|
||||||
|
];
|
||||||
|
states.forEach((s) => {
|
||||||
|
if (!s.el) return;
|
||||||
|
const list = pipelineCache[s.key] || [];
|
||||||
|
if (!list.length) {
|
||||||
|
s.el.innerHTML = '<div class=\"ticket-empty\">Keine Tickets.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.el.innerHTML = '';
|
||||||
|
list.forEach((t) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'ticket-list-item';
|
||||||
|
card.innerHTML =
|
||||||
|
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
|
||||||
|
(t.topic || 'Ticket') +
|
||||||
|
'</div><div class=\"ticket-status-badge\">' +
|
||||||
|
(t.status || '') +
|
||||||
|
'</div></div>' +
|
||||||
|
'<div class=\"ticket-meta\">Erstellt: ' +
|
||||||
|
(t.createdAt ? new Date(t.createdAt).toLocaleString() : '-') +
|
||||||
|
'</div>' +
|
||||||
|
'<div class=\"ticket-meta\">User: ' +
|
||||||
|
(t.userId || '-') +
|
||||||
|
(t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') +
|
||||||
|
'</div>';
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.innerHTML =
|
||||||
|
'<option value=\"neu\">Neu</option><option value=\"in_bearbeitung\">In Bearbeitung</option><option value=\"warten_auf_user\">Warten auf User</option><option value=\"erledigt\">Erledigt</option>';
|
||||||
|
select.value = ['neu', 'in_bearbeitung', 'warten_auf_user', 'erledigt'].includes(t.status) ? t.status : 'neu';
|
||||||
|
select.addEventListener('change', async (e) => {
|
||||||
|
await fetch('/api/tickets/' + t.id + '/status', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status: e.target.value })
|
||||||
|
});
|
||||||
|
await loadPipeline();
|
||||||
|
await loadTickets();
|
||||||
|
});
|
||||||
|
select.style.marginTop = '6px';
|
||||||
|
card.appendChild(select);
|
||||||
|
s.el.appendChild(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSla() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const days = document.getElementById('slaRange')?.value || '30';
|
||||||
|
const res = await fetch('/api/tickets/sla?guildId=' + encodeURIComponent(currentGuild) + '&days=' + days);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
slaSupporters = data.supporters || [];
|
||||||
|
slaDays = data.days || [];
|
||||||
|
renderSla();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSla() {
|
||||||
|
const supBody = document.getElementById('slaSupporterBody');
|
||||||
|
const dayBody = document.getElementById('slaDaysBody');
|
||||||
|
if (supBody) {
|
||||||
|
supBody.innerHTML = '';
|
||||||
|
if (!slaSupporters.length) supBody.innerHTML = '<tr><td colspan=\"4\" class=\"muted\">Keine Daten</td></tr>';
|
||||||
|
slaSupporters.forEach((s) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td>' +
|
||||||
|
(s.supporterId || '-') +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
(s.tickets ?? '-') +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
formatDuration(s.avgTTC) +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
formatDuration(s.avgTTFR) +
|
||||||
|
'</td>';
|
||||||
|
supBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (dayBody) {
|
||||||
|
dayBody.innerHTML = '';
|
||||||
|
if (!slaDays.length) dayBody.innerHTML = '<tr><td colspan=\"4\" class=\"muted\">Keine Daten</td></tr>';
|
||||||
|
slaDays.forEach((d) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td>' +
|
||||||
|
(d.date || '-') +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
(d.tickets ?? '-') +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
formatDuration(d.avgTTC) +
|
||||||
|
'</td><td style=\"text-align:center;\">' +
|
||||||
|
formatDuration(d.avgTTFR) +
|
||||||
|
'</td>';
|
||||||
|
dayBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAutomations() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const res = await fetch('/api/automations?guildId=' + encodeURIComponent(currentGuild));
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
automationCache = data.rules || [];
|
||||||
|
renderAutomations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAutomations() {
|
||||||
|
const list = document.getElementById('automationList');
|
||||||
|
if (!list) return;
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (!automationCache.length) {
|
||||||
|
list.innerHTML = '<div class=\"ticket-empty\">Keine Regeln.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
automationCache.forEach((r) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'ticket-list-item';
|
||||||
|
row.innerHTML =
|
||||||
|
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
|
||||||
|
(r.name || 'Regel') +
|
||||||
|
'</div><div class=\"ticket-status-badge\">' +
|
||||||
|
(r.active ? 'aktiv' : 'inaktiv') +
|
||||||
|
'</div></div>' +
|
||||||
|
'<div class=\"ticket-meta\">Condition: ' +
|
||||||
|
JSON.stringify(r.condition || {}) +
|
||||||
|
'</div>' +
|
||||||
|
'<div class=\"ticket-meta\">Action: ' +
|
||||||
|
JSON.stringify(r.action || {}) +
|
||||||
|
'</div>';
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'row';
|
||||||
|
const edit = document.createElement('button');
|
||||||
|
edit.className = 'secondary-btn';
|
||||||
|
edit.textContent = 'Bearbeiten';
|
||||||
|
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||||
|
const del = document.createElement('button');
|
||||||
|
del.className = 'danger-btn';
|
||||||
|
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);
|
||||||
|
if (res.ok) loadAutomations();
|
||||||
|
});
|
||||||
|
actions.appendChild(edit);
|
||||||
|
actions.appendChild(del);
|
||||||
|
row.appendChild(actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillAutomationForm(rule) {
|
||||||
|
document.getElementById('automationId').value = rule.id || '';
|
||||||
|
document.getElementById('automationName').value = rule.name || '';
|
||||||
|
document.getElementById('automationConditionType').value =
|
||||||
|
rule.condition?.category ? 'category' : rule.condition?.status ? 'status' : rule.condition?.minHours ? 'age' : 'category';
|
||||||
|
document.getElementById('automationConditionValue').value =
|
||||||
|
rule.condition?.category || rule.condition?.status || rule.condition?.minHours || '';
|
||||||
|
document.getElementById('automationActionType').value =
|
||||||
|
rule.action?.type === 'reminder' ? 'reminder' : rule.action?.type === 'flag' ? 'flag' : 'pingRole';
|
||||||
|
document.getElementById('automationActionValue').value =
|
||||||
|
rule.action?.roleId || rule.action?.message || rule.action?.status || '';
|
||||||
|
setSwitch(document.getElementById('automationActive'), rule.active !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadKb() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const res = await fetch('/api/kb?guildId=' + encodeURIComponent(currentGuild));
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
kbCache = data.articles || [];
|
||||||
|
renderKb();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKb() {
|
||||||
|
const list = document.getElementById('kbList');
|
||||||
|
if (!list) return;
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (!kbCache.length) {
|
||||||
|
list.innerHTML = '<div class=\"ticket-empty\">Keine Artikel.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kbCache.forEach((a) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'ticket-list-item';
|
||||||
|
row.innerHTML =
|
||||||
|
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
|
||||||
|
(a.title || 'Artikel') +
|
||||||
|
'</div></div><div class=\"ticket-meta\">Keywords: ' +
|
||||||
|
(Array.isArray(a.keywords) ? a.keywords.join(', ') : '') +
|
||||||
|
'</div><div class=\"ticket-meta\">' +
|
||||||
|
(a.content || '') +
|
||||||
|
'</div>';
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'row';
|
||||||
|
const edit = document.createElement('button');
|
||||||
|
edit.className = 'secondary-btn';
|
||||||
|
edit.textContent = 'Bearbeiten';
|
||||||
|
edit.addEventListener('click', () => fillKbForm(a));
|
||||||
|
const del = document.createElement('button');
|
||||||
|
del.className = 'danger-btn';
|
||||||
|
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);
|
||||||
|
if (res.ok) loadKb();
|
||||||
|
});
|
||||||
|
actions.appendChild(edit);
|
||||||
|
actions.appendChild(del);
|
||||||
|
row.appendChild(actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillKbForm(a) {
|
||||||
|
document.getElementById('kbId').value = a.id || '';
|
||||||
|
document.getElementById('kbTitle').value = a.title || '';
|
||||||
|
document.getElementById('kbKeywords').value = Array.isArray(a.keywords) ? a.keywords.join(', ') : '';
|
||||||
|
document.getElementById('kbContent').value = a.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
function parseCategories(raw) {
|
function parseCategories(raw) {
|
||||||
const lines = (raw || '').split('\\n').map((l) => l.trim()).filter(Boolean);
|
const lines = (raw || '').split('\\n').map((l) => l.trim()).filter(Boolean);
|
||||||
return lines.slice(0, 5).map((line) => {
|
return lines.slice(0, 5).map((line) => {
|
||||||
@@ -1726,7 +2116,7 @@ router.get('/', (req, res) => {
|
|||||||
(s.userId || '-') +
|
(s.userId || '-') +
|
||||||
'</strong></div><div class="muted">Ende: ' +
|
'</strong></div><div class="muted">Ende: ' +
|
||||||
formatDate(s.endedAt || Date.now()) +
|
formatDate(s.endedAt || Date.now()) +
|
||||||
' · Dauer: ' +
|
' · Dauer: ' +
|
||||||
dur +
|
dur +
|
||||||
'</div>';
|
'</div>';
|
||||||
supportRecentList.appendChild(div);
|
supportRecentList.appendChild(div);
|
||||||
@@ -1784,9 +2174,9 @@ router.get('/', (req, res) => {
|
|||||||
(ev.repeatType || 'none') +
|
(ev.repeatType || 'none') +
|
||||||
'</span></div><div class="ticket-meta">Start: ' +
|
'</span></div><div class="ticket-meta">Start: ' +
|
||||||
formatDate(ev.startTime) +
|
formatDate(ev.startTime) +
|
||||||
' · Channel: ' +
|
' · Channel: ' +
|
||||||
(ev.channelId || '-') +
|
(ev.channelId || '-') +
|
||||||
' · Anmeldungen: ' +
|
' · Anmeldungen: ' +
|
||||||
(ev._count?.signups ?? 0) +
|
(ev._count?.signups ?? 0) +
|
||||||
'</div>';
|
'</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
@@ -1999,7 +2389,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');
|
||||||
@@ -2200,6 +2590,10 @@ router.get('/', (req, res) => {
|
|||||||
await loadSettings(currentGuild);
|
await loadSettings(currentGuild);
|
||||||
await loadOverview();
|
await loadOverview();
|
||||||
await loadTickets();
|
await loadTickets();
|
||||||
|
await loadPipeline();
|
||||||
|
await loadSla();
|
||||||
|
await loadAutomations();
|
||||||
|
await loadKb();
|
||||||
await loadModules();
|
await loadModules();
|
||||||
await loadStatuspage();
|
await loadStatuspage();
|
||||||
});
|
});
|
||||||
@@ -2214,6 +2608,102 @@ router.get('/', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.ticket-tab-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
document.querySelectorAll('.ticket-tab-btn').forEach((b) => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const tab = btn.dataset.tab;
|
||||||
|
document.querySelectorAll('.ticket-tab').forEach((t) => t.classList.remove('active'));
|
||||||
|
const target = document.getElementById('ticketTab' + (tab ? tab.charAt(0).toUpperCase() + tab.slice(1) : ''));
|
||||||
|
if (target) target.classList.add('active');
|
||||||
|
if (tab === 'pipeline') await loadPipeline();
|
||||||
|
if (tab === 'sla') await loadSla();
|
||||||
|
if (tab === 'automations') await loadAutomations();
|
||||||
|
if (tab === 'kb') await loadKb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const slaRange = document.getElementById('slaRange');
|
||||||
|
if (slaRange) slaRange.addEventListener('change', loadSla);
|
||||||
|
|
||||||
|
const automationForm = document.getElementById('automationForm');
|
||||||
|
if (automationForm) {
|
||||||
|
automationForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const id = document.getElementById('automationId')?.value || '';
|
||||||
|
const name = document.getElementById('automationName')?.value || '';
|
||||||
|
const condType = document.getElementById('automationConditionType')?.value;
|
||||||
|
const condVal = document.getElementById('automationConditionValue')?.value;
|
||||||
|
const actionType = document.getElementById('automationActionType')?.value;
|
||||||
|
const actionVal = document.getElementById('automationActionValue')?.value;
|
||||||
|
const active = getSwitch(document.getElementById('automationActive'));
|
||||||
|
const condition: any = {};
|
||||||
|
if (condType === 'category') condition.category = condVal;
|
||||||
|
if (condType === 'status') condition.status = condVal;
|
||||||
|
if (condType === 'age') condition.minHours = Number(condVal || 0);
|
||||||
|
const action: any = { type: actionType };
|
||||||
|
if (actionType === 'pingRole') action.roleId = actionVal;
|
||||||
|
if (actionType === 'reminder') action.message = actionVal;
|
||||||
|
if (actionType === 'flag') action.status = actionVal;
|
||||||
|
const payload = { guildId: currentGuild, name, condition, action, active };
|
||||||
|
const url = id ? '/api/automations/' + id : '/api/automations';
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||||
|
showToast(res.ok ? 'Regel gespeichert' : 'Fehler', !res.ok);
|
||||||
|
if (res.ok) {
|
||||||
|
document.getElementById('automationId').value = '';
|
||||||
|
automationForm.reset();
|
||||||
|
setSwitch(document.getElementById('automationActive'), true);
|
||||||
|
await loadAutomations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const automationReset = document.getElementById('automationReset');
|
||||||
|
if (automationReset) automationReset.addEventListener('click', () => {
|
||||||
|
document.getElementById('automationId').value = '';
|
||||||
|
if (automationForm) automationForm.reset();
|
||||||
|
setSwitch(document.getElementById('automationActive'), true);
|
||||||
|
});
|
||||||
|
const addAutomation = document.getElementById('addAutomation');
|
||||||
|
if (addAutomation) addAutomation.addEventListener('click', () => {
|
||||||
|
document.getElementById('automationId').value = '';
|
||||||
|
if (automationForm) automationForm.reset();
|
||||||
|
setSwitch(document.getElementById('automationActive'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const kbForm = document.getElementById('kbForm');
|
||||||
|
if (kbForm) {
|
||||||
|
kbForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const id = document.getElementById('kbId')?.value || '';
|
||||||
|
const title = document.getElementById('kbTitle')?.value || '';
|
||||||
|
const keywords = document.getElementById('kbKeywords')?.value || '';
|
||||||
|
const content = document.getElementById('kbContent')?.value || '';
|
||||||
|
const payload = { guildId: currentGuild, title, keywords, content };
|
||||||
|
const url = id ? '/api/kb/' + id : '/api/kb';
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||||
|
showToast(res.ok ? 'Artikel gespeichert' : 'Fehler', !res.ok);
|
||||||
|
if (res.ok) {
|
||||||
|
document.getElementById('kbId').value = '';
|
||||||
|
kbForm.reset();
|
||||||
|
await loadKb();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const kbReset = document.getElementById('kbReset');
|
||||||
|
if (kbReset) kbReset.addEventListener('click', () => {
|
||||||
|
document.getElementById('kbId').value = '';
|
||||||
|
if (kbForm) kbForm.reset();
|
||||||
|
});
|
||||||
|
const addKb = document.getElementById('addKb');
|
||||||
|
if (addKb) addKb.addEventListener('click', () => {
|
||||||
|
document.getElementById('kbId').value = '';
|
||||||
|
if (kbForm) kbForm.reset();
|
||||||
|
});
|
||||||
|
|
||||||
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].forEach((el) => {
|
||||||
@@ -2392,3 +2882,4 @@ router.get('/settings', (_req, res) => {
|
|||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user