Files
TicketSystem/src/main/java/de/ticketsystem/web/handlers/TicketsHandler.java
2026-04-15 19:11:02 +02:00

379 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package de.ticketsystem.web.handlers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.ticketsystem.TicketPlugin;
import de.ticketsystem.database.DatabaseManager;
import de.ticketsystem.model.*;
import de.ticketsystem.web.SessionManager;
import de.ticketsystem.web.WebSession;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* /tickets Liste aller Tickets (gefiltert, paginiert)
* /ticket/{id} Detailansicht eines Tickets
*/
public class TicketsHandler extends BaseHandler implements HttpHandler {
private static final int PAGE_SIZE = 20;
private final SimpleDateFormat SDF = new SimpleDateFormat("dd.MM.yyyy HH:mm");
private final TicketPlugin plugin;
private final SessionManager sessionManager;
public TicketsHandler(TicketPlugin plugin, SessionManager sessionManager) {
this.plugin = plugin;
this.sessionManager = sessionManager;
}
@Override
public void handle(HttpExchange ex) throws IOException {
WebSession session = requireSession(ex, sessionManager);
if (session == null) return;
String path = ex.getRequestURI().getPath();
if (path.startsWith("/ticket/") && path.length() > 8) {
String idStr = path.substring(8).replaceAll("[^0-9]", "");
if (!idStr.isEmpty()) {
handleDetail(ex, session, Integer.parseInt(idStr));
return;
}
}
handleList(ex, session);
}
// ─────────────────────────── Ticket-Liste ──────────────────────────────
private void handleList(HttpExchange ex, WebSession session) throws IOException {
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
String filterStatus = params.getOrDefault("status", "all");
String filterCat = params.getOrDefault("category", "all");
String filterPrio = params.getOrDefault("priority", "all");
String filterSearch = params.getOrDefault("q", "").trim();
int page = parseInt(params.getOrDefault("page", "1"), 1);
DatabaseManager db = plugin.getDatabaseManager();
List<Ticket> all = db.getAllTickets();
// ── Filter ──
List<Ticket> filtered = all.stream()
.filter(t -> filterStatus.equals("all") || t.getStatus().name().equalsIgnoreCase(filterStatus))
.filter(t -> filterCat.equals("all") || t.getCategoryKey().equalsIgnoreCase(filterCat))
.filter(t -> filterPrio.equals("all") || t.getPriority().name().equalsIgnoreCase(filterPrio))
.filter(t -> filterSearch.isEmpty()
|| t.getCreatorName().toLowerCase().contains(filterSearch.toLowerCase())
|| t.getMessage().toLowerCase().contains(filterSearch.toLowerCase())
|| String.valueOf(t.getId()).contains(filterSearch))
.sorted(Comparator.comparingInt(Ticket::getId).reversed())
.collect(Collectors.toList());
// ── Paginierung ──
int total = filtered.size();
int totalPages = Math.max(1, (int) Math.ceil(total / (double) PAGE_SIZE));
page = Math.max(1, Math.min(page, totalPages));
int fromIdx = (page - 1) * PAGE_SIZE;
int toIdx = Math.min(fromIdx + PAGE_SIZE, total);
List<Ticket> pageTickets = filtered.subList(fromIdx, toIdx);
String content = buildList(pageTickets, total, page, totalPages,
filterStatus, filterCat, filterPrio, filterSearch);
sendHtml(ex, 200, layout(wl(plugin, "tickets-title"), content, session, plugin));
}
private String buildList(List<Ticket> tickets, int total, int page, int totalPages,
String filterStatus, String filterCat, String filterPrio, String filterSearch) {
StringBuilder sb = new StringBuilder();
sb.append("<h1 class='page-title'>")
.append(escHtml(wl(plugin, "tickets-title")))
.append(" <span>").append(total).append(" ")
.append(escHtml(wl(plugin, "tickets-total")))
.append("</span></h1>");
// ── Filter-Bar ──
sb.append("<div class='filter-bar'>");
sb.append(selectFilter("status", filterStatus, List.of(
entry("all", wl(plugin, "filter-all-status")),
entry("OPEN", wl(plugin, "filter-open")),
entry("CLAIMED", wl(plugin, "filter-claimed")),
entry("FORWARDED", wl(plugin, "filter-forwarded")),
entry("CLOSED", wl(plugin, "filter-closed")))));
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
List<Map.Entry<String,String>> catOpts = new ArrayList<>();
catOpts.add(entry("all", wl(plugin, "filter-all-cat")));
plugin.getCategoryManager().getAll().forEach(c -> catOpts.add(entry(c.getKey(), c.getName())));
sb.append(selectFilter("category", filterCat, catOpts));
}
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
sb.append(selectFilter("priority", filterPrio, List.of(
entry("all", wl(plugin, "filter-all-prio")),
entry("LOW", wl(plugin, "filter-low")),
entry("NORMAL", wl(plugin, "filter-normal")),
entry("HIGH", wl(plugin, "filter-high")),
entry("URGENT", wl(plugin, "filter-urgent")))));
}
sb.append("<form method='GET' action='/tickets' style='display:flex;gap:.5rem'>");
sb.append("<input name='q' placeholder='")
.append(escHtml(wl(plugin, "tickets-search-ph")))
.append("' value='").append(escHtml(filterSearch)).append("'style='width:180px'>");
sb.append("<input name='status' type='hidden' value='").append(escHtml(filterStatus)).append("'>");
sb.append("<input name='category' type='hidden' value='").append(escHtml(filterCat)).append("'>");
sb.append("<input name='priority' type='hidden' value='").append(escHtml(filterPrio)).append("'>");
sb.append("<button type='submit' class='btn btn-secondary btn-sm'>🔍</button></form>");
sb.append("</div>");
// ── Tabelle ──
sb.append("<div class='card'><table><thead><tr>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-id"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-player"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-message"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-cat"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-prio"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-status"))).append("</th>")
.append("<th>").append(escHtml(wl(plugin, "tickets-col-created"))).append("</th>")
.append("<th></th>")
.append("</tr></thead><tbody>");
for (Ticket t : tickets) {
String msg = t.getMessage() != null && t.getMessage().length() > 60
? t.getMessage().substring(0, 60) + "" : t.getMessage();
String created = t.getCreatedAt() != null ? SDF.format(t.getCreatedAt()) : "";
String catName = getCategoryName(t);
sb.append("<tr>")
.append("<td><strong>#").append(t.getId()).append("</strong></td>")
.append("<td>").append(escHtml(t.getCreatorName())).append("</td>")
.append("<td>").append(escHtml(msg)).append("</td>")
.append("<td>").append(escHtml(catName)).append("</td>")
.append("<td>").append(priorityBadge(t)).append("</td>")
.append("<td>").append(statusBadge(t)).append("</td>")
.append("<td style='white-space:nowrap'>").append(created).append("</td>")
.append("<td><a href='/ticket/").append(t.getId())
.append("' class='btn btn-sm btn-secondary'>")
.append(escHtml(wl(plugin, "btn-details")))
.append("</a></td>")
.append("</tr>");
}
if (tickets.isEmpty()) {
sb.append("<tr><td colspan='8' style='text-align:center;color:var(--muted);padding:2rem'>")
.append(escHtml(wl(plugin, "tickets-empty")))
.append("</td></tr>");
}
sb.append("</tbody></table></div>");
// ── Paginierung ──
if (totalPages > 1) {
sb.append("<div class='pagination'>");
for (int i = 1; i <= totalPages; i++) {
String active = i == page ? " active" : "";
sb.append("<a class='page-btn").append(active).append("' href='/tickets?page=").append(i)
.append("&status=").append(filterStatus)
.append("&category=").append(filterCat)
.append("&priority=").append(filterPrio)
.append("&q=").append(escHtml(filterSearch))
.append("'>").append(i).append("</a>");
}
sb.append("</div>");
}
return sb.toString();
}
// ─────────────────────────── Ticket-Detail ─────────────────────────────
private void handleDetail(HttpExchange ex, WebSession session, int ticketId) throws IOException {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) {
sendHtml(ex, 404, errorPage(
wl(plugin, "detail-not-found"),
wl(plugin, "detail-not-found") + " #" + ticketId,
plugin));
return;
}
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticketId);
String content = buildDetail(ticket, comments, session);
sendHtml(ex, 200, layout("Ticket #" + ticketId, content, session, plugin));
}
private String buildDetail(Ticket t, List<TicketComment> comments, WebSession session) {
StringBuilder sb = new StringBuilder();
String created = t.getCreatedAt() != null ? SDF.format(t.getCreatedAt()) : "";
String catName = getCategoryName(t);
// ── Header ──
sb.append("<div style='margin-bottom:1.5rem'>");
sb.append("<a href='/tickets' class='btn btn-sm btn-secondary' style='margin-bottom:1rem'>")
.append(escHtml(wl(plugin, "detail-back"))).append("</a>");
sb.append("</div>");
sb.append("<div class='ticket-header'>");
sb.append("<div class='ticket-id'>#").append(t.getId()).append("</div>");
sb.append("<div class='ticket-meta'>");
sb.append(statusBadge(t)).append(" ").append(priorityBadge(t));
sb.append("<span class='badge' style='background:var(--surface2);color:var(--muted)'>")
.append(escHtml(catName)).append("</span>");
if (plugin.isBungeeCordEnabled()) {
sb.append("<span class='badge' style='background:var(--surface2);color:var(--muted)'>🌐 ")
.append(escHtml(t.getServerName())).append("</span>");
}
sb.append("</div></div>");
// ── Info-Grid ──
sb.append("<div class='info-grid'>");
infoRow(sb, wl(plugin, "detail-info-creator"), t.getCreatorName());
infoRow(sb, wl(plugin, "detail-info-created"), created);
infoRow(sb, wl(plugin, "detail-info-claimer"), t.getClaimerName() != null ? t.getClaimerName() : "");
infoRow(sb, wl(plugin, "detail-info-forwarded"), t.getForwardedToName() != null ? t.getForwardedToName() : "");
if (t.getWorldName() != null) {
infoRow(sb, wl(plugin, "detail-info-position"), String.format("%s %.0f / %.0f / %.0f",
t.getWorldName(), t.getX(), t.getY(), t.getZ()));
}
if (t.getPlayerRating() != null) {
String ratingText = "THUMBS_UP".equals(t.getPlayerRating())
? wl(plugin, "detail-rating-pos")
: wl(plugin, "detail-rating-neg");
infoRow(sb, wl(plugin, "detail-info-rating"), ratingText);
}
sb.append("</div>");
// ── Nachricht ──
sb.append("<div class='card-title'>").append(escHtml(wl(plugin, "detail-section-msg"))).append("</div>");
sb.append("<div class='ticket-message'>").append(escHtml(t.getMessage())).append("</div>");
if (t.getCloseComment() != null && !t.getCloseComment().isEmpty()) {
sb.append("<div class='card-title'>").append(escHtml(wl(plugin, "detail-section-closecomment"))).append("</div>");
sb.append("<div class='ticket-message'>").append(escHtml(t.getCloseComment())).append("</div>");
}
// ── Aktionen (nur bei aktiven Tickets) ──
if (t.getStatus() != TicketStatus.CLOSED) {
sb.append("<div class='card'><div class='card-title'>")
.append(escHtml(wl(plugin, "detail-section-actions")))
.append("</div><div style='display:flex;flex-wrap:wrap;gap:.75rem'>");
// Claim
if (t.getStatus() == TicketStatus.OPEN) {
sb.append("<button class='btn btn-success' onclick='claimTicket(").append(t.getId()).append(")'>")
.append(escHtml(wl(plugin, "detail-btn-claim"))).append("</button>");
}
// Priorität
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
sb.append("<select id='prio-select-").append(t.getId())
.append("' class='btn btn-secondary' style='width:auto' onchange='setPriority(")
.append(t.getId()).append(", this.value)'>");
for (TicketPriority p : TicketPriority.values()) {
String sel = p == t.getPriority() ? " selected" : "";
sb.append("<option value='").append(p.name()).append("'").append(sel).append(">")
.append(escHtml(p.getDisplayName())).append("</option>");
}
sb.append("</select>");
}
// Weiterleiten (nur Admin)
if (session.isAdmin()) {
sb.append("<input id='forward-target-").append(t.getId())
.append("' class='btn btn-secondary' style='width:180px' placeholder='")
.append(escHtml(wl(plugin, "detail-ph-forward"))).append("'>");
sb.append("<button class='btn btn-warning' onclick='forwardTicket(").append(t.getId()).append(")'>")
.append(escHtml(wl(plugin, "detail-btn-forward"))).append("</button>");
}
// Schließen
sb.append("<div style='display:flex;gap:.5rem;align-items:center'>");
sb.append("<input id='close-comment-").append(t.getId())
.append("' placeholder='").append(escHtml(wl(plugin, "detail-ph-comment")))
.append("' style='width:220px'>");
sb.append("<button class='btn btn-danger' onclick='closeTicket(").append(t.getId()).append(")'>")
.append(escHtml(wl(plugin, "detail-btn-close"))).append("</button>");
sb.append("</div>");
sb.append("</div></div>");
}
// ── Kommentare ──
sb.append("<div class='card'><div class='card-title'>")
.append(escHtml(wl(plugin, "detail-section-comments")))
.append(" (").append(comments.size()).append(")</div>");
sb.append("<div class='comments'>");
for (TicketComment c : comments) {
String cTime = c.getCreatedAt() != null ? SDF.format(c.getCreatedAt()) : "";
sb.append("<div class='comment'>");
sb.append("<span class='comment-author'>").append(escHtml(c.getAuthorName())).append("</span>");
sb.append("<span class='comment-time'>").append(cTime).append("</span>");
sb.append("<div class='comment-text'>").append(escHtml(c.getMessage())).append("</div>");
sb.append("</div>");
}
if (comments.isEmpty()) {
sb.append("<div style='color:var(--muted);font-size:.875rem'>")
.append(escHtml(wl(plugin, "detail-no-comments"))).append("</div>");
}
sb.append("</div>");
// Neuer Kommentar
if (t.getStatus() != TicketStatus.CLOSED) {
sb.append("<div style='display:flex;gap:.75rem;margin-top:1rem'>");
sb.append("<textarea id='comment-input-").append(t.getId())
.append("' placeholder='").append(escHtml(wl(plugin, "detail-ph-newcomment")))
.append("' style='flex:1;min-height:60px'></textarea>");
sb.append("<button class='btn btn-primary' onclick='addComment(").append(t.getId()).append(")'>")
.append(escHtml(wl(plugin, "detail-btn-send"))).append("</button>");
sb.append("</div>");
}
sb.append("</div>");
return sb.toString();
}
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
private String statusBadge(Ticket t) {
String s = t.getStatus().name().toLowerCase();
return "<span class='badge badge-" + s + "'>" + escHtml(t.getStatus().getDisplayName()) + "</span>";
}
private String priorityBadge(Ticket t) {
String p = t.getPriority().name().toLowerCase();
return "<span class='badge badge-" + p + "'>" + escHtml(t.getPriority().getDisplayName()) + "</span>";
}
private String getCategoryName(Ticket t) {
var cat = plugin.getCategoryManager().fromKey(t.getCategoryKey());
return cat != null ? cat.getName() : t.getCategoryKey();
}
private void infoRow(StringBuilder sb, String label, String value) {
sb.append("<div class='info-row'><span class='info-label'>").append(escHtml(label)).append("</span>")
.append("<span>").append(escHtml(value != null ? value : "")).append("</span></div>");
}
private String selectFilter(String name, String current, List<Map.Entry<String,String>> options) {
StringBuilder sb = new StringBuilder("<select name='").append(name)
.append("' class='btn btn-secondary btn-sm' onchange='this.form.submit()'>");
for (var opt : options) {
String sel = opt.getKey().equals(current) ? " selected" : "";
sb.append("<option value='").append(escHtml(opt.getKey())).append("'").append(sel).append(">")
.append(escHtml(opt.getValue())).append("</option>");
}
return sb.append("</select>").toString();
}
private static Map.Entry<String,String> entry(String k, String v) {
return Map.entry(k, v);
}
private static int parseInt(String s, int fallback) {
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return fallback; }
}
}