From 02811bafbd3863dc3e3d0a3e34823c924a49af7c Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 23 Feb 2026 13:06:59 +0100 Subject: [PATCH] Update from Git Manager GUI --- .../java/de/ticketsystem/TicketPlugin.java | 74 ++- .../java/de/ticketsystem/UpdateChecker.java | 56 ++- .../de/ticketsystem/cache/TicketCache.java | 93 ++++ .../ticketsystem/commands/TicketCommand.java | 334 ++++++++----- .../database/DatabaseManager.java | 6 +- src/main/java/de/ticketsystem/gui/FaqGUI.java | 449 ++++++++++++++++++ .../de/ticketsystem/manager/FaqManager.java | 181 +++++++ .../java/de/ticketsystem/model/FaqEntry.java | 29 ++ src/main/resources/plugin.yml | 2 +- 9 files changed, 1050 insertions(+), 174 deletions(-) create mode 100644 src/main/java/de/ticketsystem/cache/TicketCache.java create mode 100644 src/main/java/de/ticketsystem/gui/FaqGUI.java create mode 100644 src/main/java/de/ticketsystem/manager/FaqManager.java create mode 100644 src/main/java/de/ticketsystem/model/FaqEntry.java diff --git a/src/main/java/de/ticketsystem/TicketPlugin.java b/src/main/java/de/ticketsystem/TicketPlugin.java index a9e8774..f115350 100644 --- a/src/main/java/de/ticketsystem/TicketPlugin.java +++ b/src/main/java/de/ticketsystem/TicketPlugin.java @@ -1,12 +1,15 @@ package de.ticketsystem; import de.ticketsystem.bungee.BungeeMessenger; +import de.ticketsystem.cache.TicketCache; import de.ticketsystem.commands.TicketCommand; import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.discord.DiscordWebhook; +import de.ticketsystem.gui.FaqGUI; import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.manager.CategoryManager; +import de.ticketsystem.manager.FaqManager; import de.ticketsystem.manager.TicketManager; import de.ticketsystem.model.Ticket; import org.bukkit.ChatColor; @@ -23,16 +26,18 @@ public class TicketPlugin extends JavaPlugin { /** * Name dieses Servers im BungeeCord-Netzwerk. * Konfigurierbar in config.yml → server-name - * Wird in Tickets gespeichert und in Benachrichtigungen angezeigt. */ private String serverName; private DatabaseManager databaseManager; private TicketManager ticketManager; private CategoryManager categoryManager; + private FaqManager faqManager; private TicketGUI ticketGUI; + private FaqGUI faqGUI; private DiscordWebhook discordWebhook; private BungeeMessenger bungeeMessenger; + private TicketCache ticketCache; @Override public void onEnable() { @@ -44,9 +49,7 @@ public class TicketPlugin extends JavaPlugin { Ticket.register(); // ── BungeeCord Plugin-Messaging-Kanäle registrieren ─────────────── - // Ausgehend: BungeeCord-Standardkanal (für Forward / Message) getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.BUNGEE_CHANNEL); - // Eingehend & Ausgehend: Eigener Kanal für Team- und Spielerbenachrichtigungen getServer().getMessenger().registerOutgoingPluginChannel(this, BungeeMessenger.CUSTOM_CHANNEL); bungeeMessenger = new BungeeMessenger(this); @@ -55,67 +58,65 @@ public class TicketPlugin extends JavaPlugin { // Server-Name aus Config lesen serverName = getConfig().getString("server-name", "unknown"); if ("unknown".equals(serverName)) { - getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert! " + - "Setze 'server-name: dein-server' für korrekte Cross-Server-Anzeige."); - } else { - getLogger().info("[BungeeCord] Server-Name: §e" + serverName); + getLogger().warning("[BungeeCord] Kein 'server-name' in der config.yml definiert!"); } - // BungeeCord-Hinweis prüfen + // BungeeCord-Hinweis nur bei deaktiviertem Feature ausgeben if (!getConfig().getBoolean("bungeecord", false)) { - getLogger().info("[BungeeCord] Hinweis: Cross-Server-Features sind deaktiviert. " + - "Setze 'bungeecord: true' in der config.yml und stelle sicher, " + - "dass 'bungeecord: true' auch in spigot.yml gesetzt ist."); - } else { - getLogger().info("[BungeeCord] Cross-Server-Benachrichtigungen aktiviert."); + getLogger().info("[BungeeCord] Cross-Server-Features deaktiviert. Setze 'bungeecord: true' um sie zu aktivieren."); } - // Update-Checker + // Update-Checker (nur Warnung wenn Update verfügbar – kein API-Raw-Log) int resourceId = 132757; new UpdateChecker(this, resourceId).getVersion(version -> { String current = getDescription().getVersion(); if (!current.equals(version)) { String msg = ChatColor.translateAlternateColorCodes('&', - "&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); - getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")"); - getServer().getScheduler().runTaskLater(this, () -> { + "&6[TicketSystem] &eNeue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); + getLogger().warning("Neue Version verfügbar: " + version + " (aktuell: " + current + ")"); + getServer().getScheduler().runTaskLater(this, () -> getServer().getOnlinePlayers().stream() .filter(p -> p.hasPermission("ticket.admin")) - .forEach(p -> p.sendMessage(msg)); - }, 20L); - } else { - getLogger().info("TicketSystem ist aktuell (Version " + current + ")"); + .forEach(p -> p.sendMessage(msg)), 20L); } }); - // Versionsprüfung + // Versionsprüfung der config.yml String configVersion = getConfig().getString("version", ""); String expectedVersion = "2.0"; if (!expectedVersion.equals(configVersion)) { - getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion + getLogger().warning("[WARNUNG] config.yml-Version (" + configVersion + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!"); } debug = getConfig().getBoolean("debug", false); + // ── Performance: Ticket-Cache ────────────────────────────────────── + long cacheTtl = getConfig().getLong("cache-ttl-seconds", 60) * 1000L; + ticketCache = new TicketCache(cacheTtl); + + // Regelmäßige Cache-Bereinigung alle 5 Minuten + getServer().getScheduler().runTaskTimerAsynchronously(this, + () -> ticketCache.evictExpired(), 6000L, 6000L); + // Datenbankverbindung databaseManager = new DatabaseManager(this); if (!databaseManager.connect()) { getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); } - // Manager, GUI & Discord-Webhook initialisieren + // Manager, GUI, FAQ & Discord-Webhook initialisieren categoryManager = new CategoryManager(this); ticketManager = new TicketManager(this); + faqManager = new FaqManager(this); ticketGUI = new TicketGUI(this); + faqGUI = new FaqGUI(this); discordWebhook = new DiscordWebhook(this); if (getConfig().getBoolean("discord.enabled", false)) { String url = getConfig().getString("discord.webhook-url", ""); if (url.isEmpty()) { - getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!"); - } else { - getLogger().info("[DiscordWebhook] Integration aktiv."); + getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in config.yml eingetragen!"); } } @@ -126,6 +127,7 @@ public class TicketPlugin extends JavaPlugin { getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(ticketGUI, this); + getServer().getPluginManager().registerEvents(faqGUI, this); // Automatische Archivierung int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); @@ -137,18 +139,17 @@ public class TicketPlugin extends JavaPlugin { getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); } }, ticks, ticks); - getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert."); } - getLogger().info("TicketSystem erfolgreich gestartet!"); + getLogger().info("TicketSystem v" + getDescription().getVersion() + " erfolgreich gestartet!"); } @Override public void onDisable() { - // Plugin-Messaging-Kanäle abmelden getServer().getMessenger().unregisterOutgoingPluginChannel(this); getServer().getMessenger().unregisterIncomingPluginChannel(this); + if (ticketCache != null) ticketCache.clear(); if (databaseManager != null) databaseManager.disconnect(); getLogger().info("TicketSystem wurde deaktiviert."); } @@ -171,20 +172,13 @@ public class TicketPlugin extends JavaPlugin { public DatabaseManager getDatabaseManager() { return databaseManager; } public TicketManager getTicketManager() { return ticketManager; } public CategoryManager getCategoryManager() { return categoryManager; } + public FaqManager getFaqManager() { return faqManager; } public TicketGUI getTicketGUI() { return ticketGUI; } + public FaqGUI getFaqGUI() { return faqGUI; } public DiscordWebhook getDiscordWebhook() { return discordWebhook; } public BungeeMessenger getBungeeMessenger() { return bungeeMessenger; } + public TicketCache getTicketCache() { return ticketCache; } public boolean isDebug() { return debug; } - - /** - * BungeeCord: Gibt den konfigurierten Server-Namen zurück. - * Entspricht dem Wert aus config.yml → server-name. - */ public String getServerName() { return serverName; } - - /** - * BungeeCord: Gibt zurück ob Cross-Server-Features aktiviert sind. - * Entspricht config.yml → bungeecord: true - */ public boolean isBungeeCordEnabled() { return getConfig().getBoolean("bungeecord", false); } } \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/UpdateChecker.java b/src/main/java/de/ticketsystem/UpdateChecker.java index 7cd4f8d..9829de4 100644 --- a/src/main/java/de/ticketsystem/UpdateChecker.java +++ b/src/main/java/de/ticketsystem/UpdateChecker.java @@ -10,31 +10,65 @@ import java.util.function.Consumer; /** * UpdateChecker für SpigotMC-Plugins. - * Prüft asynchron, ob eine neue Version verfügbar ist. - * Quelle: https://www.spigotmc.org/wiki/creating-an-update-checker-that-checks-for-updates + * Prüft asynchron ob eine neue Version verfügbar ist. + * Gibt den Consumer nur aus, wenn die Spigot-Version NEUER ist als die lokale. */ public class UpdateChecker { private final JavaPlugin plugin; private final int resourceId; public UpdateChecker(JavaPlugin plugin, int resourceId) { - this.plugin = plugin; + this.plugin = plugin; this.resourceId = resourceId; } public void getVersion(final Consumer consumer) { Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { - try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) { + try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + + this.resourceId).openStream(); + Scanner scann = new Scanner(is)) { if (scann.hasNext()) { - String latest = scann.next(); - plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'"); - consumer.accept(latest); - } else { - plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!"); + String spigotVersion = scann.next().trim(); + String localVersion = plugin.getDescription().getVersion().trim(); + + // Nur melden wenn Spigot-Version wirklich neuer ist + if (isNewerVersion(spigotVersion, localVersion)) { + consumer.accept(spigotVersion); + } } } catch (IOException e) { - plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); + // Netzwerkfehler schweigen – kein Spam in der Konsole + if (((TicketPlugin) plugin).isDebug()) { + plugin.getLogger().info("[UpdateChecker] Konnte nicht prüfen: " + e.getMessage()); + } } }); } -} + + /** + * Vergleicht zwei semantische Versionen (z.B. "1.0.5" und "1.0.6"). + * + * @param spigot Version von SpigotMC + * @param local Lokale Plugin-Version + * @return true wenn spigot NEUER ist als local + */ + private boolean isNewerVersion(String spigot, String local) { + try { + String[] spigotParts = spigot.split("\\."); + String[] localParts = local.split("\\."); + + int length = Math.max(spigotParts.length, localParts.length); + for (int i = 0; i < length; i++) { + int s = i < spigotParts.length ? Integer.parseInt(spigotParts[i]) : 0; + int l = i < localParts.length ? Integer.parseInt(localParts[i]) : 0; + + if (s > l) return true; // Spigot ist neuer + if (s < l) return false; // Lokal ist neuer (z.B. noch nicht veröffentlicht) + } + return false; // Versionen identisch + } catch (NumberFormatException e) { + // Fallback: einfacher String-Vergleich falls Format ungewöhnlich + return !spigot.equals(local); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/cache/TicketCache.java b/src/main/java/de/ticketsystem/cache/TicketCache.java new file mode 100644 index 0000000..6d0ca52 --- /dev/null +++ b/src/main/java/de/ticketsystem/cache/TicketCache.java @@ -0,0 +1,93 @@ +package de.ticketsystem.cache; + +import de.ticketsystem.model.Ticket; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Einfacher TTL-basierter In-Memory-Cache für Ticket-Objekte. + * + * Reduziert wiederholte Datenbankabfragen beim häufigen Lesen desselben + * Tickets (z.B. beim Öffnen der GUI, Kommentarbenachrichtigungen usw.). + * + * Standard-TTL: 60 Sekunden (konfigurierbar per Konstruktor). + * + * Thread-sicher (ConcurrentHashMap) – kann aus asynchronen Tasks heraus + * lese- und schreibend aufgerufen werden. + */ +public class TicketCache { + + private static final long DEFAULT_TTL_MS = 60_000L; // 60 Sekunden + + private final long ttlMs; + + /** Cache-Eintrag: Ticket + Verfallszeitstempel */ + private record CacheEntry(Ticket ticket, long expiresAt) {} + + private final Map cache = new ConcurrentHashMap<>(); + + // ─────────────────────────── Konstruktor ─────────────────────────────── + + public TicketCache() { + this(DEFAULT_TTL_MS); + } + + public TicketCache(long ttlMs) { + this.ttlMs = ttlMs; + } + + // ─────────────────────────── Public API ──────────────────────────────── + + /** + * Gibt ein gecachtes Ticket zurück oder {@code null} wenn nicht vorhanden + * oder der Eintrag abgelaufen ist. + */ + public Ticket get(int ticketId) { + CacheEntry entry = cache.get(ticketId); + if (entry == null) return null; + if (System.currentTimeMillis() > entry.expiresAt()) { + cache.remove(ticketId); + return null; + } + return entry.ticket(); + } + + /** + * Speichert ein Ticket im Cache. + * Überschreibt vorhandene Einträge. + */ + public void put(Ticket ticket) { + if (ticket == null) return; + cache.put(ticket.getId(), new CacheEntry(ticket, System.currentTimeMillis() + ttlMs)); + } + + /** + * Entfernt ein Ticket aus dem Cache (z.B. nach einem Update). + */ + public void invalidate(int ticketId) { + cache.remove(ticketId); + } + + /** + * Leert den gesamten Cache (z.B. nach einem Plugin-Reload). + */ + public void clear() { + cache.clear(); + } + + /** + * Entfernt alle abgelaufenen Einträge. + * Sollte periodisch aufgerufen werden um Speicher freizugeben. + */ + public void evictExpired() { + long now = System.currentTimeMillis(); + cache.entrySet().removeIf(e -> now > e.getValue().expiresAt()); + } + + /** Gibt die aktuelle Anzahl der (möglicherweise teils abgelaufenen) Einträge zurück. */ + public int size() { + return cache.size(); + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/commands/TicketCommand.java b/src/main/java/de/ticketsystem/commands/TicketCommand.java index ccbf1cf..d436cf1 100644 --- a/src/main/java/de/ticketsystem/commands/TicketCommand.java +++ b/src/main/java/de/ticketsystem/commands/TicketCommand.java @@ -1,6 +1,7 @@ package de.ticketsystem.commands; import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.FaqEntry; import de.ticketsystem.model.Ticket; import de.ticketsystem.manager.CategoryManager; @@ -35,27 +36,152 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (args.length == 0) { plugin.getTicketManager().sendHelpMessage(player); return true; } switch (args[0].toLowerCase()) { - case "create" -> handleCreate(player, args); - case "list" -> handleList(player); - case "claim" -> handleClaim(player, args); - case "close" -> handleClose(player, args); - case "forward" -> handleForward(player, args); - case "reload" -> handleReload(player); - case "migrate" -> handleMigrate(player, args); - case "export" -> handleExport(player, args); - case "import" -> handleImport(player, args); - case "stats" -> handleStats(player); - case "top" -> handleTop(player); - case "archive" -> handleArchive(player); - case "comment" -> handleComment(player, args); - case "blacklist" -> handleBlacklist(player, args); - case "rate" -> handleRate(player, args); + case "create" -> handleCreate(player, args); + case "list" -> handleList(player); + case "claim" -> handleClaim(player, args); + case "close" -> handleClose(player, args); + case "forward" -> handleForward(player, args); + case "reload" -> handleReload(player); + case "migrate" -> handleMigrate(player, args); + case "export" -> handleExport(player, args); + case "import" -> handleImport(player, args); + case "stats" -> handleStats(player); + case "top" -> handleTop(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); + case "faq" -> handleFaq(player, args); + default -> plugin.getTicketManager().sendHelpMessage(player); } return true; } + // ─────────────────────────── /ticket faq ─────────────────────────────── + + /** + * /ticket faq – öffnet die FAQ-GUI (alle Spieler) + * /ticket faq add | – fügt ein FAQ hinzu (ticket.admin) + * /ticket faq edit | – bearbeitet ein FAQ (ticket.admin) + * /ticket faq delete – löscht ein FAQ (ticket.admin) + * /ticket faq reload – lädt FAQs neu (ticket.admin) + */ + private void handleFaq(Player player, String[] args) { + // Kein Subbefehl → GUI öffnen + if (args.length == 1) { + plugin.getFaqGUI().openFaqGUI(player); + return; + } + + switch (args[1].toLowerCase()) { + + // ── /ticket faq add | ──────────────────────── + case "add" -> { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (args.length < 3) { + player.sendMessage(plugin.color("&cBenutzung: /ticket faq add | ")); + player.sendMessage(plugin.color("&7Beispiel: &e/ticket faq add Wie erstelle ich ein Ticket? | Nutze /ticket create.")); + return; + } + String full = String.join(" ", Arrays.copyOfRange(args, 2, args.length)); + String[] parts = full.split("\\s*\\|\\s*", 2); + if (parts.length < 2 || parts[0].isBlank() || parts[1].isBlank()) { + player.sendMessage(plugin.color("&cTrenne Frage und Antwort mit &e|&c, z.B.:")); + player.sendMessage(plugin.color("&e/ticket faq add Wie erstelle ich ein Ticket? | Nutze /ticket create.")); + return; + } + FaqEntry created = plugin.getFaqManager().add(parts[0].trim(), parts[1].trim()); + player.sendMessage(plugin.color("&aFAQ &e#" + created.getId() + " &awurde erfolgreich erstellt!")); + player.sendMessage(plugin.color("&7Frage: &e" + created.getQuestion())); + player.sendMessage(plugin.color("&7Antwort: &f" + created.getAnswer())); + } + + // ── /ticket faq edit | ────────────────── + case "edit" -> { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (args.length < 4) { + player.sendMessage(plugin.color("&cBenutzung: /ticket faq edit | ")); + return; + } + int id; + try { id = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige FAQ-ID: &e" + args[2])); return; + } + String full = String.join(" ", Arrays.copyOfRange(args, 3, args.length)); + String[] parts = full.split("\\s*\\|\\s*", 2); + if (parts.length < 2 || parts[0].isBlank() || parts[1].isBlank()) { + player.sendMessage(plugin.color("&cTrenne Frage und Antwort mit &e|&c.")); + return; + } + boolean ok = plugin.getFaqManager().edit(id, parts[0].trim(), parts[1].trim()); + if (ok) player.sendMessage(plugin.color("&aFAQ &e#" + id + " &awurde erfolgreich aktualisiert!")); + else player.sendMessage(plugin.color("&cFAQ &e#" + id + " &cwurde nicht gefunden.")); + } + + // ── /ticket faq delete ───────────────────────────────────── + case "delete", "remove" -> { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + if (args.length < 3) { + player.sendMessage(plugin.color("&cBenutzung: /ticket faq delete ")); return; + } + int id; + try { id = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cUngültige FAQ-ID: &e" + args[2])); return; + } + boolean ok = plugin.getFaqManager().delete(id); + if (ok) player.sendMessage(plugin.color("&aFAQ &e#" + id + " &awurde gelöscht.")); + else player.sendMessage(plugin.color("&cFAQ &e#" + id + " &cwurde nicht gefunden.")); + } + + // ── /ticket faq reload ────────────────────────────────────────── + case "reload" -> { + if (!player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; + } + plugin.getFaqManager().reload(); + player.sendMessage(plugin.color("&aFAQs wurden neu geladen. (" + + plugin.getFaqManager().getAll().size() + " Einträge)")); + } + + // ── /ticket faq list ──────────────────────────────────────────── + case "list" -> { + List all = plugin.getFaqManager().getAll(); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6Häufige Fragen (FAQ) &7— " + all.size() + " Einträge")); + player.sendMessage(plugin.color("&8&m ")); + if (all.isEmpty()) { + player.sendMessage(plugin.color("&7Noch keine FAQs vorhanden.")); + } else { + for (FaqEntry e : all) { + player.sendMessage(plugin.color("&e#" + e.getId() + " &f" + e.getQuestion())); + player.sendMessage(plugin.color(" &7→ &f" + e.getAnswer())); + } + } + player.sendMessage(plugin.color("&8&m ")); + if (player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.color("&7Befehle: &e/ticket faq add &8| &e/ticket faq edit &8| &e/ticket faq delete ")); + } + } + + default -> { + player.sendMessage(plugin.color("&cUnbekannter FAQ-Befehl.")); + player.sendMessage(plugin.color("&7Benutze &e/ticket faq &7zum Öffnen der GUI.")); + if (player.hasPermission("ticket.admin")) { + player.sendMessage(plugin.color("&7Admin-Befehle: &e/ticket faq add | edit | delete | reload | list")); + } + } + } + } + // ─────────────────────────── /ticket create ──────────────────────────── private void handleCreate(Player player, String[] args) { @@ -63,7 +189,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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; @@ -91,7 +216,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { return; } - // Kategorie und Priorität optional parsen CategoryManager cm = plugin.getCategoryManager(); ConfigCategory category = cm.getDefault(); TicketPriority priority = TicketPriority.NORMAL; @@ -147,13 +271,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation()); ticket.setCategoryKey(finalCategory.getKey()); ticket.setPriority(finalPriority); - // BungeeCord: Server-Name des erstellenden Servers speichern ticket.setServerName(plugin.getServerName()); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int id = plugin.getDatabaseManager().createTicket(ticket); if (id == -1) { player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!")); return; } ticket.setId(id); + // Cache befüllen + plugin.getTicketCache().put(ticket); plugin.getTicketManager().setCooldown(player.getUniqueId()); Bukkit.getScheduler().runTask(plugin, () -> { String catInfo = plugin.getConfig().getBoolean("categories-enabled", true) @@ -194,14 +319,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter { boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; } - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) return; player.sendMessage(plugin.formatMessage("messages.ticket-claimed") .replace("{id}", String.valueOf(ticketId)) .replace("{player}", ticket.getCreatorName())); plugin.getTicketManager().notifyCreatorClaimed(ticket); - // Teleport beim Annehmen entfernt – Teleport nur noch über das GUI-Item möglich. + plugin.getTicketCache().invalidate(ticketId); // Stale cache löschen }); }); } @@ -224,10 +349,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); if (success) { - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); - // Ticket in persistente Stats-Tabelle eintragen (bleibt auch nach Löschung erhalten). - // player.getName() = der Admin der /ticket close ausgeführt hat – nicht zwingend der Claimer. + Ticket ticket = getCachedOrFetch(ticketId); if (ticket != null) plugin.getDatabaseManager().recordClosedTicket(ticket, player.getName()); + plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); if (ticket != null) { @@ -252,29 +376,26 @@ public class TicketCommand implements CommandExecutor, TabCompleter { try { id = Integer.parseInt(args[1]); } catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; } - // BungeeCord: Ziel-Spieler lokal suchen Player localTarget = Bukkit.getPlayer(args[2]); - if (localTarget == null) { if (plugin.isBungeeCordEnabled()) { - player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2] - + " &7ist auf diesem Server nicht online.")); - player.sendMessage(plugin.color("&7Tipp: Forwarden geht nur zu Spielern auf &bdemselben Server&7.")); + player.sendMessage(plugin.color("&7[BungeeCord] Spieler &e" + args[2] + " &7ist auf diesem Server nicht online.")); } else { player.sendMessage(plugin.color("&cSpieler nicht gefunden!")); } return; } - final int ticketId = id; - final String fromName = player.getName(); - final Player t = localTarget; + final int ticketId = id; + final String fromName = player.getName(); + final Player t = localTarget; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { boolean success = plugin.getDatabaseManager().forwardTicket(ticketId, t.getUniqueId(), t.getName()); + plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { if (!success) { player.sendMessage(plugin.formatMessage("messages.ticket-not-found")); return; } - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) return; player.sendMessage(plugin.color("&aTicket &e#" + ticketId + " &awurde an &e" + t.getName() + " &aweitergeleitet.")); plugin.getTicketManager().notifyForwardedTo(ticket, fromName); @@ -297,17 +418,17 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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; + final int ticketId = id; + final String message = msg; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) { Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); return; } - boolean isOwner = ticket.getCreatorUUID().equals(player.getUniqueId()); - boolean isStaff = player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"); + 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; @@ -327,59 +448,27 @@ public class TicketCommand implements CommandExecutor, TabCompleter { }); } - /** - * Benachrichtigt alle relevanten Empfänger über einen neuen Kommentar. - * - * ── BUG FIX #2 ────────────────────────────────────────────────────────── - * Vorher: broadcastTeamNotification() wurde am Ende ZUSÄTZLICH aufgerufen – - * obwohl alle lokalen Supporter bereits einzeln per Schleife - * benachrichtigt wurden. Das führte zu: - * a) Doppelter Nachricht für lokale Supporter - * b) broadcastTeamNotification() sendet intern ebenfalls lokal → - * lokale Supporter sahen die Nachricht dreifach - * c) Das Forward-Paket an andere Server war korrekt, aber die - * Empfänger auf anderen Servern sahen auch Duplikate da - * broadcastTeamNotification() wiederum lokal sendet - * - * Fix: broadcastTeamNotification() ERSETZT die lokale Supporter-Schleife - * komplett. Die Methode sendet bereits lokal direkt und forwardet - * gleichzeitig an alle anderen BungeeCord-Server. - * Im Standalone-Modus bleibt die lokale Schleife erhalten. - * ──────────────────────────────────────────────────────────────────────── - */ 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; - // ── 1. 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 if (plugin.isBungeeCordEnabled()) { - // BungeeCord: Zustellung via Plugin-Messaging, kein Pending-Eintrag - // (PlayerJoinListener übernimmt Offline-Fallback via close_notified-Logik) - plugin.getBungeeMessenger().sendMessageToPlayer( - ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg); + plugin.getBungeeMessenger().sendMessageToPlayer(ticket.getCreatorUUID(), ticket.getCreatorName(), onlineMsg); } else { - // Standalone: Offline → für nächsten Login speichern Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getDatabaseManager().addPendingNotification(ticket.getCreatorUUID(), offlineMsg)); } } - // ── 2. Supporter/Admin benachrichtigen (wenn Kommentar vom Spieler kommt) ── if (!author.hasPermission("ticket.support") && !author.hasPermission("ticket.admin")) { - if (plugin.isBungeeCordEnabled()) { - // BungeeCord-Modus: broadcastTeamNotification() übernimmt ALLES – - // lokal direkt + Forward an alle anderen Server in einem Paket. - // KEINE zusätzliche lokale Schleife, da das zu Duplikaten führt. plugin.getBungeeMessenger().broadcastTeamNotification(onlineMsg); - } else { - // Standalone-Modus: Claimer gezielt benachrichtigen - UUID claimerUUID = ticket.getClaimerUUID(); + var claimerUUID = ticket.getClaimerUUID(); if (claimerUUID != null && !claimerUUID.equals(author.getUniqueId())) { Player claimer = Bukkit.getPlayer(claimerUUID); if (claimer != null && claimer.isOnline()) { @@ -391,8 +480,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { plugin.getDatabaseManager().addPendingNotification(claimerUUID, claimerOffline)); } } - - // Alle anderen Online-Supporter auf diesem Server informieren for (Player p : Bukkit.getOnlinePlayers()) { if (p.getUniqueId().equals(author.getUniqueId())) continue; if (claimerUUID != null && p.getUniqueId().equals(claimerUUID)) continue; @@ -426,11 +513,11 @@ public class TicketCommand implements CommandExecutor, TabCompleter { return; } - final int ticketId = id; + final int ticketId = id; final String finalRating = rating; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); + Ticket ticket = getCachedOrFetch(ticketId); if (ticket == null) { Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.formatMessage("messages.ticket-not-found"))); return; @@ -445,6 +532,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } boolean success = plugin.getDatabaseManager().rateTicket(ticketId, finalRating); + plugin.getTicketCache().invalidate(ticketId); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { String emoji = "THUMBS_UP".equals(finalRating) ? "§a👍 Positiv" : "§c👎 Negativ"; @@ -478,27 +566,22 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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()); + 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.")); + if (success) player.sendMessage(plugin.color("&a" + targetName + " &awurde zur Ticket-Blacklist hinzugefügt. &7Grund: &e" + reason)); + else player.sendMessage(plugin.color("&cSpieler ist bereits auf der Blacklist.")); }); }); } case "remove" -> { if (args.length < 3) { player.sendMessage(plugin.color("&cBenutzung: /ticket blacklist remove ")); return; } - String targetName = args[2]; @SuppressWarnings("deprecation") - OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); - + OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]); 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.")); + if (success) player.sendMessage(plugin.color("&a" + args[2] + " &awurde von der Blacklist entfernt.")); + else player.sendMessage(plugin.color("&cSpieler war nicht auf der Blacklist.")); }); }); } @@ -527,16 +610,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter { // ─────────────────────────── /ticket top ────────────────────────────── - /** - * Zeigt das Leaderboard der Top-5 Ticket-Ersteller. - * Basiert auf der ticket_creator_stats-Tabelle, die Werte auch nach - * dem Löschen oder Archivieren von Tickets beibehält. - * Berechtigung: ticket.create (alle Spieler) - */ private void handleTop(Player player) { if (!player.hasPermission("ticket.create") && !player.hasPermission("ticket.admin")) { - player.sendMessage(plugin.formatMessage("messages.no-permission")); - return; + player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { List top = plugin.getDatabaseManager().getTopCreators(5); @@ -553,8 +629,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { String medal = rankIdx < medals.length ? medals[rankIdx] : "&7#" + entry[0]; String name = entry[1]; String count = entry[2]; - player.sendMessage(plugin.color( - medal + " &f" + String.format("%-16s", name) + player.sendMessage(plugin.color(medal + " &f" + String.format("%-16s", name) + " &e" + count + " &7Ticket" + (Integer.parseInt(count) == 1 ? "" : "s"))); } } @@ -570,7 +645,9 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (!player.hasPermission("ticket.admin")) { player.sendMessage(plugin.formatMessage("messages.no-permission")); return; } plugin.reloadConfig(); plugin.getCategoryManager().reload(); - player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(inkl. Kategorien)")); + plugin.getFaqManager().reload(); + plugin.getTicketCache().clear(); + player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen. &7(Kategorien, FAQs, Cache geleert)")); if (plugin.isBungeeCordEnabled()) { player.sendMessage(plugin.color("&8[BungeeCord] &7Server: &b" + plugin.getServerName())); } @@ -584,7 +661,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { int count = plugin.getDatabaseManager().archiveClosedTickets(); Bukkit.getScheduler().runTask(plugin, () -> { if (count > 0) player.sendMessage(plugin.formatMessage("messages.archive-success").replace("{count}", String.valueOf(count))); - else player.sendMessage(plugin.formatMessage("messages.archive-fail")); + else player.sendMessage(plugin.formatMessage("messages.archive-fail")); }); }); } @@ -596,7 +673,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { var stats = plugin.getDatabaseManager().getTicketStats(); var staffRatings = plugin.getDatabaseManager().getStaffRatings(); - // Persistente Ersteller-Statistik – überlebt Löschen/Archivieren var topCreators = plugin.getDatabaseManager().getTopCreators(5); Bukkit.getScheduler().runTask(plugin, () -> { player.sendMessage(plugin.color("&8&m ")); @@ -617,26 +693,21 @@ public class TicketCommand implements CommandExecutor, TabCompleter { int percent = (int) Math.round(stats.thumbsUp * 100.0 / totalRated); player.sendMessage(plugin.color("&7Zufriedenheit: &e" + percent + "%")); } - - // Bewertungen pro Support-Mitarbeiter if (!staffRatings.isEmpty()) { player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&6Bewertungen nach Support-Mitarbeiter:")); player.sendMessage(plugin.color("&7 Name 👍 👎 Tickets Zufrieden")); for (String[] row : staffRatings) { - // row: [name, up, down, totalClosed, percent] String name = String.format("%-16s", row[0]); String up = String.format("%-5s", row[1]); String down = String.format("%-5s", row[2]); String total = String.format("%-8s", row[3]); String percent = row[4]; - player.sendMessage(plugin.color( - "&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent)); + player.sendMessage(plugin.color("&e " + name + " &a" + up + " &c" + down + " &7" + total + " &e" + percent)); } } } - // BungeeCord: Tickets pro Server anzeigen if (plugin.isBungeeCordEnabled() && !stats.byServer.isEmpty()) { player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&6Tickets nach Server:")); @@ -662,6 +733,10 @@ public class TicketCommand implements CommandExecutor, TabCompleter { } } player.sendMessage(plugin.color("&8&m ")); + + // Cache-Status anzeigen + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&7Cache: &e" + plugin.getTicketCache().size() + " &7gecachte Ticket(s)")); }); }); } @@ -680,7 +755,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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")); + else player.sendMessage(plugin.formatMessage("messages.migration-fail")); }); }); } @@ -696,7 +771,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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")); + else player.sendMessage(plugin.formatMessage("messages.export-fail")); }); }); } @@ -713,7 +788,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { 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")); + else player.sendMessage(plugin.formatMessage("messages.import-fail")); }); }); } @@ -739,20 +814,31 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (priority == null) { player.sendMessage(plugin.color("&cUngültige Priorität! Gültig: &alow&7, &enormal&7, &6high&7, &curgent")); return; } + final int finalId = ticketId; Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().setTicketPriority(ticketId, priority); + boolean success = plugin.getDatabaseManager().setTicketPriority(finalId, priority); + plugin.getTicketCache().invalidate(finalId); 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.")); - } + if (success) player.sendMessage(plugin.color("&aPriorität von Ticket &e#" + finalId + " &awurde auf " + priority.getColored() + " &agesetzt.")); + else player.sendMessage(plugin.color("&cTicket &e#" + finalId + " &cwurde nicht gefunden.")); }); }); } - /** Parst Benutzer-Eingaben zu TicketPriority. Gibt null zurück wenn keine Übereinstimmung. */ + // ─────────────────────────── Hilfsmethoden ───────────────────────────── + + /** + * Gibt ein Ticket aus dem Cache zurück, oder lädt es aus der Datenbank + * und legt es anschließend in den Cache. Gibt null zurück wenn nicht gefunden. + */ + private Ticket getCachedOrFetch(int ticketId) { + Ticket cached = plugin.getTicketCache().get(ticketId); + if (cached != null) return cached; + Ticket fresh = plugin.getDatabaseManager().getTicketById(ticketId); + if (fresh != null) plugin.getTicketCache().put(fresh); + return fresh; + } + private TicketPriority parsePriority(String input) { if (input == null) return null; return switch (input.toLowerCase()) { @@ -772,7 +858,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (!(sender instanceof Player player)) return completions; if (args.length == 1) { - List subs = new ArrayList<>(List.of("create", "list", "comment", "top")); + List subs = new ArrayList<>(List.of("create", "list", "comment", "top", "faq")); if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) subs.addAll(List.of("claim", "close")); if (plugin.getConfig().getBoolean("rating-enabled", true)) subs.add("rate"); @@ -783,6 +869,17 @@ public class TicketCommand implements CommandExecutor, TabCompleter { subs.add("setpriority"); for (String s : subs) if (s.startsWith(args[0].toLowerCase())) completions.add(s); + } else if (args.length == 2 && args[0].equalsIgnoreCase("faq")) { + List faqSubs = new ArrayList<>(List.of("list")); + if (player.hasPermission("ticket.admin")) faqSubs.addAll(List.of("add", "edit", "delete", "reload")); + for (String s : faqSubs) if (s.startsWith(args[1].toLowerCase())) completions.add(s); + + } else if (args.length == 3 && args[0].equalsIgnoreCase("faq") + && (args[1].equalsIgnoreCase("edit") || args[1].equalsIgnoreCase("delete")) + && player.hasPermission("ticket.admin")) { + for (FaqEntry e : plugin.getFaqManager().getAll()) + completions.add(String.valueOf(e.getId())); + } else if (args.length == 2 && args[0].equalsIgnoreCase("create") && plugin.getConfig().getBoolean("categories-enabled", true)) { for (ConfigCategory c : plugin.getCategoryManager().getAll()) @@ -801,7 +898,6 @@ public class TicketCommand implements CommandExecutor, TabCompleter { if (p.startsWith(args[2].toLowerCase())) completions.add(p); } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { - // BungeeCord: Nur lokal online Spieler als Tab-Completion for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName()); diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java index 6aac17f..f52cbea 100644 --- a/src/main/java/de/ticketsystem/database/DatabaseManager.java +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -128,7 +128,7 @@ public class DatabaseManager { dataSource = new HikariDataSource(config); createTables(); ensureColumns(); - plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung erfolgreich hergestellt."); return true; } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); @@ -143,7 +143,7 @@ public class DatabaseManager { return true; } } else { - plugin.getLogger().info("MySQL deaktiviert. Verwende Datei-Speicherung (data.yml)."); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL deaktiviert. Verwende Datei-Speicherung (data.yml)."); return true; } } @@ -151,7 +151,7 @@ public class DatabaseManager { public void disconnect() { if (useMySQL && dataSource != null && !dataSource.isClosed()) { dataSource.close(); - plugin.getLogger().info("MySQL-Verbindung getrennt."); + if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] MySQL-Verbindung getrennt."); } } diff --git a/src/main/java/de/ticketsystem/gui/FaqGUI.java b/src/main/java/de/ticketsystem/gui/FaqGUI.java new file mode 100644 index 0000000..de619ed --- /dev/null +++ b/src/main/java/de/ticketsystem/gui/FaqGUI.java @@ -0,0 +1,449 @@ +package de.ticketsystem.gui; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.FaqEntry; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; + +import java.net.URL; +import java.util.*; + +/** + * FAQ GUI für Spieler (Lesemodus) und Admins (Verwaltungsmodus). + * + * ──── Spieler-GUI (/ticket faq) ───────────────────────────────────────── + * Slots 0-44 : FAQ-Einträge als Custom-Skull-Items + * › Name = Frage + * › Lore = Antwort (aufgeteilt auf 40-Zeichen-Zeilen) + * Slot 49 : Seitenanzeige + * Slot 45/53 : Vorherige / Nächste Seite + * + * ──── Admin-GUI (ticket.admin-Berechtigung) ───────────────────────────── + * Wie Spieler-GUI, zusätzlich: + * Slot 50 : "Neues FAQ hinzufügen" (Lime Wool) + * Klick auf FAQ-Item → Aktions-GUI + * + * ──── Aktions-GUI (27 Slots) ──────────────────────────────────────────── + * Slot 4 : FAQ-Info (Skull) + * Slot 10 : Bearbeiten (Book & Quill) + * Slot 12 : Löschen (Barrier) + * Slot 16 : Zurück (Arrow) + * + * ──── Chat-Eingabe (Hinzufügen / Bearbeiten) ──────────────────────────── + * Step 1: Admin gibt Frage ein + * Step 2: Admin gibt Antwort ein → FAQ wird gespeichert + */ +public class FaqGUI implements Listener { + + // ─────────────────────────── Titel-Konstanten ────────────────────────── + + private static final String FAQ_GUI_TITLE = "§8§lHäufige Fragen (FAQ)"; + private static final String FAQ_ADMIN_TITLE = "§8§lFAQ verwalten"; + private static final String FAQ_ACTION_TITLE = "§8§lFAQ Aktionen"; + + /** FAQ-Einträge pro Seite (Zeilen 0-4, Slots 0-44). */ + private static final int PAGE_SIZE = 45; + + /** + * Texture-URL für die Custom-Skull-Items. + * http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e + */ + private static final String FAQ_SKIN_URL = + "http://textures.minecraft.net/texture/da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e"; + + private final TicketPlugin plugin; + + // ─────────────────────────── State ───────────────────────────────────── + + /** Slot-Map für Spieler-FAQ-GUI: UUID → (Slot → FaqEntry) */ + private final Map> slotMap = new HashMap<>(); + /** Aktuelle Seite pro Spieler in der FAQ-GUI */ + private final Map faqPage = new HashMap<>(); + /** Ob der Spieler sich in der Admin-Ansicht befindet */ + private final Set adminView = new HashSet<>(); + /** Aktuell ausgewähltes FAQ für die Aktions-GUI */ + private final Map actionEntry = new HashMap<>(); + + // ─── Chat-Input-States ────────────────────────────────────────────── + /** Wartet auf Frage-Eingabe: null = neu, "edit:" = bearbeiten */ + private final Map awaitingQuestion = new HashMap<>(); + /** Wartet auf Antwort-Eingabe: key = Frage-Text (|id) bei Edit */ + private final Map awaitingAnswer = new HashMap<>(); + + // ─────────────────────────── Konstruktor ─────────────────────────────── + + public FaqGUI(TicketPlugin plugin) { + this.plugin = plugin; + } + + // ═══════════════════════════════════════════════════════════════════════ + // PUBLIC OPEN-METHODEN + // ═══════════════════════════════════════════════════════════════════════ + + /** Öffnet die Spieler-FAQ-GUI (Seite 0). */ + public void openFaqGUI(Player player) { + openFaqGUI(player, faqPage.getOrDefault(player.getUniqueId(), 0)); + } + + /** Öffnet die Spieler-FAQ-GUI auf der angegebenen Seite. */ + public void openFaqGUI(Player player, int page) { + boolean isAdmin = player.hasPermission("ticket.admin"); + String title = isAdmin ? FAQ_ADMIN_TITLE : FAQ_GUI_TITLE; + + List all = plugin.getFaqManager().getAll(); + int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, totalPages - 1)); + faqPage.put(player.getUniqueId(), page); + + Inventory inv = Bukkit.createInventory(null, 54, title); + Map sm = new HashMap<>(); + + int start = page * PAGE_SIZE; + for (int i = 0; i < PAGE_SIZE && (start + i) < all.size(); i++) { + FaqEntry entry = all.get(start + i); + inv.setItem(i, buildFaqSkull(entry, isAdmin)); + sm.put(i, entry); + } + + slotMap.put(player.getUniqueId(), sm); + + if (isAdmin) adminView.add(player.getUniqueId()); + else adminView.remove(player.getUniqueId()); + + // ── Navigationsleiste ────────────────────────────────────────────── + fillNavBar(inv, page, totalPages, isAdmin, all.isEmpty()); + player.openInventory(inv); + } + + // ═══════════════════════════════════════════════════════════════════════ + // CLICK-EVENTS + // ═══════════════════════════════════════════════════════════════════════ + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + String title = event.getView().getTitle(); + + if (!title.equals(FAQ_GUI_TITLE) && !title.equals(FAQ_ADMIN_TITLE) + && !title.equals(FAQ_ACTION_TITLE)) return; + + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot < 0) return; + + // ── Aktions-GUI ──────────────────────────────────────────────────── + if (title.equals(FAQ_ACTION_TITLE)) { + FaqEntry entry = actionEntry.get(player.getUniqueId()); + if (entry == null) return; + switch (slot) { + case 10 -> startEditFlow(player, entry); + case 12 -> deleteFaq(player, entry); + case 16 -> openFaqGUI(player); + } + return; + } + + // ── FAQ-Listen-GUI ───────────────────────────────────────────────── + boolean isAdmin = adminView.contains(player.getUniqueId()); + int curPage = faqPage.getOrDefault(player.getUniqueId(), 0); + + // Navigationsslots + if (slot == 45) { openFaqGUI(player, curPage - 1); return; } + if (slot == 53) { openFaqGUI(player, curPage + 1); return; } + + // Admin-spezifisch: Neues FAQ hinzufügen + if (slot == 50 && isAdmin) { + startAddFlow(player); + return; + } + + // FAQ-Item angeklickt + if (slot < PAGE_SIZE) { + Map sm = slotMap.get(player.getUniqueId()); + if (sm == null) return; + FaqEntry entry = sm.get(slot); + if (entry == null) return; + + if (isAdmin) { + openActionGUI(player, entry); + } else { + // Spieler: Antwort im Chat ausgeben + player.closeInventory(); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + ": &e" + entry.getQuestion())); + player.sendMessage(plugin.color("&f" + entry.getAnswer())); + player.sendMessage(plugin.color("&8&m ")); + } + } + } + + // ─────────────────────────── Admin-Aktions-GUI ───────────────────────── + + private void openActionGUI(Player player, FaqEntry entry) { + actionEntry.put(player.getUniqueId(), entry); + Inventory inv = Bukkit.createInventory(null, 27, FAQ_ACTION_TITLE); + + // Slot 4: FAQ-Info + inv.setItem(4, buildFaqSkull(entry, false)); + + // Slot 10: Bearbeiten + inv.setItem(10, buildItem(Material.WRITABLE_BOOK, "§a§lFAQ bearbeiten", + List.of("§7Ändere Frage und Antwort", "§7dieses FAQ-Eintrags."))); + + // Slot 12: Löschen + inv.setItem(12, buildItem(Material.BARRIER, "§c§lFAQ löschen", + List.of("§7Löscht diesen FAQ-Eintrag.", "§c§lACHTUNG: §cNicht rückgängig zu machen!"))); + + // Slot 16: Zurück + inv.setItem(16, buildItem(Material.ARROW, "§7§lZurück", + List.of("§7Zurück zur FAQ-Übersicht."))); + + fillGlass(inv); + player.openInventory(inv); + } + + // ─────────────────────────── Chat-Flow: Hinzufügen ───────────────────── + + private void startAddFlow(Player player) { + player.closeInventory(); + awaitingQuestion.put(player.getUniqueId(), "new"); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6&lNeues FAQ erstellen")); + player.sendMessage(plugin.color("&7Gib die &eFrage &7ein (oder &ccancel&7):")); + player.sendMessage(plugin.color("&8&m ")); + } + + // ─────────────────────────── Chat-Flow: Bearbeiten ───────────────────── + + private void startEditFlow(Player player, FaqEntry entry) { + player.closeInventory(); + awaitingQuestion.put(player.getUniqueId(), "edit:" + entry.getId()); + player.sendMessage(plugin.color("&8&m ")); + player.sendMessage(plugin.color("&6&lFAQ #" + entry.getId() + " bearbeiten")); + player.sendMessage(plugin.color("&7Aktuelle Frage: &e" + entry.getQuestion())); + player.sendMessage(plugin.color("&7Gib die neue &eFrage &7ein (oder &ccancel&7):")); + player.sendMessage(plugin.color("&8&m ")); + } + + // ─────────────────────────── Löschen ─────────────────────────────────── + + private void deleteFaq(Player player, FaqEntry entry) { + player.closeInventory(); + boolean success = plugin.getFaqManager().delete(entry.getId()); + if (success) { + player.sendMessage(plugin.color("&aFAQ #" + entry.getId() + " &a(§e" + entry.getQuestion() + "§a) wurde gelöscht.")); + } else { + player.sendMessage(plugin.color("&cFehler: FAQ #" + entry.getId() + " konnte nicht gelöscht werden.")); + } + openFaqGUI(player); + } + + // ═══════════════════════════════════════════════════════════════════════ + // CHAT-EVENTS (Frage & Antwort Eingabe) + // ═══════════════════════════════════════════════════════════════════════ + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + UUID uuid = player.getUniqueId(); + + // ── Schritt 1: Warte auf Frage ───────────────────────────────────── + if (awaitingQuestion.containsKey(uuid)) { + event.setCancelled(true); + String state = awaitingQuestion.remove(uuid); + String input = event.getMessage().trim(); + + if (input.equalsIgnoreCase("cancel")) { + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.color("&cAbgebrochen.")); + openFaqGUI(player); + }); + return; + } + + // Frage gespeichert → jetzt auf Antwort warten + // Encoded state: "new" oder "edit:" + awaitingAnswer.put(uuid, state + "§§" + input); // "§§" as internal separator + + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.color("&7Frage gesetzt: &e" + input)); + player.sendMessage(plugin.color("&7Gib jetzt die &eAntwort &7ein (oder &ccancel&7):")); + }); + return; + } + + // ── Schritt 2: Warte auf Antwort ─────────────────────────────────── + if (awaitingAnswer.containsKey(uuid)) { + event.setCancelled(true); + String stateAndQuestion = awaitingAnswer.remove(uuid); + String input = event.getMessage().trim(); + + if (input.equalsIgnoreCase("cancel")) { + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.color("&cAbgebrochen.")); + openFaqGUI(player); + }); + return; + } + + // stateAndQuestion = "§§" + int sep = stateAndQuestion.indexOf("§§"); + String state = stateAndQuestion.substring(0, sep); + String question = stateAndQuestion.substring(sep + 2); + String answer = input; + + Bukkit.getScheduler().runTask(plugin, () -> { + if (state.equals("new")) { + // Hinzufügen + FaqEntry created = plugin.getFaqManager().add(question, answer); + player.sendMessage(plugin.color("&aFAQ #" + created.getId() + " wurde erfolgreich erstellt!")); + } else { + // Bearbeiten: state = "edit:" + int id; + try { + id = Integer.parseInt(state.substring(5)); // "edit:".length() = 5 + } catch (NumberFormatException e) { + player.sendMessage(plugin.color("&cInterner Fehler beim Bearbeiten des FAQs.")); + openFaqGUI(player); + return; + } + boolean ok = plugin.getFaqManager().edit(id, question, answer); + if (ok) player.sendMessage(plugin.color("&aFAQ #" + id + " wurde erfolgreich aktualisiert!")); + else player.sendMessage(plugin.color("&cFAQ #" + id + " wurde nicht gefunden.")); + } + openFaqGUI(player); + }); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // ITEM-BUILDER + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Baut ein Custom-Skull-Item für einen FAQ-Eintrag. + * Nutzt die Textur: da2fde34d34c8588e58bfd790ce18025f7843399dee2ab4cedc2c0b463fd1e + * + * Bei einem Fehler mit der Textur wird auf BOOK zurückgefallen. + */ + private ItemStack buildFaqSkull(FaqEntry entry, boolean adminHint) { + ItemStack skull; + + try { + skull = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + if (meta != null) { + PlayerProfile profile = Bukkit.createPlayerProfile( + UUID.nameUUIDFromBytes(("FAQ_" + entry.getId()).getBytes()), "FAQ_" + entry.getId()); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(new URL(FAQ_SKIN_URL)); + profile.setTextures(textures); + meta.setOwnerProfile(profile); + meta.setDisplayName("§e§l" + entry.getQuestion()); + meta.setLore(buildFaqLore(entry, adminHint)); + skull.setItemMeta(meta); + } + } catch (Exception e) { + // Fallback auf BOOK wenn Textur nicht gesetzt werden kann + skull = new ItemStack(Material.BOOK); + ItemMeta meta = skull.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§e§l" + entry.getQuestion()); + meta.setLore(buildFaqLore(entry, adminHint)); + skull.setItemMeta(meta); + } + } + + return skull; + } + + /** Erstellt die Lore-Zeilen für ein FAQ-Item (Antwort aufgeteilt in 40er-Zeilen). */ + private List buildFaqLore(FaqEntry entry, boolean adminHint) { + List lore = new ArrayList<>(); + lore.add("§8§m "); + lore.add("§7FAQ #" + entry.getId()); + lore.add("§8§m "); + + // Antwort in 40-Zeichen-Abschnitte aufteilen + String answer = entry.getAnswer(); + int chunkSize = 40; + for (int i = 0; i < answer.length(); i += chunkSize) { + int end = Math.min(i + chunkSize, answer.length()); + // Wortgrenzen bevorzugen + if (end < answer.length() && answer.charAt(end) != ' ') { + int lastSpace = answer.lastIndexOf(' ', end); + if (lastSpace > i) end = lastSpace; + } + lore.add("§f" + answer.substring(i, end).trim()); + i = end - chunkSize; // Schleifeninkrement korrigieren + } + + lore.add("§8§m "); + if (adminHint) { + lore.add("§e» Klicken zum Bearbeiten / Löschen"); + } else { + lore.add("§e» Klicken für mehr Details im Chat"); + } + return lore; + } + + private ItemStack buildItem(Material material, String displayName, List lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + meta.setDisplayName(displayName); + meta.setLore(lore); + item.setItemMeta(meta); + return item; + } + + // ─────────────────────────── Navigationsleiste ───────────────────────── + + private void fillNavBar(Inventory inv, int page, int totalPages, boolean isAdmin, boolean isEmpty) { + ItemStack glass = makeGlass(); + for (int i = 45; i < 54; i++) inv.setItem(i, glass); + + if (page > 0) { + inv.setItem(45, buildItem(Material.ARROW, "§7§l◄ Zurück", + List.of("§7Seite " + page + " von " + totalPages))); + } + if (page < totalPages - 1) { + inv.setItem(53, buildItem(Material.ARROW, "§7§lWeiter ►", + List.of("§7Seite " + (page + 2) + " von " + totalPages))); + } + + inv.setItem(49, buildItem(Material.PAPER, "§8Seite " + (page + 1) + "/" + totalPages, + List.of("§7Gesamt: " + (isEmpty ? 0 : Math.min(PAGE_SIZE, totalPages * PAGE_SIZE)) + " FAQ(s)"))); + + if (isAdmin) { + inv.setItem(50, buildItem(Material.LIME_WOOL, "§a§lNeues FAQ hinzufügen", + List.of("§7Fügt einen neuen FAQ-Eintrag hinzu.", "§7Du wirst nach Frage und Antwort gefragt."))); + } + } + + private void fillGlass(Inventory inv) { + ItemStack glass = makeGlass(); + for (int i = 0; i < inv.getSize(); i++) { + if (inv.getItem(i) == null) inv.setItem(i, glass); + } + } + + private ItemStack makeGlass() { + ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = glass.getItemMeta(); + if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } + return glass; + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/manager/FaqManager.java b/src/main/java/de/ticketsystem/manager/FaqManager.java new file mode 100644 index 0000000..505cb76 --- /dev/null +++ b/src/main/java/de/ticketsystem/manager/FaqManager.java @@ -0,0 +1,181 @@ +package de.ticketsystem.manager; + +import de.ticketsystem.TicketPlugin; +import de.ticketsystem.model.FaqEntry; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * Manages FAQ entries stored in faqs.yml. + * + * Admins can add, edit and delete FAQs in-game. + * All changes are saved immediately to faqs.yml. + * + * faqs.yml layout: + * + * faqs: + * 1: + * question: "Wie erstelle ich ein Ticket?" + * answer: "Nutze /ticket create [Kategorie] [Beschreibung]." + * 2: + * question: "..." + * answer: "..." + */ +public class FaqManager { + + private final TicketPlugin plugin; + private final File faqFile; + private YamlConfiguration faqConfig; + private final List entries = new ArrayList<>(); + private int nextId = 1; + + public FaqManager(TicketPlugin plugin) { + this.plugin = plugin; + this.faqFile = new File(plugin.getDataFolder(), "faqs.yml"); + load(); + } + + // ─────────────────────────── Loading & Saving ─────────────────────────── + + private void load() { + entries.clear(); + nextId = 1; + + if (!faqFile.exists()) { + try { + faqFile.getParentFile().mkdirs(); + faqFile.createNewFile(); + } catch (IOException e) { + plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht erstellen: " + e.getMessage()); + } + faqConfig = new YamlConfiguration(); + loadDefaults(); + save(); + return; + } + + faqConfig = YamlConfiguration.loadConfiguration(faqFile); + ConfigurationSection section = faqConfig.getConfigurationSection("faqs"); + + if (section != null) { + for (String key : section.getKeys(false)) { + try { + int id = Integer.parseInt(key); + String question = faqConfig.getString("faqs." + key + ".question", ""); + String answer = faqConfig.getString("faqs." + key + ".answer", ""); + if (!question.isBlank() && !answer.isBlank()) { + entries.add(new FaqEntry(id, question, answer)); + if (id >= nextId) nextId = id + 1; + } + } catch (NumberFormatException ignored) {} + } + } + + entries.sort(Comparator.comparingInt(FaqEntry::getId)); + + if (plugin.isDebug()) { + plugin.getLogger().info("[FaqManager] " + entries.size() + " FAQ(s) geladen."); + } + } + + /** Writes the example FAQs into a freshly created faqs.yml. */ + private void loadDefaults() { + writeEntry(1, "Wie erstelle ich ein Ticket?", + "Nutze den Befehl /ticket create [Kategorie] [Prio][Beschreibung] um ein neues Ticket zu erstellen."); + writeEntry(2, "Wie lange dauert die Bearbeitung?", + "Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld."); + writeEntry(3, "Kann ich mein Ticket löschen?", + "Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen."); + writeEntry(4, "Wie kann ich meinen Support bewerten?", + "Nach dem Schließen eines Tickets kannst du mit /ticket rate good/bad eine Bewertung abgeben."); + nextId = 5; + // Sync entries list with what we just wrote + entries.add(new FaqEntry(1, "Wie erstelle ich ein Ticket?", + "Nutze den Befehl /ticket create [Kategorie] [Beschreibung] um ein neues Ticket zu erstellen.")); + entries.add(new FaqEntry(2, "Wie lange dauert die Bearbeitung?", + "Unser Support-Team bearbeitet Tickets so schnell wie möglich. Bitte habe etwas Geduld.")); + entries.add(new FaqEntry(3, "Kann ich mein Ticket löschen?", + "Ja! Öffne /ticket list und klicke auf dein Ticket, um es aus der Übersicht zu entfernen.")); + entries.add(new FaqEntry(4, "Wie kann ich meinen Support bewerten?", + "Nach dem Schließen eines Tickets kannst du mit /ticket rate good/bad eine Bewertung abgeben.")); + } + + private void writeEntry(int id, String question, String answer) { + faqConfig.set("faqs." + id + ".question", question); + faqConfig.set("faqs." + id + ".answer", answer); + } + + private void save() { + try { + faqConfig.save(faqFile); + } catch (IOException e) { + plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht speichern: " + e.getMessage()); + } + } + + // ─────────────────────────── Public API ──────────────────────────────── + + /** Returns an unmodifiable view of all FAQ entries in ID order. */ + public List getAll() { + return Collections.unmodifiableList(entries); + } + + /** Looks up an entry by its numeric ID. Returns null if not found. */ + public FaqEntry getById(int id) { + return entries.stream().filter(e -> e.getId() == id).findFirst().orElse(null); + } + + /** + * Adds a new FAQ entry and saves immediately. + * + * @param question The question text. + * @param answer The answer text. + * @return The newly created {@link FaqEntry}. + */ + public FaqEntry add(String question, String answer) { + int id = nextId++; + FaqEntry entry = new FaqEntry(id, question, answer); + entries.add(entry); + writeEntry(id, question, answer); + save(); + return entry; + } + + /** + * Edits an existing FAQ entry and saves immediately. + * + * @return true if the entry was found and updated, false otherwise. + */ + public boolean edit(int id, String question, String answer) { + FaqEntry entry = getById(id); + if (entry == null) return false; + entry.setQuestion(question); + entry.setAnswer(answer); + writeEntry(id, question, answer); + save(); + return true; + } + + /** + * Deletes a FAQ entry and saves immediately. + * + * @return true if the entry was found and deleted, false otherwise. + */ + public boolean delete(int id) { + FaqEntry entry = getById(id); + if (entry == null) return false; + entries.remove(entry); + faqConfig.set("faqs." + id, null); + save(); + return true; + } + + /** Reloads FAQs from faqs.yml without restarting the server. */ + public void reload() { + load(); + } +} \ No newline at end of file diff --git a/src/main/java/de/ticketsystem/model/FaqEntry.java b/src/main/java/de/ticketsystem/model/FaqEntry.java new file mode 100644 index 0000000..05715aa --- /dev/null +++ b/src/main/java/de/ticketsystem/model/FaqEntry.java @@ -0,0 +1,29 @@ +package de.ticketsystem.model; + +/** + * Represents a single FAQ entry stored in faqs.yml. + */ +public class FaqEntry { + + private int id; + private String question; + private String answer; + + public FaqEntry(int id, String question, String answer) { + this.id = id; + this.question = question; + this.answer = answer; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getQuestion() { return question; } + public void setQuestion(String q) { this.question = q; } + public String getAnswer() { return answer; } + public void setAnswer(String a) { this.answer = a; } + + @Override + public String toString() { + return "FaqEntry{id=" + id + ", question='" + question + "'}"; + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bb95baa..9df353e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.0.5 +version: 1.0.6 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper