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 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 all = db.getAllTickets(); // ── Filter ── List 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 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 tickets, int total, int page, int totalPages, String filterStatus, String filterCat, String filterPrio, String filterSearch) { StringBuilder sb = new StringBuilder(); sb.append("

") .append(escHtml(wl(plugin, "tickets-title"))) .append(" ").append(total).append(" ") .append(escHtml(wl(plugin, "tickets-total"))) .append("

"); // ── Filter-Bar ── sb.append("
"); 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> 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("
"); sb.append(""); sb.append(""); sb.append(""); sb.append(""); sb.append("
"); sb.append("
"); // ── Tabelle ── sb.append("
") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append(""); 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("") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append(""); } if (tickets.isEmpty()) { sb.append(""); } sb.append("
").append(escHtml(wl(plugin, "tickets-col-id"))).append("").append(escHtml(wl(plugin, "tickets-col-player"))).append("").append(escHtml(wl(plugin, "tickets-col-message"))).append("").append(escHtml(wl(plugin, "tickets-col-cat"))).append("").append(escHtml(wl(plugin, "tickets-col-prio"))).append("").append(escHtml(wl(plugin, "tickets-col-status"))).append("").append(escHtml(wl(plugin, "tickets-col-created"))).append("
#").append(t.getId()).append("").append(escHtml(t.getCreatorName())).append("").append(escHtml(msg)).append("").append(escHtml(catName)).append("").append(priorityBadge(t)).append("").append(statusBadge(t)).append("").append(created).append("") .append(escHtml(wl(plugin, "btn-details"))) .append("
") .append(escHtml(wl(plugin, "tickets-empty"))) .append("
"); // ── Paginierung ── if (totalPages > 1) { sb.append(""); } 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 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 comments, WebSession session) { StringBuilder sb = new StringBuilder(); String created = t.getCreatedAt() != null ? SDF.format(t.getCreatedAt()) : "—"; String catName = getCategoryName(t); // ── Header ── sb.append(""); sb.append("
"); sb.append("
#").append(t.getId()).append("
"); sb.append("
"); sb.append(statusBadge(t)).append(" ").append(priorityBadge(t)); sb.append("") .append(escHtml(catName)).append(""); if (plugin.isBungeeCordEnabled()) { sb.append("🌐 ") .append(escHtml(t.getServerName())).append(""); } sb.append("
"); // ── Info-Grid ── sb.append("
"); 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("
"); // ── Nachricht ── sb.append("
").append(escHtml(wl(plugin, "detail-section-msg"))).append("
"); sb.append("
").append(escHtml(t.getMessage())).append("
"); if (t.getCloseComment() != null && !t.getCloseComment().isEmpty()) { sb.append("
").append(escHtml(wl(plugin, "detail-section-closecomment"))).append("
"); sb.append("
").append(escHtml(t.getCloseComment())).append("
"); } // ── Aktionen (nur bei aktiven Tickets) ── if (t.getStatus() != TicketStatus.CLOSED) { sb.append("
") .append(escHtml(wl(plugin, "detail-section-actions"))) .append("
"); // Claim if (t.getStatus() == TicketStatus.OPEN) { sb.append(""); } // Priorität if (plugin.getConfig().getBoolean("priorities-enabled", true)) { sb.append(""); } // Weiterleiten (nur Admin) if (session.isAdmin()) { sb.append(""); sb.append(""); } // Schließen sb.append("
"); sb.append(""); sb.append(""); sb.append("
"); sb.append("
"); } // ── Kommentare ── sb.append("
") .append(escHtml(wl(plugin, "detail-section-comments"))) .append(" (").append(comments.size()).append(")
"); sb.append("
"); for (TicketComment c : comments) { String cTime = c.getCreatedAt() != null ? SDF.format(c.getCreatedAt()) : ""; sb.append("
"); sb.append("").append(escHtml(c.getAuthorName())).append(""); sb.append("").append(cTime).append(""); sb.append("
").append(escHtml(c.getMessage())).append("
"); sb.append("
"); } if (comments.isEmpty()) { sb.append("
") .append(escHtml(wl(plugin, "detail-no-comments"))).append("
"); } sb.append("
"); // Neuer Kommentar if (t.getStatus() != TicketStatus.CLOSED) { sb.append("
"); sb.append(""); sb.append(""); sb.append("
"); } sb.append("
"); return sb.toString(); } // ─────────────────────────── Hilfsmethoden ───────────────────────────── private String statusBadge(Ticket t) { String s = t.getStatus().name().toLowerCase(); return "" + escHtml(t.getStatus().getDisplayName()) + ""; } private String priorityBadge(Ticket t) { String p = t.getPriority().name().toLowerCase(); return "" + escHtml(t.getPriority().getDisplayName()) + ""; } 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("
").append(escHtml(label)).append("") .append("").append(escHtml(value != null ? value : "—")).append("
"); } private String selectFilter(String name, String current, List> options) { StringBuilder sb = new StringBuilder("").toString(); } private static Map.Entry 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; } } }