Update from Git Manager GUI

This commit is contained in:
2026-02-21 00:55:27 +01:00
parent a91e17a097
commit b7de357e81
15 changed files with 2313 additions and 1034 deletions

View File

@@ -5,8 +5,8 @@ import de.ticketsystem.database.DatabaseManager;
import de.ticketsystem.discord.DiscordWebhook; 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.CategoryManager;
import de.ticketsystem.manager.TicketManager; import de.ticketsystem.manager.TicketManager;
// WICHTIG: Import hinzugefügt
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@@ -20,6 +20,7 @@ public class TicketPlugin extends JavaPlugin {
private boolean debug; private boolean debug;
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private TicketManager ticketManager; private TicketManager ticketManager;
private CategoryManager categoryManager;
private TicketGUI ticketGUI; private TicketGUI ticketGUI;
private DiscordWebhook discordWebhook; private DiscordWebhook discordWebhook;
@@ -29,9 +30,8 @@ public class TicketPlugin extends JavaPlugin {
saveDefaultConfig(); saveDefaultConfig();
// --- WICHTIG: Ticket-Klasse registrieren --- // Ticket-Klasse für YAML-Serialisierung registrieren
Ticket.register(); Ticket.register();
// -------------------------------------------
// Update-Checker // Update-Checker
int resourceId = 132757; int resourceId = 132757;
@@ -68,6 +68,7 @@ public class TicketPlugin extends JavaPlugin {
} }
// Manager, GUI & Discord-Webhook initialisieren // Manager, GUI & Discord-Webhook initialisieren
categoryManager = new CategoryManager(this);
ticketManager = new TicketManager(this); ticketManager = new TicketManager(this);
ticketGUI = new TicketGUI(this); ticketGUI = new TicketGUI(this);
discordWebhook = new DiscordWebhook(this); discordWebhook = new DiscordWebhook(this);
@@ -128,6 +129,7 @@ public class TicketPlugin extends JavaPlugin {
public static TicketPlugin getInstance() { return instance; } public static TicketPlugin getInstance() { return instance; }
public DatabaseManager getDatabaseManager() { return databaseManager; } public DatabaseManager getDatabaseManager() { return databaseManager; }
public TicketManager getTicketManager() { return ticketManager; } public TicketManager getTicketManager() { return ticketManager; }
public CategoryManager getCategoryManager() { return categoryManager; }
public TicketGUI getTicketGUI() { return ticketGUI; } public TicketGUI getTicketGUI() { return ticketGUI; }
public DiscordWebhook getDiscordWebhook() { return discordWebhook; } public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
public boolean isDebug() { return debug; } public boolean isDebug() { return debug; }

View File

@@ -2,7 +2,13 @@ package de.ticketsystem.commands;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import de.ticketsystem.manager.CategoryManager;
import de.ticketsystem.model.ConfigCategory;
import de.ticketsystem.model.TicketComment;
import de.ticketsystem.model.TicketPriority;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@@ -12,14 +18,13 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID;
public class TicketCommand implements CommandExecutor, TabCompleter { public class TicketCommand implements CommandExecutor, TabCompleter {
private final TicketPlugin plugin; private final TicketPlugin plugin;
public TicketCommand(TicketPlugin plugin) { public TicketCommand(TicketPlugin plugin) { this.plugin = plugin; }
this.plugin = plugin;
}
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
@@ -27,10 +32,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden."); sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden.");
return true; return true;
} }
if (args.length == 0) { if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; }
plugin.getTicketManager().sendHelpMessage(player);
return true;
}
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "create" -> handleCreate(player, args); case "create" -> handleCreate(player, args);
case "list" -> handleList(player); case "list" -> handleList(player);
@@ -43,6 +46,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
case "import" -> handleImport(player, args); case "import" -> handleImport(player, args);
case "stats" -> handleStats(player); case "stats" -> handleStats(player);
case "archive" -> handleArchive(player); case "archive" -> handleArchive(player);
case "comment" -> handleComment(player, args);
case "blacklist" -> handleBlacklist(player, args);
case "rate" -> handleRate(player, args);
case "setpriority" -> handleSetPriority(player, args);
default -> plugin.getTicketManager().sendHelpMessage(player); default -> plugin.getTicketManager().sendHelpMessage(player);
} }
return true; return true;
@@ -52,44 +59,113 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
private void handleCreate(Player player, String[] args) { private void handleCreate(Player player, String[] args) {
if (!player.hasPermission("ticket.create")) { if (!player.hasPermission("ticket.create")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
// Blacklist-Check
if (plugin.getDatabaseManager().isBlacklisted(player.getUniqueId())) {
player.sendMessage(plugin.color("&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen."));
return; return;
} }
if (args.length < 2) { if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket create <Beschreibung>")); player.sendMessage(plugin.color("&cBenutzung: /ticket create [Kategorie] [Priorität] <Beschreibung>"));
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
player.sendMessage(plugin.color("&7Kategorien: &ebug&7, &efrage&7, &ebeschwerde&7, &esonstiges&7, &eallgemein"));
}
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
player.sendMessage(plugin.color("&7Prioritäten: &alow&7, &enormal&7, &6high&7, &curgent"));
}
return; return;
} }
if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) { if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) {
long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId()); long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId());
player.sendMessage(plugin.formatMessage("messages.cooldown") player.sendMessage(plugin.formatMessage("messages.cooldown").replace("{seconds}", String.valueOf(remaining)));
.replace("{seconds}", String.valueOf(remaining)));
return; return;
} }
if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) { if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
player.sendMessage(plugin.color("&cDu hast bereits &e" + max player.sendMessage(plugin.color("&cDu hast bereits &e" + max + " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde."));
+ " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde."));
return; return;
} }
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
// Kategorie und Priorität optional parsen
CategoryManager cm = plugin.getCategoryManager();
ConfigCategory category = cm.getDefault();
TicketPriority priority = TicketPriority.NORMAL;
int messageStartIndex = 1;
boolean categoriesOn = plugin.getConfig().getBoolean("categories-enabled", true);
boolean prioritiesOn = plugin.getConfig().getBoolean("priorities-enabled", true);
if (args.length >= 3) {
// args[1]: erst als Kategorie prüfen, dann als Priorität
if (categoriesOn) {
ConfigCategory parsedCat = cm.resolve(args[1]);
if (parsedCat != null) {
category = parsedCat;
messageStartIndex = 2;
// args[2]: Priorität prüfen (nur wenn danach noch Text kommt)
if (prioritiesOn && args.length >= 4) {
TicketPriority parsedPrio = parsePriority(args[2]);
if (parsedPrio != null) {
priority = parsedPrio;
messageStartIndex = 3;
}
}
} else {
// Keine Kategorie erkannt → args[1] als Priorität prüfen
if (prioritiesOn) {
TicketPriority parsedPrio = parsePriority(args[1]);
if (parsedPrio != null) {
priority = parsedPrio;
messageStartIndex = 2;
}
}
}
} else if (prioritiesOn) {
// Kategorien aus → args[1] direkt als Priorität prüfen
TicketPriority parsedPrio = parsePriority(args[1]);
if (parsedPrio != null) {
priority = parsedPrio;
messageStartIndex = 2;
}
}
} else if (args.length == 2) {
// Nur ein Argument: könnte Kategorie oder Priorität sein, aber kein Text danach
// → einfach als Beschreibung behandeln, nichts parsen
}
String message = String.join(" ", Arrays.copyOfRange(args, messageStartIndex, args.length));
int maxLen = plugin.getConfig().getInt("max-description-length", 100); int maxLen = plugin.getConfig().getInt("max-description-length", 100);
if (message.isEmpty()) {
player.sendMessage(plugin.color("&cBitte gib eine Beschreibung für dein Ticket an."));
return;
}
if (message.length() > maxLen) { if (message.length() > maxLen) {
player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen.")); player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen."));
return; return;
} }
final ConfigCategory finalCategory = category;
final TicketPriority finalPriority = priority;
Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
ticket.setCategoryKey(finalCategory.getKey());
ticket.setPriority(finalPriority);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int id = plugin.getDatabaseManager().createTicket(ticket); int id = plugin.getDatabaseManager().createTicket(ticket);
if (id == -1) { if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; }
player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!"));
return;
}
ticket.setId(id); ticket.setId(id);
plugin.getTicketManager().setCooldown(player.getUniqueId()); plugin.getTicketManager().setCooldown(player.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-created") String catInfo = plugin.getConfig().getBoolean("categories-enabled", true)
.replace("{id}", String.valueOf(id))); ? " §7[" + finalCategory.getColored() + "§7]" : "";
plugin.getTicketManager().notifyTeam(ticket); // ruft auch Discord-Webhook auf String prioInfo = plugin.getConfig().getBoolean("priorities-enabled", true)
? " §7[" + finalPriority.getColored() + "§7]" : "";
player.sendMessage(plugin.formatMessage("messages.ticket-created").replace("{id}", String.valueOf(id)) + catInfo + prioInfo);
plugin.getTicketManager().notifyTeam(ticket);
}); });
}); });
} }
@@ -99,12 +175,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
private void handleList(Player player) { private void handleList(Player player) {
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () -> plugin.getTicketGUI().openGUI(player)));
plugin.getTicketGUI().openGUI(player)));
} else { } else {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () -> plugin.getTicketGUI().openPlayerGUI(player)));
plugin.getTicketGUI().openPlayerGUI(player)));
} }
} }
@@ -112,13 +186,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
private void handleClaim(Player player, String[] args) { private void handleClaim(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket claim <ID>"));
return;
} }
if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket claim <ID>")); return; }
int id; int id;
try { id = Integer.parseInt(args[1]); } try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
@@ -143,40 +213,30 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
private void handleClose(Player player, String[] args) { private void handleClose(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket close <ID> [Kommentar]"));
return;
} }
if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket close <ID> [Kommentar]")); return; }
int id; int id;
try { id = Integer.parseInt(args[1]); } try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
String closeComment = args.length > 2 String closeComment = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : "";
? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : "";
final int ticketId = id; final int ticketId = id;
final String comment = closeComment; final String comment = closeComment;
final String closer = player.getName();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
if (success) { if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-closed") player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
.replace("{id}", String.valueOf(ticketId)));
if (ticket != null) { if (ticket != null) {
ticket.setCloseComment(comment); ticket.setCloseComment(comment);
// closerName für Discord-Nachricht mitgeben plugin.getTicketManager().notifyCreatorClosed(ticket, player.getName());
plugin.getTicketManager().notifyCreatorClosed(ticket, closer);
} }
}); });
} else { } else {
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
} }
}); });
} }
@@ -185,161 +245,260 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
private void handleForward(Player player, String[] args) { private void handleForward(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")); return;
return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket forward <ID> <Spieler>"));
return;
} }
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket forward <ID> <Spieler>")); return; }
int id; int id;
try { id = Integer.parseInt(args[1]); } try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
Player target = Bukkit.getPlayer(args[2]); Player target = Bukkit.getPlayer(args[2]);
if (target == null || !target.isOnline()) { if (target == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); return; }
player.sendMessage(plugin.color("&cSpieler &e" + args[2] + " &cist nicht online!"));
final int ticketId = id;
final String fromName = player.getName();
final Player t = target;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName());
Bukkit.getScheduler().runTask(plugin, () -> {
if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; }
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) return;
player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet."));
plugin.getTicketManager().notifyForwardedTo(ticket, fromName);
plugin.getTicketManager().notifyCreatorForwarded(ticket);
});
});
}
// ─────────────────────────── /ticket comment ───────────────────────────
private void handleComment(Player player, String[] args) {
if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket comment <ID> <Nachricht>")); return; }
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
String msg = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
if (msg.length() > 500) { player.sendMessage(plugin.color("&cNachricht zu lang! Maximal 500 Zeichen.")); return; }
final int ticketId = id;
final String message = msg;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
return;
}
// Spieler darf nur auf eigene Tickets kommentieren (Supporter/Admin: alle)
boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId());
boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin");
if (!isOwner && !isStaff) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets kommentieren.")));
return;
}
TicketComment comment = new TicketComment(ticketId, player.getUniqueId(), player.getName(), message);
boolean success = plugin.getDatabaseManager().addComment(comment);
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
player.sendMessage(plugin.color("&aDein Kommentar zu Ticket &e#" + ticketId + " &awurde gespeichert."));
// Supporter/Admin und Ticket-Ersteller benachrichtigen
notifyCommentReceivers(player, ticket, message);
} else {
player.sendMessage(plugin.color("&cFehler beim Speichern des Kommentars."));
}
});
});
}
private void notifyCommentReceivers(Player author, Ticket ticket, String message) {
String onlineMsg = plugin.color("&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert: &f" + message);
String offlineMsg = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat kommentiert (während du offline warst): &f" + message;
// Ticket-Ersteller benachrichtigen (wenn nicht der Autor selbst)
if (!ticket.getCreatorUUID().equals(author.getUniqueId())) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) {
creator.sendMessage(onlineMsg);
} else {
// Offline → für nächsten Login speichern
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg));
}
}
// Supporter/Admin benachrichtigen (wenn Autor kein Supporter ist)
if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) {
// Claimer des Tickets bevorzugt benachrichtigen
UUID claimerUUID = ticket.getClaimerUUID();
if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) {
Player claimer = Bukkit.getPlayer(claimerUUID);
if (claimer != null && claimer.isOnline()) {
claimer.sendMessage(onlineMsg);
} else {
String claimerOffline = "&e[Ticket #" + ticket.getId() + "] &f" + author.getName() + " &7hat auf dein bearbeitetes Ticket kommentiert (offline): &f" + message;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline));
}
}
// Alle anderen Online-Supporter zusätzlich informieren
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.getUniqueId().equals(author.getUniqueId())) continue;
if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; // schon oben
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(onlineMsg);
}
}
}
}
// ─────────────────────────── /ticket rate ──────────────────────────────
private void handleRate(Player player, String[] args) {
if (!plugin.getConfig().getBoolean("rating-enabled", true)) {
player.sendMessage(plugin.color("&cBewertungen sind deaktiviert.")); return;
}
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket rate <ID> <good|bad>")); return; }
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
String ratingArg = args[2].toLowerCase();
String rating;
if (ratingArg.equals("good") || ratingArg.equals("gut") || ratingArg.equals("thumbsup")) {
rating = "THUMBS_UP";
} else if (ratingArg.equals("bad") || ratingArg.equals("schlecht") || ratingArg.equals("thumbsdown")) {
rating = "THUMBS_DOWN";
} else {
player.sendMessage(plugin.color("&cUngültige Bewertung! Benutze &egood &coder &ebad&c."));
return; return;
} }
final int ticketId = id; final int ticketId = id;
final Player finalTarget = target; final String finalRating = rating;
final String fromName = player.getName();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager()
.forwardTicket(ticketId, finalTarget.getUniqueId(), finalTarget.getName());
if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
return;
}
if (!ticket.getCreatorUUID().equals(player.getUniqueId())) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu kannst nur deine eigenen Tickets bewerten.")));
return;
}
if (ticket.hasRating()) {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cDu hast dieses Ticket bereits bewertet.")));
return;
}
boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-forwarded") if (success) {
.replace("{id}", String.valueOf(ticketId)) String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ";
.replace("{player}", finalTarget.getName())); player.sendMessage(plugin.color("&aDanke für deine Bewertung! (" + emoji + "§a)"));
if (ticket != null) {
// fromName für Discord mitgeben
plugin.getTicketManager().notifyForwardedTo(ticket, fromName);
plugin.getTicketManager().notifyCreatorForwarded(ticket);
}
});
} else { } else {
Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cBewertung konnte nicht gespeichert werden. Ticket geschlossen?"));
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
} }
}); });
});
}
// ─────────────────────────── /ticket blacklist ─────────────────────────
private void handleBlacklist(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist <add|remove|list> [Spieler] [Grund]"));
return;
}
switch (args[1].toLowerCase()) {
case "add" -> {
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist add <Spieler> [Grund]")); return; }
String targetName = args[2];
String reason = args.length > 3 ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "Missbrauch";
@SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
if (target.getUniqueId() == null) { player.sendMessage(plugin.color("&cSpieler nicht gefunden.")); return; }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().addBlacklist(
target.getUniqueId(), targetName, reason, player.getName());
Bukkit.getScheduler().runTask(plugin, () -> {
if (success)
player.sendMessage(plugin.color("&a" + targetName + " &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e" + reason));
else
player.sendMessage(plugin.color("&cSpieler ist bereits auf der Blacklist."));
});
});
}
case "remove" -> {
if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove <Spieler>")); return; }
String targetName = args[2];
@SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().removeBlacklist(target.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) player.sendMessage(plugin.color("&a" + targetName + " &awurde von der Blacklist entfernt."));
else player.sendMessage(plugin.color("&cSpieler war nicht auf der Blacklist."));
});
});
}
case "list" -> {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var list = plugin.getDatabaseManager().getBlacklist();
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Ticket-Blacklist &7(" + list.size() + " Einträge)"));
player.sendMessage(plugin.color("&8&m "));
if (list.isEmpty()) {
player.sendMessage(plugin.color("&7Keine gesperrten Spieler."));
} else {
for (String[] entry : list) {
// {uuid, name, reason, bannedBy, bannedAt}
player.sendMessage(plugin.color("&e" + entry[1] + " &7 &f" + entry[2]
+ " &7(gesperrt von &e" + entry[3] + "&7)"));
}
}
player.sendMessage(plugin.color("&8&m "));
});
});
}
default -> player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist <add|remove|list> [Spieler] [Grund]"));
}
} }
// ─────────────────────────── /ticket reload ──────────────────────────── // ─────────────────────────── /ticket reload ────────────────────────────
private void handleReload(Player player) { private void handleReload(Player player) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
plugin.reloadConfig(); plugin.reloadConfig();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen.")); plugin.getCategoryManager().reload();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)"));
} }
// ─────────────────────────── /ticket archive ─────────────────────────── // ─────────────────────────── /ticket archive ───────────────────────────
private void handleArchive(Player player) { private void handleArchive(Player player) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().archiveClosedTickets(); int count = plugin.getDatabaseManager().archiveClosedTickets();
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) { if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count)));
player.sendMessage(plugin.formatMessage("messages.archive-success") else player.sendMessage(plugin.formatMessage("messages.archive-fail"));
.replace("{count}", String.valueOf(count)));
} else {
player.sendMessage(plugin.formatMessage("messages.archive-fail"));
}
});
});
}
// ─────────────────────────── /ticket migrate ───────────────────────────
private void handleMigrate(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket migrate <tomysql|tofile>"));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int migrated = 0;
String mode = args[1].toLowerCase();
if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL();
else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile();
else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; }
int finalMigrated = migrated;
Bukkit.getScheduler().runTask(plugin, () -> {
if (finalMigrated > 0) {
player.sendMessage(plugin.formatMessage("messages.migration-success")
.replace("{count}", String.valueOf(finalMigrated)));
} else {
player.sendMessage(plugin.formatMessage("messages.migration-fail"));
}
});
});
}
// ─────────────────────────── /ticket export ────────────────────────────
private void handleExport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket export <Dateiname>"));
return;
}
String filename = args[1];
File exportFile = new File(plugin.getDataFolder(), filename);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().exportTickets(exportFile);
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.export-success")
.replace("{count}", String.valueOf(count)).replace("{file}", filename));
} else {
player.sendMessage(plugin.formatMessage("messages.export-fail"));
}
});
});
}
// ─────────────────────────── /ticket import ────────────────────────────
private void handleImport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket import <Dateiname>"));
return;
}
String filename = args[1];
File importFile = new File(plugin.getDataFolder(), filename);
if (!importFile.exists()) {
player.sendMessage(plugin.formatMessage("messages.file-not-found").replace("{file}", filename));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().importTickets(importFile);
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.import-success")
.replace("{count}", String.valueOf(count)));
} else {
player.sendMessage(plugin.formatMessage("messages.import-fail"));
}
}); });
}); });
} }
@@ -347,44 +506,190 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
// ─────────────────────────── /ticket stats ───────────────────────────── // ─────────────────────────── /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")); return; }
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
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(plugin.color("&6--- Ticket Statistik ---")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&eGesamt: &a" + stats.total player.sendMessage(plugin.color("&6Ticket Statistik"));
+ " &7| &eOffen: &a" + stats.open player.sendMessage(plugin.color("&8&m "));
+ " &7| &eGeschlossen: &a" + stats.closed player.sendMessage(plugin.color("&eGesamt: &a" + stats.total));
+ " &7| &eWeitergeleitet: &a" + stats.forwarded)); player.sendMessage(plugin.color("&eOffen: &a" + stats.open));
player.sendMessage(plugin.color("&eGeschlossen: &a" + stats.closed));
player.sendMessage(plugin.color("&eWeitergeleitet: &a" + stats.forwarded));
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Support-Bewertungen"));
player.sendMessage(plugin.color("&a👍 Positiv: &f" + stats.thumbsUp
+ " &c👎 Negativ: &f" + stats.thumbsDown));
int total = stats.thumbsUp + stats.thumbsDown;
if (total > 0) {
int percent = (int) Math.round(stats.thumbsUp * 100.0 / total);
player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%"));
}
}
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Top Ersteller:")); player.sendMessage(plugin.color("&6Top Ersteller:"));
stats.byPlayer.entrySet().stream() stats.byPlayer.entrySet().stream()
.sorted((a, b) -> b.getValue() - a.getValue()) .sorted((a, b) -> b.getValue() - a.getValue())
.limit(5) .limit(5)
.forEach(e -> player.sendMessage(plugin.color("&e " + e.getKey() + ": &a" + e.getValue()))); .forEach(e -> player.sendMessage(plugin.color("&e " + e.getKey() + ": &a" + e.getValue())));
player.sendMessage(plugin.color("&8&m "));
}); });
}); });
} }
// ─────────────────────────── /ticket migrate ───────────────────────────
private void handleMigrate(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket migrate <tomysql|tofile>")); return; }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int migrated = 0;
String mode = args[1].toLowerCase();
if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL();
else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile();
else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; }
int f = migrated;
Bukkit.getScheduler().runTask(plugin, () -> {
if (f > 0) player.sendMessage(plugin.formatMessage("messages.migration-success").replace("{count}", String.valueOf(f)));
else player.sendMessage(plugin.formatMessage("messages.migration-fail"));
});
});
}
// ─────────────────────────── /ticket export ────────────────────────────
private void handleExport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket export <Dateiname>")); return; }
String filename = args[1];
File exportFile = new File(plugin.getDataFolder(), filename);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().exportTickets(exportFile);
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) player.sendMessage(plugin.formatMessage("messages.export-success").replace("{count}", String.valueOf(count)).replace("{file}", filename));
else player.sendMessage(plugin.formatMessage("messages.export-fail"));
});
});
}
// ─────────────────────────── /ticket import ────────────────────────────
private void handleImport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; }
if (args.length < 2) { player.sendMessage(plugin.color("&cBenutzung: /ticket import <Dateiname>")); return; }
String filename = args[1];
File importFile = new File(plugin.getDataFolder(), filename);
if (!importFile.exists()) { player.sendMessage(plugin.formatMessage("messages.file-not-found").replace("{file}", filename)); return; }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().importTickets(importFile);
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) player.sendMessage(plugin.formatMessage("messages.import-success").replace("{count}", String.valueOf(count)));
else player.sendMessage(plugin.formatMessage("messages.import-fail"));
});
});
}
// ─────────────────────────── /ticket setpriority ──────────────────────
private void handleSetPriority(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); return;
}
if (!plugin.getConfig().getBoolean("priorities-enabled", true)) {
player.sendMessage(plugin.color("&cDas Prioritäten-System ist deaktiviert.")); return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket setpriority <ID> <low|normal|high|urgent>")); return;
}
int ticketId;
try { ticketId = Integer.parseInt(args[1]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige Ticket-ID: &e" + args[1])); return;
}
TicketPriority priority = parsePriority(args[2]);
if (priority == null) {
player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().setTicketPriority(ticketId, priority);
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + ticketId
+ " &awurde auf " + priority.getColored() + " &agesetzt."));
} else {
player.sendMessage(plugin.color("&cTicket &e#" + ticketId + " &cwurde nicht gefunden."));
}
});
});
}
/** Parst Benutzer-Eingaben wie "high", "hoch", "urgent", "dringend" etc. zu TicketPriority.
* Gibt null zurück wenn keine Übereinstimmung. */
private TicketPriority parsePriority(String input) {
if (input == null) return null;
return switch (input.toLowerCase()) {
case "low", "niedrig" -> TicketPriority.LOW;
case "normal" -> TicketPriority.NORMAL;
case "high", "hoch" -> TicketPriority.HIGH;
case "urgent", "dringend" -> TicketPriority.URGENT;
default -> null;
};
}
// ─────────────────────────── Tab-Completion ──────────────────────────── // ─────────────────────────── Tab-Completion ────────────────────────────
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) { public List<String> onTabComplete(CommandSender sender, Command command, 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.of("create", "list")); List<String> subs = new ArrayList<>(List.of("create", "list", "comment"));
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("claim", "close"));
if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate");
if (player.hasPermission("ticket.admin")) if (player.hasPermission("ticket.admin"))
subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import")); subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import", "blacklist"));
for (String s : subs) if ((player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"))
if (s.startsWith(args[0].toLowerCase())) completions.add(s); && plugin.getConfig().getBoolean("priorities-enabled", true))
subs.add("setpriority");
for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s);
} else if (args.length == 2 && args[0].equalsIgnoreCase("create")
&& plugin.getConfig().getBoolean("categories-enabled", true)) {
for (ConfigCategory c : plugin.getCategoryManager().getAll())
if (c.getKey().startsWith(args[1].toLowerCase())) completions.add(c.getKey());
// auch Priorität direkt ohne Kategorie anbieten
if (plugin.getConfig().getBoolean("priorities-enabled", true))
for (String p : List.of("low", "normal", "high", "urgent"))
if (p.startsWith(args[1].toLowerCase())) completions.add(p);
} else if (args.length == 3 && args[0].equalsIgnoreCase("create")
&& plugin.getConfig().getBoolean("priorities-enabled", true)) {
// Priorität nach Kategorie
for (String p : List.of("low", "normal", "high", "urgent"))
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
} else if (args.length == 3 && args[0].equalsIgnoreCase("setpriority")) {
for (String p : List.of("low", "normal", "high", "urgent"))
if (p.startsWith(args[2].toLowerCase())) completions.add(p);
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { } 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())) completions.add(p.getName()); if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
} else if (args.length == 2 && args[0].equalsIgnoreCase("blacklist")) {
completions.addAll(List.of("add", "remove", "list"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("blacklist")
&& (args[1].equalsIgnoreCase("add") || args[1].equalsIgnoreCase("remove"))) {
for (Player p : Bukkit.getOnlinePlayers())
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
} else if (args.length == 3 && args[0].equalsIgnoreCase("rate")) {
completions.addAll(List.of("good", "bad"));
} }
return completions; return completions;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package de.ticketsystem.discord; package de.ticketsystem.discord;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.ConfigCategory;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -12,7 +13,39 @@ import java.time.Instant;
/** /**
* Sendet Benachrichtigungen an einen Discord-Webhook. * Sendet Benachrichtigungen an einen Discord-Webhook.
* Unterstützt Embeds mit Farbe, Feldern und Timestamp. * Unterstützt Embeds mit Farbe, Feldern, Timestamp, Kategorie, Priorität und Rollen-Ping.
*
* Relevante config.yml-Felder:
*
* discord:
* enabled: true
* webhook-url: "https://discord.com/api/webhooks/..."
* role-ping-id: "" # Rollen-ID für @Ping, leer = kein Ping
* messages:
* new-ticket:
* title: "🎫 Neues Ticket erstellt"
* color: "3066993"
* footer: "TicketSystem"
* show-position: true
* show-category: true
* show-priority: true
* role-ping: true # Ping bei neuem Ticket an/aus
* ticket-closed:
* enabled: false
* title: "🔒 Ticket geschlossen"
* color: "15158332"
* footer: "TicketSystem"
* show-category: true
* show-priority: true
* role-ping: false
* ticket-forwarded:
* enabled: false
* title: "🔀 Ticket weitergeleitet"
* color: "15105570"
* footer: "TicketSystem"
* show-category: true
* show-priority: true
* role-ping: false
*/ */
public class DiscordWebhook { public class DiscordWebhook {
@@ -30,32 +63,37 @@ public class DiscordWebhook {
public void sendNewTicket(Ticket ticket) { public void sendNewTicket(Ticket ticket) {
if (!isEnabled()) return; if (!isEnabled()) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); String webhookUrl = getWebhookUrl();
if (webhookUrl.isEmpty()) return; if (webhookUrl == null) return;
// Felder aus Config lesen
String title = plugin.getConfig().getString ("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt"); 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 color = plugin.getConfig().getString ("discord.messages.new-ticket.color", "3066993");
String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem"); String footer = plugin.getConfig().getString ("discord.messages.new-ticket.footer", "TicketSystem");
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true); boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
boolean showCat = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-category", true);
boolean showPri = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-priority", true);
boolean ping = plugin.getConfig().getBoolean("discord.messages.new-ticket.role-ping", true);
// JSON-Embed aufbauen
StringBuilder fields = new StringBuilder(); StringBuilder fields = new StringBuilder();
fields.append(field("Spieler", ticket.getCreatorName(), true)); fields.append(field("Spieler", ticket.getCreatorName(), true));
fields.append(","); fields.append(",").append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(field("Ticket ID", "#" + ticket.getId(), true)); fields.append(",").append(field("Anliegen", ticket.getMessage(), false));
fields.append(",");
fields.append(field("Anliegen", ticket.getMessage(), false));
if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
fields.append(",").append(field("Kategorie", cat.getName(), true));
}
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
}
if (showPos) { if (showPos) {
fields.append(","); fields.append(",").append(field("Welt", ticket.getWorldName(), true));
fields.append(field("Welt", ticket.getWorldName(), true)); fields.append(",").append(field("Position",
fields.append(",");
fields.append(field("Position",
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true)); String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
} }
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); String content = ping ? buildRolePing() : "";
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json); sendAsync(webhookUrl, json);
} }
@@ -66,25 +104,34 @@ public class DiscordWebhook {
if (!isEnabled()) return; if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return; if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); String webhookUrl = getWebhookUrl();
if (webhookUrl.isEmpty()) return; if (webhookUrl == null) return;
String title = plugin.getConfig().getString ("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen"); 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 color = plugin.getConfig().getString ("discord.messages.ticket-closed.color", "15158332");
String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem"); String footer = plugin.getConfig().getString ("discord.messages.ticket-closed.footer", "TicketSystem");
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-category", true);
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-closed.show-priority", true);
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-closed.role-ping", false);
StringBuilder fields = new StringBuilder(); StringBuilder fields = new StringBuilder();
fields.append(field("Ticket ID", "#" + ticket.getId(), true)); fields.append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(","); fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
fields.append(field("Ersteller", ticket.getCreatorName(), true)); fields.append(",").append(field("Geschlossen von", closerName, true));
fields.append(",");
fields.append(field("Geschlossen von", closerName, true)); if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
fields.append(",").append(field("Kategorie", cat.getName(), true));
}
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
}
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
fields.append(","); fields.append(",").append(field("Kommentar", ticket.getCloseComment(), false));
fields.append(field("Kommentar", ticket.getCloseComment(), false));
} }
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer); String content = ping ? buildRolePing() : "";
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json); sendAsync(webhookUrl, json);
} }
@@ -95,23 +142,32 @@ public class DiscordWebhook {
if (!isEnabled()) return; if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return; if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", ""); String webhookUrl = getWebhookUrl();
if (webhookUrl.isEmpty()) return; if (webhookUrl == null) return;
String title = plugin.getConfig().getString ("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet"); 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 color = plugin.getConfig().getString ("discord.messages.ticket-forwarded.color", "15105570");
String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem"); String footer = plugin.getConfig().getString ("discord.messages.ticket-forwarded.footer", "TicketSystem");
boolean showCat = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-category", true);
boolean showPri = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.show-priority", true);
boolean ping = plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.role-ping", false);
StringBuilder fields = new StringBuilder(); StringBuilder fields = new StringBuilder();
fields.append(field("Ticket ID", "#" + ticket.getId(), true)); fields.append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(","); fields.append(",").append(field("Ersteller", ticket.getCreatorName(), true));
fields.append(field("Ersteller", ticket.getCreatorName(), true)); fields.append(",").append(field("Weitergeleitet von", fromName, true));
fields.append(","); fields.append(",").append(field("Weitergeleitet an", ticket.getForwardedToName(), true));
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); if (showCat && plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
fields.append(",").append(field("Kategorie", cat.getName(), true));
}
if (showPri && plugin.getConfig().getBoolean("priorities-enabled", true)) {
fields.append(",").append(field("Priorität", ticket.getPriority().getDisplayName(), true));
}
String content = ping ? buildRolePing() : "";
String json = buildPayload(content, title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json); sendAsync(webhookUrl, json);
} }
@@ -121,11 +177,26 @@ public class DiscordWebhook {
return plugin.getConfig().getBoolean("discord.enabled", false); return plugin.getConfig().getBoolean("discord.enabled", false);
} }
/** Gibt die Webhook-URL zurück oder null wenn nicht gesetzt. */
private String getWebhookUrl() {
String url = plugin.getConfig().getString("discord.webhook-url", "");
return url.isEmpty() ? null : url;
}
/**
* Baut den @Rollen-Ping-String aus der konfigurierten Rollen-ID.
* Leer wenn keine ID gesetzt.
*/
private String buildRolePing() {
String roleId = plugin.getConfig().getString("discord.role-ping-id", "").trim();
if (roleId.isEmpty()) return "";
return "<@&" + roleId + ">";
}
/** /**
* Baut einen einzelnen Embed-Field als JSON-String. * Baut einen einzelnen Embed-Field als JSON-String.
*/ */
private String field(String name, String value, boolean inline) { private String field(String name, String value, boolean inline) {
// Anführungszeichen und Backslashes im Wert escapen
String safeValue = value != null String safeValue = value != null
? value.replace("\\", "\\\\").replace("\"", "\\\"") ? value.replace("\\", "\\\\").replace("\"", "\\\"")
: ""; : "";
@@ -136,11 +207,17 @@ public class DiscordWebhook {
/** /**
* Baut den kompletten Webhook-Payload als JSON. * Baut den kompletten Webhook-Payload als JSON.
* content = optionaler Ping-Text außerhalb des Embeds.
*/ */
private String buildPayload(String title, int color, String fieldsJson, String footer) { private String buildPayload(String content, String title, int color, String fieldsJson, String footer) {
String timestamp = Instant.now().toString(); // ISO-8601 String timestamp = Instant.now().toString();
String safeTitle = title.replace("\"", "\\\"");
String safeFooter = footer.replace("\"", "\\\"");
String safeContent = content.replace("\"", "\\\"");
return String.format(""" return String.format("""
{ {
"content": "%s",
"embeds": [{ "embeds": [{
"title": "%s", "title": "%s",
"color": %d, "color": %d,
@@ -149,10 +226,11 @@ public class DiscordWebhook {
"timestamp": "%s" "timestamp": "%s"
}] }]
}""", }""",
title.replace("\"", "\\\""), safeContent,
safeTitle,
color, color,
fieldsJson, fieldsJson,
footer.replace("\"", "\\\""), safeFooter,
timestamp); timestamp);
} }
@@ -180,7 +258,6 @@ public class DiscordWebhook {
plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode); plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode);
} }
// 204 = No Content → Erfolg bei Discord
if (responseCode != 200 && responseCode != 204) { if (responseCode != 200 && responseCode != 204) {
plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode); plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode);
} }

View File

@@ -2,6 +2,10 @@ package de.ticketsystem.gui;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import de.ticketsystem.manager.CategoryManager;
import de.ticketsystem.model.ConfigCategory;
import de.ticketsystem.model.TicketComment;
import de.ticketsystem.model.TicketPriority;
import de.ticketsystem.model.TicketStatus; import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
@@ -16,127 +20,144 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class TicketGUI implements Listener { public class TicketGUI implements Listener {
// ─────────────────────────── Titel-Konstanten ────────────────────────── // ─────────────────────────── Titel-Konstanten ──────────────────────────
private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht private static final String GUI_TITLE = "§8§lTicket-Übersicht";
private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv"; // Admin: Geschlossene Tickets private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv";
private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; // Spieler: eigene Tickets private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets";
private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details"; // Admin: Detail-Ansicht private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details";
/** Permission für den Zugriff auf das Archiv */
private static final String ARCHIVE_PERMISSION = "ticket.archive"; private static final String ARCHIVE_PERMISSION = "ticket.archive";
/** Ticket-Slots pro Seite (Reihen 04, Slots 044) */
private static final int PAGE_SIZE = 45;
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;
// ─────────────────────────── State-Maps ────────────────────────────────
/** Admin-Übersicht: Slot → Ticket */ /** Admin-Übersicht: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>();
/** Admin-Archiv: Slot → Ticket */ /** Admin-Archiv: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerClosedSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerClosedSlotMap = new HashMap<>();
/** Spieler-GUI: Slot → Ticket */ /** Spieler-GUI: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>();
/** Detail-Ansicht: UUID → Ticket */
/** Detail-Ansicht: Player-UUID → Ticket */
private final Map<UUID, Ticket> detailTicketMap = new HashMap<>(); private final Map<UUID, Ticket> detailTicketMap = new HashMap<>();
/** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */ /** Aktuelle Seite pro Spieler (Admin, Archiv, Spieler) */
private final Map<UUID, Integer> adminPage = new HashMap<>();
private final Map<UUID, Integer> archivePage= new HashMap<>();
private final Map<UUID, Integer> playerPage = new HashMap<>();
/** Kategorie-Filter für die Admin-GUI: null = alle */
private final Map<UUID, ConfigCategory> categoryFilter = new HashMap<>();
/** Wartet auf Chat-Eingabe für Close-Kommentar */
private final Map<UUID, Integer> awaitingComment = new HashMap<>(); private final Map<UUID, Integer> awaitingComment = new HashMap<>();
/** /** Aus Archiv heraus in Detail gegangen */
* Merkt, welche Spieler die Detail-Ansicht aus dem Archiv heraus geöffnet haben,
* damit der Zurück-Button wieder ins Archiv führt (statt in die Hauptübersicht).
*/
private final Set<UUID> viewingFromArchive = new HashSet<>(); private final Set<UUID> viewingFromArchive = new HashSet<>();
public TicketGUI(TicketPlugin plugin) { // ─────────────────────────── Konstruktor ───────────────────────────────
this.plugin = plugin;
} public TicketGUI(TicketPlugin plugin) { this.plugin = plugin; }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ADMIN / SUPPORTER GUI (Feste 54 Slots mit Archiv-Button) // ADMIN / SUPPORTER GUI (paginiert, mit Kategorie-Filter)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openGUI(Player player) { public void openGUI(Player player) { openGUI(player, adminPage.getOrDefault(player.getUniqueId(), 0)); }
// Lade nur offene/aktive Tickets
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus( public void openGUI(Player player, int page) {
adminPage.put(player.getUniqueId(), page);
List<Ticket> all = plugin.getDatabaseManager().getTicketsByStatus(
TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED);
// Admin GUI hat immer 54 Slots (6 Reihen) für feste Buttons // Kategorie-Filter anwenden
ConfigCategory filter = categoryFilter.getOrDefault(player.getUniqueId(), null);
if (filter != null && plugin.getConfig().getBoolean("categories-enabled", true)) {
all.removeIf(t -> !t.getCategoryKey().equals(filter.getKey()));
}
// Priorität-Sortierung (URGENT → HIGH → NORMAL → LOW)
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
all.sort((a, b) -> b.getPriority().ordinal() - a.getPriority().ordinal());
}
int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE));
page = Math.max(0, Math.min(page, totalPages - 1));
adminPage.put(player.getUniqueId(), page);
Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE); Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>(); Map<Integer, Ticket> slotMap = new HashMap<>();
// Tickets in die ersten 5 Reihen (0-44) füllen int start = page * PAGE_SIZE;
for (int i = 0; i < tickets.size() && i < 45; i++) { for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) {
Ticket ticket = tickets.get(i); Ticket ticket = all.get(start + i);
inv.setItem(i, buildAdminListItem(ticket)); inv.setItem(i, buildAdminListItem(ticket));
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
// Letzte Reihe (45-53) mit Navigations-Items füllen fillAdminNavigation(inv, false, player, page, totalPages);
// Archiv-Button nur anzeigen wenn der Spieler die Archiv-Permission hat
fillAdminNavigation(inv, false, player);
playerSlotMap.put(player.getUniqueId(), slotMap); playerSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ADMIN ARCHIV GUI (Geschlossene Tickets) nur mit ticket.archive // ADMIN ARCHIV GUI
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openClosedGUI(Player player) { public void openClosedGUI(Player player) { openClosedGUI(player, archivePage.getOrDefault(player.getUniqueId(), 0)); }
// ── Permission-Check ──────────────────────────────────────────────
public void openClosedGUI(Player player, int page) {
if (!player.hasPermission(ARCHIVE_PERMISSION)) { if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen.")); player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
return; return;
} }
archivePage.put(player.getUniqueId(), page);
// Lade nur geschlossene Tickets
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED); List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED);
int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE));
page = Math.max(0, Math.min(page, totalPages - 1));
archivePage.put(player.getUniqueId(), page);
Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE); Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>(); Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 45; i++) { int start = page * PAGE_SIZE;
Ticket ticket = tickets.get(i); for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) {
Ticket ticket = tickets.get(start + i);
inv.setItem(i, buildAdminListItem(ticket)); inv.setItem(i, buildAdminListItem(ticket));
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
// Navigation (Zurück-Button statt Archiv-Button) fillAdminNavigation(inv, true, player, page, totalPages);
fillAdminNavigation(inv, true, player);
playerClosedSlotMap.put(player.getUniqueId(), slotMap); playerClosedSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// SPIELER-GUI (Filtert 'playerDeleted' Tickets) // SPIELER-GUI (paginiert)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openPlayerGUI(Player player) { public void openPlayerGUI(Player player) { openPlayerGUI(player, playerPage.getOrDefault(player.getUniqueId(), 0)); }
public void openPlayerGUI(Player player, int page) {
playerPage.put(player.getUniqueId(), page);
List<Ticket> all = plugin.getDatabaseManager().getTicketsByStatus( List<Ticket> all = plugin.getDatabaseManager().getTicketsByStatus(
TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED); TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED);
List<Ticket> tickets = new ArrayList<>(); List<Ticket> tickets = new ArrayList<>();
for (Ticket t : all) { for (Ticket t : all) {
// Verstecke Tickets, die der Spieler als gelöscht markiert hat if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) tickets.add(t);
if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) {
tickets.add(t);
}
} }
if (tickets.isEmpty()) { if (tickets.isEmpty()) {
@@ -144,17 +165,23 @@ public class TicketGUI implements Listener {
return; return;
} }
int size = calcSize(tickets.size()); int totalPages = Math.max(1, (int) Math.ceil((double) tickets.size() / PAGE_SIZE));
Inventory inv = Bukkit.createInventory(null, size, PLAYER_GUI_TITLE); page = Math.max(0, Math.min(page, totalPages - 1));
playerPage.put(player.getUniqueId(), page);
Inventory inv = Bukkit.createInventory(null, 54, PLAYER_GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>(); Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 54; i++) { int start = page * PAGE_SIZE;
Ticket ticket = tickets.get(i); for (int i = 0; i < PAGE_SIZE && (start + i) < tickets.size(); i++) {
Ticket ticket = tickets.get(start + i);
inv.setItem(i, buildPlayerTicketItem(ticket)); inv.setItem(i, buildPlayerTicketItem(ticket));
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
fillEmpty(inv); // Nav-Leiste für Spieler-GUI (nur Prev/Next, kein Filter/Archiv)
fillPlayerNavigation(inv, page, totalPages);
playerOwnSlotMap.put(player.getUniqueId(), slotMap); playerOwnSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
@@ -170,51 +197,51 @@ public class TicketGUI implements Listener {
inv.setItem(4, buildDetailInfoItem(ticket)); inv.setItem(4, buildDetailInfoItem(ticket));
// Slot 10: Teleportieren // Slot 10: Teleportieren
inv.setItem(10, buildActionItem( inv.setItem(10, buildActionItem(Material.ENDER_PEARL, "§b§lTeleportieren",
Material.ENDER_PEARL,
"§b§lTeleportieren",
List.of("§7Teleportiert dich zur", "§7Position des Tickets."))); List.of("§7Teleportiert dich zur", "§7Position des Tickets.")));
// Slot 12: Claimen (nur wenn OPEN) / Permanent löschen (wenn CLOSED + ticket.archive) / Grau // Slot 12: Claimen / Löschen / Grau
if (ticket.getStatus() == TicketStatus.OPEN) { if (ticket.getStatus() == TicketStatus.OPEN) {
inv.setItem(12, buildActionItem( inv.setItem(12, buildActionItem(Material.LIME_WOOL, "§a§lTicket annehmen",
Material.LIME_WOOL,
"§a§lTicket annehmen",
List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet."))); List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet.")));
} else if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { } else if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) {
// ── NEU: Löschen-Button nur für Archiv-berechtigte Spieler ── inv.setItem(12, buildActionItem(Material.BARRIER, "§4§lTicket permanent löschen",
inv.setItem(12, buildActionItem( List.of("§7Löscht dieses Ticket", "§7unwiderruflich aus der Datenbank.",
Material.BARRIER, "§8§m ", "§c§lACHTUNG: §cNicht rückgängig zu machen!")));
"§4§lTicket permanent löschen",
List.of(
"§7Löscht dieses Ticket",
"§7unwiderruflich aus der Datenbank.",
"§8§m ",
"§c§lACHTUNG: §cNicht rückgängig zu machen!")));
} else { } else {
inv.setItem(12, buildActionItem( inv.setItem(12, buildActionItem(Material.GRAY_WOOL, "§8Bereits angenommen",
Material.GRAY_WOOL,
"§8Bereits angenommen",
List.of("§7Dieses Ticket wurde bereits", "§7angenommen."))); List.of("§7Dieses Ticket wurde bereits", "§7angenommen.")));
} }
// Slot 14: Schließen // Slot 14: Schließen
if (ticket.getStatus() != TicketStatus.CLOSED) { if (ticket.getStatus() != TicketStatus.CLOSED) {
inv.setItem(14, buildActionItem( inv.setItem(14, buildActionItem(Material.RED_WOOL, "§c§lTicket schließen",
Material.RED_WOOL,
"§c§lTicket schließen",
List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe."))); List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe.")));
} else { } else {
inv.setItem(14, buildActionItem( inv.setItem(14, buildActionItem(Material.GRAY_WOOL, "§8Bereits geschlossen",
Material.GRAY_WOOL,
"§8Bereits geschlossen",
List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); List.of("§7Dieses Ticket ist bereits", "§7geschlossen.")));
} }
// Slot 22: Kommentare anzeigen
inv.setItem(22, buildActionItem(Material.BOOK, "§e§lKommentare anzeigen",
List.of("§7Zeigt alle Nachrichten/Antworten", "§7zu diesem Ticket im Chat.")));
// Slot 20: Priorität ändern (nur wenn priorities-enabled und nicht geschlossen)
if (plugin.getConfig().getBoolean("priorities-enabled", true)
&& player.hasPermission("ticket.support")
&& ticket.getStatus() != TicketStatus.CLOSED) {
TicketPriority cur = ticket.getPriority();
List<String> prioLore = new ArrayList<>();
prioLore.add("§7Aktuell: " + cur.getColored());
prioLore.add("§8Klicken zum Wechseln");
prioLore.add("§8§m ");
for (TicketPriority p : TicketPriority.values())
prioLore.add((p == cur ? "§a» " : "§7 ") + p.getColored());
inv.setItem(20, buildActionItem(cur.getGuiMaterial(), "§6§lPriorität ändern", prioLore));
}
// Slot 16: Zurück // Slot 16: Zurück
inv.setItem(16, buildActionItem( inv.setItem(16, buildActionItem(Material.ARROW, "§7§lZurück",
Material.ARROW,
"§7§lZurück",
List.of("§7Zurück zur Ticket-Übersicht."))); List.of("§7Zurück zur Ticket-Übersicht.")));
fillEmpty(inv); fillEmpty(inv);
@@ -231,70 +258,61 @@ public class TicketGUI implements Listener {
if (!(event.getWhoClicked() instanceof Player player)) return; if (!(event.getWhoClicked() instanceof Player player)) return;
String title = event.getView().getTitle(); String title = event.getView().getTitle();
if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_GUI_TITLE) && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return; if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_GUI_TITLE)
&& !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return;
event.setCancelled(true); event.setCancelled(true);
int slot = event.getRawSlot(); int slot = event.getRawSlot();
if (slot < 0) return; if (slot < 0) return;
// ── Admin Haupt-Übersicht ────────────────────────────────────────────── // ── Admin Haupt-Übersicht ───────────────────────────────────────────
if (title.equals(GUI_TITLE)) { if (title.equals(GUI_TITLE)) {
// Klick auf die Truhe (Archiv-Button) in Slot 49 handleAdminNavClick(player, slot, false);
if (slot == 49) { if (slot < PAGE_SIZE) {
// ── Permission-Check beim Klick ──
if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
return;
}
openClosedGUI(player);
return;
}
// Klick auf ein Ticket
Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId()); Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId());
if (slotMap == null) return; if (slotMap == null) return;
Ticket ticket = slotMap.get(slot); Ticket ticket = slotMap.get(slot);
if (ticket != null) { if (ticket != null) {
viewingFromArchive.remove(player.getUniqueId()); // Kommt aus Hauptübersicht viewingFromArchive.remove(player.getUniqueId());
player.closeInventory(); player.closeInventory();
openTicketDetailAsync(player, ticket); openTicketDetailAsync(player, ticket);
} }
}
return; return;
} }
// ── Admin Archiv (Geschlossene Tickets) ───────────────────────────────── // ── Admin Archiv ───────────────────────────────────────────────────
if (title.equals(CLOSED_GUI_TITLE)) { if (title.equals(CLOSED_GUI_TITLE)) {
// Klick auf den Zurück-Pfeil (Slot 49) handleArchiveNavClick(player, slot);
if (slot == 49) { if (slot < PAGE_SIZE) {
openGUI(player);
return;
}
// Klick auf ein Ticket
Map<Integer, Ticket> slotMap = playerClosedSlotMap.get(player.getUniqueId()); Map<Integer, Ticket> slotMap = playerClosedSlotMap.get(player.getUniqueId());
if (slotMap == null) return; if (slotMap == null) return;
Ticket ticket = slotMap.get(slot); Ticket ticket = slotMap.get(slot);
if (ticket != null) { if (ticket != null) {
viewingFromArchive.add(player.getUniqueId()); // Kommt aus Archiv viewingFromArchive.add(player.getUniqueId());
player.closeInventory(); player.closeInventory();
openTicketDetailAsync(player, ticket); openTicketDetailAsync(player, ticket);
} }
}
return; return;
} }
// ── Spieler-GUI ────────────────────────────────────────────────────── // ── Spieler-GUI ────────────────────────────────────────────────────
if (title.equals(PLAYER_GUI_TITLE)) { if (title.equals(PLAYER_GUI_TITLE)) {
// Navigationstasten
int curPage = playerPage.getOrDefault(player.getUniqueId(), 0);
if (slot == 45) { openPlayerGUI(player, curPage - 1); return; }
if (slot == 53) { openPlayerGUI(player, curPage + 1); return; }
if (slot < PAGE_SIZE) {
Map<Integer, Ticket> slotMap = playerOwnSlotMap.get(player.getUniqueId()); Map<Integer, Ticket> slotMap = playerOwnSlotMap.get(player.getUniqueId());
if (slotMap == null) return; if (slotMap == null) return;
Ticket ticket = slotMap.get(slot); Ticket ticket = slotMap.get(slot);
if (ticket == null) return; if (ticket == null) return;
player.closeInventory(); player.closeInventory();
// Nur löschen wenn OFFEN oder GESCHLOSSEN
if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) { if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) {
boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId()); boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (success) {
player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt.")); player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt."));
@@ -304,23 +322,20 @@ public class TicketGUI implements Listener {
} }
}); });
} else { } else {
// Ticket wird bearbeitet (Claimed oder Forwarded) -> Löschen verweigern
player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird.")); player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird."));
} }
}
return; return;
} }
// ── Admin Detail-GUI ───────────────────────────────────────────────── // ── Admin Detail-GUI ───────────────────────────────────────────────
if (title.equals(DETAIL_GUI_TITLE)) { if (title.equals(DETAIL_GUI_TITLE)) {
Ticket ticket = detailTicketMap.get(player.getUniqueId()); Ticket ticket = detailTicketMap.get(player.getUniqueId());
if (ticket == null) return; if (ticket == null) return;
player.closeInventory(); player.closeInventory();
switch (slot) { switch (slot) {
case 10 -> handleDetailTeleport(player, ticket); case 10 -> handleDetailTeleport(player, ticket);
case 12 -> { case 12 -> {
// Wenn CLOSED + archive-Permission → permanent löschen, sonst claimen
if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) { if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) {
handleDetailPermanentDelete(player, ticket); handleDetailPermanentDelete(player, ticket);
} else { } else {
@@ -328,28 +343,74 @@ public class TicketGUI implements Listener {
} }
} }
case 14 -> handleDetailClose(player, ticket); case 14 -> handleDetailClose(player, ticket);
case 20 -> handleDetailCyclePriority(player, ticket);
case 22 -> handleShowComments(player, ticket);
case 16 -> { case 16 -> {
// Zurück zur richtigen GUI je nach Herkunft if (viewingFromArchive.remove(player.getUniqueId())) openClosedGUI(player);
if (viewingFromArchive.remove(player.getUniqueId())) { else openGUI(player);
openClosedGUI(player);
} else {
openGUI(player);
}
} }
} }
} }
} }
// ─────────────────────────── Detail-Aktionen & Helpers ────────────────── // ─────────────────────────── Navigation-Handler ─────────────────────────
/**
* Verarbeitet Klicks auf die Navigationsleiste der Admin-Übersicht (Slots 4553).
*/
private void handleAdminNavClick(Player player, int slot, boolean isArchive) {
int curPage = adminPage.getOrDefault(player.getUniqueId(), 0);
switch (slot) {
case 45 -> openGUI(player, curPage - 1); // Zurück
case 53 -> openGUI(player, curPage + 1); // Vor
case 49 -> { // Archiv-Button oder Zurück im Archiv
if (player.hasPermission(ARCHIVE_PERMISSION)) openClosedGUI(player);
else player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
}
case 47 -> { // Kategorie-Filter (wenn aktiviert)
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
cycleCategoryFilter(player);
openGUI(player, 0);
}
}
}
}
private void handleArchiveNavClick(Player player, int slot) {
int curPage = archivePage.getOrDefault(player.getUniqueId(), 0);
switch (slot) {
case 45 -> openClosedGUI(player, curPage - 1);
case 53 -> openClosedGUI(player, curPage + 1);
case 49 -> openGUI(player); // Zurück zur Hauptübersicht
}
}
/** Wechselt zum nächsten Kategorie-Filter */
private void cycleCategoryFilter(Player player) {
CategoryManager cm = plugin.getCategoryManager();
List<ConfigCategory> all = cm.getAll();
ConfigCategory current = categoryFilter.getOrDefault(player.getUniqueId(), null);
if (current == null) {
if (!all.isEmpty()) categoryFilter.put(player.getUniqueId(), all.get(0));
} else {
int idx = all.indexOf(current);
int next = idx + 1;
if (next >= all.size()) categoryFilter.remove(player.getUniqueId()); // Zurück zu Alle
else categoryFilter.put(player.getUniqueId(), all.get(next));
}
ConfigCategory newFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
String filterName = newFilter != null ? newFilter.getColored() : "§7Alle";
player.sendMessage(plugin.color("&7Filter: " + filterName));
}
// ─────────────────────────── Detail-Aktionen ────────────────────────────
private void openTicketDetailAsync(Player player, Ticket currentTicket) { private void openTicketDetailAsync(Player player, Ticket currentTicket) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(currentTicket.getId()); Ticket fresh = plugin.getDatabaseManager().getTicketById(currentTicket.getId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (fresh == null) { if (fresh == null) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; }
player.sendMessage(plugin.formatMessage("messages.ticket-not-found"));
return;
}
openDetailGUI(player, fresh); openDetailGUI(player, fresh);
}); });
}); });
@@ -372,34 +433,22 @@ public class TicketGUI implements Listener {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName()); boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; }
player.sendMessage(plugin.formatMessage("messages.ticket-claimed") player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticket.getId())) .replace("{id}", String.valueOf(ticket.getId()))
.replace("{player}", ticket.getCreatorName())); .replace("{player}", ticket.getCreatorName()));
ticket.setClaimerUUID(player.getUniqueId()); ticket.setClaimerUUID(player.getUniqueId());
ticket.setClaimerName(player.getName()); ticket.setClaimerName(player.getName());
plugin.getTicketManager().notifyCreatorClaimed(ticket); plugin.getTicketManager().notifyCreatorClaimed(ticket);
if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> { if (fresh != null) openDetailGUI(player, fresh); });
if (fresh != null) openDetailGUI(player, fresh);
}); });
}); });
} else {
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
}
});
}); });
} }
/**
* Löscht ein geschlossenes Ticket permanent aus der Datenbank.
* Nur für Spieler mit der Permission ticket.archive.
*/
private void handleDetailPermanentDelete(Player player, Ticket ticket) { private void handleDetailPermanentDelete(Player player, Ticket ticket) {
if (!player.hasPermission(ARCHIVE_PERMISSION)) { if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, Tickets permanent zu löschen.")); player.sendMessage(plugin.color("&cDu hast keine Berechtigung, Tickets permanent zu löschen."));
@@ -409,13 +458,11 @@ public class TicketGUI implements Listener {
player.sendMessage(plugin.color("&cNur geschlossene Tickets können permanent gelöscht werden.")); player.sendMessage(plugin.color("&cNur geschlossene Tickets können permanent gelöscht werden."));
return; return;
} }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().deleteTicket(ticket.getId()); boolean success = plugin.getDatabaseManager().deleteTicket(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (success) {
player.sendMessage(plugin.color( player.sendMessage(plugin.color("&aTicket &e#" + ticket.getId() + " &awurde permanent gelöscht."));
"&aTicket &e#" + ticket.getId() + " &awurde permanent aus der Datenbank gelöscht."));
viewingFromArchive.remove(player.getUniqueId()); viewingFromArchive.remove(player.getUniqueId());
openClosedGUI(player); openClosedGUI(player);
} else { } else {
@@ -439,6 +486,57 @@ public class TicketGUI implements Listener {
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
} }
private void handleDetailCyclePriority(Player player, Ticket ticket) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, die Priorität zu ändern."));
return;
}
if (!plugin.getConfig().getBoolean("priorities-enabled", true)) return;
if (ticket.getStatus() == TicketStatus.CLOSED) {
player.sendMessage(plugin.color("&cDie Priorität geschlossener Tickets kann nicht geändert werden."));
return;
}
TicketPriority[] values = TicketPriority.values();
TicketPriority next = values[(ticket.getPriority().ordinal() + 1) % values.length];
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().setTicketPriority(ticket.getId(), next);
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
ticket.setPriority(next);
player.sendMessage(plugin.color("&aPriorität auf " + next.getColored() + " &agesetzt."));
openDetailGUI(player, ticket);
} else {
player.sendMessage(plugin.color("&cFehler beim Ändern der Priorität."));
openDetailGUI(player, ticket);
}
});
});
}
private void handleShowComments(Player player, Ticket ticket) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
List<TicketComment> comments = plugin.getDatabaseManager().getComments(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Kommentare zu Ticket #" + ticket.getId()));
if (comments.isEmpty()) {
player.sendMessage(plugin.color("&7Noch keine Kommentare vorhanden."));
} else {
for (TicketComment c : comments) {
String time = DATE_FORMAT.format(c.getCreatedAt());
player.sendMessage(plugin.color("&e" + c.getAuthorName() + " &7(" + time + ")&8: &f" + c.getMessage()));
}
}
player.sendMessage(plugin.color("&8&m "));
// Gleich wieder Detail-GUI öffnen
openDetailGUI(player, ticket);
});
});
}
// ─────────────────────────── Chat-Events ────────────────────────────────
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerChat(AsyncPlayerChatEvent event) { public void onPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
@@ -470,72 +568,109 @@ public class TicketGUI implements Listener {
}); });
} }
// ─────────────────────────── Item-Builder & Füll-Methoden ───────────── // ─────────────────────────── Item-Builder ──────────────────────────────
/** /**
* Füllt die Navigationsleiste (letzte Reihe) der Admin-GUIs. * Füllt die Navigationsleiste (letzte Reihe, Slots 4553).
* Der Archiv-Button (Truhe) wird nur angezeigt, wenn der Spieler ticket.archive besitzt. * Layout: [45]=Zurück | [47]=Filter | [49]=Archiv/Hauptmenü | [51]=leer | [53]=Weiter
*/ */
private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player) { private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player, int page, int totalPages) {
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack glass = makeGlass();
ItemMeta meta = glass.getItemMeta(); for (int i = 45; i < 54; i++) inv.setItem(i, glass);
if (meta != null) {
meta.setDisplayName(" "); // Zurück (Slot 45)
glass.setItemMeta(meta); if (page > 0) {
inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück",
List.of("§7Seite " + page + " von " + totalPages)));
} }
// Letzte Reihe (45-53) füllen // Weiter (Slot 53)
for (int i = 45; i < 54; i++) { if (page < totalPages - 1) {
if (i != 49) inv.setItem(i, glass); inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►",
List.of("§7Seite " + (page + 2) + " von " + totalPages)));
} }
if (isArchiveView) { // Seitenanzeige (Slot 49)
// Im Archiv: Zurück-Pfeil in Slot 49 if (!isArchiveView) {
inv.setItem(49, buildActionItem(
Material.ARROW,
"§7§lZurück zur Übersicht",
List.of("§7Zeigt alle offenen Tickets.")));
} else {
// In der Übersicht: Archiv-Truhe nur mit Permission
if (player.hasPermission(ARCHIVE_PERMISSION)) { if (player.hasPermission(ARCHIVE_PERMISSION)) {
inv.setItem(49, buildActionItem( inv.setItem(49, buildActionItem(Material.CHEST, "§7§lGeschlossene Tickets",
Material.CHEST,
"§7§lGeschlossene Tickets",
List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an."))); List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an.")));
}
// Kategorie-Filter (Slot 47), nur wenn aktiviert
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory currentFilter = categoryFilter.getOrDefault(player.getUniqueId(), null);
String filterLabel = currentFilter != null ? currentFilter.getColored() : "§7Alle";
List<String> filterLore = new ArrayList<>();
filterLore.add("§7Aktuell: " + filterLabel);
filterLore.add("§8Klicken zum Wechseln");
filterLore.add("§8§m ");
for (ConfigCategory cat : plugin.getCategoryManager().getAll()) {
filterLore.add((cat.equals(currentFilter) ? "§a» " : "§7 ") + cat.getColored());
}
filterLore.add((currentFilter == null ? "§a» " : "§7 ") + "§7Alle (kein Filter)");
inv.setItem(47, buildActionItem(Material.HOPPER, "§e§lKategorie-Filter", filterLore));
}
} else { } else {
// Kein Archiv-Zugriff → Slot 49 bleibt Glas (kein Button) // Im Archiv: Zurück-Button in Slot 49
inv.setItem(49, glass); inv.setItem(49, buildActionItem(Material.ARROW, "§7§lZurück zur Übersicht",
List.of("§7Zeigt alle offenen Tickets.")));
} }
// Seitenanzeige Mitte oben (Slot 48)
inv.setItem(48, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages,
List.of("§7Gesamt: " + (playerSlotMap.containsKey(player.getUniqueId())
? playerSlotMap.get(player.getUniqueId()).size() + "+" : "?") + " Tickets auf dieser Seite")));
} }
private void fillPlayerNavigation(Inventory inv, int page, int totalPages) {
ItemStack glass = makeGlass();
for (int i = 45; i < 54; i++) inv.setItem(i, glass);
if (page > 0) inv.setItem(45, buildActionItem(Material.ARROW, "§7§l◄ Zurück", List.of("§7Seite " + page + " von " + totalPages)));
if (page < totalPages - 1) inv.setItem(53, buildActionItem(Material.ARROW, "§7§lWeiter ►", List.of("§7Seite " + (page + 2) + " von " + totalPages)));
inv.setItem(49, buildActionItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, List.of()));
} }
// ─────────────────────────── Item-Builder ──────────────────────────────
private ItemStack buildAdminListItem(Ticket ticket) { private ItemStack buildAdminListItem(Ticket ticket) {
Material mat = switch (ticket.getStatus()) { // Material: Kategorie aus Config (z.B. REDSTONE für Bug, BOOK für Frage)
// Fallback auf Status-Material wenn categories-enabled: false
Material mat;
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
mat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey()).getMaterial();
} else {
mat = switch (ticket.getStatus()) {
case OPEN -> Material.PAPER; case OPEN -> Material.PAPER;
case CLAIMED -> Material.YELLOW_DYE; case CLAIMED -> Material.YELLOW_DYE;
case FORWARDED -> Material.ORANGE_DYE; case FORWARDED -> Material.ORANGE_DYE;
case CLOSED -> Material.GRAY_DYE; case CLOSED -> Material.GRAY_DYE;
}; };
}
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;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); // Priorität farblich im Titel anzeigen (wenn aktiviert)
String priorityPrefix = plugin.getConfig().getBoolean("priorities-enabled", true)
? ticket.getPriority().getColored() + " §8| " : "";
meta.setDisplayName(priorityPrefix + "§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<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());
lore.add("§7Anliegen: §f" + ticket.getMessage()); lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { if (plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
lore.add("§7Kategorie: " + _cat.getColored());
}
if (plugin.getConfig().getBoolean("priorities-enabled", true))
lore.add("§7Priorität: " + ticket.getPriority().getColored());
if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
lore.add("§7Kommentar: §f" + ticket.getCloseComment()); lore.add("§7Kommentar: §f" + ticket.getCloseComment());
} if (ticket.isPlayerDeleted()) lore.add("§cSpieler hat Ticket gelöscht.");
if (ticket.isPlayerDeleted()) {
lore.add("§cSpieler hat Ticket gelöscht.");
}
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§e§l» KLICKEN für Details"); lore.add("§e§l» KLICKEN für Details");
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); item.setItemMeta(meta);
return item; return item;
@@ -548,7 +683,6 @@ public class TicketGUI implements Listener {
case FORWARDED -> Material.ORANGE_DYE; case FORWARDED -> Material.ORANGE_DYE;
case CLOSED -> Material.GRAY_DYE; case CLOSED -> Material.GRAY_DYE;
}; };
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;
@@ -561,17 +695,27 @@ 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 (plugin.getConfig().getBoolean("categories-enabled", true)) {
ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
lore.add("§7Kategorie: " + _cat.getColored());
}
if (plugin.getConfig().getBoolean("priorities-enabled", true))
lore.add("§7Priorität: " + ticket.getPriority().getColored());
if (ticket.getClaimerName() != null) { if (ticket.getClaimerName() != null) {
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Angenommen von: §a" + ticket.getClaimerName()); lore.add("§7Angenommen von: §a" + ticket.getClaimerName());
if (ticket.getClaimedAt() != null) if (ticket.getClaimedAt() != null) lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt()));
lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt()));
} }
if (ticket.getStatus() == TicketStatus.CLOSED) { if (ticket.getStatus() == TicketStatus.CLOSED) {
if (ticket.getClosedAt() != null) if (ticket.getClosedAt() != null) lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt()));
lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt()));
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
lore.add("§7Kommentar: §f" + ticket.getCloseComment()); lore.add("§7Kommentar: §f" + ticket.getCloseComment());
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
String rating = ticket.getPlayerRating();
String ratingStr = rating == null ? "§7Keine Bewertung" :
"THUMBS_UP".equals(rating) ? "§a👍 Positiv" : "§c👎 Negativ";
lore.add("§7Bewertung: " + ratingStr);
}
} }
lore.add("§8§m "); lore.add("§8§m ");
meta.setLore(lore); meta.setLore(lore);
@@ -586,7 +730,6 @@ public class TicketGUI implements Listener {
case FORWARDED -> Material.ORANGE_DYE; case FORWARDED -> Material.ORANGE_DYE;
case CLOSED -> Material.GRAY_DYE; case CLOSED -> Material.GRAY_DYE;
}; };
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;
@@ -598,23 +741,26 @@ 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.getStatus() == TicketStatus.CLOSED if (plugin.getConfig().getBoolean("categories-enabled", true)) {
&& ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { ConfigCategory _cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
lore.add("§7Kategorie: " + _cat.getColored());
}
if (ticket.getStatus() == TicketStatus.CLOSED) {
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Kommentar des Supports:"); lore.add("§7Kommentar des Supports:");
lore.add("§f" + ticket.getCloseComment()); lore.add("§f" + ticket.getCloseComment());
} }
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
String rating = ticket.getPlayerRating();
if (rating == null) lore.add("§e» /ticket rate " + ticket.getId() + " good/bad");
else lore.add("§7Bewertet: " + ("THUMBS_UP".equals(rating) ? "§a👍" : "§c👎"));
}
}
lore.add("§8§m "); lore.add("§8§m ");
switch (ticket.getStatus()) { switch (ticket.getStatus()) {
case OPEN, CLOSED -> { case OPEN, CLOSED -> { lore.add("§c§l» KLICKEN zum Löschen"); lore.add("§7Entferne dieses Ticket aus deiner Übersicht."); }
lore.add("§c§l» KLICKEN zum Löschen"); default -> { lore.add("§e» Ticket wird bearbeitet..."); lore.add("§7Kann nicht mehr gelöscht werden."); }
lore.add("§7Entferne dieses Ticket aus deiner Übersicht.");
}
default -> {
lore.add("§e» Ticket wird bearbeitet...");
lore.add("§7Kann nicht mehr gelöscht werden.");
}
} }
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); item.setItemMeta(meta);
@@ -631,20 +777,15 @@ public class TicketGUI implements Listener {
return item; return item;
} }
private int calcSize(int ticketCount) { private ItemStack makeGlass() {
int size = (int) Math.ceil(ticketCount / 9.0) * 9; ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
return Math.max(9, Math.min(54, size)); ItemMeta meta = glass.getItemMeta();
if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); }
return glass;
} }
private void fillEmpty(Inventory inv) { private void fillEmpty(Inventory inv) {
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack glass = makeGlass();
ItemMeta meta = glass.getItemMeta(); for (int i = 0; i < inv.getSize(); i++) { if (inv.getItem(i) == null) inv.setItem(i, glass); }
if (meta != null) {
meta.setDisplayName(" ");
glass.setItemMeta(meta);
}
for (int i = 0; i < inv.getSize(); i++) {
if (inv.getItem(i) == null) inv.setItem(i, glass);
}
} }
} }

