From df6878db2fef25a1f5e168611ecb54eeefecbc37 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 20 Feb 2026 12:31:38 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/de/ticketsystem/TicketPlugin.java | 81 +- .../java/de/ticketsystem/UpdateChecker.java | 6 +- .../ticketsystem/commands/TicketCommand.java | 589 +++++++------- .../database/DatabaseManager.java | 753 ++++++++++-------- .../ticketsystem/discord/DiscordWebhook.java | 195 +++++ .../java/de/ticketsystem/gui/TicketGUI.java | 517 +++++++++--- .../listeners/PlayerJoinListener.java | 64 +- .../ticketsystem/manager/TicketManager.java | 90 ++- .../java/de/ticketsystem/model/Ticket.java | 114 ++- src/main/resources/config.yml | 22 +- src/main/resources/plugin.yml | 2 +- 11 files changed, 1622 insertions(+), 811 deletions(-) create mode 100644 src/main/java/de/ticketsystem/discord/DiscordWebhook.java diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java index ed14952..3e4b083 100644 --- a/src/main/java/de/ticketsystem/TicketPlugin.java +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -1,11 +1,13 @@ - package de.ticketsystem; import de.ticketsystem.commands.TicketCommand; import de.ticketsystem.database.DatabaseManager; +import de.ticketsystem.discord.DiscordWebhook; import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.manager.TicketManager; +// WICHTIG: Import hinzugefügt +import de.ticketsystem.model.Ticket; import org.bukkit.ChatColor; import org.bukkit.plugin.java.JavaPlugin; @@ -19,77 +21,85 @@ public class TicketPlugin extends JavaPlugin { private DatabaseManager databaseManager; private TicketManager ticketManager; private TicketGUI ticketGUI; + private DiscordWebhook discordWebhook; @Override public void onEnable() { instance = this; - // Config speichern falls nicht vorhanden saveDefaultConfig(); - // Update-Checker (Spigot Resource-ID anpassen!) - int resourceId = 132757; + // --- WICHTIG: Ticket-Klasse registrieren --- + Ticket.register(); + // ------------------------------------------- + + // Update-Checker + int resourceId = 132757; new UpdateChecker(this, resourceId).getVersion(version -> { String current = getDescription().getVersion(); if (!current.equals(version)) { String msg = ChatColor.translateAlternateColorCodes('&', "&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")"); - // Sende Nachricht an alle Admins (online) mit 1 Sekunde Verzögerung getServer().getScheduler().runTaskLater(this, () -> { getServer().getOnlinePlayers().stream() .filter(p -> p.hasPermission("ticket.admin")) .forEach(p -> p.sendMessage(msg)); - }, 20L); // 20 Ticks = 1 Sekunde + }, 20L); } else { getLogger().info("TicketSystem ist aktuell (Version " + current + ")"); } }); // Versionsprüfung - String configVersion = getConfig().getString("version", ""); + String configVersion = getConfig().getString("version", ""); String expectedVersion = "2.0"; if (!expectedVersion.equals(configVersion)) { - getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein! Bitte prüfe, ob deine Konfiguration aktuell ist."); + getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion + + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!"); } - // Debug-Status aus Config lesen debug = getConfig().getBoolean("debug", false); - // Datenbankverbindung aufbauen + // Datenbankverbindung databaseManager = new DatabaseManager(this); if (!databaseManager.connect()) { getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); - if (isDebug()) getLogger().warning("[DEBUG] DatabaseManager.connect() fehlgeschlagen, Datei-Modus aktiviert."); - // Plugin bleibt aktiv, DatabaseManager wechselt auf Datei-Storage } - // Manager und GUI initialisieren - ticketManager = new TicketManager(this); - ticketGUI = new TicketGUI(this); + // Manager, GUI & Discord-Webhook initialisieren + ticketManager = new TicketManager(this); + ticketGUI = new TicketGUI(this); + discordWebhook = new DiscordWebhook(this); - // Commands registrieren + if (getConfig().getBoolean("discord.enabled", false)) { + String url = getConfig().getString("discord.webhook-url", ""); + if (url.isEmpty()) { + getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!"); + } else { + getLogger().info("[DiscordWebhook] Integration aktiv."); + } + } + + // Commands & Events TicketCommand ticketCommand = new TicketCommand(this); Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand); - // Events registrieren getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(ticketGUI, this); - // Automatische Archivierung nach Zeitplan (Intervall in Stunden, Standard: 24h) + // Automatische Archivierung int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); if (archiveIntervalH > 0) { - long ticks = archiveIntervalH * 60L * 60L * 20L; // Stunden → Ticks + long ticks = archiveIntervalH * 60L * 60L * 20L; getServer().getScheduler().runTaskTimer(this, () -> { int archived = databaseManager.archiveClosedTickets(); if (archived > 0) { getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); - if (isDebug()) getLogger().info("[DEBUG] Archivierung ausgeführt, " + archived + " Tickets verschoben."); } }, ticks, ticks); getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert."); - if (isDebug()) getLogger().info("[DEBUG] Archivierungs-Timer gesetzt: alle " + archiveIntervalH + " Stunden."); } getLogger().info("TicketSystem erfolgreich gestartet!"); @@ -97,39 +107,28 @@ public class TicketPlugin extends JavaPlugin { @Override public void onDisable() { - if (databaseManager != null) { - databaseManager.disconnect(); - } + if (databaseManager != null) databaseManager.disconnect(); getLogger().info("TicketSystem wurde deaktiviert."); } // ─────────────────────────── Hilfsmethoden ───────────────────────────── - /** - * Formatiert eine Nachricht aus der Config mit Prefix und Farben. - */ public String formatMessage(String path) { - String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r")); + String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r")); String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path); return prefix + color(message); } - /** - * Konvertiert Farbcodes (&x → §x). - */ public String color(String text) { return ChatColor.translateAlternateColorCodes('&', text); } // ─────────────────────────── Getter ──────────────────────────────────── - public static TicketPlugin getInstance() { return instance; } - public DatabaseManager getDatabaseManager() { return databaseManager; } - public TicketManager getTicketManager() { return ticketManager; } - public TicketGUI getTicketGUI() { return ticketGUI; } - - /** - * Gibt zurück, ob der Debug-Modus aktiv ist (aus config.yml) - */ - public boolean isDebug() { return debug; } -} + 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; } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/UpdateChecker.java b/src/main/java/de/ticketsystem/UpdateChecker.java index fd7768c..7cd4f8d 100644 --- a/src/main/java/de/ticketsystem/UpdateChecker.java +++ b/src/main/java/de/ticketsystem/UpdateChecker.java @@ -26,7 +26,11 @@ public class UpdateChecker { Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) { if (scann.hasNext()) { - consumer.accept(scann.next()); + String latest = scann.next(); + plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'"); + consumer.accept(latest); + } else { + plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!"); } } catch (IOException e) { plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java index b8cc4d2..95af481 100644 --- a/src/main/java/de/ticketsystem/commands/TicketCommand.java +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -1,4 +1,3 @@ - package de.ticketsystem.commands; import de.ticketsystem.TicketPlugin; @@ -9,19 +8,259 @@ import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class TicketCommand implements CommandExecutor, TabCompleter { - // Platzhalter für Admin-Kommandos + + private final TicketPlugin plugin; + + public TicketCommand(TicketPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden."); + 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); + } + return true; + } + + // ─────────────────────────── /ticket create ──────────────────────────── + + private void handleCreate(Player player, String[] args) { + if (!player.hasPermission("ticket.create")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + if (args.length < 2) { + player.sendMessage(plugin.color("&cBenutzung: /ticket create ")); + 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))); + 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.")); + return; + } + String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + int maxLen = plugin.getConfig().getInt("max-description-length", 100); + if (message.length() > maxLen) { + player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); + return; + } + Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int id = plugin.getDatabaseManager().createTicket(ticket); + 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 + }); + }); + } + + // ─────────────────────────── /ticket list ────────────────────────────── + + 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))); + } else { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> + Bukkit.getScheduler().runTask(plugin, () -> + plugin.getTicketGUI().openPlayerGUI(player))); + } + } + + // ─────────────────────────── /ticket claim ───────────────────────────── + + 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; + } + int id; + try { id = Integer.parseInt(args[1]); } + catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } + + final int ticketId = id; + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } + Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + if (ticket == null) return; + player.sendMessage(plugin.formatMessage("messages.ticket-claimed") + .replace("{id}", String.valueOf(ticketId)) + .replace("{player}", ticket.getCreatorName())); + plugin.getTicketManager().notifyCreatorClaimed(ticket); + if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); + }); + }); + } + + // ─────────────────────────── /ticket close ───────────────────────────── + + 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; + } + 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(); + + 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))); + if (ticket != null) { + ticket.setCloseComment(comment); + // closerName für Discord-Nachricht mitgeben + plugin.getTicketManager().notifyCreatorClosed(ticket, closer); + } + }); + } else { + Bukkit.getScheduler().runTask(plugin, () -> + player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + } + }); + } + + // ─────────────────────────── /ticket forward ─────────────────────────── + + 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; + } + 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!")); + return; + } + + final int ticketId = id; + final Player finalTarget = target; + final String fromName = player.getName(); + + 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 reload ──────────────────────────── + + private void handleReload(Player player) { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); + return; + } + plugin.reloadConfig(); + player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen.")); + } + + // ─────────────────────────── /ticket archive ─────────────────────────── + + private void handleArchive(Player player) { + 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")); @@ -34,19 +273,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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; - } + 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))); + .replace("{count}", String.valueOf(finalMigrated))); } else { player.sendMessage(plugin.formatMessage("messages.migration-fail")); } @@ -54,6 +288,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter { }); } + // ─────────────────────────── /ticket export ──────────────────────────── + private void handleExport(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); @@ -70,7 +306,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTask(plugin, () -> { if (count > 0) { player.sendMessage(plugin.formatMessage("messages.export-success") - .replace("{count}", String.valueOf(count)).replace("{file}", filename)); + .replace("{count}", String.valueOf(count)).replace("{file}", filename)); } else { player.sendMessage(plugin.formatMessage("messages.export-fail")); } @@ -78,6 +314,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter { }); } + // ─────────────────────────── /ticket import ──────────────────────────── + private void handleImport(Player player, String[] args) { if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); @@ -98,7 +336,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTask(plugin, () -> { if (count > 0) { player.sendMessage(plugin.formatMessage("messages.import-success") - .replace("{count}", String.valueOf(count))); + .replace("{count}", String.valueOf(count))); } else { player.sendMessage(plugin.formatMessage("messages.import-fail")); } @@ -106,6 +344,8 @@ 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")); @@ -114,309 +354,38 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var stats = plugin.getDatabaseManager().getTicketStats(); Bukkit.getScheduler().runTask(plugin, () -> { - player.sendMessage("§6--- Ticket Statistik ---"); - player.sendMessage("§eGesamt: §a" + stats.total + " §7| §eOffen: §a" + stats.open + " §7| §eGeschlossen: §a" + stats.closed + " §7| §eWeitergeleitet: §a" + stats.forwarded); - player.sendMessage("§6Top Ersteller:"); - stats.byPlayer.entrySet().stream().sorted((a,b)->b.getValue()-a.getValue()).limit(5).forEach(e -> - player.sendMessage("§e" + e.getKey() + ": §a" + e.getValue()) - ); + 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("&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()))); }); }); } - // ─────────────────────────── /ticket archive ──────────────────────────── - private void handleArchive(Player player) { - 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")); - } - }); - }); - } - - private final TicketPlugin plugin; - - public TicketCommand(TicketPlugin plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, - String label, String[] args) { - - if (!(sender instanceof Player player)) { - sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden."); - 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); - } - return true; - } - - - // Methoden wie handleMigrate, handleCreate, handleList, handleClaim, handleClose, handleForward, handleReload, handleStats müssen auf Klassenebene stehen und dürfen nicht innerhalb von onCommand oder anderen Methoden verschachtelt sein. - // Entferne alle verschachtelten Methoden und stelle sicher, dass jede Methode nur einmal und auf Klassenebene existiert. - - // ─────────────────────────── /ticket create ──────────────────────────── - - private void handleCreate(Player player, String[] args) { - if (!player.hasPermission("ticket.create")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - if (args.length < 2) { - player.sendMessage(plugin.color("&cBenutzung: /ticket create ")); - return; - } - - // Cooldown-Check - if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) { - long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); - player.sendMessage(plugin.formatMessage("messages.cooldown") - .replace("{seconds}", String.valueOf(remaining))); - return; - } - - // Ticket-Limit-Check - 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.")); - return; - } - - // Nachricht zusammenbauen - String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); - int maxLen = plugin.getConfig().getInt("max-description-length", 100); - if (message.length() > maxLen) { - player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); - return; - } - - // Ticket asynchron in DB speichern - Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); - - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int id = plugin.getDatabaseManager().createTicket(ticket); - if (id == -1) { - player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets! Bitte wende dich an einen Admin.")); - 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))); - - // Team benachrichtigen - plugin.getTicketManager().notifyTeam(ticket); - }); - }); - } - - // ─────────────────────────── /ticket list ────────────────────────────── - - private void handleList(Player player) { - if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - // GUI öffnen (synchron, Datenbankabfrage läuft darin async) - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> - Bukkit.getScheduler().runTask(plugin, () -> - plugin.getTicketGUI().openGUI(player))); - } - - // ─────────────────────────── /ticket claim ───────────────────────────── - - 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; - } - - int id; - try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { - player.sendMessage(plugin.color("&cUngültige ID!")); - return; - } - - final int ticketId = id; - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().claimTicket( - ticketId, player.getUniqueId(), player.getName()); - - Bukkit.getScheduler().runTask(plugin, () -> { - if (!success) { - player.sendMessage(plugin.formatMessage("messages.already-claimed")); - return; - } - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); - if (ticket == null) return; - - player.sendMessage(plugin.formatMessage("messages.ticket-claimed") - .replace("{id}", String.valueOf(ticketId)) - .replace("{player}", ticket.getCreatorName())); - - plugin.getTicketManager().notifyCreatorClaimed(ticket); - - // Zur Ticket-Position teleportieren - if (ticket.getLocation() != null) { - player.teleport(ticket.getLocation()); - } - }); - }); - } - - // ─────────────────────────── /ticket close ───────────────────────────── - - 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 ")); - return; - } - - int id; - try { id = Integer.parseInt(args[1]); } - catch (NumberFormatException e) { - player.sendMessage(plugin.color("&cUngültige ID!")); - return; - } - - final int ticketId = id; - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().closeTicket(ticketId); - Bukkit.getScheduler().runTask(plugin, () -> { - if (success) { - player.sendMessage(plugin.formatMessage("messages.ticket-closed") - .replace("{id}", String.valueOf(ticketId))); - } else { - player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); - } - }); - }); - } - - // ─────────────────────────── /ticket forward ─────────────────────────── - - 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; - } - - 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!")); - return; - } - - final int ticketId = id; - final Player finalTarget = target; - - 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) plugin.getTicketManager().notifyForwardedTo(ticket); - }); - } else { - Bukkit.getScheduler().runTask(plugin, () -> - player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); - } - }); - } - - // ─────────────────────────── /ticket reload ──────────────────────────── - - private void handleReload(Player player) { - if (!player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; - } - plugin.reloadConfig(); - player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen.")); - } - // ─────────────────────────── Tab-Completion ──────────────────────────── @Override - public List onTabComplete(CommandSender sender, Command command, - String label, String[] args) { + 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<>(); - subs.add("create"); - if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { - subs.addAll(List.of("list", "claim", "close")); - } - if (player.hasPermission("ticket.admin")) { - subs.addAll(List.of("forward", "reload")); - } - for (String s : subs) { + List subs = new ArrayList<>(List.of("create", "list")); + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) + subs.addAll(List.of("claim", "close")); + 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); - } } 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()); - } + for (Player p : Bukkit.getOnlinePlayers()) + if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); } return completions; } -} +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java index 130e548..0948c97 100644 --- a/src/main/java/de/ticketsystem/database/DatabaseManager.java +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -1,4 +1,3 @@ - package de.ticketsystem.database; import java.io.File; @@ -14,7 +13,6 @@ import de.ticketsystem.model.Ticket; import de.ticketsystem.model.TicketStatus; import java.sql.*; import java.util.ArrayList; - import java.util.List; import java.util.UUID; import java.util.logging.Level; @@ -23,295 +21,9 @@ import java.io.FileWriter; import org.bukkit.Bukkit; public class DatabaseManager { - // Test-Konstruktor für Unit-Tests (ohne Bukkit/Plugin) - public DatabaseManager(File dataFile, YamlConfiguration dataConfig) { - this.plugin = null; - this.useMySQL = false; - this.useJson = false; - this.dataFileName = dataFile.getName(); - this.archiveFileName = "archive.json"; - this.dataFile = dataFile; - this.dataConfig = dataConfig; - validateLoadedTickets(); - } - /** - * Archiviert alle geschlossenen Tickets in eine separate Datei und entfernt sie aus dem aktiven Speicher. - * @return Anzahl archivierter Tickets - */ - public int archiveClosedTickets() { - List all = getAllTickets(); - List toArchive = new ArrayList<>(); - 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(); - // Bestehendes Archiv laden - if (archiveFile.exists()) { - try (FileReader fr = new FileReader(archiveFile)) { - JSONParser parser = new JSONParser(); - Object parsed = parser.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; - } - // Entferne archivierte Tickets aus aktivem Speicher - 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()); - } - } 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()); } - } - return removed; - } - /** - * Liefert Statistiken über Tickets. - */ - public TicketStats getTicketStats() { - List all = getAllTickets(); - int open = 0, closed = 0, forwarded = 0; - java.util.Map byPlayer = new java.util.HashMap<>(); - for (Ticket t : all) { - switch (t.getStatus()) { - case OPEN -> open++; - case CLOSED -> closed++; - case FORWARDED -> forwarded++; - } - byPlayer.merge(t.getCreatorName(), 1, Integer::sum); - } - return new TicketStats(all.size(), open, closed, forwarded, byPlayer); - } - public static class TicketStats { - public final int total, open, closed, forwarded; - 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; - } - } - /** - * Exportiert alle Tickets als JSON-Datei. - * @param exportFile Ziel-Datei - * @return Anzahl exportierter Tickets - */ - 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; - } - } + // ─────────────────────────── Felder ──────────────────────────────────── - /** - * Importiert Tickets aus einer JSON-Datei. - * @param importFile Quell-Datei - * @return Anzahl importierter Tickets - */ - public int importTickets(File importFile) { - int imported = 0; - try (FileReader fr = new FileReader(importFile)) { - JSONParser parser = new JSONParser(); - JSONArray arr = (JSONArray) parser.parse(fr); - for (Object o : arr) { - JSONObject obj = (JSONObject) o; - Ticket t = ticketFromJson(obj); - if (t != null) { - int id = createTicket(t); - if (id != -1) imported++; - } - } - } catch (Exception e) { - sendError("Fehler beim Import: " + e.getMessage()); - } - return imported; - } - - /** - * Gibt alle Tickets (egal welcher Status) zurück. - */ - public List getAllTickets() { - List list = new ArrayList<>(); - if (useMySQL) { - try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { - ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); - while (rs.next()) list.add(mapRow(rs)); - } catch (SQLException e) { - 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); - } - } - } - return list; - } - - // Hilfsmethoden für JSON-Konvertierung - 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); - 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()); - return obj; - } - - private Ticket ticketFromJson(JSONObject obj) { - try { - Ticket t = new Ticket(); - t.setId(((Long)obj.get("id")).intValue()); - t.setCreatorUUID(UUID.fromString((String)obj.get("creatorUUID"))); - 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.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 java.sql.Timestamp((Long)obj.get("createdAt"))); - if (obj.get("claimedAt") != null) t.setClaimedAt(new java.sql.Timestamp((Long)obj.get("claimedAt"))); - if (obj.get("closedAt") != null) t.setClosedAt(new java.sql.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")); - return t; - } catch (Exception e) { - plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); - return null; - } - } - /** - * Migriert alle Tickets aus data.yml nach MySQL. - */ - public int migrateToMySQL() { - if (useMySQL || dataConfig == null) return 0; - int migrated = 0; - try { - for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { - Ticket t = (Ticket) dataConfig.get("tickets." + key); - if (t != null) { - // Ticket in MySQL speichern - 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()); - } - return migrated; - } - - /** - * Migriert alle Tickets aus MySQL nach data.yml. - */ - public int migrateToFile() { - if (!useMySQL) return 0; - int migrated = 0; - try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { - 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++; - } - } - } catch (Exception e) { - plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); - } - return migrated; - } - private String dataFileName; - private String archiveFileName; - // Prüft geladene Tickets auf Korrektheit (Platzhalter) - private void validateLoadedTickets() { - if (dataConfig == null || !dataConfig.contains("tickets")) return; - 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 (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 ? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid)) : (invalid + " ungültige Tickets beim Laden gefunden."); - sendError(msg); - } - } - - // Backup der MySQL-Datenbank (Platzhalter) - private void backupMySQL() { - // TODO: Implementiere Backup-Logik für MySQL - } - - // Backup der Datei-basierten Daten (Platzhalter) - private void backupDataFile() { - // TODO: Implementiere Backup-Logik für data.yml/data.json - } private final TicketPlugin plugin; private HikariDataSource dataSource; private boolean useMySQL; @@ -319,22 +31,26 @@ public class DatabaseManager { private File dataFile; private YamlConfiguration dataConfig; private JSONArray dataJson; + private String dataFileName; + private String archiveFileName; + + // ─────────────────────────── Konstruktoren ───────────────────────────── public DatabaseManager(TicketPlugin plugin) { this.plugin = plugin; this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); - this.useJson = plugin.getConfig().getBoolean("use-json", false); + this.useJson = plugin.getConfig().getBoolean("use-json", false); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson); - // Speicherpfade aus config.yml (absolut oder relativ zum Plugin-Ordner) - String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml"); + + String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml"); String archivePath = plugin.getConfig().getString("archive-file", "archive.json"); - this.dataFileName = dataPath; + this.dataFileName = dataPath; this.archiveFileName = archivePath; + if (!useMySQL) { if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath); if (useJson) { dataFile = resolvePath(dataPath); - if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] JSON-Datei: " + dataFile.getAbsolutePath()); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); @@ -346,7 +62,7 @@ public class DatabaseManager { } else { try { JSONParser parser = new JSONParser(); - dataJson = (JSONArray) parser.parse(new java.io.FileReader(dataFile)); + dataJson = (JSONArray) parser.parse(new FileReader(dataFile)); } catch (Exception e) { sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); dataJson = new JSONArray(); @@ -354,7 +70,6 @@ public class DatabaseManager { } } else { dataFile = resolvePath(dataPath); - if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] YAML-Datei: " + dataFile.getAbsolutePath()); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); @@ -369,18 +84,33 @@ public class DatabaseManager { } } - // Hilfsfunktion: Absoluten oder relativen Pfad auflösen + // Konstruktor für Tests + public DatabaseManager(File dataFile, YamlConfiguration dataConfig) { + this.plugin = null; + this.useMySQL = false; + this.useJson = false; + this.dataFileName = dataFile.getName(); + this.archiveFileName = "archive.json"; + this.dataFile = dataFile; + this.dataConfig = dataConfig; + validateLoadedTickets(); + } + + // ─────────────────────────── Hilfsmethoden ───────────────────────────── + private File resolvePath(String path) { File f = new File(path); if (f.isAbsolute()) return f; - return new File(plugin.getDataFolder(), path); + return new File(plugin != null ? plugin.getDataFolder() : new File("."), path); } - // Fehlerausgabe im Chat und Log private void sendError(String msg) { if (plugin != null) plugin.getLogger().severe(msg); - // Fehler an alle Admins im Chat senden - Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); + if (Bukkit.getServer() != null) { + Bukkit.getOnlinePlayers().stream() + .filter(p -> p.hasPermission("ticket.admin")) + .forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); + } } // ─────────────────────────── Verbindung ──────────────────────────────── @@ -389,7 +119,8 @@ public class DatabaseManager { if (useMySQL) { try { HikariConfig config = new HikariConfig(); - config.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8", + config.setJdbcUrl(String.format( + "jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8", plugin.getConfig().getString("mysql.host"), plugin.getConfig().getInt("mysql.port"), plugin.getConfig().getString("mysql.database"))); @@ -403,14 +134,17 @@ public class DatabaseManager { 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) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!"); useMySQL = false; - // Datei-Storage initialisieren dataFile = new File(plugin.getDataFolder(), "data.yml"); if (!dataFile.exists()) { try { @@ -434,7 +168,6 @@ public class DatabaseManager { dataSource.close(); plugin.getLogger().info("MySQL-Verbindung getrennt."); } - // Bei Datei-Storage nichts zu tun } private Connection getConnection() throws SQLException { @@ -444,26 +177,28 @@ public class DatabaseManager { // ─────────────────────────── Tabellen erstellen ──────────────────────── private void createTables() { + // close_comment ist jetzt von Anfang an in der CREATE-Anweisung enthalten String sql = """ CREATE TABLE IF NOT EXISTS tickets ( - id INT AUTO_INCREMENT PRIMARY KEY, - creator_uuid VARCHAR(36) NOT NULL, - creator_name VARCHAR(16) NOT NULL, - message VARCHAR(255) NOT NULL, - world VARCHAR(64) NOT NULL, - x DOUBLE NOT NULL, - y DOUBLE NOT NULL, - z DOUBLE NOT NULL, - yaw FLOAT NOT NULL DEFAULT 0, - pitch FLOAT NOT NULL DEFAULT 0, - status VARCHAR(16) NOT NULL DEFAULT 'OPEN', - claimer_uuid VARCHAR(36), - claimer_name VARCHAR(16), + id INT AUTO_INCREMENT PRIMARY KEY, + creator_uuid VARCHAR(36) NOT NULL, + creator_name VARCHAR(16) NOT NULL, + message VARCHAR(255) NOT NULL, + world VARCHAR(64) NOT NULL, + x DOUBLE NOT NULL, + y DOUBLE NOT NULL, + z DOUBLE NOT NULL, + yaw FLOAT NOT NULL DEFAULT 0, + pitch FLOAT NOT NULL DEFAULT 0, + status VARCHAR(16) NOT NULL DEFAULT 'OPEN', + claimer_uuid VARCHAR(36), + claimer_name VARCHAR(16), forwarded_to_uuid VARCHAR(36), forwarded_to_name VARCHAR(16), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - claimed_at TIMESTAMP, - closed_at TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + claimed_at TIMESTAMP NULL, + closed_at TIMESTAMP NULL, + close_comment VARCHAR(500) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { @@ -473,6 +208,32 @@ public class DatabaseManager { } } + /** + * Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch. + * Wichtig für Server, die das Plugin bereits installiert hatten bevor + * close_comment existierte. + */ + private void ensureColumns() { + // close_comment hinzufügen, falls nicht vorhanden + String checkSql = """ + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'tickets' + AND COLUMN_NAME = 'close_comment' + """; + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(checkSql); + if (rs.next() && rs.getInt(1) == 0) { + // Spalte existiert nicht → hinzufügen + stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL"); + plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt."); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e); + } + } + // ─────────────────────────── CRUD ────────────────────────────────────── /** @@ -507,7 +268,6 @@ public class DatabaseManager { } return -1; } else { - // Datei-Storage: Ticket-ID generieren int id = dataConfig.getInt("lastId", 0) + 1; ticket.setId(id); dataConfig.set("lastId", id); @@ -517,7 +277,6 @@ public class DatabaseManager { backupDataFile(); } catch (IOException e) { plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); - Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] Fehler beim Speichern von data.yml: " + e.getMessage())); } return id; } @@ -547,7 +306,7 @@ public class DatabaseManager { t.setStatus(TicketStatus.CLAIMED); t.setClaimerUUID(claimerUUID); t.setClaimerName(claimerName); - t.setClaimedAt(new java.sql.Timestamp(System.currentTimeMillis())); + t.setClaimedAt(new Timestamp(System.currentTimeMillis())); dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); @@ -562,11 +321,15 @@ public class DatabaseManager { /** * Schließt ein Ticket (Status → CLOSED). */ - public boolean closeTicket(int ticketId) { + public boolean closeTicket(int ticketId, String closeComment) { if (useMySQL) { - String sql = "UPDATE tickets SET status = 'CLOSED', closed_at = NOW() WHERE id = ? AND status != 'CLOSED'"; + String sql = """ + UPDATE tickets SET status = 'CLOSED', closed_at = NOW(), close_comment = ? + WHERE id = ? AND status != 'CLOSED' + """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setInt(1, ticketId); + ps.setString(1, closeComment != null ? closeComment : ""); + ps.setInt(2, ticketId); return ps.executeUpdate() > 0; } catch (SQLException e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e); @@ -576,7 +339,8 @@ public class DatabaseManager { Ticket t = getTicketById(ticketId); if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; t.setStatus(TicketStatus.CLOSED); - t.setClosedAt(new java.sql.Timestamp(System.currentTimeMillis())); + t.setClosedAt(new Timestamp(System.currentTimeMillis())); + t.setCloseComment(closeComment != null ? closeComment : ""); dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); @@ -588,6 +352,38 @@ public class DatabaseManager { } } + /** + * Löscht ein Ticket anhand der ID. + */ + public boolean deleteTicket(int id) { + if (useMySQL) { + 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; + } + } 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; + } + } + /** * Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). */ @@ -623,6 +419,8 @@ public class DatabaseManager { } } + // ─────────────────────────── Abfragen ────────────────────────────────── + /** * Gibt alle Tickets mit einem bestimmten Status zurück. */ @@ -642,7 +440,6 @@ public class DatabaseManager { } return list; } else { - // Datei-Storage: Alle Tickets filtern if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); @@ -655,6 +452,29 @@ public class DatabaseManager { } } + /** + * Gibt alle Tickets zurück (alle Status). + */ + public List getAllTickets() { + List list = new ArrayList<>(); + if (useMySQL) { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("SELECT * FROM tickets"); + while (rs.next()) list.add(mapRow(rs)); + } catch (SQLException e) { + 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); + } + } + } + return list; + } + /** * Gibt ein einzelnes Ticket anhand der ID zurück. */ @@ -678,11 +498,11 @@ public class DatabaseManager { } /** - * Anzahl offener Tickets (OPEN + FORWARDED) – für Join-Benachrichtigung. + * Anzahl offener Tickets (OPEN) – für Join-Benachrichtigung. */ public int countOpenTickets() { if (useMySQL) { - String sql = "SELECT COUNT(*) FROM tickets WHERE status IN ('OPEN', 'FORWARDED', 'CLAIMED')"; + String sql = "SELECT COUNT(*) FROM tickets WHERE status = 'OPEN'"; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery(sql); if (rs.next()) return rs.getInt(1); @@ -695,7 +515,7 @@ public class DatabaseManager { if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); - if (t != null && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.FORWARDED || t.getStatus() == TicketStatus.CLAIMED)) count++; + if (t != null && t.getStatus() == TicketStatus.OPEN) count++; } } return count; @@ -721,17 +541,176 @@ public class DatabaseManager { if (dataConfig.contains("tickets")) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { Ticket t = (Ticket) dataConfig.get("tickets." + key); - if (t != null && uuid.equals(t.getCreatorUUID()) && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.CLAIMED || t.getStatus() == TicketStatus.FORWARDED)) count++; + if (t != null && uuid.equals(t.getCreatorUUID()) + && (t.getStatus() == TicketStatus.OPEN + || t.getStatus() == TicketStatus.CLAIMED + || t.getStatus() == TicketStatus.FORWARDED)) count++; } } return count; } } + // ─────────────────────────── Archivierung ────────────────────────────── + + /** + * Archiviert alle geschlossenen Tickets in eine separate Datei. + */ + public int archiveClosedTickets() { + List all = getAllTickets(); + List toArchive = new ArrayList<>(); + 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); + 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; + } + + 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()); + } + } 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()); + } + } + return removed; + } + + // ─────────────────────────── Statistiken ─────────────────────────────── + + public TicketStats getTicketStats() { + List all = getAllTickets(); + int open = 0, claimed = 0, forwarded = 0, closed = 0; + java.util.Map byPlayer = new java.util.HashMap<>(); + for (Ticket t : all) { + switch (t.getStatus()) { + case OPEN -> open++; + case CLAIMED -> claimed++; + case FORWARDED -> forwarded++; + case CLOSED -> closed++; + } + byPlayer.merge(t.getCreatorName(), 1, Integer::sum); + } + return new TicketStats(all.size(), open, closed, forwarded, byPlayer); + } + + public static class TicketStats { + public final int total, open, closed, forwarded; + 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; + } + } + + // ─────────────────────────── 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; + } + } + + public int importTickets(File importFile) { + int imported = 0; + try (FileReader fr = new FileReader(importFile)) { + JSONParser parser = new JSONParser(); + JSONArray arr = (JSONArray) parser.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()); + } + return imported; + } + + // ─────────────────────────── Migration ───────────────────────────────── + + public int migrateToMySQL() { + if (useMySQL || dataConfig == null) return 0; + int migrated = 0; + 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++; + } + } + } catch (Exception e) { + plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage()); + } + return migrated; + } + + public int migrateToFile() { + if (!useMySQL) return 0; + int migrated = 0; + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + 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++; + } + } + } catch (Exception e) { + plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage()); + } + return migrated; + } + // ─────────────────────────── Mapping ─────────────────────────────────── + /** + * Liest eine Zeile aus dem ResultSet und erstellt ein Ticket-Objekt. + * close_comment wird mit try-catch abgesichert, damit ältere Datenbanken + * ohne diese Spalte nicht abstürzen. + */ private Ticket mapRow(ResultSet rs) throws SQLException { - File archiveFile = new File(plugin.getDataFolder(), archiveFileName); Ticket t = new Ticket(); t.setId(rs.getInt("id")); t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid"))); @@ -748,6 +727,16 @@ public class DatabaseManager { t.setClaimedAt(rs.getTimestamp("claimed_at")); t.setClosedAt(rs.getTimestamp("closed_at")); + // ── BUGFIX: close_comment mit try-catch absichern ────────────────── + // Wenn die Spalte in einer alten DB noch nicht existiert, wird der + // Fehler ignoriert statt die gesamte Ticket-Liste leer zu lassen. + try { + String closeComment = rs.getString("close_comment"); + if (closeComment != null) t.setCloseComment(closeComment); + } catch (SQLException ignored) { + // Spalte existiert noch nicht – ensureColumns() ergänzt sie beim nächsten Start + } + String claimerUUID = rs.getString("claimer_uuid"); if (claimerUUID != null) { t.setClaimerUUID(UUID.fromString(claimerUUID)); @@ -760,4 +749,100 @@ public class DatabaseManager { } return t; } -} + + // ─────────────────────────── JSON-Hilfsmethoden ───────────────────────── + + 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); + 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()); + return obj; + } + + private Ticket ticketFromJson(JSONObject obj) { + try { + Ticket t = new Ticket(); + t.setId(((Long) obj.get("id")).intValue()); + t.setCreatorUUID(UUID.fromString((String) obj.get("creatorUUID"))); + 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.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")); + return t; + } catch (Exception e) { + if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); + return null; + } + } + + // ─────────────────────────── Validierung ─────────────────────────────── + + private void validateLoadedTickets() { + if (dataConfig == null || !dataConfig.contains("tickets")) return; + 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 (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 + ? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid)) + : invalid + " ungültige Tickets beim Laden gefunden."; + sendError(msg); + } + } + + // ─────────────────────────── Backup (Platzhalter) ────────────────────── + + private void backupMySQL() { + // TODO: MySQL-Backup implementieren + } + + private void backupDataFile() { + // TODO: Datei-Backup implementieren + } +} \ 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 new file mode 100644 index 0000000..53e8a4e --- /dev/null +++ b/src/main/java/de/ticketsystem/discord/DiscordWebhook.java @@ -0,0 +1,195 @@ +package de.ticketsystem.discord; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import org.bukkit.Bukkit; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Instant; + +/** + * Sendet Benachrichtigungen an einen Discord-Webhook. + * Unterstützt Embeds mit Farbe, Feldern und Timestamp. + */ +public class DiscordWebhook { + + private final TicketPlugin plugin; + + public DiscordWebhook(TicketPlugin plugin) { + this.plugin = plugin; + } + + // ─────────────────────────── Öffentliche Methoden ────────────────────── + + /** + * Sendet eine Benachrichtigung wenn ein neues Ticket erstellt wurde. + */ + public void sendNewTicket(Ticket ticket) { + if (!isEnabled()) return; + + String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); + if (webhookUrl.isEmpty()) 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"); + boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", 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)); + + if (showPos) { + fields.append(","); + fields.append(field("Welt", ticket.getWorldName(), true)); + fields.append(","); + fields.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); + sendAsync(webhookUrl, json); + } + + /** + * Sendet eine Benachrichtigung wenn ein Ticket geschlossen wurde. + */ + public void sendTicketClosed(Ticket ticket, String closerName) { + 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 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"); + + 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)); + if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { + fields.append(","); + fields.append(field("Kommentar", ticket.getCloseComment(), false)); + } + + String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); + sendAsync(webhookUrl, json); + } + + /** + * Sendet eine Benachrichtigung wenn ein Ticket weitergeleitet wurde. + */ + public void sendTicketForwarded(Ticket ticket, String fromName) { + 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 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"); + + 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)); + + String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); + sendAsync(webhookUrl, json); + } + + // ─────────────────────────── Private Hilfsmethoden ───────────────────── + + private boolean isEnabled() { + return plugin.getConfig().getBoolean("discord.enabled", false); + } + + /** + * 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("\"", "\\\""); + return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}", + safeName, safeValue, inline); + } + + /** + * Baut den kompletten Webhook-Payload als JSON. + */ + private String buildPayload(String title, int color, String fieldsJson, String footer) { + String timestamp = Instant.now().toString(); // ISO-8601 + return String.format(""" + { + "embeds": [{ + "title": "%s", + "color": %d, + "fields": [%s], + "footer": { "text": "%s" }, + "timestamp": "%s" + }] + }""", + title.replace("\"", "\\\""), + color, + fieldsJson, + footer.replace("\"", "\\\""), + timestamp); + } + + /** + * Sendet den JSON-Payload asynchron an den Webhook. + */ + private void sendAsync(String webhookUrl, String json) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + URL url = new URL(webhookUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("User-Agent", "TicketSystem-Plugin"); + conn.setDoOutput(true); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + try (OutputStream os = conn.getOutputStream()) { + os.write(json.getBytes(StandardCharsets.UTF_8)); + } + + int responseCode = conn.getResponseCode(); + if (plugin.isDebug()) { + 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); + } + + conn.disconnect(); + } catch (Exception e) { + plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage()); + if (plugin.isDebug()) e.printStackTrace(); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/gui/TicketGUI.java b/src/main/java/de/ticketsystem/gui/TicketGUI.java index 9bf0ce4..023c0e2 100644 --- a/src/main/java/de/ticketsystem/gui/TicketGUI.java +++ b/src/main/java/de/ticketsystem/gui/TicketGUI.java @@ -4,12 +4,13 @@ import de.ticketsystem.TicketPlugin; import de.ticketsystem.model.Ticket; import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -23,19 +24,35 @@ import java.util.UUID; public class TicketGUI implements Listener { - private static final String GUI_TITLE = "§8§lTicket-Übersicht"; + // ─────────────────────────── Titel-Konstanten ────────────────────────── + + private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht + 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 + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); private final TicketPlugin plugin; - // Speichert welcher Spieler welches Ticket an welchem Slot hat + /** Admin-Übersicht: Slot → Ticket */ private final Map> playerSlotMap = new HashMap<>(); + /** Spieler-GUI: Slot → Ticket */ + private final Map> playerOwnSlotMap = new HashMap<>(); + + /** Detail-Ansicht: Player-UUID → Ticket */ + private final Map detailTicketMap = new HashMap<>(); + + /** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */ + private final Map awaitingComment = new HashMap<>(); + public TicketGUI(TicketPlugin plugin) { this.plugin = plugin; } - // ─────────────────────────── GUI öffnen ──────────────────────────────── + // ═══════════════════════════════════════════════════════════════════════ + // ADMIN / SUPPORTER GUI (Übersicht aller Tickets) + // ═══════════════════════════════════════════════════════════════════════ public void openGUI(Player player) { List tickets = plugin.getDatabaseManager().getTicketsByStatus( @@ -46,47 +63,326 @@ public class TicketGUI implements Listener { return; } - // Inventar-Größe: nächste Vielfaches von 9 (max. 54 Slots) - int size = Math.min(54, (int) (Math.ceil(tickets.size() / 9.0) * 9)); - if (size < 9) size = 9; - + int size = calcSize(tickets.size()); Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); Map slotMap = new HashMap<>(); for (int i = 0; i < tickets.size() && i < 54; i++) { Ticket ticket = tickets.get(i); - ItemStack item = buildTicketItem(ticket); - inv.setItem(i, item); + inv.setItem(i, buildAdminListItem(ticket)); slotMap.put(i, ticket); } - // Trennlinie am Ende, wenn Platz fillEmpty(inv); - playerSlotMap.put(player.getUniqueId(), slotMap); player.openInventory(inv); } - // ─────────────────────────── Item bauen ──────────────────────────────── + // ═══════════════════════════════════════════════════════════════════════ + // SPIELER-GUI (nur eigene Tickets, mit Lösch-Option bei OPEN) + // ═══════════════════════════════════════════════════════════════════════ - private ItemStack buildTicketItem(Ticket ticket) { - // Material je nach Status - Material mat; - switch (ticket.getStatus()) { - case OPEN -> mat = Material.PAPER; - case CLAIMED -> mat = Material.YELLOW_DYE; - case FORWARDED -> mat = Material.ORANGE_DYE; - default -> mat = Material.PAPER; + public void openPlayerGUI(Player player) { + List all = plugin.getDatabaseManager().getTicketsByStatus( + TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED); + + List tickets = new ArrayList<>(); + for (Ticket t : all) { + if (t.getCreatorUUID().equals(player.getUniqueId())) tickets.add(t); } + if (tickets.isEmpty()) { + player.sendMessage(plugin.color("&aDu hast aktuell keine Tickets.")); + return; + } + + int size = calcSize(tickets.size()); + Inventory inv = Bukkit.createInventory(null, size, PLAYER_GUI_TITLE); + Map slotMap = new HashMap<>(); + + for (int i = 0; i < tickets.size() && i < 54; i++) { + Ticket ticket = tickets.get(i); + inv.setItem(i, buildPlayerTicketItem(ticket)); + slotMap.put(i, ticket); + } + + fillEmpty(inv); + playerOwnSlotMap.put(player.getUniqueId(), slotMap); + player.openInventory(inv); + } + + // ═══════════════════════════════════════════════════════════════════════ + // ADMIN DETAIL-GUI (Aktionen für ein einzelnes Ticket) + // ═══════════════════════════════════════════════════════════════════════ + + public void openDetailGUI(Player player, Ticket ticket) { + Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE); + + // Slot 4: Ticket-Info (Mitte oben) + inv.setItem(4, buildDetailInfoItem(ticket)); + + // Slot 10: Teleportieren (immer verfügbar) + inv.setItem(10, buildActionItem( + Material.ENDER_PEARL, + "§b§lTeleportieren", + List.of("§7Teleportiert dich zur", "§7Position des Tickets."))); + + // Slot 12: Claimen (nur wenn OPEN), sonst grauer Platzhalter + if (ticket.getStatus() == TicketStatus.OPEN) { + inv.setItem(12, buildActionItem( + Material.LIME_WOOL, + "§a§lTicket annehmen", + List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet."))); + } else { + inv.setItem(12, buildActionItem( + Material.GRAY_WOOL, + "§8Bereits angenommen", + List.of("§7Dieses Ticket wurde bereits", "§7angenommen."))); + } + + // Slot 14: Schließen — für OPEN, CLAIMED und FORWARDED; grauer Block wenn bereits CLOSED + if (ticket.getStatus() != TicketStatus.CLOSED) { + inv.setItem(14, buildActionItem( + Material.RED_WOOL, + "§c§lTicket schließen", + List.of( + "§7Schließt das Ticket.", + "§8§m ", + "§eNach dem Klick kannst du im", + "§eChat einen Kommentar eingeben.", + "§7Tippe §c- §7für keinen Kommentar.", + "§7Tippe §ccancel §7zum Abbrechen."))); + } else { + inv.setItem(14, buildActionItem( + Material.GRAY_WOOL, + "§8Bereits geschlossen", + List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); + } + + // Slot 16: Zurück zur Übersicht + inv.setItem(16, buildActionItem( + Material.ARROW, + "§7§lZurück", + List.of("§7Zurück zur Ticket-Übersicht."))); + + fillEmpty(inv); + detailTicketMap.put(player.getUniqueId(), ticket); + player.openInventory(inv); + } + + // ═══════════════════════════════════════════════════════════════════════ + // CLICK-EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + String title = event.getView().getTitle(); + + if (!title.equals(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-Übersicht ────────────────────────────────────────────── + if (title.equals(GUI_TITLE)) { + Map slotMap = playerSlotMap.get(player.getUniqueId()); + if (slotMap == null) return; + Ticket ticket = slotMap.get(slot); + if (ticket == null) return; + + player.closeInventory(); + + // Frische Daten aus DB holen, dann Detail-GUI öffnen + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (fresh == null) { + player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); + return; + } + openDetailGUI(player, fresh); + }); + }); + return; + } + + // ── 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; + + player.closeInventory(); + + if (ticket.getStatus() == TicketStatus.OPEN) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + boolean deleted = plugin.getDatabaseManager().deleteTicket(ticket.getId()); + Bukkit.getScheduler().runTask(plugin, () -> { + if (deleted) { + player.sendMessage(plugin.color( + "&aDein Ticket &e#" + ticket.getId() + " &awurde gelöscht.")); + openPlayerGUI(player); + } else { + player.sendMessage(plugin.color("&cFehler beim Löschen des Tickets.")); + } + }); + }); + } else { + player.sendMessage(plugin.color( + "&cDieses Ticket kann nicht mehr gelöscht werden, " + + "da es bereits angenommen oder geschlossen wurde.")); + } + return; + } + + // ── 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 -> handleDetailClaim(player, ticket); + case 14 -> handleDetailClose(player, ticket); + case 16 -> openGUI(player); + // Glasscheiben und andere Slots → nichts tun + } + } + } + + // ─────────────────────────── Detail-Aktionen ─────────────────────────── + + private void handleDetailTeleport(Player player, Ticket ticket) { + if (ticket.getLocation() != null) { + player.teleport(ticket.getLocation()); + player.sendMessage(plugin.color( + "&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); + } else { + player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); + } + } + + private void handleDetailClaim(Player player, Ticket ticket) { + if (ticket.getStatus() != TicketStatus.OPEN) { + player.sendMessage(plugin.formatMessage("messages.already-claimed")); + return; + } + + 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())); + + plugin.getTicketManager().notifyCreatorClaimed(ticket); + + if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); + + // ── BUGFIX: Detail-GUI mit frischen DB-Daten neu öffnen ── + // Dadurch verschwindet der Claim-Button und der Schließen-Button + // ist sofort korrekt sichtbar, ohne dass der Admin die GUI + // erst schließen und neu öffnen muss. + 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")); + } + }); + }); + } + + private void handleDetailClose(Player player, Ticket ticket) { + if (ticket.getStatus() == TicketStatus.CLOSED) { + player.sendMessage(plugin.color("&cDieses Ticket ist bereits geschlossen.")); + return; + } + + awaitingComment.put(player.getUniqueId(), ticket.getId()); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Ticket #" + ticket.getId() + " schließen")); + player.sendMessage(plugin.color("&7Gib einen Kommentar für den Spieler ein.")); + player.sendMessage(plugin.color("&7Kein Kommentar? Tippe: &e-")); + player.sendMessage(plugin.color("&7Abbrechen? Tippe: &ccancel")); + player.sendMessage(plugin.color("&8&m ")); + } + + // ─────────────────────────── Chat-Listener (Kommentar-Eingabe) ───────── + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + if (!awaitingComment.containsKey(player.getUniqueId())) return; + + event.setCancelled(true); + int ticketId = awaitingComment.remove(player.getUniqueId()); + String input = event.getMessage().trim(); + + if (input.equalsIgnoreCase("cancel")) { + Bukkit.getScheduler().runTask(plugin, () -> + player.sendMessage(plugin.color("&cSchließen abgebrochen."))); + return; + } + + // "-" = bewusst kein Kommentar + final String comment = input.equals("-") ? "" : input; + + 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))); + + if (!comment.isEmpty()) { + player.sendMessage(plugin.color("&7Kommentar gespeichert: &f" + comment)); + } + + if (ticket != null) { + ticket.setCloseComment(comment); + plugin.getTicketManager().notifyCreatorClosed(ticket); + } + }); + } else { + Bukkit.getScheduler().runTask(plugin, () -> + player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); + } + }); + } + + // ═══════════════════════════════════════════════════════════════════════ + // 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; + default -> Material.PAPER; + }; + ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); if (meta == null) return item; - // Display-Name meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); - // Lore aufbauen List lore = new ArrayList<>(); lore.add("§8§m "); lore.add("§7Ersteller: §e" + ticket.getCreatorName()); @@ -94,22 +390,97 @@ 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.getClaimerName() != null) + lore.add("§7Angenommen: §a" + ticket.getClaimerName()); + if (ticket.getForwardedToName() != null) + lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); + lore.add("§8§m "); + lore.add("§e§l» KLICKEN für Details & Aktionen"); + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + private ItemStack buildDetailInfoItem(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; + }; + + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + meta.setDisplayName("§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())); + lore.add("§7Welt: §e" + ticket.getWorldName()); + lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); if (ticket.getClaimerName() != null) { lore.add("§8§m "); - lore.add("§7Geclaimt von: §a" + ticket.getClaimerName()); + lore.add("§7Angenommen von: §a" + ticket.getClaimerName()); if (ticket.getClaimedAt() != null) - lore.add("§7Geclaimt am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); + lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); } - if (ticket.getForwardedToName() != null) { + if (ticket.getForwardedToName() != null) lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); + if (ticket.getStatus() == TicketStatus.CLOSED) { + 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()); } - lore.add("§8§m "); - if (ticket.getStatus() == TicketStatus.OPEN) { - lore.add("§a§l» KLICKEN zum Claimen & Teleportieren"); - } else { - lore.add("§e§l» KLICKEN zum Teleportieren"); + + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + private ItemStack buildPlayerTicketItem(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; + }; + + ItemStack item = new ItemStack(mat); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); + + List lore = new ArrayList<>(); + lore.add("§8§m "); + lore.add("§7Anliegen: §f" + ticket.getMessage()); + 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.CLAIMED && ticket.getClaimerName() != null) + lore.add("§7Angenommen von: §a" + ticket.getClaimerName()); + if (ticket.getStatus() == TicketStatus.FORWARDED && ticket.getForwardedToName() != null) + lore.add("§7Bearbeiter: §6" + ticket.getForwardedToName()); + 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()); + } + lore.add("§8§m "); + switch (ticket.getStatus()) { + case OPEN -> { lore.add("§c§l» KLICKEN zum Löschen"); + lore.add("§7Nur möglich solange noch nicht angenommen."); } + case CLOSED -> lore.add("§8» Dieses Ticket ist abgeschlossen."); + default -> { lore.add("§e» Ticket wird bearbeitet..."); + lore.add("§7Kann nicht mehr gelöscht werden."); } } meta.setLore(lore); @@ -117,72 +488,32 @@ public class TicketGUI implements Listener { return item; } + private ItemStack buildActionItem(Material material, String displayName, List lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + meta.setDisplayName(displayName); + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + // ─────────────────────────── Hilfsmethoden ───────────────────────────── + + private int calcSize(int ticketCount) { + int size = (int) Math.ceil(ticketCount / 9.0) * 9; + return Math.max(9, Math.min(54, size)); + } + 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); } + 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); } } - - // ─────────────────────────── Klick-Event ─────────────────────────────── - - @EventHandler - public void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player)) return; - if (!event.getView().getTitle().equals(GUI_TITLE)) return; - - event.setCancelled(true); - - Map slotMap = playerSlotMap.get(player.getUniqueId()); - if (slotMap == null) return; - - int slot = event.getRawSlot(); - Ticket ticket = slotMap.get(slot); - if (ticket == null) return; - - player.closeInventory(); - - // Asynchron aus DB neu laden (aktuelle Daten) - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); - if (fresh == null) { - player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); - return; - } - - Bukkit.getScheduler().runTask(plugin, () -> handleTicketClick(player, fresh)); - }); - } - - private void handleTicketClick(Player player, Ticket ticket) { - // Versuche zu claimen, wenn noch OPEN - if (ticket.getStatus() == TicketStatus.OPEN) { - boolean success = plugin.getDatabaseManager().claimTicket( - ticket.getId(), player.getUniqueId(), player.getName()); - - if (success) { - ticket.setStatus(TicketStatus.CLAIMED); - ticket.setClaimerUUID(player.getUniqueId()); - ticket.setClaimerName(player.getName()); - - player.sendMessage(plugin.formatMessage("messages.ticket-claimed") - .replace("{id}", String.valueOf(ticket.getId())) - .replace("{player}", ticket.getCreatorName())); - - plugin.getTicketManager().notifyCreatorClaimed(ticket); - } else { - player.sendMessage(plugin.formatMessage("messages.already-claimed")); - } - } - - // Teleportation zur Ticket-Position - if (ticket.getLocation() != null) { - player.teleport(ticket.getLocation()); - player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); - } else { - player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); - } - } -} +} \ 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 420b829..a820698 100644 --- a/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java +++ b/src/main/java/de/ticketsystem/listeners/PlayerJoinListener.java @@ -1,7 +1,12 @@ package de.ticketsystem.listeners; +import java.util.List; + import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.Ticket; +import de.ticketsystem.model.TicketStatus; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -19,21 +24,56 @@ public class PlayerJoinListener implements Listener { public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - // Nur Supporter und Admins erhalten die Join-Benachrichtigung - if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) return; + // ── Supporter/Admin: offene Tickets anzeigen ────────────────────── + if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + int count = plugin.getDatabaseManager().countOpenTickets(); + if (count > 0) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + String msg = plugin.formatMessage("messages.join-open-tickets") + .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 + } + }); + } - // Verzögerung von 2 Sekunden damit die Join-Sequenz abgeschlossen ist + // ── Spieler: über geschlossene Tickets mit Kommentar informieren ── + // Nur wenn der Ersteller noch nicht live benachrichtigt wurde Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - int count = plugin.getDatabaseManager().countOpenTickets(); + List closed = plugin.getDatabaseManager() + .getTicketsByStatus(TicketStatus.CLOSED); - if (count > 0) { - Bukkit.getScheduler().runTaskLater(plugin, () -> { - String msg = plugin.formatMessage("messages.join-open-tickets") - .replace("{count}", String.valueOf(count)); - player.sendMessage(msg); - player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); - }, 40L); // 40 Ticks = 2 Sekunden + 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, () -> + plugin.getTicketManager().notifyCreatorClosed(t)); } }); + + // ── Update-Hinweis für OPs/Admins ──────────────────────────────── + if (player.isOp() || player.hasPermission("ticket.admin")) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + int resourceId = 132757; + new de.ticketsystem.UpdateChecker(plugin, resourceId).getVersion(version -> { + String current = plugin.getDescription().getVersion(); + if (!current.equals(version)) { + String bar = ChatColor.GOLD + "===================================================="; + player.sendMessage(bar); + player.sendMessage(ChatColor.GOLD + "[TicketSystem] " + + ChatColor.YELLOW + "NEUES UPDATE VERFÜGBAR: v" + version); + player.sendMessage(ChatColor.GOLD + "[TicketSystem] " + + ChatColor.YELLOW + "Download: https://www.spigotmc.org/resources/132757"); + player.sendMessage(bar); + } + }); + }, 20L); // 1 Sekunde + } } -} +} \ 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 33a2f6d..6bdda61 100644 --- a/src/main/java/de/ticketsystem/manager/TicketManager.java +++ b/src/main/java/de/ticketsystem/manager/TicketManager.java @@ -7,16 +7,21 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; public class TicketManager { private final TicketPlugin plugin; - // Cooldown Map: UUID → Zeit in Millis, wann das letzte Ticket erstellt wurde + /** Cooldown Map: UUID → Zeitstempel letztes Ticket */ private final Map cooldowns = new HashMap<>(); + /** Ticket-IDs für die der Ersteller bereits über Schließung informiert wurde */ + private final Set notifiedClosedTickets = new HashSet<>(); + public TicketManager(TicketPlugin plugin) { this.plugin = plugin; } @@ -42,7 +47,8 @@ public class TicketManager { // ─────────────────────────── Benachrichtigungen ──────────────────────── /** - * Benachrichtigt alle Online-Supporter und Admins über ein neues Ticket. + * Benachrichtigt alle Online-Supporter/Admins über ein neues Ticket + * und sendet optional eine Discord-Webhook-Nachricht. */ public void notifyTeam(Ticket ticket) { String msg = plugin.formatMessage("messages.new-ticket-notify") @@ -53,15 +59,16 @@ public class TicketManager { for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { p.sendMessage(msg); - - // Klickbaren Hinweis senden (Bukkit Chat-Component) p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.")); } } + + // Discord-Webhook (asynchron, kein Einfluss auf Server-Performance) + plugin.getDiscordWebhook().sendNewTicket(ticket); } /** - * Benachrichtigt den Ersteller des Tickets, wenn es geclaimt wurde. + * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. */ public void notifyCreatorClaimed(Ticket ticket) { Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); @@ -74,9 +81,24 @@ public class TicketManager { } /** - * Sendet dem weitergeleiteten Supporter eine Benachrichtigung. + * Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde. */ - public void notifyForwardedTo(Ticket ticket) { + public void notifyCreatorForwarded(Ticket ticket) { + Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); + 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("{supporter}", forwardedTo); + creator.sendMessage(msg); + } + } + + /** + * Sendet dem weitergeleiteten Supporter eine Benachrichtigung + * und informiert optional Discord. + */ + public void notifyForwardedTo(Ticket ticket, String fromName) { Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); if (target != null && target.isOnline()) { String msg = plugin.formatMessage("messages.ticket-forwarded-notify") @@ -84,13 +106,55 @@ public class TicketManager { .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. + * @param closerName Name des Admins/Supporters der es geschlossen hat (für Discord, kann null sein) + */ + 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 msg = plugin.formatMessage("messages.ticket-closed-notify") + .replace("{id}", String.valueOf(ticket.getId())) + .replace("{comment}", comment); + creator.sendMessage(msg); + + if (!comment.isEmpty()) { + creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment)); + } + } + + // 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); } // ─────────────────────────── Hilfsmethoden ───────────────────────────── - /** - * Prüft, ob ein Spieler zu viele offene Tickets hat. - */ public boolean hasReachedTicketLimit(UUID uuid) { int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); if (max <= 0) return false; @@ -102,10 +166,10 @@ public class TicketManager { 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 list &7– Deine Tickets ansehen (GUI)")); if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.color("&e/ticket list &7– Ticket-Übersicht (GUI)")); player.sendMessage(plugin.color("&e/ticket claim &7– Ticket annehmen")); - player.sendMessage(plugin.color("&e/ticket close &7– Ticket schließen")); + 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")); @@ -113,4 +177,4 @@ public class TicketManager { } player.sendMessage(plugin.color("&8&m ")); } -} +} \ 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 8836c6e..f5a7a16 100644 --- a/src/main/java/de/ticketsystem/model/Ticket.java +++ b/src/main/java/de/ticketsystem/model/Ticket.java @@ -3,18 +3,25 @@ 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; import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; -public class Ticket { + +@SerializableAs("Ticket") +public class Ticket implements ConfigurationSerializable { private int id; private UUID creatorUUID; private String creatorName; private String message; - // Location-Felder (werden separat gespeichert) private String worldName; private double x, y, z; private float yaw, pitch; @@ -27,9 +34,12 @@ public class Ticket { private Timestamp createdAt; private Timestamp claimedAt; private Timestamp closedAt; + private String closeComment; + public Ticket() {} + public Ticket(UUID creatorUUID, String creatorName, String message, Location location) { this.creatorUUID = creatorUUID; this.creatorName = creatorName; @@ -44,6 +54,101 @@ public class Ticket { 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); + + 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.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()); + } + + 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); + 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); + this.forwardedToName = (String) map.get("forwardedToName"); + } + } + + // --- 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("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 (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); + } + + return map; + } + + // --- NEU: Registrierung --- + 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; @@ -105,4 +210,7 @@ public class Ticket { 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; } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4b68bc3..881444d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -59,6 +59,16 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg # ---------------------------------------------------- auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) +# ---------------------------------------------------- +# DISCORD WEBHOOK (Optional) +# ---------------------------------------------------- +discord: + # Auf true setzen um Discord-Benachrichtigungen zu aktivieren + enabled: false + + # Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen) + webhook-url: "" + # ---------------------------------------------------- # SYSTEM-NACHRICHTEN (mit &-Farbcodes) # ---------------------------------------------------- @@ -82,13 +92,19 @@ messages: ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." ticket-closed: "&aTicket &e#{id} &awurde geschlossen." ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." - ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen." + 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 + 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." # --- FEHLER & HINWEISE --- no-permission: "&cDu hast keine Berechtigung!" no-open-tickets: "&aAktuell gibt es keine offenen Tickets." join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" - new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message}" + new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)" already-claimed: "&cDieses Ticket wurde bereits geclaimt!" ticket-not-found: "&cTicket nicht gefunden!" - cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." + cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 8ced8e5..4f3422e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.0.1 +version: 1.0.2 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper