Erster Commit
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Support-Tickets</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.filter-row { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; }
|
||||
select, button, input, textarea {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
}
|
||||
button { cursor: pointer; background: #6a5acd; }
|
||||
button.secondary { background: #444; }
|
||||
button.danger { background: #a83232; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
th { color: #888; font-weight: normal; }
|
||||
tr.ticket-list-row { cursor: pointer; }
|
||||
tr.ticket-list-row:hover { background: #202020; }
|
||||
|
||||
.ticket-status { padding: 2px 8px; border-radius: 10px; font-size: 11px; white-space: nowrap; }
|
||||
.ticket-status.open { background: #2c5a3d; color: #9be6b0; }
|
||||
.ticket-status.in_progress { background: #5a4a2c; color: #e6c89b; }
|
||||
.ticket-status.closed { background: #444; color: #aaa; }
|
||||
|
||||
.empty-hint { color: #666; padding: 10px 0; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Detail-Overlay */
|
||||
#detailOverlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7); z-index: 1000;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
#detailBox {
|
||||
background: #1a1a1a; border: 2px solid #6a5acd; border-radius: 8px;
|
||||
padding: 20px; width: 500px; max-height: 80vh; display: flex; flex-direction: column;
|
||||
}
|
||||
#detailMessages { flex: 1; overflow-y: auto; margin: 12px 0; display: flex; flex-direction: column; gap: 8px; max-height: 400px; }
|
||||
.ticket-msg { padding: 8px 10px; border-radius: 8px; font-size: 13px; max-width: 85%; }
|
||||
.ticket-msg.staff { background: #3a2f6b; align-self: flex-end; }
|
||||
.ticket-msg.player { background: #262633; align-self: flex-start; }
|
||||
.ticket-msg .ticket-msg-meta { font-size: 10px; color: #999; margin-bottom: 3px; }
|
||||
#detailReply { width: 100%; box-sizing: border-box; min-height: 60px; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a class="back" href="/uebersicht.html">← zurück zur Übersicht</a>
|
||||
<h1>🎫 Support-Tickets</h1>
|
||||
|
||||
<div class="panel">
|
||||
<div class="filter-row">
|
||||
<select id="statusFilter">
|
||||
<option value="all">Alle Status</option>
|
||||
<option value="open" selected>Offen</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="closed">Geschlossen</option>
|
||||
</select>
|
||||
<button onclick="loadTickets()">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Betreff</th><th>Von</th><th>Kategorie</th><th>Status</th><th>Aktualisiert</th></tr></thead>
|
||||
<tbody id="ticketsTableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-hint hidden" id="ticketsEmpty">Keine Tickets in dieser Ansicht.</div>
|
||||
</div>
|
||||
|
||||
<div id="detailOverlay" class="hidden">
|
||||
<div id="detailBox">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 id="detailTitle" style="margin:0;"></h3>
|
||||
<button class="secondary" onclick="closeDetail()">✕</button>
|
||||
</div>
|
||||
<div id="detailMeta" style="color:#888; font-size:12px; margin-top:4px;"></div>
|
||||
|
||||
<div id="detailMessages"></div>
|
||||
|
||||
<textarea id="detailReply" placeholder="Antworten..."></textarea>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button onclick="sendDetailReply()">Antworten</button>
|
||||
<button class="secondary" onclick="setTicketStatus('in_progress')">In Bearbeitung</button>
|
||||
<button class="secondary" onclick="setTicketStatus('closed')">Schließen</button>
|
||||
<button class="secondary" onclick="setTicketStatus('open')">Wieder öffnen</button>
|
||||
</div>
|
||||
<div class="msg" id="detailMsg" style="font-size:12px; margin-top:6px; min-height:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder keine Berechtigung. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const statusLabels = { open: "Offen", in_progress: "In Bearbeitung", closed: "Geschlossen" };
|
||||
let allTickets = [];
|
||||
let currentDetailId = null;
|
||||
|
||||
async function loadTickets() {
|
||||
const status = document.getElementById("statusFilter").value;
|
||||
const res = await authFetch("/api/admin/tickets?status=" + encodeURIComponent(status));
|
||||
const data = await res.json();
|
||||
allTickets = data.tickets || [];
|
||||
renderTicketsTable();
|
||||
}
|
||||
|
||||
function renderTicketsTable() {
|
||||
const tbody = document.getElementById("ticketsTableBody");
|
||||
const empty = document.getElementById("ticketsEmpty");
|
||||
|
||||
if (allTickets.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
empty.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
|
||||
tbody.innerHTML = allTickets.map(t => `
|
||||
<tr class="ticket-list-row" onclick="openDetail(${t.id})">
|
||||
<td>#${t.id}</td>
|
||||
<td>${escapeHtml(t.subject)} <span style="color:#666;">(${t.message_count} Nachrichten)</span></td>
|
||||
<td>${escapeHtml(t.username)}</td>
|
||||
<td>${escapeHtml(t.category)}</td>
|
||||
<td><span class="ticket-status ${t.status}">${statusLabels[t.status] || t.status}</span></td>
|
||||
<td>${new Date(t.updated_at).toLocaleString("de-DE")}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
async function openDetail(ticketId) {
|
||||
currentDetailId = ticketId;
|
||||
const res = await authFetch("/api/tickets/" + ticketId);
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
alert("Fehler: " + (data.error || "unbekannt"));
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("detailTitle").textContent = `#${data.ticket.id} ${data.ticket.subject}`;
|
||||
document.getElementById("detailMeta").innerHTML =
|
||||
`Von ${escapeHtml(data.ticket.username || "")} · Kategorie: ${escapeHtml(data.ticket.category)} · Status: <span class="ticket-status ${data.ticket.status}">${statusLabels[data.ticket.status] || data.ticket.status}</span>`;
|
||||
|
||||
const container = document.getElementById("detailMessages");
|
||||
container.innerHTML = data.messages.map(m => `
|
||||
<div class="ticket-msg ${m.is_admin_reply ? "staff" : "player"}">
|
||||
<div class="ticket-msg-meta">${escapeHtml(m.username)}${m.is_admin_reply ? " (Team)" : ""} · ${new Date(m.created_at).toLocaleString("de-DE")}</div>
|
||||
${escapeHtml(m.message)}
|
||||
</div>
|
||||
`).join("");
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
document.getElementById("detailReply").value = "";
|
||||
document.getElementById("detailOverlay").classList.remove("hidden");
|
||||
document.getElementById("detailOverlay").style.display = "flex";
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
document.getElementById("detailOverlay").classList.add("hidden");
|
||||
document.getElementById("detailOverlay").style.display = "none";
|
||||
currentDetailId = null;
|
||||
loadTickets();
|
||||
}
|
||||
|
||||
async function sendDetailReply() {
|
||||
if (!currentDetailId) return;
|
||||
const message = document.getElementById("detailReply").value.trim();
|
||||
if (!message) return;
|
||||
|
||||
const res = await authFetch(`/api/tickets/${currentDetailId}/reply`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
await openDetail(currentDetailId);
|
||||
} else {
|
||||
showDetailMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function setTicketStatus(status) {
|
||||
if (!currentDetailId) return;
|
||||
const res = await authFetch(`/api/admin/tickets/${currentDetailId}/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showDetailMsg("Status aktualisiert.");
|
||||
await openDetail(currentDetailId);
|
||||
} else {
|
||||
showDetailMsg("Fehler beim Ändern des Status.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function showDetailMsg(text, isError) {
|
||||
const el = document.getElementById("detailMsg");
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// INIT
|
||||
// -------------------------------------------------------------
|
||||
loadTickets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user