View File

@@ -34,13 +34,39 @@ public class PlayerJoinListener implements Listener {
.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); // 2 Sekunden Verzögerung }, 40L);
} }
}); });
} }
// ── Ausstehende Kommentar-/Schließ-Benachrichtigungen anzeigen ────
// (Nachrichten die ankamen während der Spieler offline war)
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
List<String> pending = plugin.getDatabaseManager().getPendingNotifications(player.getUniqueId());
if (pending.isEmpty()) return;
Bukkit.getScheduler().runTask(plugin, () -> {
if (!player.isOnline()) return;
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Ticket-Benachrichtigungen &7(während du offline warst):"));
for (String msg : pending) {
player.sendMessage(plugin.color(msg));
}
player.sendMessage(plugin.color("&8&m "));
});
plugin.getDatabaseManager().clearPendingNotifications(player.getUniqueId());
});
}, 60L);
// ── [NEU] Spieler: Ticket-claimed-Benachrichtigung für Offline-Zeit ──
// Läuft mit 60 Ticks Verzögerung (3 Sek) damit der Spieler zuerst normal spawnt
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
plugin.getTicketManager().notifyClaimedWhileOffline(player);
}, 60L);
// ── Spieler: über geschlossene Tickets mit Kommentar informieren ── // ── Spieler: über geschlossene Tickets mit Kommentar informieren ──
// Nur wenn der Ersteller noch nicht live benachrichtigt wurde
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
List<Ticket> closed = plugin.getDatabaseManager() List<Ticket> closed = plugin.getDatabaseManager()
.getTicketsByStatus(TicketStatus.CLOSED); .getTicketsByStatus(TicketStatus.CLOSED);
@@ -48,8 +74,6 @@ public class PlayerJoinListener implements Listener {
for (Ticket t : closed) { for (Ticket t : closed) {
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue; if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) 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; if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue;
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () ->
@@ -73,7 +97,7 @@ public class PlayerJoinListener implements Listener {
player.sendMessage(bar); player.sendMessage(bar);
} }
}); });
}, 20L); // 1 Sekunde }, 20L);
} }
} }
} }

