Files
bordervillegame/admin_tickets.html
2026-08-22 08:40:29 +02:00

266 lines
9.5 KiB
HTML

<!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">&larr; 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>