diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java index 3e4b083..0293c2f 100644 --- a/src/main/java/de/ticketsystem/TicketPlugin.java +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -5,9 +5,9 @@ import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.discord.DiscordWebhook; import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.listeners.PlayerJoinListener; +import de.ticketsystem.manager.CategoryManager; import de.ticketsystem.manager.TicketManager; -// WICHTIG: Import hinzugefügt -import de.ticketsystem.model.Ticket; +import de.ticketsystem.model.Ticket; import org.bukkit.ChatColor; import org.bukkit.plugin.java.JavaPlugin; @@ -18,10 +18,11 @@ public class TicketPlugin extends JavaPlugin { private static TicketPlugin instance; private boolean debug; - private DatabaseManager databaseManager; - private TicketManager ticketManager; - private TicketGUI ticketGUI; - private DiscordWebhook discordWebhook; + private DatabaseManager databaseManager; + private TicketManager ticketManager; + private CategoryManager categoryManager; + private TicketGUI ticketGUI; + private DiscordWebhook discordWebhook; @Override public void onEnable() { @@ -29,9 +30,8 @@ public class TicketPlugin extends JavaPlugin { saveDefaultConfig(); - // --- WICHTIG: Ticket-Klasse registrieren --- + // Ticket-Klasse für YAML-Serialisierung registrieren Ticket.register(); - // ------------------------------------------- // Update-Checker int resourceId = 132757; @@ -68,9 +68,10 @@ public class TicketPlugin extends JavaPlugin { } // Manager, GUI & Discord-Webhook initialisieren - ticketManager = new TicketManager(this); - ticketGUI = new TicketGUI(this); - discordWebhook = new DiscordWebhook(this); + categoryManager = new CategoryManager(this); + ticketManager = new TicketManager(this); + ticketGUI = new TicketGUI(this); + discordWebhook = new DiscordWebhook(this); if (getConfig().getBoolean("discord.enabled", false)) { String url = getConfig().getString("discord.webhook-url", ""); @@ -127,8 +128,9 @@ public class TicketPlugin extends JavaPlugin { public static TicketPlugin getInstance() { return instance; } public DatabaseManager getDatabaseManager() { return databaseManager; } - public TicketManager getTicketManager() { return ticketManager; } - public TicketGUI getTicketGUI() { return ticketGUI; } - public DiscordWebhook getDiscordWebhook() { return discordWebhook; } - public boolean isDebug() { return debug; } + public TicketManager getTicketManager() { return ticketManager; } + public CategoryManager getCategoryManager() { return categoryManager; } + public TicketGUI getTicketGUI() { return ticketGUI; } + public DiscordWebhook getDiscordWebhook() { return discordWebhook; } + public boolean isDebug() { return debug; } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java index 95af481..ae57bab 100644 --- a/src/main/java/de/ticketsystem/commands/TicketCommand.java +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -2,7 +2,13 @@ package de.ticketsystem.commands; import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.Ticket; + +import de.ticketsystem.manager.CategoryManager; +import de.ticketsystem.model.ConfigCategory; +import de.ticketsystem.model.TicketComment; +import de.ticketsystem.model.TicketPriority; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -12,14 +18,13 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; public class TicketCommand implements CommandExecutor, TabCompleter { private final TicketPlugin plugin; - public TicketCommand(TicketPlugin plugin) { - this.plugin = plugin; - } + public TicketCommand(TicketPlugin plugin) { this.plugin = plugin; } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { @@ -27,23 +32,25 @@ public class TicketCommand implements CommandExecutor, TabCompleter { sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden."); return true; } - if (args.length == 0) { - plugin.getTicketManager().sendHelpMessage(player); - return true; - } + if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; } + switch (args[0].toLowerCase()) { - case "create" -> handleCreate(player, args); - case "list" -> handleList(player); - case "claim" -> handleClaim(player, args); - case "close" -> handleClose(player, args); - case "forward" -> handleForward(player, args); - case "reload" -> handleReload(player); - case "migrate" -> handleMigrate(player, args); - case "export" -> handleExport(player, args); - case "import" -> handleImport(player, args); - case "stats" -> handleStats(player); - case "archive" -> handleArchive(player); - default -> plugin.getTicketManager().sendHelpMessage(player); + case "create" -> handleCreate(player, args); + case "list" -> handleList(player); + case "claim" -> handleClaim(player, args); + case "close" -> handleClose(player, args); + case "forward" -> handleForward(player, args); + case "reload" -> handleReload(player); + case "migrate" -> handleMigrate(player, args); + case "export" -> handleExport(player, args); + case "import" -> handleImport(player, args); + case "stats" -> handleStats(player); + case "archive" -> handleArchive(player); + case "comment" -> handleComment(player, args); + case "blacklist" -> handleBlacklist(player, args); + case "rate" -> handleRate(player, args); + case "setpriority" -> handleSetPriority(player, args); + default -> plugin.getTicketManager().sendHelpMessage(player); } return true; } @@ -52,44 +59,113 @@ public class TicketCommand implements CommandExecutor, TabCompleter { private void handleCreate(Player player, String[] args) { if (!player.hasPermission("ticket.create")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + + // Blacklist-Check + if (plugin.getDatabaseManager().isBlacklisted(player.getUniqueId())) { + player.sendMessage(plugin.color("&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen.")); return; } + if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket create ")); + player.sendMessage(plugin.color("&cBenutzung: /ticket create [Kategorie] [Priorität] ")); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + player.sendMessage(plugin.color("&7Kategorien: &ebug&7, &efrage&7, &ebeschwerde&7, &esonstiges&7, &eallgemein")); + } + if (plugin.getConfig().getBoolean("priorities-enabled", true)) { + player.sendMessage(plugin.color("&7Prioritäten: &alow&7, &enormal&7, &6high&7, &curgent")); + } return; } + if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) { long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); - player.sendMessage(plugin.formatMessage("messages.cooldown") - .replace("{seconds}", String.valueOf(remaining))); + player.sendMessage(plugin.formatMessage("messages.cooldown").replace("{seconds}", String.valueOf(remaining))); return; } if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) { int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); - player.sendMessage(plugin.color("&cDu hast bereits &e" + max - + " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde.")); + player.sendMessage(plugin.color("&cDu hast bereits &e" + max + " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde.")); return; } - String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + + // Kategorie und Priorität optional parsen + CategoryManager cm = plugin.getCategoryManager(); + ConfigCategory category = cm.getDefault(); + TicketPriority priority = TicketPriority.NORMAL; + int messageStartIndex = 1; + + boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true); + boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true); + + if (args.length >= 3) { + // args[1]: erst als Kategorie prüfen, dann als Priorität + if (categoriesOn) { + ConfigCategory parsedCat = cm.resolve(args[1]); + if (parsedCat != null) { + category = parsedCat; + messageStartIndex = 2; + // args[2]: Priorität prüfen (nur wenn danach noch Text kommt) + if (prioritiesOn && args.length >= 4) { + TicketPriority parsedPrio = parsePriority(args[2]); + if (parsedPrio != null) { + priority = parsedPrio; + messageStartIndex = 3; + } + } + } else { + // Keine Kategorie erkannt → args[1] als Priorität prüfen + if (prioritiesOn) { + TicketPriority parsedPrio = parsePriority(args[1]); + if (parsedPrio != null) { + priority = parsedPrio; + messageStartIndex = 2; + } + } + } + } else if (prioritiesOn) { + // Kategorien aus → args[1] direkt als Priorität prüfen + TicketPriority parsedPrio = parsePriority(args[1]); + if (parsedPrio != null) { + priority = parsedPrio; + messageStartIndex = 2; + } + } + } else if (args.length == 2) { + // Nur ein Argument: könnte Kategorie oder Priorität sein, aber kein Text danach + // → einfach als Beschreibung behandeln, nichts parsen + } + + String message = String.join(" ", Arrays.copyOfRange(args, messageStartIndex, args.length)); int maxLen = plugin.getConfig().getInt("max-description-length", 100); + if (message.isEmpty()) { + player.sendMessage(plugin.color("&cBitte gib eine Beschreibung für dein Ticket an.")); + return; + } if (message.length() > maxLen) { player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); return; } + + final ConfigCategory finalCategory = category; + final TicketPriority finalPriority = priority; Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); + ticket.setCategoryKey(finalCategory.getKey()); + ticket.setPriority(finalPriority); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int id = plugin.getDatabaseManager().createTicket(ticket); - if (id == -1) { - player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); - return; - } + if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; } ticket.setId(id); plugin.getTicketManager().setCooldown(player.getUniqueId()); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.formatMessage("messages.ticket-created") - .replace("{id}", String.valueOf(id))); - plugin.getTicketManager().notifyTeam(ticket); // ruft auch Discord-Webhook auf + String catInfo = plugin.getConfig().getBoolean("categories-enabled", true) + ? " §7[" + finalCategory.getColored() + "§7]" : ""; + String prioInfo = plugin.getConfig().getBoolean("priorities-enabled", true) + ? " §7[" + finalPriority.getColored() + "§7]" : ""; + player.sendMessage(plugin.formatMessage("messages.ticket-created").replace("{id}", String.valueOf(id)) + catInfo + prioInfo); + plugin.getTicketManager().notifyTeam(ticket); }); }); } @@ -99,12 +175,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter { private void handleList(Player player) { if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> - Bukkit.getScheduler().runTask(plugin, () -> - plugin.getTicketGUI().openGUI(player))); + Bukkit.getScheduler().runTask(plugin, () -> plugin.getTicketGUI().openGUI(player))); } else { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> - Bukkit.getScheduler().runTask(plugin, () -> - plugin.getTicketGUI().openPlayerGUI(player))); + Bukkit.getScheduler().runTask(plugin, () -> plugin.getTicketGUI().openPlayerGUI(player))); } } @@ -112,13 +186,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter { private void handleClaim(Player player, String[] args) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket claim ")); - return; + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket claim ")); return; } int id; try { id = Integer.parseInt(args[1]); } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } @@ -143,40 +213,30 @@ public class TicketCommand implements CommandExecutor, TabCompleter { private void handleClose(Player player, String[] args) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket close [Kommentar]")); - return; + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket close [Kommentar]")); return; } int id; try { id = Integer.parseInt(args[1]); } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } - String closeComment = args.length > 2 - ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : ""; - - final int ticketId = id; - final String comment = closeComment; - final String closer = player.getName(); + String closeComment = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : ""; + final int ticketId = id; + final String comment = closeComment; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); if (success) { Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.formatMessage("messages.ticket-closed") - .replace("{id}", String.valueOf(ticketId))); + player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); if (ticket != null) { ticket.setCloseComment(comment); - // closerName für Discord-Nachricht mitgeben - plugin.getTicketManager().notifyCreatorClosed(ticket, closer); + plugin.getTicketManager().notifyCreatorClosed(ticket, player.getName()); } }); } else { - Bukkit.getScheduler().runTask(plugin, () -> - player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); } }); } @@ -185,161 +245,260 @@ public class TicketCommand implements CommandExecutor, TabCompleter { private void handleForward(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 3) { - player.sendMessage(plugin.color("&cBenutzung: /ticket forward ")); - return; + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket forward ")); return; } int id; try { id = Integer.parseInt(args[1]); } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } Player target = Bukkit.getPlayer(args[2]); - if (target == null || !target.isOnline()) { - player.sendMessage(plugin.color("&cSpieler &e" + args[2] + " &cist nicht online!")); + if (target == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); return; } + + final int ticketId = id; + final String fromName = player.getName(); + final Player t = target; + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } + Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + if (ticket == null) return; + player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet.")); + plugin.getTicketManager().notifyForwardedTo(ticket, fromName); + plugin.getTicketManager().notifyCreatorForwarded(ticket); + }); + }); + } + + // ─────────────────────────── /ticket comment ─────────────────────────── + + private void handleComment(Player player, String[] args) { + if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket comment ")); return; } + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + + String msg = String.join(" ", Arrays.copyOfRange(args, 2, args.length)); + if (msg.length() > 500) { player.sendMessage(plugin.color("&cNachricht zu lang! Maximal 500 Zeichen.")); return; } + + final int ticketId = id; + final String message = msg; + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + if (ticket == null) { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + return; + } + // Spieler darf nur auf eigene Tickets kommentieren (Supporter/Admin: alle) + boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId()); + boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); + if (!isOwner && !isStaff) { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets kommentieren."))); + return; + } + + TicketComment comment = new TicketComment(ticketId, player.getUniqueId(), player.getName(), message); + boolean success = plugin.getDatabaseManager().addComment(comment); + + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert.")); + // Supporter/Admin und Ticket-Ersteller benachrichtigen + notifyCommentReceivers(player, ticket, message); + } else { + player.sendMessage(plugin.color("&cFehler beim Speichern des Kommentars.")); + } + }); + }); + } + + private void notifyCommentReceivers(Player author, Ticket ticket, String message) { + String onlineMsg = plugin.color("&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert: &f" + message); + String offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message; + + // Ticket-Ersteller benachrichtigen (wenn nicht der Autor selbst) + if (!ticket.getCreatorUUID().equals(author.getUniqueId())) { + Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); + if (creator != null && creator.isOnline()) { + creator.sendMessage(onlineMsg); + } else { + // Offline → für nächsten Login speichern + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> + plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg)); + } + } + + // Supporter/Admin benachrichtigen (wenn Autor kein Supporter ist) + if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) { + // Claimer des Tickets bevorzugt benachrichtigen + UUID claimerUUID = ticket.getClaimerUUID(); + if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) { + Player claimer = Bukkit.getPlayer(claimerUUID); + if (claimer != null && claimer.isOnline()) { + claimer.sendMessage(onlineMsg); + } else { + String claimerOffline = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f" + message; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> + plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline)); + } + } + // Alle anderen Online-Supporter zusätzlich informieren + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getUniqueId().equals(author.getUniqueId())) continue; + if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; // schon oben + if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { + p.sendMessage(onlineMsg); + } + } + } + } + + // ─────────────────────────── /ticket rate ────────────────────────────── + + private void handleRate(Player player, String[] args) { + if (!plugin.getConfig().getBoolean("rating-enabled", true)) { + player.sendMessage(plugin.color("&cBewertungen sind deaktiviert.")); return; + } + if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket rate ")); return; } + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + + String ratingArg = args[2].toLowerCase(); + String rating; + if (ratingArg.equals("good") || ratingArg.equals("gut") || ratingArg.equals("thumbsup")) { + rating = "THUMBS_UP"; + } else if (ratingArg.equals("bad") || ratingArg.equals("schlecht") || ratingArg.equals("thumbsdown")) { + rating = "THUMBS_DOWN"; + } else { + player.sendMessage(plugin.color("&cUngültige Bewertung! Benutze &egood &coder &ebad&c.")); return; } - final int ticketId = id; - final Player finalTarget = target; - final String fromName = player.getName(); + final int ticketId = id; + final String finalRating = rating; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager() - .forwardTicket(ticketId, finalTarget.getUniqueId(), finalTarget.getName()); - if (success) { - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); - Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.formatMessage("messages.ticket-forwarded") - .replace("{id}", String.valueOf(ticketId)) - .replace("{player}", finalTarget.getName())); - if (ticket != null) { - // fromName für Discord mitgeben - plugin.getTicketManager().notifyForwardedTo(ticket, fromName); - plugin.getTicketManager().notifyCreatorForwarded(ticket); - } - }); - } else { - Bukkit.getScheduler().runTask(plugin, () -> - player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + if (ticket == null) { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + return; } + if (!ticket.getCreatorUUID().equals(player.getUniqueId())) { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets bewerten."))); + return; + } + if (ticket.hasRating()) { + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu hast dieses Ticket bereits bewertet."))); + return; + } + + boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ"; + player.sendMessage(plugin.color("&aDanke für deine Bewertung! (" + emoji + "§a)")); + } else { + player.sendMessage(plugin.color("&cBewertung konnte nicht gespeichert werden. Ticket geschlossen?")); + } + }); }); } + // ─────────────────────────── /ticket blacklist ───────────────────────── + + private void handleBlacklist(Player player, String[] args) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (args.length < 2) { + player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist [Spieler] [Grund]")); + return; + } + + switch (args[1].toLowerCase()) { + case "add" -> { + if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist add [Grund]")); return; } + String targetName = args[2]; + String reason = args.length > 3 ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "Missbrauch"; + + @SuppressWarnings("deprecation") + OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); + if (target.getUniqueId() == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden.")); return; } + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().addBlacklist( + target.getUniqueId(), targetName, reason, player.getName()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) + player.sendMessage(plugin.color("&a" + targetName + " &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e" + reason)); + else + player.sendMessage(plugin.color("&cSpieler ist bereits auf der Blacklist.")); + }); + }); + } + case "remove" -> { + if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove ")); return; } + String targetName = args[2]; + @SuppressWarnings("deprecation") + OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) player.sendMessage(plugin.color("&a" + targetName + " &awurde von der Blacklist entfernt.")); + else player.sendMessage(plugin.color("&cSpieler war nicht auf der Blacklist.")); + }); + }); + } + case "list" -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + var list = plugin.getDatabaseManager().getBlacklist(); + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Ticket-Blacklist &7(" + list.size() + " Einträge)")); + player.sendMessage(plugin.color("&8&m ")); + if (list.isEmpty()) { + player.sendMessage(plugin.color("&7Keine gesperrten Spieler.")); + } else { + for (String[] entry : list) { + // {uuid, name, reason, bannedBy, bannedAt} + player.sendMessage(plugin.color("&e" + entry[1] + " &7– &f" + entry[2] + + " &7(gesperrt von &e" + entry[3] + "&7)")); + } + } + player.sendMessage(plugin.color("&8&m ")); + }); + }); + } + default -> player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist [Spieler] [Grund]")); + } + } + // ─────────────────────────── /ticket reload ──────────────────────────── private void handleReload(Player player) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } plugin.reloadConfig(); - player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen.")); + plugin.getCategoryManager().reload(); + player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)")); } // ─────────────────────────── /ticket archive ─────────────────────────── private void handleArchive(Player player) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = plugin.getDatabaseManager().archiveClosedTickets(); Bukkit.getScheduler().runTask(plugin, () -> { - if (count > 0) { - player.sendMessage(plugin.formatMessage("messages.archive-success") - .replace("{count}", String.valueOf(count))); - } else { - player.sendMessage(plugin.formatMessage("messages.archive-fail")); - } - }); - }); - } - - // ─────────────────────────── /ticket migrate ─────────────────────────── - - private void handleMigrate(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket migrate ")); - return; - } - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int migrated = 0; - String mode = args[1].toLowerCase(); - if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL(); - else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile(); - else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; } - int finalMigrated = migrated; - Bukkit.getScheduler().runTask(plugin, () -> { - if (finalMigrated > 0) { - player.sendMessage(plugin.formatMessage("messages.migration-success") - .replace("{count}", String.valueOf(finalMigrated))); - } else { - player.sendMessage(plugin.formatMessage("messages.migration-fail")); - } - }); - }); - } - - // ─────────────────────────── /ticket export ──────────────────────────── - - private void handleExport(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket export ")); - return; - } - String filename = args[1]; - File exportFile = new File(plugin.getDataFolder(), filename); - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int count = plugin.getDatabaseManager().exportTickets(exportFile); - Bukkit.getScheduler().runTask(plugin, () -> { - if (count > 0) { - player.sendMessage(plugin.formatMessage("messages.export-success") - .replace("{count}", String.valueOf(count)).replace("{file}", filename)); - } else { - player.sendMessage(plugin.formatMessage("messages.export-fail")); - } - }); - }); - } - - // ─────────────────────────── /ticket import ──────────────────────────── - - private void handleImport(Player player, String[] args) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket import ")); - return; - } - String filename = args[1]; - File importFile = new File(plugin.getDataFolder(), filename); - if (!importFile.exists()) { - player.sendMessage(plugin.formatMessage("messages.file-not-found").replace("{file}", filename)); - return; - } - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int count = plugin.getDatabaseManager().importTickets(importFile); - Bukkit.getScheduler().runTask(plugin, () -> { - if (count > 0) { - player.sendMessage(plugin.formatMessage("messages.import-success") - .replace("{count}", String.valueOf(count))); - } else { - player.sendMessage(plugin.formatMessage("messages.import-fail")); - } + if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count))); + else player.sendMessage(plugin.formatMessage("messages.archive-fail")); }); }); } @@ -347,44 +506,190 @@ public class TicketCommand implements CommandExecutor, TabCompleter { // ─────────────────────────── /ticket stats ───────────────────────────── private void handleStats(Player player) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var stats = plugin.getDatabaseManager().getTicketStats(); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage(plugin.color("&6--- Ticket Statistik ---")); - player.sendMessage(plugin.color("&eGesamt: &a" + stats.total - + " &7| &eOffen: &a" + stats.open - + " &7| &eGeschlossen: &a" + stats.closed - + " &7| &eWeitergeleitet: &a" + stats.forwarded)); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Ticket Statistik")); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&eGesamt: &a" + stats.total)); + player.sendMessage(plugin.color("&eOffen: &a" + stats.open)); + player.sendMessage(plugin.color("&eGeschlossen: &a" + stats.closed)); + player.sendMessage(plugin.color("&eWeitergeleitet: &a" + stats.forwarded)); + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Support-Bewertungen")); + player.sendMessage(plugin.color("&a👍 Positiv: &f" + stats.thumbsUp + + " &c👎 Negativ: &f" + stats.thumbsDown)); + int total = stats.thumbsUp + stats.thumbsDown; + if (total > 0) { + int percent = (int) Math.round(stats.thumbsUp * 100.0 / total); + player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%")); + } + } + player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&6Top Ersteller:")); stats.byPlayer.entrySet().stream() .sorted((a, b) -> b.getValue() - a.getValue()) .limit(5) - .forEach(e -> player.sendMessage(plugin.color("&e" + e.getKey() + ": &a" + e.getValue()))); + .forEach(e -> player.sendMessage(plugin.color("&e " + e.getKey() + ": &a" + e.getValue()))); + player.sendMessage(plugin.color("&8&m ")); }); }); } + // ─────────────────────────── /ticket migrate ─────────────────────────── + + private void handleMigrate(Player player, String[] args) { + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket migrate ")); return; } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int migrated = 0; + String mode = args[1].toLowerCase(); + if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL(); + else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile(); + else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; } + int f = migrated; + Bukkit.getScheduler().runTask(plugin, () -> { + if (f > 0) player.sendMessage(plugin.formatMessage("messages.migration-success").replace("{count}", String.valueOf(f))); + else player.sendMessage(plugin.formatMessage("messages.migration-fail")); + }); + }); + } + + // ─────────────────────────── /ticket export ──────────────────────────── + + private void handleExport(Player player, String[] args) { + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket export ")); return; } + String filename = args[1]; + File exportFile = new File(plugin.getDataFolder(), filename); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int count = plugin.getDatabaseManager().exportTickets(exportFile); + Bukkit.getScheduler().runTask(plugin, () -> { + if (count > 0) player.sendMessage(plugin.formatMessage("messages.export-success").replace("{count}", String.valueOf(count)).replace("{file}", filename)); + else player.sendMessage(plugin.formatMessage("messages.export-fail")); + }); + }); + } + + // ─────────────────────────── /ticket import ──────────────────────────── + + private void handleImport(Player player, String[] args) { + if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } + if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket import ")); return; } + String filename = args[1]; + File importFile = new File(plugin.getDataFolder(), filename); + if (!importFile.exists()) { player.sendMessage(plugin.formatMessage("messages.file-not-found").replace("{file}", filename)); return; } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int count = plugin.getDatabaseManager().importTickets(importFile); + Bukkit.getScheduler().runTask(plugin, () -> { + if (count > 0) player.sendMessage(plugin.formatMessage("messages.import-success").replace("{count}", String.valueOf(count))); + else player.sendMessage(plugin.formatMessage("messages.import-fail")); + }); + }); + } + + // ─────────────────────────── /ticket setpriority ────────────────────── + + private void handleSetPriority(Player player, String[] args) { + if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (!plugin.getConfig().getBoolean("priorities-enabled", true)) { + player.sendMessage(plugin.color("&cDas Prioritäten-System ist deaktiviert.")); return; + } + if (args.length < 3) { + player.sendMessage(plugin.color("&cBenutzung: /ticket setpriority ")); return; + } + int ticketId; + try { ticketId = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige Ticket-ID: &e" + args[1])); return; + } + TicketPriority priority = parsePriority(args[2]); + if (priority == null) { + player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return; + } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().setTicketPriority(ticketId, priority); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + ticketId + + " &awurde auf " + priority.getColored() + " &agesetzt.")); + } else { + player.sendMessage(plugin.color("&cTicket &e#" + ticketId + " &cwurde nicht gefunden.")); + } + }); + }); + } + + /** Parst Benutzer-Eingaben wie "high", "hoch", "urgent", "dringend" etc. zu TicketPriority. + * Gibt null zurück wenn keine Übereinstimmung. */ + private TicketPriority parsePriority(String input) { + if (input == null) return null; + return switch (input.toLowerCase()) { + case "low", "niedrig" -> TicketPriority.LOW; + case "normal" -> TicketPriority.NORMAL; + case "high", "hoch" -> TicketPriority.HIGH; + case "urgent", "dringend" -> TicketPriority.URGENT; + default -> null; + }; + } + // ─────────────────────────── Tab-Completion ──────────────────────────── @Override public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player player)) return completions; + if (args.length == 1) { - List subs = new ArrayList<>(List.of("create", "list")); + List subs = new ArrayList<>(List.of("create", "list", "comment")); if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) subs.addAll(List.of("claim", "close")); + if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate"); if (player.hasPermission("ticket.admin")) - subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import")); - for (String s : subs) - if (s.startsWith(args[0].toLowerCase())) completions.add(s); + subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import", "blacklist")); + if ((player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) + && plugin.getConfig().getBoolean("priorities-enabled", true)) + subs.add("setpriority"); + for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s); + + } else if (args.length == 2 && args[0].equalsIgnoreCase("create") + && plugin.getConfig().getBoolean("categories-enabled", true)) { + for (ConfigCategory c : plugin.getCategoryManager().getAll()) + if (c.getKey().startsWith(args[1].toLowerCase())) completions.add(c.getKey()); + // auch Priorität direkt ohne Kategorie anbieten + if (plugin.getConfig().getBoolean("priorities-enabled", true)) + for (String p : List.of("low", "normal", "high", "urgent")) + if (p.startsWith(args[1].toLowerCase())) completions.add(p); + + } else if (args.length == 3 && args[0].equalsIgnoreCase("create") + && plugin.getConfig().getBoolean("priorities-enabled", true)) { + // Priorität nach Kategorie + for (String p : List.of("low", "normal", "high", "urgent")) + if (p.startsWith(args[2].toLowerCase())) completions.add(p); + + } else if (args.length == 3 && args[0].equalsIgnoreCase("setpriority")) { + for (String p : List.of("low", "normal", "high", "urgent")) + if (p.startsWith(args[2].toLowerCase())) completions.add(p); + } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); + + } else if (args.length == 2 && args[0].equalsIgnoreCase("blacklist")) { + completions.addAll(List.of("add", "remove", "list")); + + } else if (args.length == 3 && args[0].equalsIgnoreCase("blacklist") + && (args[1].equalsIgnoreCase("add") || args[1].equalsIgnoreCase("remove"))) { + for (Player p : Bukkit.getOnlinePlayers()) + if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); + + } else if (args.length == 3 && args[0].equalsIgnoreCase("rate")) { + completions.addAll(List.of("good", "bad")); } return completions; } diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java index d848870..8d78cce 100644 --- a/src/main/java/de/ticketsystem/database/DatabaseManager.java +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -10,6 +10,9 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.Ticket; + +import de.ticketsystem.model.TicketComment; +import de.ticketsystem.model.TicketPriority; import de.ticketsystem.model.TicketStatus; import java.sql.*; import java.util.ArrayList; @@ -37,7 +40,7 @@ public class DatabaseManager { // ─────────────────────────── Konstruktoren ───────────────────────────── public DatabaseManager(TicketPlugin plugin) { - this.plugin = plugin; + this.plugin = plugin; this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); this.useJson = plugin.getConfig().getBoolean("use-json", false); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson); @@ -56,27 +59,17 @@ public class DatabaseManager { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); dataJson = new JSONArray(); - } catch (IOException e) { - sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); - } + } catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); } } else { try { - JSONParser parser = new JSONParser(); - dataJson = (JSONArray) parser.parse(new FileReader(dataFile)); - } catch (Exception e) { - sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); - dataJson = new JSONArray(); - } + dataJson = (JSONArray) new JSONParser().parse(new FileReader(dataFile)); + } catch (Exception e) { sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); dataJson = new JSONArray(); } } } else { dataFile = resolvePath(dataPath); if (!dataFile.exists()) { - try { - dataFile.getParentFile().mkdirs(); - dataFile.createNewFile(); - } catch (IOException e) { - sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); - } + try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); } + catch (IOException e) { sendError("Konnte " + dataPath + " nicht erstellen: " + e.getMessage()); } } dataConfig = YamlConfiguration.loadConfiguration(dataFile); } @@ -84,7 +77,6 @@ public class DatabaseManager { } } - // Konstruktor für Tests public DatabaseManager(File dataFile, YamlConfiguration dataConfig) { this.plugin = null; this.useMySQL = false; @@ -129,16 +121,13 @@ public class DatabaseManager { config.setMaximumPoolSize(plugin.getConfig().getInt("mysql.pool-size", 10)); config.setConnectionTimeout(plugin.getConfig().getLong("mysql.connection-timeout", 30000)); config.setPoolName("TicketSystem-Pool"); - config.addDataSourceProperty("cachePrepStmts", "true"); - config.addDataSourceProperty("prepStmtCacheSize", "250"); - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit","2048"); dataSource = new HikariDataSource(config); - - // Tabellen anlegen & fehlende Spalten ergänzen createTables(); ensureColumns(); - plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); return true; } catch (Exception e) { @@ -147,12 +136,8 @@ public class DatabaseManager { useMySQL = false; dataFile = new File(plugin.getDataFolder(), "data.yml"); if (!dataFile.exists()) { - try { - dataFile.getParentFile().mkdirs(); - dataFile.createNewFile(); - } catch (IOException ex) { - plugin.getLogger().severe("Konnte data.yml nicht erstellen: " + ex.getMessage()); - } + try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); } + catch (IOException ex) { plugin.getLogger().severe("Konnte data.yml nicht erstellen: " + ex.getMessage()); } } dataConfig = YamlConfiguration.loadConfiguration(dataFile); return true; @@ -170,14 +155,13 @@ public class DatabaseManager { } } - private Connection getConnection() throws SQLException { - return dataSource.getConnection(); - } + private Connection getConnection() throws SQLException { return dataSource.getConnection(); } // ─────────────────────────── Tabellen erstellen ──────────────────────── private void createTables() { - String sql = """ + // Haupt-Tickets-Tabelle + String ticketsSql = """ CREATE TABLE IF NOT EXISTS tickets ( id INT AUTO_INCREMENT PRIMARY KEY, creator_uuid VARCHAR(36) NOT NULL, @@ -198,11 +182,54 @@ public class DatabaseManager { claimed_at TIMESTAMP NULL, closed_at TIMESTAMP NULL, close_comment VARCHAR(500) NULL, - player_deleted BOOLEAN DEFAULT FALSE + player_deleted BOOLEAN DEFAULT FALSE, + category VARCHAR(16) NOT NULL DEFAULT 'GENERAL', + priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL', + player_rating VARCHAR(16) NULL, + claimer_notified BOOLEAN DEFAULT FALSE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """; + + // Kommentare-Tabelle + String commentsSql = """ + CREATE TABLE IF NOT EXISTS ticket_comments ( + id INT AUTO_INCREMENT PRIMARY KEY, + ticket_id INT NOT NULL, + author_uuid VARCHAR(36) NOT NULL, + author_name VARCHAR(16) NOT NULL, + message VARCHAR(500) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_ticket_id (ticket_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + + // Blacklist-Tabelle + String blacklistSql = """ + CREATE TABLE IF NOT EXISTS ticket_blacklist ( + uuid VARCHAR(36) NOT NULL PRIMARY KEY, + player_name VARCHAR(16) NOT NULL, + reason VARCHAR(255) DEFAULT '', + banned_by VARCHAR(16) NOT NULL, + banned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + + // Ausstehende Benachrichtigungen für Offline-Spieler + String notifSql = """ + CREATE TABLE IF NOT EXISTS ticket_pending_notifications ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_uuid VARCHAR(36) NOT NULL, + message VARCHAR(512) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_player_uuid (player_uuid) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + """; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { - stmt.execute(sql); + stmt.execute(ticketsSql); + stmt.execute(commentsSql); + stmt.execute(blacklistSql); + stmt.execute(notifSql); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e); } @@ -212,69 +239,59 @@ public class DatabaseManager { * Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch. */ private void ensureColumns() { - // close_comment hinzufügen + ensureColumn("close_comment", "ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL"); + ensureColumn("player_deleted", "ALTER TABLE tickets ADD COLUMN player_deleted BOOLEAN DEFAULT FALSE"); + ensureColumn("category", "ALTER TABLE tickets ADD COLUMN category VARCHAR(16) NOT NULL DEFAULT 'GENERAL'"); + ensureColumn("priority", "ALTER TABLE tickets ADD COLUMN priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL'"); + ensureColumn("player_rating", "ALTER TABLE tickets ADD COLUMN player_rating VARCHAR(16) NULL"); + ensureColumn("claimer_notified", "ALTER TABLE tickets ADD COLUMN claimer_notified BOOLEAN DEFAULT FALSE"); + } + + private void ensureColumn(String columnName, String alterSql) { String checkSql = """ SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tickets' - AND COLUMN_NAME = 'close_comment' + AND COLUMN_NAME = ? """; - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement()) { - ResultSet rs = stmt.executeQuery(checkSql); + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) { + ps.setString(1, columnName); + ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt(1) == 0) { - stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL"); - plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt."); + try (Statement stmt = conn.createStatement()) { + stmt.execute(alterSql); + plugin.getLogger().info("[TicketSystem] Spalte '" + columnName + "' wurde zur Datenbank hinzugefügt."); + } } } catch (SQLException e) { - plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e); - } - - // player_deleted Spalte prüfen - String checkSqlDel = """ - SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'tickets' - AND COLUMN_NAME = 'player_deleted' - """; - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement()) { - ResultSet rs = stmt.executeQuery(checkSqlDel); - if (rs.next() && rs.getInt(1) == 0) { - stmt.execute("ALTER TABLE tickets ADD COLUMN player_deleted BOOLEAN DEFAULT FALSE"); - plugin.getLogger().info("[TicketSystem] Spalte 'player_deleted' wurde hinzugefügt."); - } - } catch (SQLException e) { - plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(player_deleted): " + e.getMessage(), e); + plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumn(" + columnName + "): " + e.getMessage(), e); } } - // ─────────────────────────── CRUD ────────────────────────────────────── + // ─────────────────────────── CRUD Tickets ────────────────────────────── public int createTicket(Ticket ticket) { if (useMySQL) { String sql = """ - INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO tickets (creator_uuid, creator_name, message, world, x, y, z, yaw, pitch, category, priority) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, ticket.getCreatorUUID().toString()); - ps.setString(2, ticket.getCreatorName()); - ps.setString(3, ticket.getMessage()); - ps.setString(4, ticket.getWorldName()); - ps.setDouble(5, ticket.getX()); - ps.setDouble(6, ticket.getY()); - ps.setDouble(7, ticket.getZ()); - ps.setFloat(8, ticket.getYaw()); - ps.setFloat(9, ticket.getPitch()); + ps.setString(1, ticket.getCreatorUUID().toString()); + ps.setString(2, ticket.getCreatorName()); + ps.setString(3, ticket.getMessage()); + ps.setString(4, ticket.getWorldName()); + ps.setDouble(5, ticket.getX()); + ps.setDouble(6, ticket.getY()); + ps.setDouble(7, ticket.getZ()); + ps.setFloat(8, ticket.getYaw()); + ps.setFloat(9, ticket.getPitch()); + ps.setString(10, ticket.getCategoryKey()); + ps.setString(11, ticket.getPriority().name()); ps.executeUpdate(); - ResultSet rs = ps.getGeneratedKeys(); - if (rs.next()) { - backupMySQL(); - return rs.getInt(1); - } + if (rs.next()) return rs.getInt(1); } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen des Tickets: " + e.getMessage(), e); } @@ -284,18 +301,11 @@ public class DatabaseManager { ticket.setId(id); dataConfig.set("lastId", id); dataConfig.set("tickets." + id, ticket); - try { - dataConfig.save(dataFile); - backupDataFile(); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); - } + saveDataConfig(); return id; } } - // ─── FIX: player_deleted wird beim Claimen zurückgesetzt, damit der Spieler - // sein Ticket wieder sieht, sobald ein Supporter es annimmt. ─────── public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) { if (useMySQL) { String sql = """ @@ -320,14 +330,9 @@ public class DatabaseManager { t.setClaimerUUID(claimerUUID); t.setClaimerName(claimerName); t.setClaimedAt(new Timestamp(System.currentTimeMillis())); - t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen + t.setPlayerDeleted(false); dataConfig.set("tickets." + ticketId, t); - try { - dataConfig.save(dataFile); - backupDataFile(); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); - } + saveDataConfig(); return true; } } @@ -353,17 +358,11 @@ public class DatabaseManager { t.setClosedAt(new Timestamp(System.currentTimeMillis())); t.setCloseComment(closeComment != null ? closeComment : ""); dataConfig.set("tickets." + ticketId, t); - try { - dataConfig.save(dataFile); - backupDataFile(); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); - } + saveDataConfig(); return true; } } - // ─── Soft Delete Methode ──────────────────────────────────────────────── public boolean markAsPlayerDeleted(int id) { if (useMySQL) { String sql = "UPDATE tickets SET player_deleted = TRUE WHERE id = ?"; @@ -375,21 +374,33 @@ public class DatabaseManager { } return false; } else { - if (dataConfig.contains("tickets." + id)) { - Ticket t = (Ticket) dataConfig.get("tickets." + id); - if (t != null) { - t.setPlayerDeleted(true); - dataConfig.set("tickets." + id, t); - try { - dataConfig.save(dataFile); - backupDataFile(); - return true; - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern (Soft Delete): " + e.getMessage()); - } - } + Ticket t = getTicketById(id); + if (t == null) return false; + t.setPlayerDeleted(true); + dataConfig.set("tickets." + id, t); + saveDataConfig(); + return true; + } + } + + public boolean setTicketPriority(int ticketId, TicketPriority priority) { + if (useMySQL) { + String sql = "UPDATE tickets SET priority = ? WHERE id = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, priority.name()); + ps.setInt(2, ticketId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Setzen der Priorität: " + e.getMessage(), e); } return false; + } else { + Ticket t = getTicketById(ticketId); + if (t == null) return false; + t.setPriority(priority); + dataConfig.set("tickets." + ticketId, t); + saveDataConfig(); + return true; } } @@ -398,32 +409,19 @@ public class DatabaseManager { String sql = "DELETE FROM tickets WHERE id = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); - int rows = ps.executeUpdate(); - if (rows > 0) { - backupMySQL(); - return true; - } + return ps.executeUpdate() > 0; } catch (SQLException e) { sendError("Fehler beim Löschen des Tickets: " + e.getMessage()); } return false; } else { - if (dataConfig.contains("tickets." + id)) { - dataConfig.set("tickets." + id, null); - try { - dataConfig.save(dataFile); - backupDataFile(); - return true; - } catch (IOException e) { - sendError("Fehler beim Löschen des Tickets: " + e.getMessage()); - } - } - return false; + if (!dataConfig.contains("tickets." + id)) return false; + dataConfig.set("tickets." + id, null); + saveDataConfig(); + return true; } } - // ─── FIX: player_deleted wird beim Weiterleiten zurückgesetzt, damit der - // Spieler sein Ticket wieder sieht, sobald es weitergeleitet wird. ── public boolean forwardTicket(int ticketId, UUID toUUID, String toName) { if (useMySQL) { String sql = """ @@ -447,27 +445,310 @@ public class DatabaseManager { t.setStatus(TicketStatus.FORWARDED); t.setForwardedToUUID(toUUID); t.setForwardedToName(toName); - t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen + t.setPlayerDeleted(false); dataConfig.set("tickets." + ticketId, t); - try { - dataConfig.save(dataFile); - backupDataFile(); - } catch (IOException e) { - plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); - } + saveDataConfig(); return true; } } + // ─────────────────────────── [NEW] Claim-Benachrichtigung markieren ──── + + /** + * Setzt claimer_notified = TRUE für ein Ticket (persistiert in DB/Datei). + */ + public void markClaimerNotified(int ticketId) { + if (useMySQL) { + String sql = "UPDATE tickets SET claimer_notified = TRUE WHERE id = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, ticketId); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei markClaimerNotified: " + e.getMessage(), e); + } + } else { + Ticket t = getTicketById(ticketId); + if (t != null) { + t.setClaimerNotified(true); + dataConfig.set("tickets." + ticketId, t); + saveDataConfig(); + } + } + } + + // ─────────────────────────── [NEW] Bewertung ─────────────────────────── + + /** + * Speichert die Bewertung eines Spielers für sein geschlossenes Ticket. + * @param ticketId ID des Tickets + * @param rating "THUMBS_UP" oder "THUMBS_DOWN" + * @return true bei Erfolg + */ + public boolean rateTicket(int ticketId, String rating) { + if (useMySQL) { + String sql = "UPDATE tickets SET player_rating = ? WHERE id = ? AND status = 'CLOSED' AND player_rating IS NULL"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, rating); + ps.setInt(2, ticketId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei rateTicket: " + e.getMessage(), e); + } + return false; + } else { + Ticket t = getTicketById(ticketId); + if (t == null || t.getStatus() != TicketStatus.CLOSED || t.hasRating()) return false; + t.setPlayerRating(rating); + dataConfig.set("tickets." + ticketId, t); + saveDataConfig(); + return true; + } + } + + // ─────────────────────────── [NEW] Kommentare ────────────────────────── + + /** + * Speichert einen neuen Kommentar/Reply auf ein Ticket. + */ + public boolean addComment(TicketComment comment) { + if (useMySQL) { + String sql = """ + INSERT INTO ticket_comments (ticket_id, author_uuid, author_name, message) + VALUES (?, ?, ?, ?) + """; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, comment.getTicketId()); + ps.setString(2, comment.getAuthorUUID().toString()); + ps.setString(3, comment.getAuthorName()); + ps.setString(4, comment.getMessage()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Speichern des Kommentars: " + e.getMessage(), e); + } + return false; + } else { + // YAML: comments.. + int index = dataConfig.getInt("comments." + comment.getTicketId() + ".count", 0); + String base = "comments." + comment.getTicketId() + "." + index + "."; + dataConfig.set(base + "authorUUID", comment.getAuthorUUID().toString()); + dataConfig.set(base + "authorName", comment.getAuthorName()); + dataConfig.set(base + "message", comment.getMessage()); + dataConfig.set(base + "createdAt", comment.getCreatedAt() != null ? comment.getCreatedAt().getTime() : System.currentTimeMillis()); + dataConfig.set("comments." + comment.getTicketId() + ".count", index + 1); + saveDataConfig(); + return true; + } + } + + /** + * Lädt alle Kommentare für ein Ticket, sortiert nach Datum. + */ + public List getComments(int ticketId) { + List list = new ArrayList<>(); + if (useMySQL) { + String sql = "SELECT * FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at ASC"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + TicketComment c = new TicketComment(); + c.setId(rs.getInt("id")); + c.setTicketId(rs.getInt("ticket_id")); + c.setAuthorUUID(UUID.fromString(rs.getString("author_uuid"))); + c.setAuthorName(rs.getString("author_name")); + c.setMessage(rs.getString("message")); + c.setCreatedAt(rs.getTimestamp("created_at")); + list.add(c); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der Kommentare: " + e.getMessage(), e); + } + } else { + if (!dataConfig.contains("comments." + ticketId)) return list; + int count = dataConfig.getInt("comments." + ticketId + ".count", 0); + for (int i = 0; i < count; i++) { + String base = "comments." + ticketId + "." + i + "."; + if (!dataConfig.contains(base + "message")) continue; + TicketComment c = new TicketComment(); + c.setTicketId(ticketId); + c.setAuthorUUID(UUID.fromString(dataConfig.getString(base + "authorUUID"))); + c.setAuthorName(dataConfig.getString(base + "authorName")); + c.setMessage(dataConfig.getString(base + "message")); + long ts = dataConfig.getLong(base + "createdAt", System.currentTimeMillis()); + c.setCreatedAt(new Timestamp(ts)); + list.add(c); + } + } + return list; + } + + // ─────────────────────────── Pending Notifications ──────────────────── + + /** + * Speichert eine Benachrichtigung für einen offline Spieler. + * Wird beim nächsten Login angezeigt. + */ + public void addPendingNotification(UUID playerUUID, String rawMessage) { + if (useMySQL) { + String sql = "INSERT INTO ticket_pending_notifications (player_uuid, message) VALUES (?, ?)"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, playerUUID.toString()); + ps.setString(2, rawMessage); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Speichern der Pending-Notification: " + e.getMessage(), e); + } + } else { + String path = "pending_notifications." + playerUUID; + List existing = dataConfig.getStringList(path); + existing.add(rawMessage); + dataConfig.set(path, existing); + saveDataConfig(); + } + } + + /** + * Lädt alle ausstehenden Benachrichtigungen für einen Spieler. + */ + public List getPendingNotifications(UUID playerUUID) { + List messages = new ArrayList<>(); + if (useMySQL) { + String sql = "SELECT message FROM ticket_pending_notifications WHERE player_uuid = ? ORDER BY created_at ASC"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, playerUUID.toString()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) messages.add(rs.getString("message")); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Laden der Pending-Notifications: " + e.getMessage(), e); + } + } else { + messages = dataConfig.getStringList("pending_notifications." + playerUUID); + } + return messages; + } + + /** + * Löscht alle ausstehenden Benachrichtigungen eines Spielers nach dem Anzeigen. + */ + public void clearPendingNotifications(UUID playerUUID) { + if (useMySQL) { + String sql = "DELETE FROM ticket_pending_notifications WHERE player_uuid = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, playerUUID.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler beim Löschen der Pending-Notifications: " + e.getMessage(), e); + } + } else { + dataConfig.set("pending_notifications." + playerUUID, null); + saveDataConfig(); + } + } + + // ─────────────────────────── [NEW] Blacklist ─────────────────────────── + + public boolean isBlacklisted(UUID uuid) { + if (useMySQL) { + String sql = "SELECT COUNT(*) FROM ticket_blacklist WHERE uuid = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + return rs.next() && rs.getInt(1) > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei isBlacklisted: " + e.getMessage(), e); + } + return false; + } else { + return dataConfig.contains("blacklist." + uuid.toString()); + } + } + + public boolean addBlacklist(UUID uuid, String playerName, String reason, String bannedBy) { + if (useMySQL) { + String sql = "INSERT IGNORE INTO ticket_blacklist (uuid, player_name, reason, banned_by) VALUES (?, ?, ?, ?)"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ps.setString(2, playerName); + ps.setString(3, reason != null ? reason : ""); + ps.setString(4, bannedBy); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei addBlacklist: " + e.getMessage(), e); + } + return false; + } else { + String base = "blacklist." + uuid.toString() + "."; + dataConfig.set(base + "playerName", playerName); + dataConfig.set(base + "reason", reason != null ? reason : ""); + dataConfig.set(base + "bannedBy", bannedBy); + dataConfig.set(base + "bannedAt", System.currentTimeMillis()); + saveDataConfig(); + return true; + } + } + + public boolean removeBlacklist(UUID uuid) { + if (useMySQL) { + String sql = "DELETE FROM ticket_blacklist WHERE uuid = ?"; + try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei removeBlacklist: " + e.getMessage(), e); + } + return false; + } else { + if (!dataConfig.contains("blacklist." + uuid.toString())) return false; + dataConfig.set("blacklist." + uuid.toString(), null); + saveDataConfig(); + return true; + } + } + + /** Gibt alle gesperrten Spieler als Liste von String-Arrays {uuid, name, reason, bannedBy} zurück. */ + public List getBlacklist() { + List list = new ArrayList<>(); + if (useMySQL) { + String sql = "SELECT uuid, player_name, reason, banned_by, banned_at FROM ticket_blacklist ORDER BY banned_at DESC"; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(sql); + while (rs.next()) { + list.add(new String[]{ + rs.getString("uuid"), + rs.getString("player_name"), + rs.getString("reason"), + rs.getString("banned_by"), + rs.getTimestamp("banned_at").toString() + }); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei getBlacklist: " + e.getMessage(), e); + } + } else { + if (!dataConfig.contains("blacklist")) return list; + for (String uuid : dataConfig.getConfigurationSection("blacklist").getKeys(false)) { + String base = "blacklist." + uuid + "."; + list.add(new String[]{ + uuid, + dataConfig.getString(base + "playerName", "?"), + dataConfig.getString(base + "reason", ""), + dataConfig.getString(base + "bannedBy", "?"), + String.valueOf(dataConfig.getLong(base + "bannedAt", 0)) + }); + } + } + return list; + } + // ─────────────────────────── Abfragen ────────────────────────────────── public List getTicketsByStatus(TicketStatus... statuses) { List list = new ArrayList<>(); if (statuses.length == 0) return list; if (useMySQL) { - StringBuilder placeholders = new StringBuilder("?"); - for (int i = 1; i < statuses.length; i++) placeholders.append(",?"); - String sql = "SELECT * FROM tickets WHERE status IN (" + placeholders + ") ORDER BY created_at ASC"; + StringBuilder ph = new StringBuilder("?"); + for (int i = 1; i < statuses.length; i++) ph.append(",?"); + String sql = "SELECT * FROM tickets WHERE status IN (" + ph + ") ORDER BY created_at ASC"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < statuses.length; i++) ps.setString(i + 1, statuses[i].name()); ResultSet rs = ps.executeQuery(); @@ -477,12 +758,11 @@ public class DatabaseManager { } return list; } else { - if (dataConfig.contains("tickets")) { - for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { - Ticket t = (Ticket) dataConfig.get("tickets." + key); - for (TicketStatus status : statuses) { - if (t != null && t.getStatus() == status) list.add(t); - } + if (!dataConfig.contains("tickets")) return list; + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + for (TicketStatus status : statuses) { + if (t != null && t.getStatus() == status) { list.add(t); break; } } } return list; @@ -499,11 +779,10 @@ public class DatabaseManager { sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage()); } } else { - if (dataConfig.contains("tickets")) { - for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { - Ticket t = (Ticket) dataConfig.get("tickets." + key); - if (t != null) list.add(t); - } + if (!dataConfig.contains("tickets")) return list; + for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { + Ticket t = (Ticket) dataConfig.get("tickets." + key); + if (t != null) list.add(t); } } return list; @@ -521,9 +800,7 @@ public class DatabaseManager { } return null; } else { - if (dataConfig.contains("tickets." + id)) { - return (Ticket) dataConfig.get("tickets." + id); - } + if (dataConfig.contains("tickets." + id)) return (Ticket) dataConfig.get("tickets." + id); return null; } } @@ -579,50 +856,33 @@ public class DatabaseManager { // ─────────────────────────── Archivierung ────────────────────────────── public int archiveClosedTickets() { - List all = getAllTickets(); + List all = getAllTickets(); List toArchive = new ArrayList<>(); - for (Ticket t : all) { - if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t); - } + for (Ticket t : all) { if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t); } if (toArchive.isEmpty()) return 0; File archiveFile = new File(plugin.getDataFolder(), archiveFileName); JSONArray arr = new JSONArray(); if (archiveFile.exists()) { try (FileReader fr = new FileReader(archiveFile)) { - JSONParser parser = new JSONParser(); - Object parsed = parser.parse(fr); + Object parsed = new JSONParser().parse(fr); if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr); } catch (Exception ignored) {} } for (Ticket t : toArchive) arr.add(ticketToJson(t)); try (FileWriter fw = new FileWriter(archiveFile)) { fw.write(arr.toJSONString()); - } catch (Exception e) { - sendError("Fehler beim Archivieren: " + e.getMessage()); - return 0; - } + } catch (Exception e) { sendError("Fehler beim Archivieren: " + e.getMessage()); return 0; } int removed = 0; if (useMySQL) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) { - for (Ticket t : toArchive) { - ps.setInt(1, t.getId()); - ps.executeUpdate(); - removed++; - } - } catch (Exception e) { - sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage()); - } + for (Ticket t : toArchive) { ps.setInt(1, t.getId()); ps.executeUpdate(); removed++; } + } catch (Exception e) { sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage()); } } else { - for (Ticket t : toArchive) { - dataConfig.set("tickets." + t.getId(), null); - removed++; - } - try { dataConfig.save(dataFile); } catch (Exception e) { - sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); - } + for (Ticket t : toArchive) { dataConfig.set("tickets." + t.getId(), null); removed++; } + try { dataConfig.save(dataFile); } catch (Exception e) { sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); } } return removed; } @@ -631,7 +891,7 @@ public class DatabaseManager { public TicketStats getTicketStats() { List all = getAllTickets(); - int open = 0, claimed = 0, forwarded = 0, closed = 0; + int open = 0, claimed = 0, forwarded = 0, closed = 0, thumbsUp = 0, thumbsDown = 0; java.util.Map byPlayer = new java.util.HashMap<>(); for (Ticket t : all) { switch (t.getStatus()) { @@ -640,47 +900,47 @@ public class DatabaseManager { case FORWARDED -> forwarded++; case CLOSED -> closed++; } + if ("THUMBS_UP".equals(t.getPlayerRating())) thumbsUp++; + if ("THUMBS_DOWN".equals(t.getPlayerRating())) thumbsDown++; byPlayer.merge(t.getCreatorName(), 1, Integer::sum); } - return new TicketStats(all.size(), open, closed, forwarded, byPlayer); + return new TicketStats(all.size(), open, closed, forwarded, thumbsUp, thumbsDown, byPlayer); } public static class TicketStats { - public final int total, open, closed, forwarded; + public final int total, open, closed, forwarded, thumbsUp, thumbsDown; public final java.util.Map byPlayer; - public TicketStats(int total, int open, int closed, int forwarded, java.util.Map byPlayer) { - this.total = total; this.open = open; this.closed = closed; - this.forwarded = forwarded; this.byPlayer = byPlayer; + public TicketStats(int total, int open, int closed, int forwarded, + int thumbsUp, int thumbsDown, java.util.Map byPlayer) { + this.total = total; + this.open = open; + this.closed = closed; + this.forwarded = forwarded; + this.thumbsUp = thumbsUp; + this.thumbsDown = thumbsDown; + this.byPlayer = byPlayer; } } - // ─────────────────────────── Export / Import ──────────────────────────── + // ─────────────────────────── Export / Import ─────────────────────────── public int exportTickets(File exportFile) { List tickets = getAllTickets(); JSONArray arr = new JSONArray(); for (Ticket t : tickets) arr.add(ticketToJson(t)); - try (FileWriter fw = new FileWriter(exportFile)) { - fw.write(arr.toJSONString()); - return tickets.size(); - } catch (IOException e) { - sendError("Fehler beim Export: " + e.getMessage()); - return 0; - } + try (FileWriter fw = new FileWriter(exportFile)) { fw.write(arr.toJSONString()); return tickets.size(); } + catch (IOException e) { sendError("Fehler beim Export: " + e.getMessage()); return 0; } } public int importTickets(File importFile) { int imported = 0; try (FileReader fr = new FileReader(importFile)) { - JSONParser parser = new JSONParser(); - JSONArray arr = (JSONArray) parser.parse(fr); + JSONArray arr = (JSONArray) new JSONParser().parse(fr); for (Object o : arr) { Ticket t = ticketFromJson((JSONObject) o); if (t != null && createTicket(t) != -1) imported++; } - } catch (Exception e) { - sendError("Fehler beim Import: " + e.getMessage()); - } + } catch (Exception e) { sendError("Fehler beim Import: " + e.getMessage()); } return imported; } @@ -692,16 +952,9 @@ public class DatabaseManager { try { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); - if (t != null) { - useMySQL = true; - int id = createTicket(t); - useMySQL = false; - if (id != -1) migrated++; - } + if (t != null) { useMySQL = true; int id = createTicket(t); useMySQL = false; if (id != -1) migrated++; } } - } catch (Exception e) { - plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); - } + } catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); } return migrated; } @@ -712,16 +965,9 @@ public class DatabaseManager { ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); while (rs.next()) { Ticket t = mapRow(rs); - if (t != null) { - useMySQL = false; - int id = createTicket(t); - useMySQL = true; - if (id != -1) migrated++; - } + if (t != null) { useMySQL = false; int id = createTicket(t); useMySQL = true; if (id != -1) migrated++; } } - } catch (Exception e) { - plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); - } + } catch (Exception e) { plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); } return migrated; } @@ -734,62 +980,58 @@ public class DatabaseManager { t.setCreatorName(rs.getString("creator_name")); t.setMessage(rs.getString("message")); t.setWorldName(rs.getString("world")); - t.setX(rs.getDouble("x")); - t.setY(rs.getDouble("y")); - t.setZ(rs.getDouble("z")); - t.setYaw(rs.getFloat("yaw")); - t.setPitch(rs.getFloat("pitch")); + t.setX(rs.getDouble("x")); t.setY(rs.getDouble("y")); t.setZ(rs.getDouble("z")); + t.setYaw(rs.getFloat("yaw")); t.setPitch(rs.getFloat("pitch")); t.setStatus(TicketStatus.valueOf(rs.getString("status"))); t.setCreatedAt(rs.getTimestamp("created_at")); t.setClaimedAt(rs.getTimestamp("claimed_at")); t.setClosedAt(rs.getTimestamp("closed_at")); - try { - String closeComment = rs.getString("close_comment"); - if (closeComment != null) t.setCloseComment(closeComment); - } catch (SQLException ignored) { } - - String claimerUUID = rs.getString("claimer_uuid"); - if (claimerUUID != null) { - t.setClaimerUUID(UUID.fromString(claimerUUID)); - t.setClaimerName(rs.getString("claimer_name")); - } - String fwdUUID = rs.getString("forwarded_to_uuid"); - if (fwdUUID != null) { - t.setForwardedToUUID(UUID.fromString(fwdUUID)); - t.setForwardedToName(rs.getString("forwarded_to_name")); - } - - // Mapping des Soft Delete Flags - t.setPlayerDeleted(rs.getBoolean("player_deleted")); + safeReadColumn(rs, "close_comment", v -> t.setCloseComment(v)); + safeReadColumn(rs, "claimer_uuid", v -> { t.setClaimerUUID(UUID.fromString(v)); }); + safeReadColumn(rs, "claimer_name", v -> t.setClaimerName(v)); + safeReadColumn(rs, "forwarded_to_uuid",v -> t.setForwardedToUUID(UUID.fromString(v))); + safeReadColumn(rs, "forwarded_to_name",v -> t.setForwardedToName(v)); + try { t.setPlayerDeleted(rs.getBoolean("player_deleted")); } catch (SQLException ignored) {} + try { t.setCategoryKey(rs.getString("category")); } catch (SQLException ignored) {} + try { t.setPriority(TicketPriority.fromString(rs.getString("priority"))); } catch (SQLException ignored) {} + try { t.setPlayerRating(rs.getString("player_rating")); } catch (SQLException ignored) {} + try { t.setClaimerNotified(rs.getBoolean("claimer_notified")); } catch (SQLException ignored) {} return t; } + @FunctionalInterface private interface StringConsumer { void accept(String s); } + private void safeReadColumn(ResultSet rs, String col, StringConsumer consumer) { + try { String v = rs.getString(col); if (v != null) consumer.accept(v); } catch (SQLException ignored) {} + } + // ─────────────────────────── JSON-Hilfsmethoden ───────────────────────── + @SuppressWarnings("unchecked") private JSONObject ticketToJson(Ticket t) { JSONObject obj = new JSONObject(); - obj.put("id", t.getId()); - obj.put("creatorUUID", t.getCreatorUUID().toString()); - obj.put("creatorName", t.getCreatorName()); - obj.put("message", t.getMessage()); - obj.put("world", t.getWorldName()); - obj.put("x", t.getX()); - obj.put("y", t.getY()); - obj.put("z", t.getZ()); - obj.put("yaw", t.getYaw()); - obj.put("pitch", t.getPitch()); - obj.put("status", t.getStatus().name()); - obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null); - obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null); - obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null); + obj.put("id", t.getId()); + obj.put("creatorUUID", t.getCreatorUUID().toString()); + obj.put("creatorName", t.getCreatorName()); + obj.put("message", t.getMessage()); + obj.put("world", t.getWorldName()); + obj.put("x", t.getX()); obj.put("y", t.getY()); obj.put("z", t.getZ()); + obj.put("yaw", t.getYaw()); obj.put("pitch", t.getPitch()); + obj.put("status", t.getStatus().name()); + obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null); + obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null); + obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null); if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString()); if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName()); if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString()); if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName()); if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment()); - obj.put("playerDeleted", t.isPlayerDeleted()); + obj.put("playerDeleted", t.isPlayerDeleted()); + obj.put("category", t.getCategoryKey()); + obj.put("priority", t.getPriority().name()); + if (t.getPlayerRating() != null) obj.put("playerRating", t.getPlayerRating()); + obj.put("claimerNotified", t.isClaimerNotified()); return obj; } @@ -801,21 +1043,23 @@ public class DatabaseManager { t.setCreatorName((String) obj.get("creatorName")); t.setMessage((String) obj.get("message")); t.setWorldName((String) obj.get("world")); - t.setX((Double) obj.get("x")); - t.setY((Double) obj.get("y")); - t.setZ((Double) obj.get("z")); + t.setX((Double) obj.get("x")); t.setY((Double) obj.get("y")); t.setZ((Double) obj.get("z")); t.setYaw(((Double) obj.get("yaw")).floatValue()); t.setPitch(((Double) obj.get("pitch")).floatValue()); t.setStatus(TicketStatus.valueOf((String) obj.get("status"))); - if (obj.get("createdAt") != null) t.setCreatedAt(new Timestamp((Long) obj.get("createdAt"))); - if (obj.get("claimedAt") != null) t.setClaimedAt(new Timestamp((Long) obj.get("claimedAt"))); - if (obj.get("closedAt") != null) t.setClosedAt(new Timestamp((Long) obj.get("closedAt"))); - if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String) obj.get("claimerUUID"))); - if (obj.get("claimerName") != null) t.setClaimerName((String) obj.get("claimerName")); - if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID"))); - if (obj.get("forwardedToName") != null) t.setForwardedToName((String) obj.get("forwardedToName")); - if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment")); - if (obj.containsKey("playerDeleted")) t.setPlayerDeleted((Boolean) obj.get("playerDeleted")); + if (obj.get("createdAt") != null) t.setCreatedAt(new Timestamp((Long) obj.get("createdAt"))); + if (obj.get("claimedAt") != null) t.setClaimedAt(new Timestamp((Long) obj.get("claimedAt"))); + if (obj.get("closedAt") != null) t.setClosedAt(new Timestamp((Long) obj.get("closedAt"))); + if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String) obj.get("claimerUUID"))); + if (obj.get("claimerName") != null) t.setClaimerName((String) obj.get("claimerName")); + if (obj.get("forwardedToUUID")!= null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID"))); + if (obj.get("forwardedToName")!= null) t.setForwardedToName((String) obj.get("forwardedToName")); + if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment")); + if (obj.containsKey("playerDeleted")) t.setPlayerDeleted((Boolean) obj.get("playerDeleted")); + if (obj.containsKey("category")) t.setCategoryKey((String) obj.get("category")); + if (obj.containsKey("priority")) t.setPriority(TicketPriority.fromString((String) obj.get("priority"))); + if (obj.containsKey("playerRating")) t.setPlayerRating((String) obj.get("playerRating")); + if (obj.containsKey("claimerNotified"))t.setClaimerNotified((Boolean) obj.get("claimerNotified")); return t; } catch (Exception e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); @@ -830,21 +1074,10 @@ public class DatabaseManager { int invalid = 0; for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Object obj = dataConfig.get("tickets." + key); - if (!(obj instanceof Ticket t)) { - sendError("Ungültiges Ticket-Objekt bei ID: " + key); - invalid++; - continue; + if (!(obj instanceof Ticket t)) { sendError("Ungültiges Ticket-Objekt bei ID: " + key); invalid++; continue; } + if (t.getCreatorUUID() == null || t.getCreatorName() == null || t.getMessage() == null || t.getStatus() == null) { + sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key); invalid++; } - if (t.getCreatorUUID() == null || t.getCreatorName() == null - || t.getMessage() == null || t.getStatus() == null) { - sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key); - invalid++; - } - try { UUID.fromString(t.getCreatorUUID().toString()); } - catch (Exception e) { sendError("Ungültige UUID bei Ticket ID: " + key); invalid++; } - - try { TicketStatus.valueOf(t.getStatus().name()); } - catch (Exception e) { sendError("Ungültiger Status bei Ticket ID: " + key); invalid++; } } if (invalid > 0) { String msg = plugin != null @@ -854,13 +1087,14 @@ public class DatabaseManager { } } + // ─────────────────────────── Persistenz-Helper ───────────────────────── + + private void saveDataConfig() { + try { dataConfig.save(dataFile); } + catch (IOException e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Speichern von " + dataFileName + ": " + e.getMessage()); } + } + // ─────────────────────────── Backup (Platzhalter) ────────────────────── - - private void backupMySQL() { - // TODO: MySQL-Backup implementieren - } - - private void backupDataFile() { - // TODO: Datei-Backup implementieren - } + private void backupMySQL() {} + private void backupDataFile() {} } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/discord/DiscordWebhook.java b/src/main/java/de/ticketsystem/discord/DiscordWebhook.java index 53e8a4e..6586a35 100644 --- a/src/main/java/de/ticketsystem/discord/DiscordWebhook.java +++ b/src/main/java/de/ticketsystem/discord/DiscordWebhook.java @@ -1,6 +1,7 @@ package de.ticketsystem.discord; import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.ConfigCategory; import de.ticketsystem.model.Ticket; import org.bukkit.Bukkit; @@ -12,7 +13,39 @@ import java.time.Instant; /** * Sendet Benachrichtigungen an einen Discord-Webhook. - * Unterstützt Embeds mit Farbe, Feldern und Timestamp. + * Unterstützt Embeds mit Farbe, Feldern, Timestamp, Kategorie, Priorität und Rollen-Ping. + * + * Relevante config.yml-Felder: + * + * discord: + * enabled: true + * webhook-url: "https://discord.com/api/webhooks/..." + * role-ping-id: "" # Rollen-ID für @Ping, leer = kein Ping + * messages: + * new-ticket: + * title: "🎫 Neues Ticket erstellt" + * color: "3066993" + * footer: "TicketSystem" + * show-position: true + * show-category: true + * show-priority: true + * role-ping: true # Ping bei neuem Ticket an/aus + * ticket-closed: + * enabled: false + * title: "🔒 Ticket geschlossen" + * color: "15158332" + * footer: "TicketSystem" + * show-category: true + * show-priority: true + * role-ping: false + * ticket-forwarded: + * enabled: false + * title: "🔀 Ticket weitergeleitet" + * color: "15105570" + * footer: "TicketSystem" + * show-category: true + * show-priority: true + * role-ping: false */ public class DiscordWebhook { @@ -30,32 +63,37 @@ public class DiscordWebhook { public void sendNewTicket(Ticket ticket) { if (!isEnabled()) return; - String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); - if (webhookUrl.isEmpty()) return; + String webhookUrl = getWebhookUrl(); + if (webhookUrl == null) return; - // Felder aus Config lesen - String title = plugin.getConfig().getString("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt"); - String color = plugin.getConfig().getString("discord.messages.new-ticket.color", "3066993"); // Grün - String footer = plugin.getConfig().getString("discord.messages.new-ticket.footer", "TicketSystem"); + String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt"); + String color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "3066993"); + String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem"); boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true); + boolean showCat = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-category", true); + boolean showPri = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-priority", true); + boolean ping = plugin.getConfig().getBoolean("discord.messages.new-ticket.role-ping", true); - // JSON-Embed aufbauen StringBuilder fields = new StringBuilder(); - fields.append(field("Spieler", ticket.getCreatorName(), true)); - fields.append(","); - fields.append(field("Ticket ID", "#" + ticket.getId(), true)); - fields.append(","); - fields.append(field("Anliegen", ticket.getMessage(), false)); + fields.append(field("Spieler", ticket.getCreatorName(), true)); + fields.append(",").append(field("Ticket ID", "#" + ticket.getId(), true)); + fields.append(",").append(field("Anliegen", ticket.getMessage(), false)); + if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + fields.append(",").append(field("Kategorie", cat.getName(), true)); + } + if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) { + fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true)); + } if (showPos) { - fields.append(","); - fields.append(field("Welt", ticket.getWorldName(), true)); - fields.append(","); - fields.append(field("Position", + fields.append(",").append(field("Welt", ticket.getWorldName(), true)); + fields.append(",").append(field("Position", String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true)); } - String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); + String content = ping ? buildRolePing() : ""; + String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer); sendAsync(webhookUrl, json); } @@ -66,25 +104,34 @@ public class DiscordWebhook { if (!isEnabled()) return; if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return; - String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); - if (webhookUrl.isEmpty()) return; + String webhookUrl = getWebhookUrl(); + if (webhookUrl == null) return; - String title = plugin.getConfig().getString("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen"); - String color = plugin.getConfig().getString("discord.messages.ticket-closed.color", "15158332"); // Rot - String footer = plugin.getConfig().getString("discord.messages.ticket-closed.footer", "TicketSystem"); + String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen"); + String color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15158332"); + String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem"); + boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-category", true); + boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-priority", true); + boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-closed.role-ping", false); StringBuilder fields = new StringBuilder(); - fields.append(field("Ticket ID", "#" + ticket.getId(), true)); - fields.append(","); - fields.append(field("Ersteller", ticket.getCreatorName(), true)); - fields.append(","); - fields.append(field("Geschlossen von", closerName, true)); + fields.append(field("Ticket ID", "#" + ticket.getId(), true)); + fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true)); + fields.append(",").append(field("Geschlossen von", closerName, true)); + + if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + fields.append(",").append(field("Kategorie", cat.getName(), true)); + } + if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) { + fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true)); + } if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { - fields.append(","); - fields.append(field("Kommentar", ticket.getCloseComment(), false)); + fields.append(",").append(field("Kommentar", ticket.getCloseComment(), false)); } - String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); + String content = ping ? buildRolePing() : ""; + String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer); sendAsync(webhookUrl, json); } @@ -95,23 +142,32 @@ public class DiscordWebhook { if (!isEnabled()) return; if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return; - String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); - if (webhookUrl.isEmpty()) return; + String webhookUrl = getWebhookUrl(); + if (webhookUrl == null) return; - String title = plugin.getConfig().getString("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet"); - String color = plugin.getConfig().getString("discord.messages.ticket-forwarded.color", "15105570"); // Orange - String footer = plugin.getConfig().getString("discord.messages.ticket-forwarded.footer", "TicketSystem"); + String title = plugin.getConfig().getString ("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet"); + String color = plugin.getConfig().getString ("discord.messages.ticket-forwarded.color", "15105570"); + String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem"); + boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-category", true); + boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-priority", true); + boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.role-ping", false); StringBuilder fields = new StringBuilder(); - fields.append(field("Ticket ID", "#" + ticket.getId(), true)); - fields.append(","); - fields.append(field("Ersteller", ticket.getCreatorName(), true)); - fields.append(","); - fields.append(field("Weitergeleitet von", fromName, true)); - fields.append(","); - fields.append(field("Weitergeleitet an", ticket.getForwardedToName(), true)); + fields.append(field("Ticket ID", "#" + ticket.getId(), true)); + fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true)); + fields.append(",").append(field("Weitergeleitet von", fromName, true)); + fields.append(",").append(field("Weitergeleitet an", ticket.getForwardedToName(), true)); - String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); + if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + fields.append(",").append(field("Kategorie", cat.getName(), true)); + } + if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) { + fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true)); + } + + String content = ping ? buildRolePing() : ""; + String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer); sendAsync(webhookUrl, json); } @@ -121,26 +177,47 @@ public class DiscordWebhook { return plugin.getConfig().getBoolean("discord.enabled", false); } + /** Gibt die Webhook-URL zurück oder null wenn nicht gesetzt. */ + private String getWebhookUrl() { + String url = plugin.getConfig().getString("discord.webhook-url", ""); + return url.isEmpty() ? null : url; + } + + /** + * Baut den @Rollen-Ping-String aus der konfigurierten Rollen-ID. + * Leer wenn keine ID gesetzt. + */ + private String buildRolePing() { + String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim(); + if (roleId.isEmpty()) return ""; + return "<@&" + roleId + ">"; + } + /** * Baut einen einzelnen Embed-Field als JSON-String. */ private String field(String name, String value, boolean inline) { - // Anführungszeichen und Backslashes im Wert escapen String safeValue = value != null ? value.replace("\\", "\\\\").replace("\"", "\\\"") : "–"; - String safeName = name.replace("\\", "\\\\").replace("\"", "\\\""); + String safeName = name.replace("\\", "\\\\").replace("\"", "\\\""); return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}", safeName, safeValue, inline); } /** * Baut den kompletten Webhook-Payload als JSON. + * content = optionaler Ping-Text außerhalb des Embeds. */ - private String buildPayload(String title, int color, String fieldsJson, String footer) { - String timestamp = Instant.now().toString(); // ISO-8601 + private String buildPayload(String content, String title, int color, String fieldsJson, String footer) { + String timestamp = Instant.now().toString(); + String safeTitle = title.replace("\"", "\\\""); + String safeFooter = footer.replace("\"", "\\\""); + String safeContent = content.replace("\"", "\\\""); + return String.format(""" { + "content": "%s", "embeds": [{ "title": "%s", "color": %d, @@ -149,10 +226,11 @@ public class DiscordWebhook { "timestamp": "%s" }] }""", - title.replace("\"", "\\\""), + safeContent, + safeTitle, color, fieldsJson, - footer.replace("\"", "\\\""), + safeFooter, timestamp); } @@ -180,7 +258,6 @@ public class DiscordWebhook { plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode); } - // 204 = No Content → Erfolg bei Discord if (responseCode != 200 && responseCode != 204) { plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode); } diff --git a/src/main/java/de/ticketsystem/gui/TicketGUI.java b/src/main/java/de/ticketsystem/gui/TicketGUI.java index e18344d..b421655 100644 --- a/src/main/java/de/ticketsystem/gui/TicketGUI.java +++ b/src/main/java/de/ticketsystem/gui/TicketGUI.java @@ -2,6 +2,10 @@ package de.ticketsystem.gui; import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.Ticket; +import de.ticketsystem.manager.CategoryManager; +import de.ticketsystem.model.ConfigCategory; +import de.ticketsystem.model.TicketComment; +import de.ticketsystem.model.TicketPriority; import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -16,127 +20,144 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; public class TicketGUI implements Listener { // ─────────────────────────── Titel-Konstanten ────────────────────────── - private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht - private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv"; // Admin: Geschlossene Tickets - private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; // Spieler: eigene Tickets - private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details"; // Admin: Detail-Ansicht - - /** Permission für den Zugriff auf das Archiv */ + private static final String GUI_TITLE = "§8§lTicket-Übersicht"; + private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv"; + private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; + private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details"; private static final String ARCHIVE_PERMISSION = "ticket.archive"; + /** Ticket-Slots pro Seite (Reihen 0–4, Slots 0–44) */ + private static final int PAGE_SIZE = 45; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); private final TicketPlugin plugin; - /** Admin-Übersicht: Slot → Ticket */ - private final Map> playerSlotMap = new HashMap<>(); + // ─────────────────────────── State-Maps ──────────────────────────────── + /** Admin-Übersicht: Slot → Ticket */ + private final Map> playerSlotMap = new HashMap<>(); /** Admin-Archiv: Slot → Ticket */ private final Map> playerClosedSlotMap = new HashMap<>(); - /** Spieler-GUI: Slot → Ticket */ - private final Map> playerOwnSlotMap = new HashMap<>(); + private final Map> playerOwnSlotMap = new HashMap<>(); + /** Detail-Ansicht: UUID → Ticket */ + private final Map detailTicketMap = new HashMap<>(); - /** Detail-Ansicht: Player-UUID → Ticket */ - private final Map detailTicketMap = new HashMap<>(); + /** Aktuelle Seite pro Spieler (Admin, Archiv, Spieler) */ + private final Map adminPage = new HashMap<>(); + private final Map archivePage= new HashMap<>(); + private final Map playerPage = new HashMap<>(); - /** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */ + /** Kategorie-Filter für die Admin-GUI: null = alle */ + private final Map categoryFilter = new HashMap<>(); + + /** Wartet auf Chat-Eingabe für Close-Kommentar */ private final Map awaitingComment = new HashMap<>(); - /** - * Merkt, welche Spieler die Detail-Ansicht aus dem Archiv heraus geöffnet haben, - * damit der Zurück-Button wieder ins Archiv führt (statt in die Hauptübersicht). - */ + /** Aus Archiv heraus in Detail gegangen */ private final Set viewingFromArchive = new HashSet<>(); - public TicketGUI(TicketPlugin plugin) { - this.plugin = plugin; - } + // ─────────────────────────── Konstruktor ─────────────────────────────── + + public TicketGUI(TicketPlugin plugin) { this.plugin = plugin; } // ═══════════════════════════════════════════════════════════════════════ - // ADMIN / SUPPORTER GUI (Feste 54 Slots mit Archiv-Button) + // ADMIN / SUPPORTER GUI (paginiert, mit Kategorie-Filter) // ═══════════════════════════════════════════════════════════════════════ - public void openGUI(Player player) { - // Lade nur offene/aktive Tickets - List tickets = plugin.getDatabaseManager().getTicketsByStatus( + public void openGUI(Player player) { openGUI(player, adminPage.getOrDefault(player.getUniqueId(), 0)); } + + public void openGUI(Player player, int page) { + adminPage.put(player.getUniqueId(), page); + + List all = plugin.getDatabaseManager().getTicketsByStatus( TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); - // Admin GUI hat immer 54 Slots (6 Reihen) für feste Buttons + // Kategorie-Filter anwenden + ConfigCategory filter = categoryFilter.getOrDefault(player.getUniqueId(), null); + if (filter != null && plugin.getConfig().getBoolean("categories-enabled", true)) { + all.removeIf(t -> !t.getCategoryKey().equals(filter.getKey())); + } + + // Priorität-Sortierung (URGENT → HIGH → NORMAL → LOW) + if (plugin.getConfig().getBoolean("priorities-enabled", true)) { + all.sort((a, b) -> b.getPriority().ordinal() - a.getPriority().ordinal()); + } + + int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + adminPage.put(player.getUniqueId(), page); + Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE); Map slotMap = new HashMap<>(); - // Tickets in die ersten 5 Reihen (0-44) füllen - for (int i = 0; i < tickets.size() && i < 45; i++) { - Ticket ticket = tickets.get(i); + int start = page * PAGE_SIZE; + for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) { + Ticket ticket = all.get(start + i); inv.setItem(i, buildAdminListItem(ticket)); slotMap.put(i, ticket); } - // Letzte Reihe (45-53) mit Navigations-Items füllen - // Archiv-Button nur anzeigen wenn der Spieler die Archiv-Permission hat - fillAdminNavigation(inv, false, player); - + fillAdminNavigation(inv, false, player, page, totalPages); playerSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } // ═══════════════════════════════════════════════════════════════════════ - // ADMIN ARCHIV GUI (Geschlossene Tickets) – nur mit ticket.archive + // ADMIN ARCHIV GUI // ═══════════════════════════════════════════════════════════════════════ - public void openClosedGUI(Player player) { - // ── Permission-Check ────────────────────────────────────────────── + public void openClosedGUI(Player player) { openClosedGUI(player, archivePage.getOrDefault(player.getUniqueId(), 0)); } + + public void openClosedGUI(Player player, int page) { if (!player.hasPermission(ARCHIVE_PERMISSION)) { player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); return; } + archivePage.put(player.getUniqueId(), page); - // Lade nur geschlossene Tickets List tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED); + int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + archivePage.put(player.getUniqueId(), page); Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE); Map slotMap = new HashMap<>(); - for (int i = 0; i < tickets.size() && i < 45; i++) { - Ticket ticket = tickets.get(i); + int start = page * PAGE_SIZE; + for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) { + Ticket ticket = tickets.get(start + i); inv.setItem(i, buildAdminListItem(ticket)); slotMap.put(i, ticket); } - // Navigation (Zurück-Button statt Archiv-Button) - fillAdminNavigation(inv, true, player); - + fillAdminNavigation(inv, true, player, page, totalPages); playerClosedSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } // ═══════════════════════════════════════════════════════════════════════ - // SPIELER-GUI (Filtert 'playerDeleted' Tickets) + // SPIELER-GUI (paginiert) // ═══════════════════════════════════════════════════════════════════════ - public void openPlayerGUI(Player player) { + public void openPlayerGUI(Player player) { openPlayerGUI(player, playerPage.getOrDefault(player.getUniqueId(), 0)); } + + public void openPlayerGUI(Player player, int page) { + playerPage.put(player.getUniqueId(), page); + List all = plugin.getDatabaseManager().getTicketsByStatus( TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED); List tickets = new ArrayList<>(); for (Ticket t : all) { - // Verstecke Tickets, die der Spieler als gelöscht markiert hat - if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) { - tickets.add(t); - } + if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) tickets.add(t); } if (tickets.isEmpty()) { @@ -144,17 +165,23 @@ public class TicketGUI implements Listener { return; } - int size = calcSize(tickets.size()); - Inventory inv = Bukkit.createInventory(null, size, PLAYER_GUI_TITLE); + int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + playerPage.put(player.getUniqueId(), page); + + Inventory inv = Bukkit.createInventory(null, 54, PLAYER_GUI_TITLE); Map slotMap = new HashMap<>(); - for (int i = 0; i < tickets.size() && i < 54; i++) { - Ticket ticket = tickets.get(i); + int start = page * PAGE_SIZE; + for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) { + Ticket ticket = tickets.get(start + i); inv.setItem(i, buildPlayerTicketItem(ticket)); slotMap.put(i, ticket); } - fillEmpty(inv); + // Nav-Leiste für Spieler-GUI (nur Prev/Next, kein Filter/Archiv) + fillPlayerNavigation(inv, page, totalPages); + playerOwnSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } @@ -170,51 +197,51 @@ public class TicketGUI implements Listener { inv.setItem(4, buildDetailInfoItem(ticket)); // Slot 10: Teleportieren - inv.setItem(10, buildActionItem( - Material.ENDER_PEARL, - "§b§lTeleportieren", + inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren", List.of("§7Teleportiert dich zur", "§7Position des Tickets."))); - // Slot 12: Claimen (nur wenn OPEN) / Permanent löschen (wenn CLOSED + ticket.archive) / Grau + // Slot 12: Claimen / Löschen / Grau if (ticket.getStatus() == TicketStatus.OPEN) { - inv.setItem(12, buildActionItem( - Material.LIME_WOOL, - "§a§lTicket annehmen", + inv.setItem(12, buildActionItem(Material.LIME_WOOL, "§a§lTicket annehmen", List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet."))); } else if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { - // ── NEU: Löschen-Button nur für Archiv-berechtigte Spieler ── - inv.setItem(12, buildActionItem( - Material.BARRIER, - "§4§lTicket permanent löschen", - List.of( - "§7Löscht dieses Ticket", - "§7unwiderruflich aus der Datenbank.", - "§8§m ", - "§c§lACHTUNG: §cNicht rückgängig zu machen!"))); + inv.setItem(12, buildActionItem(Material.BARRIER, "§4§lTicket permanent löschen", + List.of("§7Löscht dieses Ticket", "§7unwiderruflich aus der Datenbank.", + "§8§m ", "§c§lACHTUNG: §cNicht rückgängig zu machen!"))); } else { - inv.setItem(12, buildActionItem( - Material.GRAY_WOOL, - "§8Bereits angenommen", + inv.setItem(12, buildActionItem(Material.GRAY_WOOL, "§8Bereits angenommen", List.of("§7Dieses Ticket wurde bereits", "§7angenommen."))); } // Slot 14: Schließen if (ticket.getStatus() != TicketStatus.CLOSED) { - inv.setItem(14, buildActionItem( - Material.RED_WOOL, - "§c§lTicket schließen", + inv.setItem(14, buildActionItem(Material.RED_WOOL, "§c§lTicket schließen", List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe."))); } else { - inv.setItem(14, buildActionItem( - Material.GRAY_WOOL, - "§8Bereits geschlossen", + inv.setItem(14, buildActionItem(Material.GRAY_WOOL, "§8Bereits geschlossen", List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); } + // Slot 22: Kommentare anzeigen + inv.setItem(22, buildActionItem(Material.BOOK, "§e§lKommentare anzeigen", + List.of("§7Zeigt alle Nachrichten/Antworten", "§7zu diesem Ticket im Chat."))); + + // Slot 20: Priorität ändern (nur wenn priorities-enabled und nicht geschlossen) + if (plugin.getConfig().getBoolean("priorities-enabled", true) + && player.hasPermission("ticket.support") + && ticket.getStatus() != TicketStatus.CLOSED) { + TicketPriority cur = ticket.getPriority(); + List prioLore = new ArrayList<>(); + prioLore.add("§7Aktuell: " + cur.getColored()); + prioLore.add("§8Klicken zum Wechseln"); + prioLore.add("§8§m "); + for (TicketPriority p : TicketPriority.values()) + prioLore.add((p == cur ? "§a» " : "§7 ") + p.getColored()); + inv.setItem(20, buildActionItem(cur.getGuiMaterial(), "§6§lPriorität ändern", prioLore)); + } + // Slot 16: Zurück - inv.setItem(16, buildActionItem( - Material.ARROW, - "§7§lZurück", + inv.setItem(16, buildActionItem(Material.ARROW, "§7§lZurück", List.of("§7Zurück zur Ticket-Übersicht."))); fillEmpty(inv); @@ -231,96 +258,84 @@ public class TicketGUI implements Listener { if (!(event.getWhoClicked() instanceof Player player)) return; String title = event.getView().getTitle(); - if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_GUI_TITLE) && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return; + if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_GUI_TITLE) + && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return; event.setCancelled(true); int slot = event.getRawSlot(); if (slot < 0) return; - // ── Admin Haupt-Übersicht ────────────────────────────────────────────── + // ── Admin Haupt-Übersicht ─────────────────────────────────────────── if (title.equals(GUI_TITLE)) { - // Klick auf die Truhe (Archiv-Button) in Slot 49 - if (slot == 49) { - // ── Permission-Check beim Klick ── - if (!player.hasPermission(ARCHIVE_PERMISSION)) { - player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); - return; + handleAdminNavClick(player, slot, false); + if (slot < PAGE_SIZE) { + Map slotMap = playerSlotMap.get(player.getUniqueId()); + if (slotMap == null) return; + Ticket ticket = slotMap.get(slot); + if (ticket != null) { + viewingFromArchive.remove(player.getUniqueId()); + player.closeInventory(); + openTicketDetailAsync(player, ticket); } - openClosedGUI(player); - return; - } - - // Klick auf ein Ticket - Map slotMap = playerSlotMap.get(player.getUniqueId()); - if (slotMap == null) return; - Ticket ticket = slotMap.get(slot); - if (ticket != null) { - viewingFromArchive.remove(player.getUniqueId()); // Kommt aus Hauptübersicht - player.closeInventory(); - openTicketDetailAsync(player, ticket); } return; } - // ── Admin Archiv (Geschlossene Tickets) ───────────────────────────────── + // ── Admin Archiv ─────────────────────────────────────────────────── if (title.equals(CLOSED_GUI_TITLE)) { - // Klick auf den Zurück-Pfeil (Slot 49) - if (slot == 49) { - openGUI(player); - return; - } - - // Klick auf ein Ticket - Map slotMap = playerClosedSlotMap.get(player.getUniqueId()); - if (slotMap == null) return; - Ticket ticket = slotMap.get(slot); - if (ticket != null) { - viewingFromArchive.add(player.getUniqueId()); // Kommt aus Archiv - player.closeInventory(); - openTicketDetailAsync(player, ticket); + handleArchiveNavClick(player, slot); + if (slot < PAGE_SIZE) { + Map slotMap = playerClosedSlotMap.get(player.getUniqueId()); + if (slotMap == null) return; + Ticket ticket = slotMap.get(slot); + if (ticket != null) { + viewingFromArchive.add(player.getUniqueId()); + player.closeInventory(); + openTicketDetailAsync(player, ticket); + } } return; } - // ── Spieler-GUI ────────────────────────────────────────────────────── + // ── Spieler-GUI ──────────────────────────────────────────────────── if (title.equals(PLAYER_GUI_TITLE)) { - Map slotMap = playerOwnSlotMap.get(player.getUniqueId()); - if (slotMap == null) return; - Ticket ticket = slotMap.get(slot); - if (ticket == null) return; + // Navigationstasten + int curPage = playerPage.getOrDefault(player.getUniqueId(), 0); + if (slot == 45) { openPlayerGUI(player, curPage - 1); return; } + if (slot == 53) { openPlayerGUI(player, curPage + 1); return; } - player.closeInventory(); + if (slot < PAGE_SIZE) { + Map slotMap = playerOwnSlotMap.get(player.getUniqueId()); + if (slotMap == null) return; + Ticket ticket = slotMap.get(slot); + if (ticket == null) return; - // Nur löschen wenn OFFEN oder GESCHLOSSEN - if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) { - boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId()); - - Bukkit.getScheduler().runTask(plugin, () -> { - if (success) { - player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt.")); - openPlayerGUI(player); - } else { - player.sendMessage(plugin.color("&cFehler beim Entfernen des Tickets.")); - } - }); - } else { - // Ticket wird bearbeitet (Claimed oder Forwarded) -> Löschen verweigern - player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird.")); + player.closeInventory(); + if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) { + boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt.")); + openPlayerGUI(player); + } else { + player.sendMessage(plugin.color("&cFehler beim Entfernen des Tickets.")); + } + }); + } else { + player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird.")); + } } return; } - // ── Admin Detail-GUI ───────────────────────────────────────────────── + // ── Admin Detail-GUI ─────────────────────────────────────────────── if (title.equals(DETAIL_GUI_TITLE)) { Ticket ticket = detailTicketMap.get(player.getUniqueId()); if (ticket == null) return; - player.closeInventory(); - switch (slot) { case 10 -> handleDetailTeleport(player, ticket); case 12 -> { - // Wenn CLOSED + archive-Permission → permanent löschen, sonst claimen if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { handleDetailPermanentDelete(player, ticket); } else { @@ -328,28 +343,74 @@ public class TicketGUI implements Listener { } } case 14 -> handleDetailClose(player, ticket); + case 20 -> handleDetailCyclePriority(player, ticket); + case 22 -> handleShowComments(player, ticket); case 16 -> { - // Zurück zur richtigen GUI je nach Herkunft - if (viewingFromArchive.remove(player.getUniqueId())) { - openClosedGUI(player); - } else { - openGUI(player); - } + if (viewingFromArchive.remove(player.getUniqueId())) openClosedGUI(player); + else openGUI(player); } } } } - // ─────────────────────────── Detail-Aktionen & Helpers ────────────────── + // ─────────────────────────── Navigation-Handler ───────────────────────── + + /** + * Verarbeitet Klicks auf die Navigationsleiste der Admin-Übersicht (Slots 45–53). + */ + private void handleAdminNavClick(Player player, int slot, boolean isArchive) { + int curPage = adminPage.getOrDefault(player.getUniqueId(), 0); + switch (slot) { + case 45 -> openGUI(player, curPage - 1); // Zurück + case 53 -> openGUI(player, curPage + 1); // Vor + case 49 -> { // Archiv-Button oder Zurück im Archiv + if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player); + else player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); + } + case 47 -> { // Kategorie-Filter (wenn aktiviert) + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + cycleCategoryFilter(player); + openGUI(player, 0); + } + } + } + } + + private void handleArchiveNavClick(Player player, int slot) { + int curPage = archivePage.getOrDefault(player.getUniqueId(), 0); + switch (slot) { + case 45 -> openClosedGUI(player, curPage - 1); + case 53 -> openClosedGUI(player, curPage + 1); + case 49 -> openGUI(player); // Zurück zur Hauptübersicht + } + } + + /** Wechselt zum nächsten Kategorie-Filter */ + private void cycleCategoryFilter(Player player) { + CategoryManager cm = plugin.getCategoryManager(); + List all = cm.getAll(); + ConfigCategory current = categoryFilter.getOrDefault(player.getUniqueId(), null); + + if (current == null) { + if (!all.isEmpty()) categoryFilter.put(player.getUniqueId(), all.get(0)); + } else { + int idx = all.indexOf(current); + int next = idx + 1; + if (next >= all.size()) categoryFilter.remove(player.getUniqueId()); // Zurück zu Alle + else categoryFilter.put(player.getUniqueId(), all.get(next)); + } + ConfigCategory newFilter = categoryFilter.getOrDefault(player.getUniqueId(), null); + String filterName = newFilter != null ? newFilter.getColored() : "§7Alle"; + player.sendMessage(plugin.color("&7Filter: " + filterName)); + } + + // ─────────────────────────── Detail-Aktionen ──────────────────────────── private void openTicketDetailAsync(Player player, Ticket currentTicket) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Ticket fresh = plugin.getDatabaseManager().getTicketById(currentTicket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { - if (fresh == null) { - player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); - return; - } + if (fresh == null) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } openDetailGUI(player, fresh); }); }); @@ -372,34 +433,22 @@ public class TicketGUI implements Listener { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { - if (success) { - player.sendMessage(plugin.formatMessage("messages.ticket-claimed") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{player}", ticket.getCreatorName())); - - ticket.setClaimerUUID(player.getUniqueId()); - ticket.setClaimerName(player.getName()); - - plugin.getTicketManager().notifyCreatorClaimed(ticket); - if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); - - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); - Bukkit.getScheduler().runTask(plugin, () -> { - if (fresh != null) openDetailGUI(player, fresh); - }); - }); - } else { - player.sendMessage(plugin.formatMessage("messages.already-claimed")); - } + if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } + player.sendMessage(plugin.formatMessage("messages.ticket-claimed") + .replace("{id}", String.valueOf(ticket.getId())) + .replace("{player}", ticket.getCreatorName())); + ticket.setClaimerUUID(player.getUniqueId()); + ticket.setClaimerName(player.getName()); + plugin.getTicketManager().notifyCreatorClaimed(ticket); + if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); + Bukkit.getScheduler().runTask(plugin, () -> { if (fresh != null) openDetailGUI(player, fresh); }); + }); }); }); } - /** - * Löscht ein geschlossenes Ticket permanent aus der Datenbank. - * Nur für Spieler mit der Permission ticket.archive. - */ private void handleDetailPermanentDelete(Player player, Ticket ticket) { if (!player.hasPermission(ARCHIVE_PERMISSION)) { player.sendMessage(plugin.color("&cDu hast keine Berechtigung, Tickets permanent zu löschen.")); @@ -409,13 +458,11 @@ public class TicketGUI implements Listener { player.sendMessage(plugin.color("&cNur geschlossene Tickets können permanent gelöscht werden.")); return; } - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().deleteTicket(ticket.getId()); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { - player.sendMessage(plugin.color( - "&aTicket &e#" + ticket.getId() + " &awurde permanent aus der Datenbank gelöscht.")); + player.sendMessage(plugin.color("&aTicket &e#" + ticket.getId() + " &awurde permanent gelöscht.")); viewingFromArchive.remove(player.getUniqueId()); openClosedGUI(player); } else { @@ -439,6 +486,57 @@ public class TicketGUI implements Listener { player.sendMessage(plugin.color("&8&m ")); } + private void handleDetailCyclePriority(Player player, Ticket ticket) { + if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.color("&cDu hast keine Berechtigung, die Priorität zu ändern.")); + return; + } + if (!plugin.getConfig().getBoolean("priorities-enabled", true)) return; + if (ticket.getStatus() == TicketStatus.CLOSED) { + player.sendMessage(plugin.color("&cDie Priorität geschlossener Tickets kann nicht geändert werden.")); + return; + } + TicketPriority[] values = TicketPriority.values(); + TicketPriority next = values[(ticket.getPriority().ordinal() + 1) % values.length]; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().setTicketPriority(ticket.getId(), next); + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + ticket.setPriority(next); + player.sendMessage(plugin.color("&aPriorität auf " + next.getColored() + " &agesetzt.")); + openDetailGUI(player, ticket); + } else { + player.sendMessage(plugin.color("&cFehler beim Ändern der Priorität.")); + openDetailGUI(player, ticket); + } + }); + }); + } + + + private void handleShowComments(Player player, Ticket ticket) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + List comments = plugin.getDatabaseManager().getComments(ticket.getId()); + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Kommentare zu Ticket #" + ticket.getId())); + if (comments.isEmpty()) { + player.sendMessage(plugin.color("&7Noch keine Kommentare vorhanden.")); + } else { + for (TicketComment c : comments) { + String time = DATE_FORMAT.format(c.getCreatedAt()); + player.sendMessage(plugin.color("&e" + c.getAuthorName() + " &7(" + time + ")&8: &f" + c.getMessage())); + } + } + player.sendMessage(plugin.color("&8&m ")); + // Gleich wieder Detail-GUI öffnen + openDetailGUI(player, ticket); + }); + }); + } + + // ─────────────────────────── Chat-Events ──────────────────────────────── + @EventHandler(priority = EventPriority.LOWEST) public void onPlayerChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); @@ -470,72 +568,109 @@ public class TicketGUI implements Listener { }); } - // ─────────────────────────── Item-Builder & Füll-Methoden ───────────── + // ─────────────────────────── Item-Builder ────────────────────────────── /** - * Füllt die Navigationsleiste (letzte Reihe) der Admin-GUIs. - * Der Archiv-Button (Truhe) wird nur angezeigt, wenn der Spieler ticket.archive besitzt. + * Füllt die Navigationsleiste (letzte Reihe, Slots 45–53). + * Layout: [45]=Zurück | [47]=Filter | [49]=Archiv/Hauptmenü | [51]=leer | [53]=Weiter */ - private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player) { - ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.setDisplayName(" "); - glass.setItemMeta(meta); + private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, int page, int totalPages) { + ItemStack glass = makeGlass(); + for (int i = 45; i < 54; i++) inv.setItem(i, glass); + + // Zurück (Slot 45) + if (page > 0) { + inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück", + List.of("§7Seite " + page + " von " + totalPages))); } - // Letzte Reihe (45-53) füllen - for (int i = 45; i < 54; i++) { - if (i != 49) inv.setItem(i, glass); + // Weiter (Slot 53) + if (page < totalPages - 1) { + inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►", + List.of("§7Seite " + (page + 2) + " von " + totalPages))); } - if (isArchiveView) { - // Im Archiv: Zurück-Pfeil in Slot 49 - inv.setItem(49, buildActionItem( - Material.ARROW, - "§7§lZurück zur Übersicht", - List.of("§7Zeigt alle offenen Tickets."))); - } else { - // In der Übersicht: Archiv-Truhe nur mit Permission + // Seitenanzeige (Slot 49) + if (!isArchiveView) { if (player.hasPermission(ARCHIVE_PERMISSION)) { - inv.setItem(49, buildActionItem( - Material.CHEST, - "§7§lGeschlossene Tickets", + inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets", List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an."))); - } else { - // Kein Archiv-Zugriff → Slot 49 bleibt Glas (kein Button) - inv.setItem(49, glass); } + // Kategorie-Filter (Slot 47), nur wenn aktiviert + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null); + String filterLabel = currentFilter != null ? currentFilter.getColored() : "§7Alle"; + List filterLore = new ArrayList<>(); + filterLore.add("§7Aktuell: " + filterLabel); + filterLore.add("§8Klicken zum Wechseln"); + filterLore.add("§8§m "); + for (ConfigCategory cat : plugin.getCategoryManager().getAll()) { + filterLore.add((cat.equals(currentFilter) ? "§a» " : "§7 ") + cat.getColored()); + } + filterLore.add((currentFilter == null ? "§a» " : "§7 ") + "§7Alle (kein Filter)"); + inv.setItem(47, buildActionItem(Material.HOPPER, "§e§lKategorie-Filter", filterLore)); + } + } else { + // Im Archiv: Zurück-Button in Slot 49 + inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht", + List.of("§7Zeigt alle offenen Tickets."))); } + + // Seitenanzeige Mitte oben (Slot 48) + inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, + List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId()) + ? playerSlotMap.get(player.getUniqueId()).size() + "+" : "?") + " Tickets auf dieser Seite"))); } + private void fillPlayerNavigation(Inventory inv, int page, int totalPages) { + ItemStack glass = makeGlass(); + for (int i = 45; i < 54; i++) inv.setItem(i, glass); + if (page > 0) inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück", List.of("§7Seite " + page + " von " + totalPages))); + if (page < totalPages - 1) inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►", List.of("§7Seite " + (page + 2) + " von " + totalPages))); + inv.setItem(49, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, List.of())); + } + + // ─────────────────────────── Item-Builder ────────────────────────────── + private ItemStack buildAdminListItem(Ticket ticket) { - Material mat = switch (ticket.getStatus()) { - case OPEN -> Material.PAPER; - case CLAIMED -> Material.YELLOW_DYE; - case FORWARDED -> Material.ORANGE_DYE; - case CLOSED -> Material.GRAY_DYE; - }; + // Material: Kategorie aus Config (z.B. REDSTONE für Bug, BOOK für Frage) + // Fallback auf Status-Material wenn categories-enabled: false + Material mat; + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + mat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()).getMaterial(); + } else { + mat = switch (ticket.getStatus()) { + case OPEN -> Material.PAPER; + case CLAIMED -> Material.YELLOW_DYE; + case FORWARDED -> Material.ORANGE_DYE; + case CLOSED -> Material.GRAY_DYE; + }; + } ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); if (meta == null) return item; - meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + // Priorität farblich im Titel anzeigen (wenn aktiviert) + String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true) + ? ticket.getPriority().getColored() + " §8| " : ""; + meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); List lore = new ArrayList<>(); lore.add("§8§m "); lore.add("§7Ersteller: §e" + ticket.getCreatorName()); lore.add("§7Anliegen: §f" + ticket.getMessage()); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); - if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add("§7Kategorie: " + _cat.getColored()); + } + if (plugin.getConfig().getBoolean("priorities-enabled", true)) + lore.add("§7Priorität: " + ticket.getPriority().getColored()); + if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) lore.add("§7Kommentar: §f" + ticket.getCloseComment()); - } - if (ticket.isPlayerDeleted()) { - lore.add("§cSpieler hat Ticket gelöscht."); - } + if (ticket.isPlayerDeleted()) lore.add("§cSpieler hat Ticket gelöscht."); lore.add("§8§m "); lore.add("§e§l» KLICKEN für Details"); - meta.setLore(lore); item.setItemMeta(meta); return item; @@ -548,7 +683,6 @@ public class TicketGUI implements Listener { case FORWARDED -> Material.ORANGE_DYE; case CLOSED -> Material.GRAY_DYE; }; - ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); if (meta == null) return item; @@ -561,17 +695,27 @@ public class TicketGUI implements Listener { lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Welt: §e" + ticket.getWorldName()); lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add("§7Kategorie: " + _cat.getColored()); + } + if (plugin.getConfig().getBoolean("priorities-enabled", true)) + lore.add("§7Priorität: " + ticket.getPriority().getColored()); if (ticket.getClaimerName() != null) { lore.add("§8§m "); lore.add("§7Angenommen von: §a" + ticket.getClaimerName()); - if (ticket.getClaimedAt() != null) - lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); + if (ticket.getClaimedAt() != null) lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); } if (ticket.getStatus() == TicketStatus.CLOSED) { - if (ticket.getClosedAt() != null) - lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt())); + if (ticket.getClosedAt() != null) lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt())); if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) lore.add("§7Kommentar: §f" + ticket.getCloseComment()); + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + String rating = ticket.getPlayerRating(); + String ratingStr = rating == null ? "§7Keine Bewertung" : + "THUMBS_UP".equals(rating) ? "§a👍 Positiv" : "§c👎 Negativ"; + lore.add("§7Bewertung: " + ratingStr); + } } lore.add("§8§m "); meta.setLore(lore); @@ -586,7 +730,6 @@ public class TicketGUI implements Listener { case FORWARDED -> Material.ORANGE_DYE; case CLOSED -> Material.GRAY_DYE; }; - ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); if (meta == null) return item; @@ -598,23 +741,26 @@ public class TicketGUI implements Listener { lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Welt: §e" + ticket.getWorldName()); lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); - if (ticket.getStatus() == TicketStatus.CLOSED - && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { - lore.add("§8§m "); - lore.add("§7Kommentar des Supports:"); - lore.add("§f" + ticket.getCloseComment()); + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + lore.add("§7Kategorie: " + _cat.getColored()); + } + if (ticket.getStatus() == TicketStatus.CLOSED) { + if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { + lore.add("§8§m "); + lore.add("§7Kommentar des Supports:"); + lore.add("§f" + ticket.getCloseComment()); + } + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + String rating = ticket.getPlayerRating(); + if (rating == null) lore.add("§e» /ticket rate " + ticket.getId() + " good/bad"); + else lore.add("§7Bewertet: " + ("THUMBS_UP".equals(rating) ? "§a👍" : "§c👎")); + } } lore.add("§8§m "); - switch (ticket.getStatus()) { - case OPEN, CLOSED -> { - lore.add("§c§l» KLICKEN zum Löschen"); - lore.add("§7Entferne dieses Ticket aus deiner Übersicht."); - } - default -> { - lore.add("§e» Ticket wird bearbeitet..."); - lore.add("§7Kann nicht mehr gelöscht werden."); - } + case OPEN, CLOSED -> { lore.add("§c§l» KLICKEN zum Löschen"); lore.add("§7Entferne dieses Ticket aus deiner Übersicht."); } + default -> { lore.add("§e» Ticket wird bearbeitet..."); lore.add("§7Kann nicht mehr gelöscht werden."); } } meta.setLore(lore); item.setItemMeta(meta); @@ -631,20 +777,15 @@ public class TicketGUI implements Listener { return item; } - private int calcSize(int ticketCount) { - int size = (int) Math.ceil(ticketCount / 9.0) * 9; - return Math.max(9, Math.min(54, size)); + private ItemStack makeGlass() { + ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = glass.getItemMeta(); + if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } + return glass; } private void fillEmpty(Inventory inv) { - ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.setDisplayName(" "); - glass.setItemMeta(meta); - } - for (int i = 0; i < inv.getSize(); i++) { - if (inv.getItem(i) == null) inv.setItem(i, glass); - } + ItemStack glass = makeGlass(); + for (int i = 0; i < inv.getSize(); i++) { if (inv.getItem(i) == null) inv.setItem(i, glass); } } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java index a820698..f422afc 100644 --- a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java +++ b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java @@ -34,13 +34,39 @@ public class PlayerJoinListener implements Listener { .replace("{count}", String.valueOf(count)); player.sendMessage(msg); player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); - }, 40L); // 2 Sekunden Verzögerung + }, 40L); } }); } + // ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ──── + // (Nachrichten die ankamen während der Spieler offline war) + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!player.isOnline()) return; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + List pending = plugin.getDatabaseManager().getPendingNotifications(player.getUniqueId()); + if (pending.isEmpty()) return; + Bukkit.getScheduler().runTask(plugin, () -> { + if (!player.isOnline()) return; + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Ticket-Benachrichtigungen &7(während du offline warst):")); + for (String msg : pending) { + player.sendMessage(plugin.color(msg)); + } + player.sendMessage(plugin.color("&8&m ")); + }); + plugin.getDatabaseManager().clearPendingNotifications(player.getUniqueId()); + }); + }, 60L); + + // ── [NEU] Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ── + // Läuft mit 60 Ticks Verzögerung (3 Sek) damit der Spieler zuerst normal spawnt + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!player.isOnline()) return; + plugin.getTicketManager().notifyClaimedWhileOffline(player); + }, 60L); + // ── Spieler: über geschlossene Tickets mit Kommentar informieren ── - // Nur wenn der Ersteller noch nicht live benachrichtigt wurde Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { List closed = plugin.getDatabaseManager() .getTicketsByStatus(TicketStatus.CLOSED); @@ -48,8 +74,6 @@ public class PlayerJoinListener implements Listener { for (Ticket t : closed) { if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) continue; - - // Nicht erneut senden, wenn bereits live benachrichtigt (In-Memory-Set) if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue; Bukkit.getScheduler().runTask(plugin, () -> @@ -73,7 +97,7 @@ public class PlayerJoinListener implements Listener { player.sendMessage(bar); } }); - }, 20L); // 1 Sekunde + }, 20L); } } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/manager/CategoryManager.java b/src/main/java/de/ticketsystem/manager/CategoryManager.java new file mode 100644 index 0000000..6e40ebc --- /dev/null +++ b/src/main/java/de/ticketsystem/manager/CategoryManager.java @@ -0,0 +1,177 @@ +package de.ticketsystem.manager; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.ConfigCategory; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Loads and manages ticket categories defined in config.yml under the "categories" section. + * + * Example config.yml layout: + * + * categories: + * general: + * name: "Allgemein" + * color: "&7" + * material: "PAPER" + * aliases: + * - "allgemein" + * - "general" + * bug: + * name: "Bug" + * color: "&c" + * material: "REDSTONE" + * aliases: + * - "bug" + * - "fehler" + */ +public class CategoryManager { + + private final TicketPlugin plugin; + + /** Ordered map: key → ConfigCategory */ + private final Map categories = new LinkedHashMap<>(); + + /** Alias → key mapping for fast resolve() lookups */ + private final Map aliasMap = new LinkedHashMap<>(); + + public CategoryManager(TicketPlugin plugin) { + this.plugin = plugin; + load(); + } + + // ─────────────────────────── Loading ─────────────────────────────────── + + private void load() { + categories.clear(); + aliasMap.clear(); + + ConfigurationSection section = plugin.getConfig().getConfigurationSection("categories"); + + if (section == null || section.getKeys(false).isEmpty()) { + // Fallback: create built-in defaults so the plugin always works + loadDefaults(); + return; + } + + for (String key : section.getKeys(false)) { + ConfigurationSection cat = section.getConfigurationSection(key); + if (cat == null) continue; + + String name = cat.getString("name", capitalize(key)); + String color = cat.getString("color", "&7"); + String matStr = cat.getString("material", "PAPER").toUpperCase(); + Material material; + try { + material = Material.valueOf(matStr); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("[CategoryManager] Unbekanntes Material '" + matStr + + "' für Kategorie '" + key + "'. Fallback: PAPER"); + material = Material.PAPER; + } + + ConfigCategory category = new ConfigCategory(key, name, color, material); + categories.put(key.toLowerCase(), category); + + // Register key itself as alias + aliasMap.put(key.toLowerCase(), key.toLowerCase()); + + // Register additional aliases + List aliases = cat.getStringList("aliases"); + for (String alias : aliases) { + aliasMap.put(alias.toLowerCase(), key.toLowerCase()); + } + } + + if (categories.isEmpty()) { + plugin.getLogger().warning("[CategoryManager] Keine gültigen Kategorien in der config.yml gefunden. Lade Standardkategorien."); + loadDefaults(); + } else { + plugin.getLogger().info("[CategoryManager] " + categories.size() + " Kategorie(n) geladen: " + String.join(", ", categories.keySet())); + } + } + + /** Built-in fallback categories — mirrors the old TicketCategory enum */ + private void loadDefaults() { + addDefault("general", "Allgemein", "&7", Material.PAPER, "allgemein", "general"); + addDefault("bug", "Bug", "&c", Material.REDSTONE, "bug", "fehler"); + addDefault("question", "Frage", "&e", Material.BOOK, "frage", "question"); + addDefault("complaint", "Beschwerde", "&6", Material.WRITABLE_BOOK, "beschwerde", "complaint"); + addDefault("other", "Sonstiges", "&8", Material.FEATHER, "sonstiges", "other"); + plugin.getLogger().info("[CategoryManager] Standard-Kategorien geladen (5)."); + } + + private void addDefault(String key, String name, String color, Material mat, String... aliases) { + ConfigCategory cat = new ConfigCategory(key, name, color, mat); + categories.put(key, cat); + aliasMap.put(key, key); + for (String alias : aliases) aliasMap.put(alias.toLowerCase(), key); + } + + // ─────────────────────────── Public API ──────────────────────────────── + + /** + * Returns all loaded categories in config order. + */ + public List getAll() { + return Collections.unmodifiableList(new ArrayList<>(categories.values())); + } + + /** + * Returns the first category (default), or a hard-coded fallback if empty. + */ + public ConfigCategory getDefault() { + if (categories.isEmpty()) return new ConfigCategory("general", "Allgemein", "&7", Material.PAPER); + return categories.values().iterator().next(); + } + + /** + * Looks up a category by exact key (case-insensitive). + * Returns null if not found. + */ + public ConfigCategory fromKey(String key) { + if (key == null) return getDefault(); + ConfigCategory cat = categories.get(key.toLowerCase()); + return cat != null ? cat : getDefault(); + } + + /** + * Resolves a user-supplied string (key or alias) to a ConfigCategory. + * Returns null if no match is found (so callers can show an error). + */ + public ConfigCategory resolve(String input) { + if (input == null) return null; + String key = aliasMap.get(input.toLowerCase()); + return key != null ? categories.get(key) : null; + } + + /** + * Returns a human-readable comma-separated list of all category keys, + * e.g. "general, bug, question, complaint, other" + */ + public String getAvailableNames() { + return String.join(", ", categories.keySet()); + } + + /** + * Reloads categories from the (already reloaded) config. + * Call this after plugin.reloadConfig(). + */ + public void reload() { + load(); + } + + // ─────────────────────────── Helpers ─────────────────────────────────── + + private static String capitalize(String s) { + if (s == null || s.isEmpty()) return s; + return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase(); + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/manager/TicketManager.java b/src/main/java/de/ticketsystem/manager/TicketManager.java index f8ddb9a..322712e 100644 --- a/src/main/java/de/ticketsystem/manager/TicketManager.java +++ b/src/main/java/de/ticketsystem/manager/TicketManager.java @@ -1,6 +1,7 @@ package de.ticketsystem.manager; import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.ConfigCategory; import de.ticketsystem.model.Ticket; import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; @@ -40,9 +41,7 @@ public class TicketManager { return Math.max(0, (cooldownMillis - elapsed) / 1000); } - public void setCooldown(UUID uuid) { - cooldowns.put(uuid, System.currentTimeMillis()); - } + public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); } // ─────────────────────────── Benachrichtigungen ──────────────────────── @@ -51,14 +50,25 @@ public class TicketManager { * und sendet optional eine Discord-Webhook-Nachricht. */ public void notifyTeam(Ticket ticket) { - // Sicherheitschecks für null-Werte String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; - String message = ticket.getMessage() != null ? ticket.getMessage() : ""; + String message = ticket.getMessage() != null ? ticket.getMessage() : ""; + + // Kategorie & Priorität optional anzeigen + String categoryInfo = ""; + String priorityInfo = ""; + if (plugin.getConfig().getBoolean("categories-enabled", true)) { + de.ticketsystem.model.ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()); + categoryInfo = " §7[§r" + cat.getColored() + "§7]"; + } + if (plugin.getConfig().getBoolean("priorities-enabled", true)) { + priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored(); + } String msg = plugin.formatMessage("messages.new-ticket-notify") - .replace("{player}", creatorName) + .replace("{player}", creatorName) .replace("{message}", message) - .replace("{id}", String.valueOf(ticket.getId())); + .replace("{id}", String.valueOf(ticket.getId())) + + categoryInfo + priorityInfo; for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { @@ -67,34 +77,67 @@ public class TicketManager { } } - // Discord-Webhook (asynchron) plugin.getDiscordWebhook().sendNewTicket(ticket); } /** * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. - * --- FIX PROBLEMK 1: NIE "UNBEKANNT" --- + * Setzt claimer_notified = true und persistiert es. */ public void notifyCreatorClaimed(Ticket ticket) { Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); if (creator != null && creator.isOnline()) { - - // 1. Versuch: Name aus dem Ticket-Objekt String claimerName = ticket.getClaimerName(); - - // 2. Versuch: Wenn Name fehlt, aber UUID vorhanden -> Namen über Bukkit holen - if (claimerName == null && ticket.getClaimerUUID() != null) { + if (claimerName == null && ticket.getClaimerUUID() != null) claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName(); - } - - // 3. Fallback: Falls immer noch kein Name da ist, nimm "Support" (nie "Unbekannt") if (claimerName == null) claimerName = "Support"; String msg = plugin.formatMessage("messages.ticket-claimed-notify") - .replace("{id}", String.valueOf(ticket.getId())) + .replace("{id}", String.valueOf(ticket.getId())) .replace("{claimer}", claimerName); creator.sendMessage(msg); } + // Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist + plugin.getDatabaseManager().markClaimerNotified(ticket.getId()); + } + + /** + * Wird beim Server-Join aufgerufen – informiert den Spieler über Tickets, + * die geclaimt oder weitergeleitet wurden während er offline war. + */ + public void notifyClaimedWhileOffline(Player player) { + // Suche alle Tickets dieses Spielers, die CLAIMED/FORWARDED sind, + // aber noch nicht notified wurden + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + var tickets = plugin.getDatabaseManager().getTicketsByStatus( + TicketStatus.CLAIMED, TicketStatus.FORWARDED); + + for (Ticket t : tickets) { + if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; + if (t.isClaimerNotified()) continue; // wurde schon informiert + + String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support"; + final String name = claimerName; + + Bukkit.getScheduler().runTask(plugin, () -> { + if (!player.isOnline()) return; + if (t.getStatus() == TicketStatus.CLAIMED) { + String msg = plugin.formatMessage("messages.ticket-claimed-notify") + .replace("{id}", String.valueOf(t.getId())) + .replace("{claimer}", name); + player.sendMessage(msg); + } else { + String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter"; + String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") + .replace("{id}", String.valueOf(t.getId())) + .replace("{supporter}", forwardedTo); + player.sendMessage(msg); + } + }); + + plugin.getDatabaseManager().markClaimerNotified(t.getId()); + } + }); } /** @@ -105,68 +148,69 @@ public class TicketManager { if (creator != null && creator.isOnline()) { String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter"; String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify") - .replace("{id}", String.valueOf(ticket.getId())) + .replace("{id}", String.valueOf(ticket.getId())) .replace("{supporter}", forwardedTo); creator.sendMessage(msg); } + // Auch hier notified setzen + plugin.getDatabaseManager().markClaimerNotified(ticket.getId()); } /** - * Sendet dem weitergeleiteten Supporter eine Benachrichtigung - * und informiert optional Discord. + * Sendet dem weitergeleiteten Supporter eine Benachrichtigung. */ public void notifyForwardedTo(Ticket ticket, String fromName) { Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); if (target != null && target.isOnline()) { String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; - String msg = plugin.formatMessage("messages.ticket-forwarded-notify") .replace("{player}", creatorName) - .replace("{id}", String.valueOf(ticket.getId())); + .replace("{id}", String.valueOf(ticket.getId())); target.sendMessage(msg); } - - // Discord plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName); } - /** - * Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde, - * und informiert optional Discord. - */ - public void notifyCreatorClosed(Ticket ticket) { - notifyCreatorClosed(ticket, null); - } - /** * Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde. */ + public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); } + public void notifyCreatorClosed(Ticket ticket, String closerName) { notifiedClosedTickets.add(ticket.getId()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); - if (creator != null && creator.isOnline()) { - String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) - ? ticket.getCloseComment() : ""; + String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) + ? ticket.getCloseComment() : ""; + if (creator != null && creator.isOnline()) { String msg = plugin.formatMessage("messages.ticket-closed-notify") - .replace("{id}", String.valueOf(ticket.getId())) + .replace("{id}", String.valueOf(ticket.getId())) .replace("{comment}", comment); creator.sendMessage(msg); - - if (!comment.isEmpty()) { + if (!comment.isEmpty()) creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment)); + if (plugin.getConfig().getBoolean("rating-enabled", true)) { + creator.sendMessage(plugin.color("&8&m ")); + creator.sendMessage(plugin.color("&6Wie zufrieden bist du mit dem Support?")); + creator.sendMessage(plugin.color("&a/ticket rate " + ticket.getId() + " good &7– 👍 Gut")); + creator.sendMessage(plugin.color("&c/ticket rate " + ticket.getId() + " bad &7– 👎 Schlecht")); + creator.sendMessage(plugin.color("&8&m ")); } + } else { + // Offline → ausstehende Benachrichtigung speichern + String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen." + + (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment) + + (plugin.getConfig().getBoolean("rating-enabled", true) + ? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : ""); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> + plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg)); } - // Discord String closer = closerName != null ? closerName : "Unbekannt"; plugin.getDiscordWebhook().sendTicketClosed(ticket, closer); } - /** - * Prüft ob der Ersteller für dieses Ticket bereits über die Schließung informiert wurde. - */ public boolean wasClosedNotificationSent(int ticketId) { return notifiedClosedTickets.contains(ticketId); } @@ -183,15 +227,22 @@ public class TicketManager { player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&6TicketSystem &7– Befehle")); player.sendMessage(plugin.color("&8&m ")); - player.sendMessage(plugin.color("&e/ticket create &7– Neues Ticket erstellen")); + player.sendMessage(plugin.color("&e/ticket create [Kategorie] &7– Neues Ticket erstellen")); player.sendMessage(plugin.color("&e/ticket list &7– Deine Tickets ansehen (GUI)")); + player.sendMessage(plugin.color("&e/ticket comment &7– Nachricht zu einem Ticket")); + + if (plugin.getConfig().getBoolean("rating-enabled", true)) + player.sendMessage(plugin.color("&e/ticket rate &7– Support bewerten")); + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { player.sendMessage(plugin.color("&e/ticket claim &7– Ticket annehmen")); player.sendMessage(plugin.color("&e/ticket close [Kommentar] &7– Ticket schließen")); } if (player.hasPermission("ticket.admin")) { player.sendMessage(plugin.color("&e/ticket forward &7– Ticket weiterleiten")); + player.sendMessage(plugin.color("&e/ticket blacklist [Spieler] [Grund] &7– Blacklist verwalten")); player.sendMessage(plugin.color("&e/ticket reload &7– Konfiguration neu laden")); + player.sendMessage(plugin.color("&e/ticket stats &7– Statistiken anzeigen")); } player.sendMessage(plugin.color("&8&m ")); } diff --git a/src/main/java/de/ticketsystem/model/ConfigCategory.java b/src/main/java/de/ticketsystem/model/ConfigCategory.java new file mode 100644 index 0000000..7d320e9 --- /dev/null +++ b/src/main/java/de/ticketsystem/model/ConfigCategory.java @@ -0,0 +1,53 @@ +package de.ticketsystem.model; + +import org.bukkit.Material; + +/** + * Eine aus der config.yml geladene Ticket-Kategorie. + * Ersetzt das hardcodierte TicketCategory-Enum vollständig. + * + * Konfigurationsbeispiel (config.yml): + * + * categories: + * bug: + * name: "Bug" + * color: "&c" + * material: "REDSTONE" + * aliases: + * - "bug" + * - "fehler" + */ +public class ConfigCategory { + + /** Interner Schlüssel aus der Config (z.B. "bug", "general") – immer Kleinbuchstaben */ + private final String key; + + /** Anzeigename (z.B. "Bug", "Allgemein") */ + private final String name; + + /** Minecraft-Farbcode (z.B. "&c") */ + private final String color; + + /** GUI-Item-Material */ + private final Material material; + + public ConfigCategory(String key, String name, String color, Material material) { + this.key = key.toLowerCase(); + this.name = name; + this.color = color; + this.material = material; + } + + public String getKey() { return key; } + public String getName() { return name; } + public String getColor() { return color; } + public Material getMaterial() { return material; } + + /** Gibt den farbigen Anzeigenamen zurück, z.B. "§cBug" */ + public String getColored() { + return org.bukkit.ChatColor.translateAlternateColorCodes('&', color + name); + } + + @Override + public String toString() { return key; } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/model/Ticket.java b/src/main/java/de/ticketsystem/model/Ticket.java index 792d717..ab6339f 100644 --- a/src/main/java/de/ticketsystem/model/Ticket.java +++ b/src/main/java/de/ticketsystem/model/Ticket.java @@ -3,7 +3,6 @@ package de.ticketsystem.model; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.SerializableAs; import org.bukkit.configuration.serialization.ConfigurationSerialization; @@ -17,215 +16,163 @@ import java.util.UUID; @SerializableAs("Ticket") public class Ticket implements ConfigurationSerializable { - private int id; - private UUID creatorUUID; + private int id; + private UUID creatorUUID; private String creatorName; private String message; private String worldName; private double x, y, z; - private float yaw, pitch; + private float yaw, pitch; private TicketStatus status; - private UUID claimerUUID; - private String claimerName; - private UUID forwardedToUUID; - private String forwardedToName; - private Timestamp createdAt; - private Timestamp claimedAt; - private Timestamp closedAt; - private String closeComment; + private UUID claimerUUID; + private String claimerName; + private UUID forwardedToUUID; + private String forwardedToName; + private Timestamp createdAt; + private Timestamp claimedAt; + private Timestamp closedAt; + private String closeComment; - // ─── NEU: Soft Delete Flag ─── private boolean playerDeleted = false; + /** Kategorie-Key aus config.yml, z.B. "bug", "general" */ + private String categoryKey = "general"; + + private TicketPriority priority = TicketPriority.NORMAL; + + /** null = nicht bewertet | "THUMBS_UP" | "THUMBS_DOWN" */ + private String playerRating = null; + private boolean claimerNotified = false; public Ticket() {} - public Ticket(UUID creatorUUID, String creatorName, String message, Location location) { this.creatorUUID = creatorUUID; this.creatorName = creatorName; - this.message = message; - this.worldName = location.getWorld().getName(); - this.x = location.getX(); - this.y = location.getY(); - this.z = location.getZ(); - this.yaw = location.getYaw(); - this.pitch = location.getPitch(); - this.status = TicketStatus.OPEN; + this.message = message; + this.worldName = location.getWorld().getName(); + this.x = location.getX(); this.y = location.getY(); this.z = location.getZ(); + this.yaw = location.getYaw(); this.pitch = location.getPitch(); + this.status = TicketStatus.OPEN; this.createdAt = new Timestamp(System.currentTimeMillis()); } - // --- NEU: Konstruktor zum Laden aus der YAML (Deserialisierung) --- public Ticket(Map map) { this.id = (int) map.get("id"); - - // UUIDs sicher aus String konvertieren - Object creatorObj = map.get("creatorUUID"); - this.creatorUUID = creatorObj instanceof UUID ? (UUID) creatorObj : UUID.fromString((String) creatorObj); - + Object cObj = map.get("creatorUUID"); + this.creatorUUID = cObj instanceof UUID ? (UUID) cObj : UUID.fromString((String) cObj); this.creatorName = (String) map.get("creatorName"); - this.message = (String) map.get("message"); - this.worldName = (String) map.get("world"); - - // Koordinaten sicher parsen - this.x = map.get("x") instanceof Double ? (Double) map.get("x") : ((Number) map.get("x")).doubleValue(); - this.y = map.get("y") instanceof Double ? (Double) map.get("y") : ((Number) map.get("y")).doubleValue(); - this.z = map.get("z") instanceof Double ? (Double) map.get("z") : ((Number) map.get("z")).doubleValue(); - - this.yaw = map.get("yaw") instanceof Float ? (Float) map.get("yaw") : ((Number) map.get("yaw")).floatValue(); - this.pitch = map.get("pitch") instanceof Float ? (Float) map.get("pitch") : ((Number) map.get("pitch")).floatValue(); - + this.message = (String) map.get("message"); + this.worldName = (String) map.get("world"); + this.x = toDouble(map.get("x")); this.y = toDouble(map.get("y")); this.z = toDouble(map.get("z")); + this.yaw = toFloat(map.get("yaw")); this.pitch = toFloat(map.get("pitch")); this.status = TicketStatus.valueOf((String) map.get("status")); - - // Timestamps aus Long (Millis) wieder zu Timestamp machen - if (map.get("createdAt") != null) { - this.createdAt = new Timestamp(((Number) map.get("createdAt")).longValue()); - } - if (map.get("claimedAt") != null) { - this.claimedAt = new Timestamp(((Number) map.get("claimedAt")).longValue()); - } - if (map.get("closedAt") != null) { - this.closedAt = new Timestamp(((Number) map.get("closedAt")).longValue()); - } - + if (map.get("createdAt") != null) this.createdAt = new Timestamp(toLong(map.get("createdAt"))); + if (map.get("claimedAt") != null) this.claimedAt = new Timestamp(toLong(map.get("claimedAt"))); + if (map.get("closedAt") != null) this.closedAt = new Timestamp(toLong(map.get("closedAt"))); this.closeComment = (String) map.get("closeComment"); - - // Optionale Felder if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) { - Object claimerObj = map.get("claimerUUID"); - this.claimerUUID = claimerObj instanceof UUID ? (UUID) claimerObj : UUID.fromString((String) claimerObj); + Object o = map.get("claimerUUID"); + this.claimerUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o); this.claimerName = (String) map.get("claimerName"); } - if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) { - Object fwdObj = map.get("forwardedToUUID"); - this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj); + Object o = map.get("forwardedToUUID"); + this.forwardedToUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o); this.forwardedToName = (String) map.get("forwardedToName"); } - - // ─── NEU: Laden des Soft Delete Flags ─── - if (map.containsKey("playerDeleted")) { - this.playerDeleted = (boolean) map.get("playerDeleted"); - } + if (map.containsKey("playerDeleted")) this.playerDeleted = (boolean) map.get("playerDeleted"); + if (map.containsKey("category")) this.categoryKey = (String) map.get("category"); + if (map.containsKey("priority")) this.priority = TicketPriority.fromString((String) map.get("priority")); + if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating"); + if (map.containsKey("claimerNotified")) this.claimerNotified = (boolean) map.get("claimerNotified"); } - // --- NEU: Methode zum Speichern in die YAML (Serialisierung) --- @Override public Map serialize() { Map map = new HashMap<>(); - map.put("id", id); - // WICHTIG: UUID als String speichern, um !!java.util.UUID Tag zu vermeiden map.put("creatorUUID", creatorUUID.toString()); map.put("creatorName", creatorName); - map.put("message", message); - map.put("world", worldName); - - map.put("x", x); - map.put("y", y); - map.put("z", z); - map.put("yaw", yaw); - map.put("pitch", pitch); - + map.put("message", message); + map.put("world", worldName); + map.put("x", x); map.put("y", y); map.put("z", z); + map.put("yaw", yaw); map.put("pitch", pitch); map.put("status", status.name()); - - // Timestamps als Long speichern if (createdAt != null) map.put("createdAt", createdAt.getTime()); if (claimedAt != null) map.put("claimedAt", claimedAt.getTime()); - if (closedAt != null) map.put("closedAt", closedAt.getTime()); - + if (closedAt != null) map.put("closedAt", closedAt.getTime()); if (closeComment != null) map.put("closeComment", closeComment); - - if (claimerUUID != null) { - map.put("claimerUUID", claimerUUID.toString()); - map.put("claimerName", claimerName); - } - - if (forwardedToUUID != null) { - map.put("forwardedToUUID", forwardedToUUID.toString()); - map.put("forwardedToName", forwardedToName); - } - - // ─── NEU: Speichern des Soft Delete Flags ─── - map.put("playerDeleted", playerDeleted); - + if (claimerUUID != null) { map.put("claimerUUID", claimerUUID.toString()); map.put("claimerName", claimerName); } + if (forwardedToUUID != null) { map.put("forwardedToUUID", forwardedToUUID.toString()); map.put("forwardedToName", forwardedToName); } + map.put("playerDeleted", playerDeleted); + map.put("category", categoryKey); + map.put("priority", priority.name()); + if (playerRating != null) map.put("playerRating", playerRating); + map.put("claimerNotified", claimerNotified); return map; } - // --- NEU: Registrierung --- - public static void register() { - ConfigurationSerialization.registerClass(Ticket.class, "Ticket"); - } + public static void register() { ConfigurationSerialization.registerClass(Ticket.class, "Ticket"); } - // --- Deine ursprüngliche getLocation Methode (beibehalten) --- public Location getLocation() { World world = Bukkit.getWorld(worldName); - if (world == null) return null; - return new Location(world, x, y, z, yaw, pitch); + return world == null ? null : new Location(world, x, y, z, yaw, pitch); } - // ─────────────────────────── Getter & Setter ──────────────────────────── + private static double toDouble(Object o) { return o instanceof Double d ? d : ((Number) o).doubleValue(); } + private static float toFloat(Object o) { return o instanceof Float f ? f : ((Number) o).floatValue(); } + private static long toLong(Object o) { return ((Number) o).longValue(); } - public int getId() { return id; } - public void setId(int id) { this.id = id; } - - public UUID getCreatorUUID() { return creatorUUID; } - public void setCreatorUUID(UUID creatorUUID) { this.creatorUUID = creatorUUID; } - - public String getCreatorName() { return creatorName; } - public void setCreatorName(String creatorName) { this.creatorName = creatorName; } - - public String getMessage() { return message; } - public void setMessage(String message) { this.message = message; } - - public String getWorldName() { return worldName; } - public void setWorldName(String worldName) { this.worldName = worldName; } - - public double getX() { return x; } - public void setX(double x) { this.x = x; } - - public double getY() { return y; } - public void setY(double y) { this.y = y; } - - public double getZ() { return z; } - public void setZ(double z) { this.z = z; } - - public float getYaw() { return yaw; } - public void setYaw(float yaw) { this.yaw = yaw; } - - public float getPitch() { return pitch; } - public void setPitch(float pitch) { this.pitch = pitch; } - - public TicketStatus getStatus() { return status; } - public void setStatus(TicketStatus status) { this.status = status; } - - public UUID getClaimerUUID() { return claimerUUID; } - public void setClaimerUUID(UUID claimerUUID) { this.claimerUUID = claimerUUID; } - - public String getClaimerName() { return claimerName; } - public void setClaimerName(String claimerName) { this.claimerName = claimerName; } - - public UUID getForwardedToUUID() { return forwardedToUUID; } - public void setForwardedToUUID(UUID forwardedToUUID) { this.forwardedToUUID = forwardedToUUID; } - - public String getForwardedToName() { return forwardedToName; } - public void setForwardedToName(String forwardedToName) { this.forwardedToName = forwardedToName; } - - public Timestamp getCreatedAt() { return createdAt; } - public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } - - public Timestamp getClaimedAt() { return claimedAt; } - public void setClaimedAt(Timestamp claimedAt) { this.claimedAt = claimedAt; } - - public Timestamp getClosedAt() { return closedAt; } - public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; } - - public String getCloseComment() { return closeComment; } - public void setCloseComment(String closeComment) { this.closeComment = closeComment; } - - // ─── NEU: Getter/Setter für Soft Delete ─── - public boolean isPlayerDeleted() { return playerDeleted; } - public void setPlayerDeleted(boolean playerDeleted) { this.playerDeleted = playerDeleted; } + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public UUID getCreatorUUID() { return creatorUUID; } + public void setCreatorUUID(UUID v) { this.creatorUUID = v; } + public String getCreatorName() { return creatorName; } + public void setCreatorName(String v) { this.creatorName = v; } + public String getMessage() { return message; } + public void setMessage(String v) { this.message = v; } + public String getWorldName() { return worldName; } + public void setWorldName(String v) { this.worldName = v; } + public double getX() { return x; } + public void setX(double v) { this.x = v; } + public double getY() { return y; } + public void setY(double v) { this.y = v; } + public double getZ() { return z; } + public void setZ(double v) { this.z = v; } + public float getYaw() { return yaw; } + public void setYaw(float v) { this.yaw = v; } + public float getPitch() { return pitch; } + public void setPitch(float v) { this.pitch = v; } + public TicketStatus getStatus() { return status; } + public void setStatus(TicketStatus v) { this.status = v; } + public UUID getClaimerUUID() { return claimerUUID; } + public void setClaimerUUID(UUID v) { this.claimerUUID = v; } + public String getClaimerName() { return claimerName; } + public void setClaimerName(String v) { this.claimerName = v; } + public UUID getForwardedToUUID() { return forwardedToUUID; } + public void setForwardedToUUID(UUID v) { this.forwardedToUUID = v; } + public String getForwardedToName() { return forwardedToName; } + public void setForwardedToName(String v) { this.forwardedToName = v; } + public Timestamp getCreatedAt() { return createdAt; } + public void setCreatedAt(Timestamp v) { this.createdAt = v; } + public Timestamp getClaimedAt() { return claimedAt; } + public void setClaimedAt(Timestamp v) { this.claimedAt = v; } + public Timestamp getClosedAt() { return closedAt; } + public void setClosedAt(Timestamp v) { this.closedAt = v; } + public String getCloseComment() { return closeComment; } + public void setCloseComment(String v) { this.closeComment = v; } + public boolean isPlayerDeleted() { return playerDeleted; } + public void setPlayerDeleted(boolean v) { this.playerDeleted = v; } + public String getCategoryKey() { return categoryKey; } + public void setCategoryKey(String v) { this.categoryKey = v != null ? v.toLowerCase() : "general"; } + public TicketPriority getPriority() { return priority; } + public void setPriority(TicketPriority v) { this.priority = v; } + public String getPlayerRating() { return playerRating; } + public void setPlayerRating(String v) { this.playerRating = v; } + public boolean hasRating() { return playerRating != null; } + public boolean isClaimerNotified() { return claimerNotified; } + public void setClaimerNotified(boolean v) { this.claimerNotified = v; } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/model/TicketCategory.java b/src/main/java/de/ticketsystem/model/TicketCategory.java new file mode 100644 index 0000000..1790dd5 --- /dev/null +++ b/src/main/java/de/ticketsystem/model/TicketCategory.java @@ -0,0 +1,33 @@ +package de.ticketsystem.model; + +import org.bukkit.Material; + +public enum TicketCategory { + GENERAL ("Allgemein", "§7", Material.PAPER), + BUG ("Bug", "§c", Material.REDSTONE), + QUESTION ("Frage", "§e", Material.BOOK), + COMPLAINT ("Beschwerde", "§6", Material.WRITABLE_BOOK), + OTHER ("Sonstiges", "§8", Material.FEATHER); + + private final String displayName; + private final String color; + private final Material guiMaterial; + + TicketCategory(String displayName, String color, Material guiMaterial) { + this.displayName = displayName; + this.color = color; + this.guiMaterial = guiMaterial; + } + + public String getDisplayName() { return displayName; } + public String getColor() { return color; } + public String getColored() { return color + displayName; } + public Material getGuiMaterial() { return guiMaterial; } + + /** Safely parse from stored string, fall back to GENERAL. */ + public static TicketCategory fromString(String s) { + if (s == null) return GENERAL; + try { return valueOf(s.toUpperCase()); } + catch (IllegalArgumentException e) { return GENERAL; } + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/model/TicketComment.java b/src/main/java/de/ticketsystem/model/TicketComment.java new file mode 100644 index 0000000..97d033d --- /dev/null +++ b/src/main/java/de/ticketsystem/model/TicketComment.java @@ -0,0 +1,47 @@ +package de.ticketsystem.model; + +import java.sql.Timestamp; +import java.util.UUID; + +/** + * Represents a player comment/reply on a ticket. + */ +public class TicketComment { + + private int id; + private int ticketId; + private UUID authorUUID; + private String authorName; + private String message; + private Timestamp createdAt; + + public TicketComment() {} + + public TicketComment(int ticketId, UUID authorUUID, String authorName, String message) { + this.ticketId = ticketId; + this.authorUUID = authorUUID; + this.authorName = authorName; + this.message = message; + this.createdAt = new Timestamp(System.currentTimeMillis()); + } + + // ─────────────── Getters / Setters ──────────────────────────────────── + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public int getTicketId() { return ticketId; } + public void setTicketId(int ticketId) { this.ticketId = ticketId; } + + public UUID getAuthorUUID() { return authorUUID; } + public void setAuthorUUID(UUID authorUUID) { this.authorUUID = authorUUID; } + + public String getAuthorName() { return authorName; } + public void setAuthorName(String authorName){ this.authorName = authorName; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public Timestamp getCreatedAt() { return createdAt; } + public void setCreatedAt(Timestamp ts) { this.createdAt = ts; } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/model/TicketPriority.java b/src/main/java/de/ticketsystem/model/TicketPriority.java new file mode 100644 index 0000000..33e2126 --- /dev/null +++ b/src/main/java/de/ticketsystem/model/TicketPriority.java @@ -0,0 +1,31 @@ +package de.ticketsystem.model; + +import org.bukkit.Material; + +public enum TicketPriority { + LOW ("Niedrig", "§a", Material.GREEN_WOOL), + NORMAL ("Normal", "§e", Material.YELLOW_WOOL), + HIGH ("Hoch", "§6", Material.ORANGE_WOOL), + URGENT ("Dringend","§c", Material.RED_WOOL); + + private final String displayName; + private final String color; + private final Material guiMaterial; + + TicketPriority(String displayName, String color, Material guiMaterial) { + this.displayName = displayName; + this.color = color; + this.guiMaterial = guiMaterial; + } + + public String getDisplayName() { return displayName; } + public String getColor() { return color; } + public String getColored() { return color + displayName; } + public Material getGuiMaterial() { return guiMaterial; } + + public static TicketPriority fromString(String s) { + if (s == null) return NORMAL; + try { return valueOf(s.toUpperCase()); } + catch (IllegalArgumentException e) { return NORMAL; } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 881444d..e6d3487 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -59,6 +59,80 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg # ---------------------------------------------------- auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) +# ---------------------------------------------------- +# OPTIONALE FEATURES +# ---------------------------------------------------- + +# Kategorie-System (true = aktiviert) +# Spieler können beim Erstellen eine Kategorie wählen: /ticket create [kategorie] [priorität] +categories-enabled: true + +# Prioritäten-System (true = aktiviert) +# Spieler können beim Erstellen eine Priorität wählen: /ticket create [kategorie] [priorität] +# Admins/Supporter können die Priorität nachträglich ändern: /ticket setpriority +priorities-enabled: true + +# Bewertungs-System (true = aktiviert) +# Spieler können nach dem Schließen den Support bewerten: /ticket rate good|bad +# Ergebnisse sind in /ticket stats sichtbar +rating-enabled: true + +# ---------------------------------------------------- +# KATEGORIEN (nur aktiv wenn categories-enabled: true) +# ---------------------------------------------------- +# Jede Kategorie hat: +# name: Anzeigename im Chat und in der GUI +# color: Farbcode mit & (Minecraft Farbcodes) +# material: Minecraft-Material für das GUI-Item (Großbuchstaben, z.B. PAPER, REDSTONE, BOOK) +# aliases: Alternative Eingaben beim /ticket create Befehl (Kleinbuchstaben!) +# +# Das erste eingetragene Item ist die Standard-Kategorie für Tickets ohne Angabe. +# Du kannst beliebig viele Kategorien hinzufügen oder entfernen. +# ---------------------------------------------------- +categories: + general: + name: "Allgemein" + color: "&7" + material: "PAPER" + aliases: + - "allgemein" + - "general" + - "default" + bug: + name: "Bug" + color: "&c" + material: "REDSTONE" + aliases: + - "bug" + - "fehler" + - "error" + question: + name: "Frage" + color: "&e" + material: "BOOK" + aliases: + - "frage" + - "question" + - "help" + - "hilfe" + complaint: + name: "Beschwerde" + color: "&6" + material: "WRITABLE_BOOK" + aliases: + - "beschwerde" + - "complaint" + - "report" + - "melden" + other: + name: "Sonstiges" + color: "&8" + material: "FEATHER" + aliases: + - "sonstiges" + - "other" + - "misc" + # ---------------------------------------------------- # DISCORD WEBHOOK (Optional) # ---------------------------------------------------- @@ -69,6 +143,41 @@ discord: # Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen) webhook-url: "" + # Rollen-Ping: Discord-Rollen-ID (Rechtsklick auf Rolle → ID kopieren) + # Leer lassen ("") = kein Ping + role-ping-id: "" + + messages: + # ── Neues Ticket ──────────────────────────────────────────────────────── + new-ticket: + title: "🎫 Neues Ticket erstellt" + color: "3066993" # Grün + footer: "TicketSystem" + show-position: true # Welt & Koordinaten im Embed anzeigen + show-category: true # Kategorie im Embed anzeigen + show-priority: true # Priorität im Embed anzeigen + role-ping: false # Rollen-Ping bei neuem Ticket senden + + # ── Ticket geschlossen ────────────────────────────────────────────────── + ticket-closed: + enabled: false # Webhook-Nachricht beim Schließen senden + title: "🔒 Ticket geschlossen" + color: "15158332" # Rot + footer: "TicketSystem" + show-category: true # Kategorie im Embed anzeigen + show-priority: true # Priorität im Embed anzeigen + role-ping: false # Rollen-Ping beim Schließen senden + + # ── Ticket weitergeleitet ─────────────────────────────────────────────── + ticket-forwarded: + enabled: false # Webhook-Nachricht beim Weiterleiten senden + title: "🔀 Ticket weitergeleitet" + color: "15105570" # Orange + footer: "TicketSystem" + show-category: true # Kategorie im Embed anzeigen + show-priority: true # Priorität im Embed anzeigen + role-ping: false # Rollen-Ping beim Weiterleiten senden + # ---------------------------------------------------- # SYSTEM-NACHRICHTEN (mit &-Farbcodes) # ---------------------------------------------------- @@ -94,12 +203,35 @@ messages: ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})" - # --- NEU: Benachrichtigungen für den Ticket-Ersteller --- - # Wird gesendet, wenn das eigene Ticket geschlossen wurde + # --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER --- ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen." - # Wird gesendet, wenn das eigene Ticket an einen anderen Supporter weitergeleitet wurde ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet." + # --- KATEGORIEN --- + # {category} wird durch den Anzeigenamen der gewählten Kategorie ersetzt + ticket-created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}" + category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}" + + # --- KOMMENTARE --- + comment-saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert." + comment-notify: "&e[Ticket #{id}] &f{author} &7kommentiert: &f{message}" + comment-no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren." + + # --- BEWERTUNGEN --- + rating-saved-good: "&aDanke für deine Bewertung! &a👍 Positiv" + rating-saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ" + rating-already-rated: "&cDu hast dieses Ticket bereits bewertet." + rating-not-yours: "&cDu kannst nur deine eigenen Tickets bewerten." + rating-disabled: "&cBewertungen sind aktuell deaktiviert." + rating-prompt: "&6Wie zufrieden bist du mit dem Support?\n&a/ticket rate {id} good &7– 👍 Gut\n&c/ticket rate {id} bad &7– 👎 Schlecht" + + # --- BLACKLIST --- + blacklist-added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}" + blacklist-removed: "&a{player} &awurde von der Blacklist entfernt." + blacklist-already: "&cSpieler ist bereits auf der Blacklist." + blacklist-not-found: "&cSpieler war nicht auf der Blacklist." + blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen." + # --- FEHLER & HINWEISE --- no-permission: "&cDu hast keine Berechtigung!" no-open-tickets: "&aAktuell gibt es keine offenen Tickets." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6e37320..b83a5e7 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.0.3 +version: 1.0.4 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper @@ -7,25 +7,50 @@ description: Ingame Support Ticket System with MySQL commands: ticket: - description: Ticket System Hauptbefehl - usage: /ticket + description: TicketSystem Hauptbefehl + usage: | + /ticket create [Kategorie] + /ticket list + /ticket comment + /ticket rate + /ticket claim + /ticket close [Kommentar] + /ticket forward + /ticket blacklist [Spieler] [Grund] + /ticket stats + /ticket archive + /ticket reload aliases: [t, support] permissions: + + # ── Spieler-Permissions ─────────────────────────────────────────────────── + ticket.create: - description: Spieler kann Tickets erstellen + description: Spieler kann Tickets erstellen und kommentieren default: true + # ── Supporter-Permissions ───────────────────────────────────────────────── + ticket.support: - description: Supporter kann Tickets einsehen und claimen + description: Supporter kann Tickets einsehen, claimen und schließen default: false ticket.archive: description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen) default: false + # ── Admin-Permissions ──────────────────────────────────────────────────── + ticket.admin: - description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload + description: > + Admin hat vollen Zugriff: Weiterleiten, Blacklist verwalten, + Statistiken, Reload, Archiv, Export/Import, Migration default: op children: - ticket.support: true \ No newline at end of file + ticket.support: true + ticket.blacklist: true + + ticket.blacklist: + description: Kann Spieler zur Ticket-Blacklist hinzufügen und entfernen + default: false \ No newline at end of file