View File

@@ -0,0 +1,177 @@
package de.ticketsystem.manager;
import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.ConfigCategory;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Loads and manages ticket categories defined in config.yml under the "categories" section.
*
* Example config.yml layout:
*
* categories:
* general:
* name: "Allgemein"
* color: "&7"
* material: "PAPER"
* aliases:
* - "allgemein"
* - "general"
* bug:
* name: "Bug"
* color: "&c"
* material: "REDSTONE"
* aliases:
* - "bug"
* - "fehler"
*/
public class CategoryManager {
private final TicketPlugin plugin;
/** Ordered map: key → ConfigCategory */
private final Map<String, ConfigCategory> categories = new LinkedHashMap<>();
/** Alias → key mapping for fast resolve() lookups */
private final Map<String, String> aliasMap = new LinkedHashMap<>();
public CategoryManager(TicketPlugin plugin) {
this.plugin = plugin;
load();
}
// ─────────────────────────── Loading ───────────────────────────────────
private void load() {
categories.clear();
aliasMap.clear();
ConfigurationSection section = plugin.getConfig().getConfigurationSection("categories");
if (section == null || section.getKeys(false).isEmpty()) {
// Fallback: create built-in defaults so the plugin always works
loadDefaults();
return;
}
for (String key : section.getKeys(false)) {
ConfigurationSection cat = section.getConfigurationSection(key);
if (cat == null) continue;
String name = cat.getString("name", capitalize(key));
String color = cat.getString("color", "&7");
String matStr = cat.getString("material", "PAPER").toUpperCase();
Material material;
try {
material = Material.valueOf(matStr);
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("[CategoryManager] Unbekanntes Material '" + matStr
+ "' für Kategorie '" + key + "'. Fallback: PAPER");
material = Material.PAPER;
}
ConfigCategory category = new ConfigCategory(key, name, color, material);
categories.put(key.toLowerCase(), category);
// Register key itself as alias
aliasMap.put(key.toLowerCase(), key.toLowerCase());
// Register additional aliases
List<String> aliases = cat.getStringList("aliases");
for (String alias : aliases) {
aliasMap.put(alias.toLowerCase(), key.toLowerCase());
}
}
if (categories.isEmpty()) {
plugin.getLogger().warning("[CategoryManager] Keine gültigen Kategorien in der config.yml gefunden. Lade Standardkategorien.");
loadDefaults();
} else {
plugin.getLogger().info("[CategoryManager] " + categories.size() + " Kategorie(n) geladen: " + String.join(", ", categories.keySet()));
}
}
/** Built-in fallback categories — mirrors the old TicketCategory enum */
private void loadDefaults() {
addDefault("general", "Allgemein", "&7", Material.PAPER, "allgemein", "general");
addDefault("bug", "Bug", "&c", Material.REDSTONE, "bug", "fehler");
addDefault("question", "Frage", "&e", Material.BOOK, "frage", "question");
addDefault("complaint", "Beschwerde", "&6", Material.WRITABLE_BOOK, "beschwerde", "complaint");
addDefault("other", "Sonstiges", "&8", Material.FEATHER, "sonstiges", "other");
plugin.getLogger().info("[CategoryManager] Standard-Kategorien geladen (5).");
}
private void addDefault(String key, String name, String color, Material mat, String... aliases) {
ConfigCategory cat = new ConfigCategory(key, name, color, mat);
categories.put(key, cat);
aliasMap.put(key, key);
for (String alias : aliases) aliasMap.put(alias.toLowerCase(), key);
}
// ─────────────────────────── Public API ────────────────────────────────
/**
* Returns all loaded categories in config order.
*/
public List<ConfigCategory> getAll() {
return Collections.unmodifiableList(new ArrayList<>(categories.values()));
}
/**
* Returns the first category (default), or a hard-coded fallback if empty.
*/
public ConfigCategory getDefault() {
if (categories.isEmpty()) return new ConfigCategory("general", "Allgemein", "&7", Material.PAPER);
return categories.values().iterator().next();
}
/**
* Looks up a category by exact key (case-insensitive).
* Returns null if not found.
*/
public ConfigCategory fromKey(String key) {
if (key == null) return getDefault();
ConfigCategory cat = categories.get(key.toLowerCase());
return cat != null ? cat : getDefault();
}
/**
* Resolves a user-supplied string (key or alias) to a ConfigCategory.
* Returns null if no match is found (so callers can show an error).
*/
public ConfigCategory resolve(String input) {
if (input == null) return null;
String key = aliasMap.get(input.toLowerCase());
return key != null ? categories.get(key) : null;
}
/**
* Returns a human-readable comma-separated list of all category keys,
* e.g. "general, bug, question, complaint, other"
*/
public String getAvailableNames() {
return String.join(", ", categories.keySet());
}
/**
* Reloads categories from the (already reloaded) config.
* Call this after plugin.reloadConfig().
*/
public void reload() {
load();
}
// ─────────────────────────── Helpers ───────────────────────────────────
private static String capitalize(String s) {
if (s == null || s.isEmpty()) return s;
return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
}
}

View File

@@ -1,6 +1,7 @@
package de.ticketsystem.manager; package de.ticketsystem.manager;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.ConfigCategory;
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;
@@ -40,9 +41,7 @@ public class TicketManager {
return Math.max(0, (cooldownMillis - elapsed) / 1000); return Math.max(0, (cooldownMillis - elapsed) / 1000);
} }
public void setCooldown(UUID uuid) { public void setCooldown(UUID uuid) { cooldowns.put(uuid, System.currentTimeMillis()); }
cooldowns.put(uuid, System.currentTimeMillis());
}
// ─────────────────────────── Benachrichtigungen ──────────────────────── // ─────────────────────────── Benachrichtigungen ────────────────────────
@@ -51,14 +50,25 @@ public class TicketManager {
* und sendet optional eine Discord-Webhook-Nachricht. * und sendet optional eine Discord-Webhook-Nachricht.
*/ */
public void notifyTeam(Ticket ticket) { public void notifyTeam(Ticket ticket) {
// Sicherheitschecks für null-Werte
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String message = ticket.getMessage() != null ? ticket.getMessage() : ""; String message = ticket.getMessage() != null ? ticket.getMessage() : "";
// Kategorie & Priorität optional anzeigen
String categoryInfo = "";
String priorityInfo = "";
if (plugin.getConfig().getBoolean("categories-enabled", true)) {
de.ticketsystem.model.ConfigCategory cat = plugin.getCategoryManager().fromKey(ticket.getCategoryKey());
categoryInfo = " §7[§r" + cat.getColored() + "§7]";
}
if (plugin.getConfig().getBoolean("priorities-enabled", true)) {
priorityInfo = " §7Priorität: §r" + ticket.getPriority().getColored();
}
String msg = plugin.formatMessage("messages.new-ticket-notify") String msg = plugin.formatMessage("messages.new-ticket-notify")
.replace("{player}", creatorName) .replace("{player}", creatorName)
.replace("{message}", message) .replace("{message}", message)
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()))
+ categoryInfo + priorityInfo;
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")) {
@@ -67,27 +77,19 @@ public class TicketManager {
} }
} }
// Discord-Webhook (asynchron)
plugin.getDiscordWebhook().sendNewTicket(ticket); plugin.getDiscordWebhook().sendNewTicket(ticket);
} }
/** /**
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
* --- FIX PROBLEMK 1: NIE "UNBEKANNT" --- * Setzt claimer_notified = true und persistiert es.
*/ */
public void notifyCreatorClaimed(Ticket ticket) { public void notifyCreatorClaimed(Ticket ticket) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) { if (creator != null && creator.isOnline()) {
// 1. Versuch: Name aus dem Ticket-Objekt
String claimerName = ticket.getClaimerName(); String claimerName = ticket.getClaimerName();
if (claimerName == null && ticket.getClaimerUUID() != null)
// 2. Versuch: Wenn Name fehlt, aber UUID vorhanden -> Namen über Bukkit holen
if (claimerName == null && ticket.getClaimerUUID() != null) {
claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName(); claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
}
// 3. Fallback: Falls immer noch kein Name da ist, nimm "Support" (nie "Unbekannt")
if (claimerName == null) claimerName = "Support"; if (claimerName == null) claimerName = "Support";
String msg = plugin.formatMessage("messages.ticket-claimed-notify") String msg = plugin.formatMessage("messages.ticket-claimed-notify")
@@ -95,6 +97,47 @@ public class TicketManager {
.replace("{claimer}", claimerName); .replace("{claimer}", claimerName);
creator.sendMessage(msg); creator.sendMessage(msg);
} }
// Persistiert setzen, damit Join-Listener weiß, dass Spieler bereits informiert ist
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
}
/**
* Wird beim Server-Join aufgerufen informiert den Spieler über Tickets,
* die geclaimt oder weitergeleitet wurden während er offline war.
*/
public void notifyClaimedWhileOffline(Player player) {
// Suche alle Tickets dieses Spielers, die CLAIMED/FORWARDED sind,
// aber noch nicht notified wurden
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var tickets = plugin.getDatabaseManager().getTicketsByStatus(
TicketStatus.CLAIMED, TicketStatus.FORWARDED);
for (Ticket t : tickets) {
if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
if (t.isClaimerNotified()) continue; // wurde schon informiert
String claimerName = t.getClaimerName() != null ? t.getClaimerName() : "Support";
final String name = claimerName;
Bukkit.getScheduler().runTask(plugin, () -> {
if (!player.isOnline()) return;
if (t.getStatus() == TicketStatus.CLAIMED) {
String msg = plugin.formatMessage("messages.ticket-claimed-notify")
.replace("{id}", String.valueOf(t.getId()))
.replace("{claimer}", name);
player.sendMessage(msg);
} else {
String forwardedTo = t.getForwardedToName() != null ? t.getForwardedToName() : "einen Supporter";
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
.replace("{id}", String.valueOf(t.getId()))
.replace("{supporter}", forwardedTo);
player.sendMessage(msg);
}
});
plugin.getDatabaseManager().markClaimerNotified(t.getId());
}
});
} }
/** /**
@@ -109,64 +152,65 @@ public class TicketManager {
.replace("{supporter}", forwardedTo); .replace("{supporter}", forwardedTo);
creator.sendMessage(msg); creator.sendMessage(msg);
} }
// Auch hier notified setzen
plugin.getDatabaseManager().markClaimerNotified(ticket.getId());
} }
/** /**
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung * Sendet dem weitergeleiteten Supporter eine Benachrichtigung.
* und informiert optional Discord.
*/ */
public void notifyForwardedTo(Ticket ticket, String fromName) { 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 creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String msg = plugin.formatMessage("messages.ticket-forwarded-notify") String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
.replace("{player}", creatorName) .replace("{player}", creatorName)
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()));
target.sendMessage(msg); target.sendMessage(msg);
} }
// Discord
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName); 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. * Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
*/ */
public void notifyCreatorClosed(Ticket ticket) { notifyCreatorClosed(ticket, null); }
public void notifyCreatorClosed(Ticket ticket, String closerName) { public void notifyCreatorClosed(Ticket ticket, String closerName) {
notifiedClosedTickets.add(ticket.getId()); notifiedClosedTickets.add(ticket.getId());
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) {
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
? ticket.getCloseComment() : ""; ? ticket.getCloseComment() : "";
if (creator != null && creator.isOnline()) {
String msg = plugin.formatMessage("messages.ticket-closed-notify") String msg = plugin.formatMessage("messages.ticket-closed-notify")
.replace("{id}", String.valueOf(ticket.getId())) .replace("{id}", String.valueOf(ticket.getId()))
.replace("{comment}", comment); .replace("{comment}", comment);
creator.sendMessage(msg); creator.sendMessage(msg);
if (!comment.isEmpty())
if (!comment.isEmpty()) {
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment)); creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
if (plugin.getConfig().getBoolean("rating-enabled", true)) {
creator.sendMessage(plugin.color("&8&m "));
creator.sendMessage(plugin.color("&6Wie zufrieden bist du mit dem Support?"));
creator.sendMessage(plugin.color("&a/ticket rate " + ticket.getId() + " good &7 👍 Gut"));
creator.sendMessage(plugin.color("&c/ticket rate " + ticket.getId() + " bad &7 👎 Schlecht"));
creator.sendMessage(plugin.color("&8&m "));
} }
} else {
// Offline → ausstehende Benachrichtigung speichern
String pendingMsg = "&e[Ticket #" + ticket.getId() + "] &7Dein Ticket wurde geschlossen."
+ (comment.isEmpty() ? "" : " &7Kommentar: &f" + comment)
+ (plugin.getConfig().getBoolean("rating-enabled", true)
? " &7Bewertung: &e/ticket rate " + ticket.getId() + " good/bad" : "");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), pendingMsg));
} }
// Discord
String closer = closerName != null ? closerName : "Unbekannt"; String closer = closerName != null ? closerName : "Unbekannt";
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer); 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) { public boolean wasClosedNotificationSent(int ticketId) {
return notifiedClosedTickets.contains(ticketId); return notifiedClosedTickets.contains(ticketId);
} }
@@ -183,15 +227,22 @@ public class TicketManager {
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
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 [Kategorie] <Text> &7 Neues Ticket erstellen"));
player.sendMessage(plugin.color("&e/ticket list &7 Deine Tickets ansehen (GUI)")); player.sendMessage(plugin.color("&e/ticket list &7 Deine Tickets ansehen (GUI)"));
player.sendMessage(plugin.color("&e/ticket comment <ID> <Text> &7 Nachricht zu einem Ticket"));
if (plugin.getConfig().getBoolean("rating-enabled", true))
player.sendMessage(plugin.color("&e/ticket rate <ID> <good|bad> &7 Support bewerten"));
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
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> [Kommentar] &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"));
player.sendMessage(plugin.color("&e/ticket blacklist <add|remove|list> [Spieler] [Grund] &7 Blacklist verwalten"));
player.sendMessage(plugin.color("&e/ticket reload &7 Konfiguration neu laden")); player.sendMessage(plugin.color("&e/ticket reload &7 Konfiguration neu laden"));
player.sendMessage(plugin.color("&e/ticket stats &7 Statistiken anzeigen"));
} }
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
} }

