Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-15 19:11:02 +02:00
parent bafddee288
commit d66871234c
20 changed files with 3854 additions and 451 deletions

View File

@@ -0,0 +1,379 @@
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; }
}
}