Update from Git Manager GUI

This commit is contained in:
2026-02-20 12:31:38 +01:00
parent 526cb8b442
commit df6878db2f
11 changed files with 1622 additions and 811 deletions

View File

@@ -1,11 +1,13 @@
package de.ticketsystem; package de.ticketsystem;
import de.ticketsystem.commands.TicketCommand; import de.ticketsystem.commands.TicketCommand;
import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.database.DatabaseManager;
import de.ticketsystem.discord.DiscordWebhook;
import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.gui.TicketGUI;
import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.listeners.PlayerJoinListener;
import de.ticketsystem.manager.TicketManager; import de.ticketsystem.manager.TicketManager;
// WICHTIG: Import hinzugefügt
import de.ticketsystem.model.Ticket;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@@ -19,15 +21,19 @@ public class TicketPlugin extends JavaPlugin {
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private TicketManager ticketManager; private TicketManager ticketManager;
private TicketGUI ticketGUI; private TicketGUI ticketGUI;
private DiscordWebhook discordWebhook;
@Override @Override
public void onEnable() { public void onEnable() {
instance = this; instance = this;
// Config speichern falls nicht vorhanden
saveDefaultConfig(); saveDefaultConfig();
// Update-Checker (Spigot Resource-ID anpassen!) // --- WICHTIG: Ticket-Klasse registrieren ---
Ticket.register();
// -------------------------------------------
// Update-Checker
int resourceId = 132757; int resourceId = 132757;
new UpdateChecker(this, resourceId).getVersion(version -> { new UpdateChecker(this, resourceId).getVersion(version -> {
String current = getDescription().getVersion(); String current = getDescription().getVersion();
@@ -35,12 +41,11 @@ public class TicketPlugin extends JavaPlugin {
String msg = ChatColor.translateAlternateColorCodes('&', String msg = ChatColor.translateAlternateColorCodes('&',
"&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); "&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 + ")"); 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().getScheduler().runTaskLater(this, () -> {
getServer().getOnlinePlayers().stream() getServer().getOnlinePlayers().stream()
.filter(p -> p.hasPermission("ticket.admin")) .filter(p -> p.hasPermission("ticket.admin"))
.forEach(p -> p.sendMessage(msg)); .forEach(p -> p.sendMessage(msg));
}, 20L); // 20 Ticks = 1 Sekunde }, 20L);
} else { } else {
getLogger().info("TicketSystem ist aktuell (Version " + current + ")"); getLogger().info("TicketSystem ist aktuell (Version " + current + ")");
} }
@@ -50,46 +55,51 @@ public class TicketPlugin extends JavaPlugin {
String configVersion = getConfig().getString("version", ""); String configVersion = getConfig().getString("version", "");
String expectedVersion = "2.0"; String expectedVersion = "2.0";
if (!expectedVersion.equals(configVersion)) { 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); debug = getConfig().getBoolean("debug", false);
// Datenbankverbindung aufbauen // Datenbankverbindung
databaseManager = new DatabaseManager(this); databaseManager = new DatabaseManager(this);
if (!databaseManager.connect()) { if (!databaseManager.connect()) {
getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); 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 // Manager, GUI & Discord-Webhook initialisieren
ticketManager = new TicketManager(this); ticketManager = new TicketManager(this);
ticketGUI = new TicketGUI(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); TicketCommand ticketCommand = new TicketCommand(this);
Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand);
Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand);
// Events registrieren
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(ticketGUI, 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); int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
if (archiveIntervalH > 0) { if (archiveIntervalH > 0) {
long ticks = archiveIntervalH * 60L * 60L * 20L; // Stunden → Ticks long ticks = archiveIntervalH * 60L * 60L * 20L;
getServer().getScheduler().runTaskTimer(this, () -> { getServer().getScheduler().runTaskTimer(this, () -> {
int archived = databaseManager.archiveClosedTickets(); int archived = databaseManager.archiveClosedTickets();
if (archived > 0) { if (archived > 0) {
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierung ausgeführt, " + archived + " Tickets verschoben.");
} }
}, ticks, ticks); }, ticks, ticks);
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert."); getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierungs-Timer gesetzt: alle " + archiveIntervalH + " Stunden.");
} }
getLogger().info("TicketSystem erfolgreich gestartet!"); getLogger().info("TicketSystem erfolgreich gestartet!");
@@ -97,26 +107,18 @@ public class TicketPlugin extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
if (databaseManager != null) { if (databaseManager != null) databaseManager.disconnect();
databaseManager.disconnect();
}
getLogger().info("TicketSystem wurde deaktiviert."); getLogger().info("TicketSystem wurde deaktiviert.");
} }
// ─────────────────────────── Hilfsmethoden ───────────────────────────── // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Formatiert eine Nachricht aus der Config mit Prefix und Farben.
*/
public String formatMessage(String path) { 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); String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path);
return prefix + color(message); return prefix + color(message);
} }
/**
* Konvertiert Farbcodes (&x → §x).
*/
public String color(String text) { public String color(String text) {
return ChatColor.translateAlternateColorCodes('&', text); return ChatColor.translateAlternateColorCodes('&', text);
} }
@@ -127,9 +129,6 @@ public class TicketPlugin extends JavaPlugin {
public DatabaseManager getDatabaseManager() { return databaseManager; } public DatabaseManager getDatabaseManager() { return databaseManager; }
public TicketManager getTicketManager() { return ticketManager; } public TicketManager getTicketManager() { return ticketManager; }
public TicketGUI getTicketGUI() { return ticketGUI; } public TicketGUI getTicketGUI() { return ticketGUI; }
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
/**
* Gibt zurück, ob der Debug-Modus aktiv ist (aus config.yml)
*/
public boolean isDebug() { return debug; } public boolean isDebug() { return debug; }
} }