View File

@@ -0,0 +1,53 @@
package de.ticketsystem.model;
import org.bukkit.Material;
/**
* Eine aus der config.yml geladene Ticket-Kategorie.
* Ersetzt das hardcodierte TicketCategory-Enum vollständig.
*
* Konfigurationsbeispiel (config.yml):
*
* categories:
* bug:
* name: "Bug"
* color: "&c"
* material: "REDSTONE"
* aliases:
* - "bug"
* - "fehler"
*/
public class ConfigCategory {
/** Interner Schlüssel aus der Config (z.B. "bug", "general") immer Kleinbuchstaben */
private final String key;
/** Anzeigename (z.B. "Bug", "Allgemein") */
private final String name;
/** Minecraft-Farbcode (z.B. "&c") */
private final String color;
/** GUI-Item-Material */
private final Material material;
public ConfigCategory(String key, String name, String color, Material material) {
this.key = key.toLowerCase();
this.name = name;
this.color = color;
this.material = material;
}
public String getKey() { return key; }
public String getName() { return name; }
public String getColor() { return color; }
public Material getMaterial() { return material; }
/** Gibt den farbigen Anzeigenamen zurück, z.B. "§cBug" */
public String getColored() {
return org.bukkit.ChatColor.translateAlternateColorCodes('&', color + name);
}
@Override
public String toString() { return key; }
}

View File

@@ -3,7 +3,6 @@ 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.ConfigurationSerializable;
import org.bukkit.configuration.serialization.SerializableAs; import org.bukkit.configuration.serialization.SerializableAs;
import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.configuration.serialization.ConfigurationSerialization;
@@ -36,196 +35,144 @@ public class Ticket implements ConfigurationSerializable {
private Timestamp closedAt; private Timestamp closedAt;
private String closeComment; private String closeComment;
// ─── NEU: Soft Delete Flag ───
private boolean playerDeleted = false; private boolean playerDeleted = false;
/** Kategorie-Key aus config.yml, z.B. "bug", "general" */
private String categoryKey = "general";
private TicketPriority priority = TicketPriority.NORMAL;
/** null = nicht bewertet | "THUMBS_UP" | "THUMBS_DOWN" */
private String playerRating = null;
private boolean claimerNotified = false;
public Ticket() {} public Ticket() {}
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;
this.message = message; this.message = message;
this.worldName = location.getWorld().getName(); this.worldName = location.getWorld().getName();
this.x = location.getX(); this.x = location.getX(); this.y = location.getY(); this.z = location.getZ();
this.y = location.getY(); this.yaw = location.getYaw(); this.pitch = location.getPitch();
this.z = location.getZ();
this.yaw = location.getYaw();
this.pitch = location.getPitch();
this.status = TicketStatus.OPEN; this.status = TicketStatus.OPEN;
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) { public Ticket(Map<String, Object> map) {
this.id = (int) map.get("id"); this.id = (int) map.get("id");
Object cObj = map.get("creatorUUID");
// UUIDs sicher aus String konvertieren this.creatorUUID = cObj instanceof UUID ? (UUID) cObj : UUID.fromString((String) cObj);
Object creatorObj = map.get("creatorUUID");
this.creatorUUID = creatorObj instanceof UUID ? (UUID) creatorObj : UUID.fromString((String) creatorObj);
this.creatorName = (String) map.get("creatorName"); this.creatorName = (String) map.get("creatorName");
this.message = (String) map.get("message"); this.message = (String) map.get("message");
this.worldName = (String) map.get("world"); this.worldName = (String) map.get("world");
this.x = toDouble(map.get("x")); this.y = toDouble(map.get("y")); this.z = toDouble(map.get("z"));
// Koordinaten sicher parsen this.yaw = toFloat(map.get("yaw")); this.pitch = toFloat(map.get("pitch"));
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")); this.status = TicketStatus.valueOf((String) map.get("status"));
if (map.get("createdAt") != null) this.createdAt = new Timestamp(toLong(map.get("createdAt")));
// Timestamps aus Long (Millis) wieder zu Timestamp machen if (map.get("claimedAt") != null) this.claimedAt = new Timestamp(toLong(map.get("claimedAt")));
if (map.get("createdAt") != null) { if (map.get("closedAt") != null) this.closedAt = new Timestamp(toLong(map.get("closedAt")));
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"); this.closeComment = (String) map.get("closeComment");
// Optionale Felder
if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) { if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) {
Object claimerObj = map.get("claimerUUID"); Object o = map.get("claimerUUID");
this.claimerUUID = claimerObj instanceof UUID ? (UUID) claimerObj : UUID.fromString((String) claimerObj); this.claimerUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
this.claimerName = (String) map.get("claimerName"); this.claimerName = (String) map.get("claimerName");
} }
if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) { if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) {
Object fwdObj = map.get("forwardedToUUID"); Object o = map.get("forwardedToUUID");
this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj); this.forwardedToUUID = o instanceof UUID ? (UUID) o : UUID.fromString((String) o);
this.forwardedToName = (String) map.get("forwardedToName"); this.forwardedToName = (String) map.get("forwardedToName");
} }
if (map.containsKey("playerDeleted")) this.playerDeleted = (boolean) map.get("playerDeleted");
// ─── NEU: Laden des Soft Delete Flags ─── if (map.containsKey("category")) this.categoryKey = (String) map.get("category");
if (map.containsKey("playerDeleted")) { if (map.containsKey("priority")) this.priority = TicketPriority.fromString((String) map.get("priority"));
this.playerDeleted = (boolean) map.get("playerDeleted"); if (map.containsKey("playerRating")) this.playerRating = (String) map.get("playerRating");
} if (map.containsKey("claimerNotified")) this.claimerNotified = (boolean) map.get("claimerNotified");
} }
// --- NEU: Methode zum Speichern in die YAML (Serialisierung) ---
@Override @Override
public Map<String, Object> serialize() { public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("id", id); map.put("id", id);
// WICHTIG: UUID als String speichern, um !!java.util.UUID Tag zu vermeiden
map.put("creatorUUID", creatorUUID.toString()); map.put("creatorUUID", creatorUUID.toString());
map.put("creatorName", creatorName); map.put("creatorName", creatorName);
map.put("message", message); map.put("message", message);
map.put("world", worldName); map.put("world", worldName);
map.put("x", x); map.put("y", y); map.put("z", z);
map.put("x", x); map.put("yaw", yaw); map.put("pitch", pitch);
map.put("y", y);
map.put("z", z);
map.put("yaw", yaw);
map.put("pitch", pitch);
map.put("status", status.name()); map.put("status", status.name());
// Timestamps als Long speichern
if (createdAt != null) map.put("createdAt", createdAt.getTime()); if (createdAt != null) map.put("createdAt", createdAt.getTime());
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime()); if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
if (closedAt != null) map.put("closedAt", closedAt.getTime()); if (closedAt != null) map.put("closedAt", closedAt.getTime());
if (closeComment != null) map.put("closeComment", closeComment); if (closeComment != null) map.put("closeComment", closeComment);
if (claimerUUID != null) { map.put("claimerUUID", claimerUUID.toString()); map.put("claimerName", claimerName); }
if (claimerUUID != null) { if (forwardedToUUID != null) { map.put("forwardedToUUID", forwardedToUUID.toString()); map.put("forwardedToName", forwardedToName); }
map.put("claimerUUID", claimerUUID.toString());
map.put("claimerName", claimerName);
}
if (forwardedToUUID != null) {
map.put("forwardedToUUID", forwardedToUUID.toString());
map.put("forwardedToName", forwardedToName);
}
// ─── NEU: Speichern des Soft Delete Flags ───
map.put("playerDeleted", playerDeleted); map.put("playerDeleted", playerDeleted);
map.put("category", categoryKey);
map.put("priority", priority.name());
if (playerRating != null) map.put("playerRating", playerRating);
map.put("claimerNotified", claimerNotified);
return map; return map;
} }
// --- NEU: Registrierung --- public static void register() { ConfigurationSerialization.registerClass(Ticket.class, "Ticket"); }
public static void register() {
ConfigurationSerialization.registerClass(Ticket.class, "Ticket");
}
// --- Deine ursprüngliche getLocation Methode (beibehalten) ---
public Location getLocation() { public Location getLocation() {
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
if (world == null) return null; return world == null ? null : new Location(world, x, y, z, yaw, pitch);
return new Location(world, x, y, z, yaw, pitch);
} }
// ─────────────────────────── Getter & Setter ──────────────────────────── private static double toDouble(Object o) { return o instanceof Double d ? d : ((Number) o).doubleValue(); }
private static float toFloat(Object o) { return o instanceof Float f ? f : ((Number) o).floatValue(); }
private static long toLong(Object o) { return ((Number) o).longValue(); }
public int getId() { return id; } public int getId() { return id; }
public void setId(int id) { this.id = id; } public void setId(int id) { this.id = id; }
public UUID getCreatorUUID() { return creatorUUID; } public UUID getCreatorUUID() { return creatorUUID; }
public void setCreatorUUID(UUID creatorUUID) { this.creatorUUID = creatorUUID; } public void setCreatorUUID(UUID v) { this.creatorUUID = v; }
public String getCreatorName() { return creatorName; } public String getCreatorName() { return creatorName; }
public void setCreatorName(String creatorName) { this.creatorName = creatorName; } public void setCreatorName(String v) { this.creatorName = v; }
public String getMessage() { return message; } public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; } public void setMessage(String v) { this.message = v; }
public String getWorldName() { return worldName; } public String getWorldName() { return worldName; }
public void setWorldName(String worldName) { this.worldName = worldName; } public void setWorldName(String v) { this.worldName = v; }
public double getX() { return x; } public double getX() { return x; }
public void setX(double x) { this.x = x; } public void setX(double v) { this.x = v; }
public double getY() { return y; } public double getY() { return y; }
public void setY(double y) { this.y = y; } public void setY(double v) { this.y = v; }
public double getZ() { return z; } public double getZ() { return z; }
public void setZ(double z) { this.z = z; } public void setZ(double v) { this.z = v; }
public float getYaw() { return yaw; } public float getYaw() { return yaw; }
public void setYaw(float yaw) { this.yaw = yaw; } public void setYaw(float v) { this.yaw = v; }
public float getPitch() { return pitch; } public float getPitch() { return pitch; }
public void setPitch(float pitch) { this.pitch = pitch; } public void setPitch(float v) { this.pitch = v; }
public TicketStatus getStatus() { return status; } public TicketStatus getStatus() { return status; }
public void setStatus(TicketStatus status) { this.status = status; } public void setStatus(TicketStatus v) { this.status = v; }
public UUID getClaimerUUID() { return claimerUUID; } public UUID getClaimerUUID() { return claimerUUID; }
public void setClaimerUUID(UUID claimerUUID) { this.claimerUUID = claimerUUID; } public void setClaimerUUID(UUID v) { this.claimerUUID = v; }
public String getClaimerName() { return claimerName; } public String getClaimerName() { return claimerName; }
public void setClaimerName(String claimerName) { this.claimerName = claimerName; } public void setClaimerName(String v) { this.claimerName = v; }
public UUID getForwardedToUUID() { return forwardedToUUID; } public UUID getForwardedToUUID() { return forwardedToUUID; }
public void setForwardedToUUID(UUID forwardedToUUID) { this.forwardedToUUID = forwardedToUUID; } public void setForwardedToUUID(UUID v) { this.forwardedToUUID = v; }
public String getForwardedToName() { return forwardedToName; } public String getForwardedToName() { return forwardedToName; }
public void setForwardedToName(String forwardedToName) { this.forwardedToName = forwardedToName; } public void setForwardedToName(String v) { this.forwardedToName = v; }
public Timestamp getCreatedAt() { return createdAt; } public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public void setCreatedAt(Timestamp v) { this.createdAt = v; }
public Timestamp getClaimedAt() { return claimedAt; } public Timestamp getClaimedAt() { return claimedAt; }
public void setClaimedAt(Timestamp claimedAt) { this.claimedAt = claimedAt; } public void setClaimedAt(Timestamp v) { this.claimedAt = v; }
public Timestamp getClosedAt() { return closedAt; } public Timestamp getClosedAt() { return closedAt; }
public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; } public void setClosedAt(Timestamp v) { this.closedAt = v; }
public String getCloseComment() { return closeComment; } public String getCloseComment() { return closeComment; }
public void setCloseComment(String closeComment) { this.closeComment = closeComment; } public void setCloseComment(String v) { this.closeComment = v; }
// ─── NEU: Getter/Setter für Soft Delete ───
public boolean isPlayerDeleted() { return playerDeleted; } public boolean isPlayerDeleted() { return playerDeleted; }
public void setPlayerDeleted(boolean playerDeleted) { this.playerDeleted = playerDeleted; } public void setPlayerDeleted(boolean v) { this.playerDeleted = v; }
public String getCategoryKey() { return categoryKey; }
public void setCategoryKey(String v) { this.categoryKey = v != null ? v.toLowerCase() : "general"; }
public TicketPriority getPriority() { return priority; }
public void setPriority(TicketPriority v) { this.priority = v; }
public String getPlayerRating() { return playerRating; }
public void setPlayerRating(String v) { this.playerRating = v; }
public boolean hasRating() { return playerRating != null; }
public boolean isClaimerNotified() { return claimerNotified; }
public void setClaimerNotified(boolean v) { this.claimerNotified = v; }
} }