View File

@@ -26,7 +26,11 @@ public class UpdateChecker {
Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { 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)) { try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) {
if (scann.hasNext()) { 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) { } catch (IOException e) {
plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); plugin.getLogger().info("Unable to check for updates: " + e.getMessage());

View File

@@ -1,4 +1,3 @@
package de.ticketsystem.commands; package de.ticketsystem.commands;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
@@ -9,19 +8,259 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; 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.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class TicketCommand implements CommandExecutor, TabCompleter { 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 <Beschreibung>"));
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 <ID>"));
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 <ID> [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 <ID> <Spieler>"));
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) { private void handleMigrate(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -34,14 +273,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int migrated = 0; int migrated = 0;
String mode = args[1].toLowerCase(); String mode = args[1].toLowerCase();
if (mode.equals("tomysql")) { if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL();
migrated = plugin.getDatabaseManager().migrateToMySQL(); else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile();
} else if (mode.equals("tofile")) { else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; }
migrated = plugin.getDatabaseManager().migrateToFile();
} else {
player.sendMessage(plugin.formatMessage("messages.unknown-mode"));
return;
}
int finalMigrated = migrated; int finalMigrated = migrated;
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (finalMigrated > 0) { if (finalMigrated > 0) {
@@ -54,6 +288,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket export ────────────────────────────
private void handleExport(Player player, String[] args) { private void handleExport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -78,6 +314,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket import ────────────────────────────
private void handleImport(Player player, String[] args) { private void handleImport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -106,6 +344,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket stats ─────────────────────────────
private void handleStats(Player player) { private void handleStats(Player player) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -114,308 +354,37 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var stats = plugin.getDatabaseManager().getTicketStats(); var stats = plugin.getDatabaseManager().getTicketStats();
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage("§6--- Ticket Statistik ---"); player.sendMessage(plugin.color("&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(plugin.color("&eGesamt: &a" + stats.total
player.sendMessage("§6Top Ersteller:"); + " &7| &eOffen: &a" + stats.open
stats.byPlayer.entrySet().stream().sorted((a,b)->b.getValue()-a.getValue()).limit(5).forEach(e -> + " &7| &eGeschlossen: &a" + stats.closed
player.sendMessage("§e" + e.getKey() + ": §a" + e.getValue()) + " &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 <Beschreibung>"));
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 <ID>"));
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 <ID>"));
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 <ID> <Spieler>"));
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 ──────────────────────────── // ─────────────────────────── Tab-Completion ────────────────────────────
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
String label, String[] args) {
List<String> completions = new ArrayList<>(); List<String> completions = new ArrayList<>();
if (!(sender instanceof Player player)) return completions; if (!(sender instanceof Player player)) return completions;
if (args.length == 1) { if (args.length == 1) {
List<String> subs = new ArrayList<>(); List<String> subs = new ArrayList<>(List.of("create", "list"));
subs.add("create"); if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"))
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { subs.addAll(List.of("claim", "close"));
subs.addAll(List.of("list", "claim", "close")); if (player.hasPermission("ticket.admin"))
} subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import"));
if (player.hasPermission("ticket.admin")) { for (String s : subs)
subs.addAll(List.of("forward", "reload"));
}
for (String s : subs) {
if (s.startsWith(args[0].toLowerCase())) completions.add(s); if (s.startsWith(args[0].toLowerCase())) completions.add(s);
}
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers())
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
completions.add(p.getName());
}
} }
return completions; return completions;
} }

View File

@@ -1,4 +1,3 @@
package de.ticketsystem.database; package de.ticketsystem.database;
import java.io.File; import java.io.File;
@@ -14,7 +13,6 @@ import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus; import de.ticketsystem.model.TicketStatus;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
@@ -23,295 +21,9 @@ import java.io.FileWriter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
public class DatabaseManager { 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<Ticket> all = getAllTickets();
List<Ticket> 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<Ticket> all = getAllTickets();
int open = 0, closed = 0, forwarded = 0;
java.util.Map<String, Integer> 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 { // ─────────────────────────── Felder ────────────────────────────────────
public final int total, open, closed, forwarded;
public final java.util.Map<String, Integer> byPlayer;
public TicketStats(int total, int open, int closed, int forwarded, java.util.Map<String, Integer> 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<Ticket> 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;
}
}
/**
* 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<Ticket> getAllTickets() {
List<Ticket> 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 final TicketPlugin plugin;
private HikariDataSource dataSource; private HikariDataSource dataSource;
private boolean useMySQL; private boolean useMySQL;
@@ -319,22 +31,26 @@ public class DatabaseManager {
private File dataFile; private File dataFile;
private YamlConfiguration dataConfig; private YamlConfiguration dataConfig;
private JSONArray dataJson; private JSONArray dataJson;
private String dataFileName;
private String archiveFileName;
// ─────────────────────────── Konstruktoren ─────────────────────────────
public DatabaseManager(TicketPlugin plugin) { public DatabaseManager(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); 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); 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"); String archivePath = plugin.getConfig().getString("archive-file", "archive.json");
this.dataFileName = dataPath; this.dataFileName = dataPath;
this.archiveFileName = archivePath; this.archiveFileName = archivePath;
if (!useMySQL) { if (!useMySQL) {
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath);
if (useJson) { if (useJson) {
dataFile = resolvePath(dataPath); dataFile = resolvePath(dataPath);
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] JSON-Datei: " + dataFile.getAbsolutePath());
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
dataFile.getParentFile().mkdirs(); dataFile.getParentFile().mkdirs();
@@ -346,7 +62,7 @@ public class DatabaseManager {
} else { } else {
try { try {
JSONParser parser = new JSONParser(); JSONParser parser = new JSONParser();
dataJson = (JSONArray) parser.parse(new java.io.FileReader(dataFile)); dataJson = (JSONArray) parser.parse(new FileReader(dataFile));
} catch (Exception e) { } catch (Exception e) {
sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage());
dataJson = new JSONArray(); dataJson = new JSONArray();
@@ -354,7 +70,6 @@ public class DatabaseManager {
} }
} else { } else {
dataFile = resolvePath(dataPath); dataFile = resolvePath(dataPath);
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] YAML-Datei: " + dataFile.getAbsolutePath());
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
dataFile.getParentFile().mkdirs(); 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) { private File resolvePath(String path) {
File f = new File(path); File f = new File(path);
if (f.isAbsolute()) return f; 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) { private void sendError(String msg) {
if (plugin != null) plugin.getLogger().severe(msg); if (plugin != null) plugin.getLogger().severe(msg);
// Fehler an alle Admins im Chat senden if (Bukkit.getServer() != null) {
Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); Bukkit.getOnlinePlayers().stream()
.filter(p -> p.hasPermission("ticket.admin"))
.forEach(p -> p.sendMessage("§c[TicketSystem] " + msg));
}
} }
// ─────────────────────────── Verbindung ──────────────────────────────── // ─────────────────────────── Verbindung ────────────────────────────────
@@ -389,7 +119,8 @@ public class DatabaseManager {
if (useMySQL) { if (useMySQL) {
try { try {
HikariConfig config = new HikariConfig(); 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().getString("mysql.host"),
plugin.getConfig().getInt("mysql.port"), plugin.getConfig().getInt("mysql.port"),
plugin.getConfig().getString("mysql.database"))); plugin.getConfig().getString("mysql.database")));
@@ -403,14 +134,17 @@ public class DatabaseManager {
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config); dataSource = new HikariDataSource(config);
// Tabellen anlegen & fehlende Spalten ergänzen
createTables(); createTables();
ensureColumns();
plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt.");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e);
plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!"); plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!");
useMySQL = false; useMySQL = false;
// Datei-Storage initialisieren
dataFile = new File(plugin.getDataFolder(), "data.yml"); dataFile = new File(plugin.getDataFolder(), "data.yml");
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
@@ -434,7 +168,6 @@ public class DatabaseManager {
dataSource.close(); dataSource.close();
plugin.getLogger().info("MySQL-Verbindung getrennt."); plugin.getLogger().info("MySQL-Verbindung getrennt.");
} }
// Bei Datei-Storage nichts zu tun
} }
private Connection getConnection() throws SQLException { private Connection getConnection() throws SQLException {
@@ -444,6 +177,7 @@ public class DatabaseManager {
// ─────────────────────────── Tabellen erstellen ──────────────────────── // ─────────────────────────── Tabellen erstellen ────────────────────────
private void createTables() { private void createTables() {
// close_comment ist jetzt von Anfang an in der CREATE-Anweisung enthalten
String sql = """ String sql = """
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -462,8 +196,9 @@ public class DatabaseManager {
forwarded_to_uuid VARCHAR(36), forwarded_to_uuid VARCHAR(36),
forwarded_to_name VARCHAR(16), forwarded_to_name VARCHAR(16),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
claimed_at TIMESTAMP, claimed_at TIMESTAMP NULL,
closed_at TIMESTAMP closed_at TIMESTAMP NULL,
close_comment VARCHAR(500) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""; """;
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { 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 ────────────────────────────────────── // ─────────────────────────── CRUD ──────────────────────────────────────
/** /**
@@ -507,7 +268,6 @@ public class DatabaseManager {
} }
return -1; return -1;
} else { } else {
// Datei-Storage: Ticket-ID generieren
int id = dataConfig.getInt("lastId", 0) + 1; int id = dataConfig.getInt("lastId", 0) + 1;
ticket.setId(id); ticket.setId(id);
dataConfig.set("lastId", id); dataConfig.set("lastId", id);
@@ -517,7 +277,6 @@ public class DatabaseManager {
backupDataFile(); backupDataFile();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); 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; return id;
} }
@@ -547,7 +306,7 @@ public class DatabaseManager {
t.setStatus(TicketStatus.CLAIMED); t.setStatus(TicketStatus.CLAIMED);
t.setClaimerUUID(claimerUUID); t.setClaimerUUID(claimerUUID);
t.setClaimerName(claimerName); t.setClaimerName(claimerName);
t.setClaimedAt(new java.sql.Timestamp(System.currentTimeMillis())); t.setClaimedAt(new Timestamp(System.currentTimeMillis()));
dataConfig.set("tickets." + ticketId, t); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); dataConfig.save(dataFile);
@@ -562,11 +321,15 @@ public class DatabaseManager {
/** /**
* Schließt ein Ticket (Status → CLOSED). * Schließt ein Ticket (Status → CLOSED).
*/ */
public boolean closeTicket(int ticketId) { public boolean closeTicket(int ticketId, String closeComment) {
if (useMySQL) { 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)) { 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; return ps.executeUpdate() > 0;
} catch (SQLException e) { } catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), 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); Ticket t = getTicketById(ticketId);
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
t.setStatus(TicketStatus.CLOSED); 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); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); 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). * 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. * Gibt alle Tickets mit einem bestimmten Status zurück.
*/ */
@@ -642,7 +440,6 @@ public class DatabaseManager {
} }
return list; return list;
} else { } else {
// Datei-Storage: Alle Tickets filtern
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); Ticket t = (Ticket) dataConfig.get("tickets." + key);
@@ -655,6 +452,29 @@ public class DatabaseManager {
} }
} }
/**
* Gibt alle Tickets zurück (alle Status).
*/
public List<Ticket> getAllTickets() {
List<Ticket> 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. * 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() { public int countOpenTickets() {
if (useMySQL) { 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()) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(sql); ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) return rs.getInt(1); if (rs.next()) return rs.getInt(1);
@@ -695,7 +515,7 @@ public class DatabaseManager {
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); 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; return count;
@@ -721,17 +541,176 @@ public class DatabaseManager {
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); 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; return count;
} }
} }
// ─────────────────────────── Archivierung ──────────────────────────────
/**
* Archiviert alle geschlossenen Tickets in eine separate Datei.
*/
public int archiveClosedTickets() {
List<Ticket> all = getAllTickets();
List<Ticket> 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<Ticket> all = getAllTickets();
int open = 0, claimed = 0, forwarded = 0, closed = 0;
java.util.Map<String, Integer> 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<String, Integer> byPlayer;
public TicketStats(int total, int open, int closed, int forwarded, java.util.Map<String, Integer> byPlayer) {
this.total = total; this.open = open; this.closed = closed;
this.forwarded = forwarded; this.byPlayer = byPlayer;
}
}
// ─────────────────────────── Export / Import ────────────────────────────
public int exportTickets(File exportFile) {
List<Ticket> 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 ─────────────────────────────────── // ─────────────────────────── 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 { private Ticket mapRow(ResultSet rs) throws SQLException {
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
Ticket t = new Ticket(); Ticket t = new Ticket();
t.setId(rs.getInt("id")); t.setId(rs.getInt("id"));
t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid"))); t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid")));
@@ -748,6 +727,16 @@ public class DatabaseManager {
t.setClaimedAt(rs.getTimestamp("claimed_at")); t.setClaimedAt(rs.getTimestamp("claimed_at"));
t.setClosedAt(rs.getTimestamp("closed_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"); String claimerUUID = rs.getString("claimer_uuid");
if (claimerUUID != null) { if (claimerUUID != null) {
t.setClaimerUUID(UUID.fromString(claimerUUID)); t.setClaimerUUID(UUID.fromString(claimerUUID));
@@ -760,4 +749,100 @@ public class DatabaseManager {
} }
return t; 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
}
} }

View File

@@ -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();
}
});
}
}

View File

@@ -4,12 +4,13 @@ import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus; import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
@@ -23,19 +24,35 @@ import java.util.UUID;
public class TicketGUI implements Listener { 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 static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm");
private final TicketPlugin plugin; private final TicketPlugin plugin;
// Speichert welcher Spieler welches Ticket an welchem Slot hat /** Admin-Übersicht: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>();
/** Spieler-GUI: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>();
/** Detail-Ansicht: Player-UUID → Ticket */
private final Map<UUID, Ticket> detailTicketMap = new HashMap<>();
/** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */
private final Map<UUID, Integer> awaitingComment = new HashMap<>();
public TicketGUI(TicketPlugin plugin) { public TicketGUI(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
// ─────────────────────────── GUI öffnen ──────────────────────────────── // ═══════════════════════════════════════════════════════════════════════
// ADMIN / SUPPORTER GUI (Übersicht aller Tickets)
// ═══════════════════════════════════════════════════════════════════════
public void openGUI(Player player) { public void openGUI(Player player) {
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus( List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(
@@ -46,47 +63,326 @@ public class TicketGUI implements Listener {
return; return;
} }
// Inventar-Größe: nächste Vielfaches von 9 (max. 54 Slots) int size = calcSize(tickets.size());
int size = Math.min(54, (int) (Math.ceil(tickets.size() / 9.0) * 9));
if (size < 9) size = 9;
Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>(); Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 54; i++) { for (int i = 0; i < tickets.size() && i < 54; i++) {
Ticket ticket = tickets.get(i); Ticket ticket = tickets.get(i);
ItemStack item = buildTicketItem(ticket); inv.setItem(i, buildAdminListItem(ticket));
inv.setItem(i, item);
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
// Trennlinie am Ende, wenn Platz
fillEmpty(inv); fillEmpty(inv);
playerSlotMap.put(player.getUniqueId(), slotMap); playerSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
// ─────────────────────────── Item bauen ──────────────────────────────── // ═══════════════════════════════════════════════════════════════════════
// SPIELER-GUI (nur eigene Tickets, mit Lösch-Option bei OPEN)
// ═══════════════════════════════════════════════════════════════════════
private ItemStack buildTicketItem(Ticket ticket) { public void openPlayerGUI(Player player) {
// Material je nach Status List<Ticket> all = plugin.getDatabaseManager().getTicketsByStatus(
Material mat; TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED);
switch (ticket.getStatus()) {
case OPEN -> mat = Material.PAPER; List<Ticket> tickets = new ArrayList<>();
case CLAIMED -> mat = Material.YELLOW_DYE; for (Ticket t : all) {
case FORWARDED -> mat = Material.ORANGE_DYE; if (t.getCreatorUUID().equals(player.getUniqueId())) tickets.add(t);
default -> mat = Material.PAPER;
} }
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<Integer, Ticket> 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<Integer, Ticket> 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<Integer, Ticket> 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); ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta(); ItemMeta meta = item.getItemMeta();
if (meta == null) return item; if (meta == null) return item;
// Display-Name
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
// Lore aufbauen
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Ersteller: §e" + ticket.getCreatorName()); 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("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName()); lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); 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<String> 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) { if (ticket.getClaimerName() != null) {
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Geclaimt von: §a" + ticket.getClaimerName()); lore.add("§7Angenommen von: §a" + ticket.getClaimerName());
if (ticket.getClaimedAt() != null) 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()); 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 ");
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<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
if (ticket.getStatus() == TicketStatus.OPEN) { lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§a§l» KLICKEN zum Claimen & Teleportieren"); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
} else { lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add("§e§l» KLICKEN zum Teleportieren"); 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); meta.setLore(lore);
@@ -117,72 +488,32 @@ public class TicketGUI implements Listener {
return item; return item;
} }
private ItemStack buildActionItem(Material material, String displayName, List<String> 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) { private void fillEmpty(Inventory inv) {
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = glass.getItemMeta(); 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++) { for (int i = 0; i < inv.getSize(); i++) {
if (inv.getItem(i) == null) inv.setItem(i, glass); 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<Integer, Ticket> 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!"));
}
}
} }

View File

@@ -1,7 +1,12 @@
package de.ticketsystem.listeners; package de.ticketsystem.listeners;
import java.util.List;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -19,21 +24,56 @@ public class PlayerJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
// Nur Supporter und Admins erhalten die Join-Benachrichtigung // ── Supporter/Admin: offene Tickets anzeigen ──────────────────────
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) return; if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
// Verzögerung von 2 Sekunden damit die Join-Sequenz abgeschlossen ist
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().countOpenTickets(); int count = plugin.getDatabaseManager().countOpenTickets();
if (count > 0) { if (count > 0) {
Bukkit.getScheduler().runTaskLater(plugin, () -> { Bukkit.getScheduler().runTaskLater(plugin, () -> {
String msg = plugin.formatMessage("messages.join-open-tickets") String msg = plugin.formatMessage("messages.join-open-tickets")
.replace("{count}", String.valueOf(count)); .replace("{count}", String.valueOf(count));
player.sendMessage(msg); player.sendMessage(msg);
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
}, 40L); // 40 Ticks = 2 Sekunden }, 40L); // 2 Sekunden Verzögerung
} }
}); });
} }
// ── Spieler: über geschlossene Tickets mit Kommentar informieren ──
// Nur wenn der Ersteller noch nicht live benachrichtigt wurde
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
List<Ticket> closed = plugin.getDatabaseManager()
.getTicketsByStatus(TicketStatus.CLOSED);
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
}
}
} }

View File

@@ -7,16 +7,21 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
public class TicketManager { public class TicketManager {
private final TicketPlugin plugin; 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<UUID, Long> cooldowns = new HashMap<>(); private final Map<UUID, Long> cooldowns = new HashMap<>();
/** Ticket-IDs für die der Ersteller bereits über Schließung informiert wurde */
private final Set<Integer> notifiedClosedTickets = new HashSet<>();
public TicketManager(TicketPlugin plugin) { public TicketManager(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@@ -42,7 +47,8 @@ public class TicketManager {
// ─────────────────────────── Benachrichtigungen ──────────────────────── // ─────────────────────────── 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) { public void notifyTeam(Ticket ticket) {
String msg = plugin.formatMessage("messages.new-ticket-notify") String msg = plugin.formatMessage("messages.new-ticket-notify")
@@ -53,15 +59,16 @@ public class TicketManager {
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(msg); p.sendMessage(msg);
// Klickbaren Hinweis senden (Bukkit Chat-Component)
p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.")); 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) { public void notifyCreatorClaimed(Ticket ticket) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); 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()); Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
if (target != null && target.isOnline()) { if (target != null && target.isOnline()) {
String msg = plugin.formatMessage("messages.ticket-forwarded-notify") String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
@@ -84,13 +106,55 @@ public class TicketManager {
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()));
target.sendMessage(msg); 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 ───────────────────────────── // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Prüft, ob ein Spieler zu viele offene Tickets hat.
*/
public boolean hasReachedTicketLimit(UUID uuid) { public boolean hasReachedTicketLimit(UUID uuid) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
if (max <= 0) return false; if (max <= 0) return false;
@@ -102,10 +166,10 @@ public class TicketManager {
player.sendMessage(plugin.color("&6TicketSystem &7 Befehle")); player.sendMessage(plugin.color("&6TicketSystem &7 Befehle"));
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&e/ticket create <Text> &7 Neues Ticket erstellen")); player.sendMessage(plugin.color("&e/ticket create <Text> &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")) { 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 <ID> &7 Ticket annehmen")); player.sendMessage(plugin.color("&e/ticket claim <ID> &7 Ticket annehmen"));
player.sendMessage(plugin.color("&e/ticket close <ID> &7 Ticket schließen")); player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7 Ticket schließen"));
} }
if (player.hasPermission("ticket.admin")) { if (player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7 Ticket weiterleiten")); player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7 Ticket weiterleiten"));

View File

@@ -3,18 +3,25 @@ package de.ticketsystem.model;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; 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.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class Ticket {
@SerializableAs("Ticket")
public class Ticket implements ConfigurationSerializable {
private int id; private int id;
private UUID creatorUUID; private UUID creatorUUID;
private String creatorName; private String creatorName;
private String message; private String message;
// Location-Felder (werden separat gespeichert)
private String worldName; private String worldName;
private double x, y, z; private double x, y, z;
private float yaw, pitch; private float yaw, pitch;
@@ -27,9 +34,12 @@ public class Ticket {
private Timestamp createdAt; private Timestamp createdAt;
private Timestamp claimedAt; private Timestamp claimedAt;
private Timestamp closedAt; private Timestamp closedAt;
private String closeComment;
public Ticket() {} public Ticket() {}
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) { public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
this.creatorUUID = creatorUUID; this.creatorUUID = creatorUUID;
this.creatorName = creatorName; this.creatorName = creatorName;
@@ -44,6 +54,101 @@ public class Ticket {
this.createdAt = new Timestamp(System.currentTimeMillis()); this.createdAt = new Timestamp(System.currentTimeMillis());
} }
// --- NEU: Konstruktor zum Laden aus der YAML (Deserialisierung) ---
public Ticket(Map<String, Object> 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<String, Object> serialize() {
Map<String, Object> 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() { public Location getLocation() {
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
if (world == null) return null; if (world == null) return null;
@@ -105,4 +210,7 @@ public class Ticket {
public Timestamp getClosedAt() { return closedAt; } public Timestamp getClosedAt() { return closedAt; }
public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; } public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; }
public String getCloseComment() { return closeComment; }
public void setCloseComment(String closeComment) { this.closeComment = closeComment; }
} }

View File

@@ -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) 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) # SYSTEM-NACHRICHTEN (mit &-Farbcodes)
# ---------------------------------------------------- # ----------------------------------------------------
@@ -82,13 +92,19 @@ messages:
ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen."
ticket-closed: "&aTicket &e#{id} &awurde geschlossen." ticket-closed: "&aTicket &e#{id} &awurde geschlossen."
ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." 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 --- # --- FEHLER & HINWEISE ---
no-permission: "&cDu hast keine Berechtigung!" no-permission: "&cDu hast keine Berechtigung!"
no-open-tickets: "&aAktuell gibt es keine offenen Tickets." no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" 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!" already-claimed: "&cDieses Ticket wurde bereits geclaimt!"
ticket-not-found: "&cTicket nicht gefunden!" 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."

View File

@@ -1,5 +1,5 @@
name: TicketSystem name: TicketSystem
version: 1.0.1 version: 1.0.2
main: de.ticketsystem.TicketPlugin main: de.ticketsystem.TicketPlugin
api-version: 1.20 api-version: 1.20
author: M_Viper author: M_Viper