View File

@@ -0,0 +1,33 @@
package de.ticketsystem.model;
import org.bukkit.Material;
public enum TicketCategory {
GENERAL ("Allgemein", "§7", Material.PAPER),
BUG ("Bug", "§c", Material.REDSTONE),
QUESTION ("Frage", "§e", Material.BOOK),
COMPLAINT ("Beschwerde", "§6", Material.WRITABLE_BOOK),
OTHER ("Sonstiges", "§8", Material.FEATHER);
private final String displayName;
private final String color;
private final Material guiMaterial;
TicketCategory(String displayName, String color, Material guiMaterial) {
this.displayName = displayName;
this.color = color;
this.guiMaterial = guiMaterial;
}
public String getDisplayName() { return displayName; }
public String getColor() { return color; }
public String getColored() { return color + displayName; }
public Material getGuiMaterial() { return guiMaterial; }
/** Safely parse from stored string, fall back to GENERAL. */
public static TicketCategory fromString(String s) {
if (s == null) return GENERAL;
try { return valueOf(s.toUpperCase()); }
catch (IllegalArgumentException e) { return GENERAL; }
}
}

View File

@@ -0,0 +1,47 @@
package de.ticketsystem.model;
import java.sql.Timestamp;
import java.util.UUID;
/**
* Represents a player comment/reply on a ticket.
*/
public class TicketComment {
private int id;
private int ticketId;
private UUID authorUUID;
private String authorName;
private String message;
private Timestamp createdAt;
public TicketComment() {}
public TicketComment(int ticketId, UUID authorUUID, String authorName, String message) {
this.ticketId = ticketId;
this.authorUUID = authorUUID;
this.authorName = authorName;
this.message = message;
this.createdAt = new Timestamp(System.currentTimeMillis());
}
// ─────────────── Getters / Setters ────────────────────────────────────
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public int getTicketId() { return ticketId; }
public void setTicketId(int ticketId) { this.ticketId = ticketId; }
public UUID getAuthorUUID() { return authorUUID; }
public void setAuthorUUID(UUID authorUUID) { this.authorUUID = authorUUID; }
public String getAuthorName() { return authorName; }
public void setAuthorName(String authorName){ this.authorName = authorName; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Timestamp getCreatedAt() { return createdAt; }
public void setCreatedAt(Timestamp ts) { this.createdAt = ts; }
}

View File

@@ -0,0 +1,31 @@
package de.ticketsystem.model;
import org.bukkit.Material;
public enum TicketPriority {
LOW ("Niedrig", "§a", Material.GREEN_WOOL),
NORMAL ("Normal", "§e", Material.YELLOW_WOOL),
HIGH ("Hoch", "§6", Material.ORANGE_WOOL),
URGENT ("Dringend","§c", Material.RED_WOOL);
private final String displayName;
private final String color;
private final Material guiMaterial;
TicketPriority(String displayName, String color, Material guiMaterial) {
this.displayName = displayName;
this.color = color;
this.guiMaterial = guiMaterial;
}
public String getDisplayName() { return displayName; }
public String getColor() { return color; }
public String getColored() { return color + displayName; }
public Material getGuiMaterial() { return guiMaterial; }
public static TicketPriority fromString(String s) {
if (s == null) return NORMAL;
try { return valueOf(s.toUpperCase()); }
catch (IllegalArgumentException e) { return NORMAL; }
}
}

View File

@@ -59,6 +59,80 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg
# ---------------------------------------------------- # ----------------------------------------------------
auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus)
# ----------------------------------------------------
# OPTIONALE FEATURES
# ----------------------------------------------------
# Kategorie-System (true = aktiviert)
# Spieler können beim Erstellen eine Kategorie wählen: /ticket create [kategorie] [priorität] <text>
categories-enabled: true
# Prioritäten-System (true = aktiviert)
# Spieler können beim Erstellen eine Priorität wählen: /ticket create [kategorie] [priorität] <text>
# Admins/Supporter können die Priorität nachträglich ändern: /ticket setpriority <id> <low|normal|high|urgent>
priorities-enabled: true
# Bewertungs-System (true = aktiviert)
# Spieler können nach dem Schließen den Support bewerten: /ticket rate <id> good|bad
# Ergebnisse sind in /ticket stats sichtbar
rating-enabled: true
# ----------------------------------------------------
# KATEGORIEN (nur aktiv wenn categories-enabled: true)
# ----------------------------------------------------
# Jede Kategorie hat:
# name: Anzeigename im Chat und in der GUI
# color: Farbcode mit & (Minecraft Farbcodes)
# material: Minecraft-Material für das GUI-Item (Großbuchstaben, z.B. PAPER, REDSTONE, BOOK)
# aliases: Alternative Eingaben beim /ticket create Befehl (Kleinbuchstaben!)
#
# Das erste eingetragene Item ist die Standard-Kategorie für Tickets ohne Angabe.
# Du kannst beliebig viele Kategorien hinzufügen oder entfernen.
# ----------------------------------------------------
categories:
general:
name: "Allgemein"
color: "&7"
material: "PAPER"
aliases:
- "allgemein"
- "general"
- "default"
bug:
name: "Bug"
color: "&c"
material: "REDSTONE"
aliases:
- "bug"
- "fehler"
- "error"
question:
name: "Frage"
color: "&e"
material: "BOOK"
aliases:
- "frage"
- "question"
- "help"
- "hilfe"
complaint:
name: "Beschwerde"
color: "&6"
material: "WRITABLE_BOOK"
aliases:
- "beschwerde"
- "complaint"
- "report"
- "melden"
other:
name: "Sonstiges"
color: "&8"
material: "FEATHER"
aliases:
- "sonstiges"
- "other"
- "misc"
# ---------------------------------------------------- # ----------------------------------------------------
# DISCORD WEBHOOK (Optional) # DISCORD WEBHOOK (Optional)
# ---------------------------------------------------- # ----------------------------------------------------
@@ -69,6 +143,41 @@ discord:
# Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen) # Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen)
webhook-url: "" webhook-url: ""
# Rollen-Ping: Discord-Rollen-ID (Rechtsklick auf Rolle → ID kopieren)
# Leer lassen ("") = kein Ping
role-ping-id: ""
messages:
# ── Neues Ticket ────────────────────────────────────────────────────────
new-ticket:
title: "🎫 Neues Ticket erstellt"
color: "3066993" # Grün
footer: "TicketSystem"
show-position: true # Welt & Koordinaten im Embed anzeigen
show-category: true # Kategorie im Embed anzeigen
show-priority: true # Priorität im Embed anzeigen
role-ping: false # Rollen-Ping bei neuem Ticket senden
# ── Ticket geschlossen ──────────────────────────────────────────────────
ticket-closed:
enabled: false # Webhook-Nachricht beim Schließen senden
title: "🔒 Ticket geschlossen"
color: "15158332" # Rot
footer: "TicketSystem"
show-category: true # Kategorie im Embed anzeigen
show-priority: true # Priorität im Embed anzeigen
role-ping: false # Rollen-Ping beim Schließen senden
# ── Ticket weitergeleitet ───────────────────────────────────────────────
ticket-forwarded:
enabled: false # Webhook-Nachricht beim Weiterleiten senden
title: "🔀 Ticket weitergeleitet"
color: "15105570" # Orange
footer: "TicketSystem"
show-category: true # Kategorie im Embed anzeigen
show-priority: true # Priorität im Embed anzeigen
role-ping: false # Rollen-Ping beim Weiterleiten senden
# ---------------------------------------------------- # ----------------------------------------------------
# SYSTEM-NACHRICHTEN (mit &-Farbcodes) # SYSTEM-NACHRICHTEN (mit &-Farbcodes)
# ---------------------------------------------------- # ----------------------------------------------------
@@ -94,12 +203,35 @@ messages:
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. &7(ID: {id})" ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
# --- NEU: Benachrichtigungen für den Ticket-Ersteller --- # --- BENACHRICHTIGUNGEN FÜR DEN TICKET-ERSTELLER ---
# Wird gesendet, wenn das eigene Ticket geschlossen wurde
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen." 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." ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
# --- KATEGORIEN ---
# {category} wird durch den Anzeigenamen der gewählten Kategorie ersetzt
ticket-created-category: "&aTicket &e#{id} &aerstellt! &7Kategorie: {category}"
category-invalid: "&cUnbekannte Kategorie: &e{input}&c. Verfügbare Kategorien: &e{categories}"
# --- KOMMENTARE ---
comment-saved: "&aDein Kommentar zu Ticket &e#{id} &awurde gespeichert."
comment-notify: "&e[Ticket #{id}] &f{author} &7kommentiert: &f{message}"
comment-no-permission: "&cDu kannst nur deine eigenen Tickets kommentieren."
# --- BEWERTUNGEN ---
rating-saved-good: "&aDanke für deine Bewertung! &a👍 Positiv"
rating-saved-bad: "&aDanke für deine Bewertung! &c👎 Negativ"
rating-already-rated: "&cDu hast dieses Ticket bereits bewertet."
rating-not-yours: "&cDu kannst nur deine eigenen Tickets bewerten."
rating-disabled: "&cBewertungen sind aktuell deaktiviert."
rating-prompt: "&6Wie zufrieden bist du mit dem Support?\n&a/ticket rate {id} good &7 👍 Gut\n&c/ticket rate {id} bad &7 👎 Schlecht"
# --- BLACKLIST ---
blacklist-added: "&a{player} &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e{reason}"
blacklist-removed: "&a{player} &awurde von der Blacklist entfernt."
blacklist-already: "&cSpieler ist bereits auf der Blacklist."
blacklist-not-found: "&cSpieler war nicht auf der Blacklist."
blacklist-blocked: "&cDu wurdest vom Ticket-System gesperrt und kannst keine Tickets erstellen."
# --- FEHLER & HINWEISE --- # --- 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."

View File

@@ -1,5 +1,5 @@
name: TicketSystem name: TicketSystem
version: 1.0.3 version: 1.0.4
main: de.ticketsystem.TicketPlugin main: de.ticketsystem.TicketPlugin
api-version: 1.20 api-version: 1.20
author: M_Viper author: M_Viper
@@ -8,24 +8,49 @@ description: Ingame Support Ticket System with MySQL
commands: commands:
ticket: ticket:
description: TicketSystem Hauptbefehl description: TicketSystem Hauptbefehl
usage: /ticket <create|list|claim|close|forward|reload> usage: |
/ticket create [Kategorie] <Text>
/ticket list
/ticket comment <ID> <Nachricht>
/ticket rate <ID> <good|bad>
/ticket claim <ID>
/ticket close <ID> [Kommentar]
/ticket forward <ID> <Spieler>
/ticket blacklist <add|remove|list> [Spieler] [Grund]
/ticket stats
/ticket archive
/ticket reload
aliases: [t, support] aliases: [t, support]
permissions: permissions:
# ── Spieler-Permissions ───────────────────────────────────────────────────
ticket.create: ticket.create:
description: Spieler kann Tickets erstellen description: Spieler kann Tickets erstellen und kommentieren
default: true default: true
# ── Supporter-Permissions ─────────────────────────────────────────────────
ticket.support: ticket.support:
description: Supporter kann Tickets einsehen und claimen description: Supporter kann Tickets einsehen, claimen und schließen
default: false default: false
ticket.archive: ticket.archive:
description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen) description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen)
default: false default: false
# ── Admin-Permissions ────────────────────────────────────────────────────
ticket.admin: ticket.admin:
description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload description: >
Admin hat vollen Zugriff: Weiterleiten, Blacklist verwalten,
Statistiken, Reload, Archiv, Export/Import, Migration
default: op default: op
children: children:
ticket.support: true ticket.support: true
ticket.blacklist: true
ticket.blacklist:
description: Kann Spieler zur Ticket-Blacklist hinzufügen und entfernen
default: false