7 Commits
1.0.1 ... 1.0.3

Author SHA1 Message Date
d14646c5ae README.md aktualisiert 2026-02-20 16:49:53 +00:00
df6878db2f Update from Git Manager GUI 2026-02-20 12:31:38 +01:00
526cb8b442 Upload pom.xml via GUI 2026-02-20 11:31:37 +00:00
b930793c50 README.md aktualisiert 2026-02-20 11:29:55 +00:00
566941d687 README.md aktualisiert 2026-02-20 11:29:46 +00:00
dc732c1410 README.md aktualisiert 2026-02-19 22:04:21 +00:00
d7fb6940cd README.md aktualisiert 2026-02-19 21:54:48 +00:00
13 changed files with 1738 additions and 879 deletions

182
README.md
View File

@@ -1,89 +1,109 @@
# TicketSystem # TicketSystem
[![Version](https://img.shields.io/badge/Version-1.18.x--1.21.x-green.svg)]() [![Java](https://img.shields.io/badge/Java-17+-orange.svg)]() [![Type](https://img.shields.io/badge/Type-Support-blue.svg)]() ![Version](https://img.shields.io/badge/Minecraft-1.18.x--1.21.x-green?style=for-the-badge) ![Java](https://img.shields.io/badge/Java-17+-orange?style=for-the-badge) ![Type](https://img.shields.io/badge/Type-Support-blue?style=for-the-badge)
**TicketSystem** ist das umfassende Support- und Feedback-Plugin für Minecraft-Server. Es bietet flexible Speicherung (MySQL oder Datei), automatische Archivierung, Migration, Export/Import, Statistiken, vollständige Validierung, Debug-Modus, eine übersichtliche config.yml mit Versionsprüfung und eine dynamische GUI. **TicketSystem** ist das flexible, moderne Support- und Feedback-Plugin für Minecraft-Server. Es bietet flexible Speicherung (MySQL oder Datei), automatische Archivierung, Migration, Export/Import, Statistiken, vollständige Validierung, Debug-Modus, eine übersichtliche config.yml mit Versionsprüfung und eine dynamische GUI.
## Features (Details) ## Features
- **MySQL oder Datei-Speicherung** (YAML/JSON) jederzeit umschaltbar, Migration und Backup inklusive - **MySQL oder Datei-Speicherung** YAML/JSON oder MySQL/MariaDB, jederzeit umschaltbar, Migration & Backup inklusive
- **Automatische Backups & Migration** zwischen Speicherarten, Datenverlust ausgeschlossen - **Automatische Backups & Migration** Sicheres Wechseln zwischen Speicherarten, Datenverlust ausgeschlossen
- **Export/Import** von Tickets (z.B. für Server-Umzüge oder Testumgebungen) - **Export/Import** Tickets einfach zwischen Servern oder Instanzen übertragen
- **Statistiken & Archivierung** (inkl. automatischer Archivierung nach Zeitplan, manuelles Archivieren möglich) - **Statistiken & Archivierung** Übersichtliche Auswertung, automatische Archivierung nach Zeitplan, manuelles Archivieren möglich
- **Konfigurierbare Speicherpfade** für Daten und Archive (relativ oder absolut) - **Rollenbasierter Archiv-Zugriff** Nur Spieler mit `ticket.archive` können das Archiv sehen, öffnen und Tickets permanent löschen unabhängig von `ticket.admin` oder OP-Status
- **Vollständige Validierung** der Daten beim Laden (Fehlerausgabe im Log & Chat, fehlerhafte Tickets werden übersprungen) - **Konfigurierbare Speicherpfade** Daten- und Archivdateien frei wählbar, auch absolute Pfade
- **Bessere Fehlerausgaben** für Admins im Chat und Log (inkl. Validierungs- und Speicherfehler) - **Vollständige Validierung** Fehlerhafte Tickets werden beim Laden erkannt, gemeldet und übersprungen
- **Debug-Modus & Versionsprüfung** für Entwickler und Admins (veraltete config.yml wird erkannt) - **Bessere Fehlerausgaben** Alle Fehler erscheinen im Log und für Admins im Chat, inkl. Validierungs- und Speicherfehler
- **Komplett anpassbar** (Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte) - **Debug-Modus & Versionsprüfung** Für Entwickler und Admins, erkennt veraltete config.yml automatisch
- **Unit-Tests** für die Speicher-Logik (maximale Zuverlässigkeit) - **Komplett anpassbar** Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte
- **Dynamische GUI**: Die Ticket-GUI passt sich automatisch der Ticketanzahl an (bis zu 54 Tickets pro Seite, Blättern bei Bedarf) - **Unit-Tests** Getestete Speicher-Logik für maximale Zuverlässigkeit
- **Performance**: Optimiert für große Server, alle Operationen laufen asynchron und ressourcenschonend - **Dynamische GUI** Die Ticket-GUI passt sich automatisch der Ticketanzahl an (bis zu 54 Tickets pro Seite)
- **Support & Erweiterbarkeit**: Sauberer Code, viele Hooks für eigene Erweiterungen - **Seiten-System** Bei sehr vielen Tickets wird automatisch geblättert
- **Performance** Optimiert für große Server, alle Operationen laufen asynchron und ressourcenschonend
- **Support & Erweiterbarkeit** Sauberer Code, viele Hooks für eigene Erweiterungen
## Einrichtung & Konfiguration ---
1. **config.yml** anpassen: ## Installation & Setup
- `data-file`, `archive-file`: Speicherorte für Tickets und Archive (relativ oder absolut)
- `use-mysql`: true/false
- `auto-archive-interval-hours`: Intervall für automatische Archivierung (0 = aus)
- `debug`: true/false für ausführliche Logs
- `version`: Versionsprüfung für die config.yml
- Nachrichten, Farben, Limits, Cooldowns, Rechte individuell anpassbar
2. **MySQL** (optional): Zugangsdaten eintragen
3. **/ticket**-Befehl nutzen (inkl. Admin-Tools für Migration, Export, Import, Statistik, Archiv, Reload)
## Kompatibilität & Zielgruppe 1. **config.yml** anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
2. **TicketSystem.jar** in den plugins-Ordner legen und Server starten
3. **/ticket**-Befehle nutzen (siehe unten)
- Minecraft 1.18.x 1.21.x (Paper, Spigot, Purpur) ---
- Java 17+
- YAML/JSON oder MySQL/MariaDB
- Für Community-Server, Citybuild, Minigames, Survival, u.v.m.
## Support & Kontakt ## Beispiel-Konfiguration
```yaml
config-version: 1
debug: false
storage:
type: "file" # "file" oder "mysql"
data-file: "data.yml"
archive-file: "archive.yml"
mysql:
host: "localhost"
port: 3306
database: "tickets"
user: "root"
password: "password"
useSSL: false
auto-archive-interval-hours: 24
messages:
prefix: "&7[&eTicket&7]"
ticket-created: "&aDein Ticket wurde erstellt!"
error: "&cEin Fehler ist aufgetreten!"
```
---
## Befehle & Rechte ## Befehle & Rechte
- `/ticket` Hauptbefehl für alle Ticket-Funktionen ### Spieler-Befehle
- `/ticket` Übersicht aller offenen Tickets (GUI)
- `/ticket create <Nachricht>` Erstellt ein neues Ticket
- `/ticket claim <ID>` Ticket übernehmen
- `/ticket close <ID>` Ticket schließen
- `/ticket forward <ID> <Spieler>` Ticket weiterleiten
- `/ticket archive` Tickets archivieren (manuell)
- `/ticket export <Datei>` Tickets exportieren
- `/ticket import <Datei>` Tickets importieren
- `/ticket migrate <tomysql|tofile>` Migration zwischen Speicherarten
- `/ticket stats` Statistiken anzeigen
- `/ticket reload` Konfiguration neu laden
- Rechte:
- `ticket.admin` Zugriff auf alle Admin- und Management-Funktionen
- `ticket.use` (Standard) Ticket erstellen und eigene Tickets verwalten
## Beispiel-Konfiguration (Ausschnitt)
```yaml
version: "2.0"
debug: false
data-file: "data.yml"
archive-file: "archive.yml"
use-mysql: false
use-json: false
auto-archive-interval-hours: 24
prefix: "&8[&6Ticket&8] &r"
ticket-cooldown: 60
max-description-length: 100
max-open-tickets-per-player: 2
``` ```
/ticket - Ticket erstellen, verwalten, Status abfragen
/ticket create <Nachricht> - Neues Ticket erstellen
/ticket list - Zeigt alle Tickets des Spielers
/ticket close <ID> - Ticket schließen
```
### Admin-Befehle
```
/ticket reload - Plugin neu laden
/ticket migrate - Speicherart wechseln (Migration)
/ticket export/import - Tickets exportieren/importieren
/ticket stats - Statistiken anzeigen
/ticket archive - Tickets archivieren
/ticket claim <ID> - Ticket übernehmen
/ticket forward <ID> <Spieler> - Ticket weiterleiten
/ticket close <ID> - Ticket schließen
```
### Permissions
| Permission | Beschreibung | Standard |
|---|---|---|
| `ticket.create` | Ticket erstellen | ✅ alle Spieler |
| `ticket.support` | Tickets einsehen, claimen & schließen | ❌ manuell vergeben |
| `ticket.archive` | Archiv öffnen, einsehen & Tickets permanent löschen | ❌ manuell vergeben |
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung & Reload (beinhaltet `ticket.support`) | OP |
> ⚠️ **Wichtig:** `ticket.archive` ist bewusst **nicht** in `ticket.admin` enthalten und wird auch **nicht automatisch an OPs vergeben**. Das Archiv-Recht muss explizit zugewiesen werden:
> ```
> /lp user <Spielername> permission set ticket.archive true
> ```
---
## FAQ ## FAQ
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?** **Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
> Ja, jederzeit per Migrationstool oder Befehl `/ticket migrate ...`. > Ja! Das Plugin migriert alle Daten automatisch und sicher.
**Wie viele Tickets passen in die GUI?** **Wie viele Tickets passen in die GUI?**
> Bis zu 54 pro Seite, bei mehr Tickets wird geblättert. > Bis zu 54 pro Seite, bei mehr Tickets wird automatisch geblättert.
**Wie kann ich Nachrichten und Limits anpassen?** **Wie kann ich Nachrichten und Limits anpassen?**
> Alle Texte, Farben und Limits findest du in der `config.yml`. > Alle Texte, Farben und Limits findest du in der `config.yml`.
@@ -92,8 +112,36 @@ max-open-tickets-per-player: 2
> Setze `debug: true` in der `config.yml`. > Setze `debug: true` in der `config.yml`.
**Wie kann ich Tickets exportieren/importieren?** **Wie kann ich Tickets exportieren/importieren?**
> Mit `/ticket export <Datei>` und `/ticket import <Datei>`. > Mit `/ticket export` und `/ticket import` ideal für Server-Umzüge.
## Support & Kontakt **Wer darf das Ticket-Archiv sehen?**
> Nur Spieler mit der Permission `ticket.archive`. Diese wird weder automatisch an OPs noch an Admins vergeben und muss explizit zugewiesen werden.
Du hast Fragen, Feedback oder möchtest das Plugin erweitern? Melde dich direkt bei **Viper Plugins** wir helfen schnell und unkompliziert! ---
## Vergleich mit anderen Plugins
| Feature | TicketSystem | SimpleTickets | AdvancedTickets |
|----------------------------------|:------------:|:-------------:|:---------------:|
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
| GUI | ✔️ | ⚠️ | ✖️ |
| Archivierung | ✔️ | ⚠️ | ✖️ |
| Rollenbasierter Archiv-Zugriff | ✔️ | ✖️ | ✖️ |
| Update-Checker | ✔️ | ✖️ | ✖️ |
---
## Support & Community
- [Discord Support](https://discord.com/invite/FdRs4BRd8D)
- [Git Issues](https://git.viper.ipv64.net/M_Viper/TicketSystem/issues)
Wir antworten in der Regel innerhalb von 24 Stunden!
---
## ⭐ Unterstütze das Projekt
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine **5-Sterne Bewertung auf spigotmc**!
Dein Feedback hilft uns, das Plugin weiter zu verbessern.

View File

@@ -6,7 +6,7 @@
<groupId>de.ticketsystem</groupId> <groupId>de.ticketsystem</groupId>
<artifactId>TicketSystem</artifactId> <artifactId>TicketSystem</artifactId>
<version>1.0.1</version> <version>1.0.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TicketSystem</name> <name>TicketSystem</name>

View File

@@ -1,11 +1,13 @@
package de.ticketsystem; package de.ticketsystem;
import de.ticketsystem.commands.TicketCommand; import de.ticketsystem.commands.TicketCommand;
import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.database.DatabaseManager;
import de.ticketsystem.discord.DiscordWebhook;
import de.ticketsystem.gui.TicketGUI; import de.ticketsystem.gui.TicketGUI;
import de.ticketsystem.listeners.PlayerJoinListener; import de.ticketsystem.listeners.PlayerJoinListener;
import de.ticketsystem.manager.TicketManager; import de.ticketsystem.manager.TicketManager;
// WICHTIG: Import hinzugefügt
import de.ticketsystem.model.Ticket;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@@ -19,77 +21,85 @@ public class TicketPlugin extends JavaPlugin {
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private TicketManager ticketManager; private TicketManager ticketManager;
private TicketGUI ticketGUI; private TicketGUI ticketGUI;
private DiscordWebhook discordWebhook;
@Override @Override
public void onEnable() { public void onEnable() {
instance = this; instance = this;
// Config speichern falls nicht vorhanden
saveDefaultConfig(); saveDefaultConfig();
// Update-Checker (Spigot Resource-ID anpassen!) // --- WICHTIG: Ticket-Klasse registrieren ---
int resourceId = 132757; Ticket.register();
// -------------------------------------------
// Update-Checker
int resourceId = 132757;
new UpdateChecker(this, resourceId).getVersion(version -> { new UpdateChecker(this, resourceId).getVersion(version -> {
String current = getDescription().getVersion(); String current = getDescription().getVersion();
if (!current.equals(version)) { if (!current.equals(version)) {
String msg = ChatColor.translateAlternateColorCodes('&', String msg = ChatColor.translateAlternateColorCodes('&',
"&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")"); "&6[TicketSystem] &eEs ist eine neue Version verfügbar: &a" + version + " &7(aktuell: " + current + ")");
getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")"); getLogger().info("Es ist eine neue Version verfügbar: " + version + " (aktuell: " + current + ")");
// Sende Nachricht an alle Admins (online) mit 1 Sekunde Verzögerung
getServer().getScheduler().runTaskLater(this, () -> { getServer().getScheduler().runTaskLater(this, () -> {
getServer().getOnlinePlayers().stream() getServer().getOnlinePlayers().stream()
.filter(p -> p.hasPermission("ticket.admin")) .filter(p -> p.hasPermission("ticket.admin"))
.forEach(p -> p.sendMessage(msg)); .forEach(p -> p.sendMessage(msg));
}, 20L); // 20 Ticks = 1 Sekunde }, 20L);
} else { } else {
getLogger().info("TicketSystem ist aktuell (Version " + current + ")"); getLogger().info("TicketSystem ist aktuell (Version " + current + ")");
} }
}); });
// Versionsprüfung // Versionsprüfung
String configVersion = getConfig().getString("version", ""); String configVersion = getConfig().getString("version", "");
String expectedVersion = "2.0"; String expectedVersion = "2.0";
if (!expectedVersion.equals(configVersion)) { if (!expectedVersion.equals(configVersion)) {
getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion + ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein! Bitte prüfe, ob deine Konfiguration aktuell ist."); getLogger().warning("[WARNUNG] Die Version deiner config.yml (" + configVersion
+ ") stimmt nicht mit der erwarteten Version (" + expectedVersion + ") überein!");
} }
// Debug-Status aus Config lesen
debug = getConfig().getBoolean("debug", false); debug = getConfig().getBoolean("debug", false);
// Datenbankverbindung aufbauen // Datenbankverbindung
databaseManager = new DatabaseManager(this); databaseManager = new DatabaseManager(this);
if (!databaseManager.connect()) { if (!databaseManager.connect()) {
getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter."); getLogger().severe("Konnte keine Datenbankverbindung herstellen! Plugin läuft im Datei-Modus weiter.");
if (isDebug()) getLogger().warning("[DEBUG] DatabaseManager.connect() fehlgeschlagen, Datei-Modus aktiviert.");
// Plugin bleibt aktiv, DatabaseManager wechselt auf Datei-Storage
} }
// Manager und GUI initialisieren // Manager, GUI & Discord-Webhook initialisieren
ticketManager = new TicketManager(this); ticketManager = new TicketManager(this);
ticketGUI = new TicketGUI(this); ticketGUI = new TicketGUI(this);
discordWebhook = new DiscordWebhook(this);
// Commands registrieren if (getConfig().getBoolean("discord.enabled", false)) {
String url = getConfig().getString("discord.webhook-url", "");
if (url.isEmpty()) {
getLogger().warning("[DiscordWebhook] Aktiviert, aber keine Webhook-URL in der config.yml eingetragen!");
} else {
getLogger().info("[DiscordWebhook] Integration aktiv.");
}
}
// Commands & Events
TicketCommand ticketCommand = new TicketCommand(this); TicketCommand ticketCommand = new TicketCommand(this);
Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setExecutor(ticketCommand);
Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand); Objects.requireNonNull(getCommand("ticket")).setTabCompleter(ticketCommand);
// Events registrieren
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(ticketGUI, this); getServer().getPluginManager().registerEvents(ticketGUI, this);
// Automatische Archivierung nach Zeitplan (Intervall in Stunden, Standard: 24h) // Automatische Archivierung
int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24); int archiveIntervalH = getConfig().getInt("auto-archive-interval-hours", 24);
if (archiveIntervalH > 0) { if (archiveIntervalH > 0) {
long ticks = archiveIntervalH * 60L * 60L * 20L; // Stunden → Ticks long ticks = archiveIntervalH * 60L * 60L * 20L;
getServer().getScheduler().runTaskTimer(this, () -> { getServer().getScheduler().runTaskTimer(this, () -> {
int archived = databaseManager.archiveClosedTickets(); int archived = databaseManager.archiveClosedTickets();
if (archived > 0) { if (archived > 0) {
getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert."); getLogger().info("Automatische Archivierung: " + archived + " Tickets archiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierung ausgeführt, " + archived + " Tickets verschoben.");
} }
}, ticks, ticks); }, ticks, ticks);
getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert."); getLogger().info("Automatische Archivierung alle " + archiveIntervalH + " Stunden aktiviert.");
if (isDebug()) getLogger().info("[DEBUG] Archivierungs-Timer gesetzt: alle " + archiveIntervalH + " Stunden.");
} }
getLogger().info("TicketSystem erfolgreich gestartet!"); getLogger().info("TicketSystem erfolgreich gestartet!");
@@ -97,39 +107,28 @@ public class TicketPlugin extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
if (databaseManager != null) { if (databaseManager != null) databaseManager.disconnect();
databaseManager.disconnect();
}
getLogger().info("TicketSystem wurde deaktiviert."); getLogger().info("TicketSystem wurde deaktiviert.");
} }
// ─────────────────────────── Hilfsmethoden ───────────────────────────── // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Formatiert eine Nachricht aus der Config mit Prefix und Farben.
*/
public String formatMessage(String path) { public String formatMessage(String path) {
String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r")); String prefix = color(getConfig().getString("prefix", "&8[&6Ticket&8] &r"));
String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path); String message = getConfig().getString(path, "&cNachricht nicht gefunden: " + path);
return prefix + color(message); return prefix + color(message);
} }
/**
* Konvertiert Farbcodes (&x → §x).
*/
public String color(String text) { public String color(String text) {
return ChatColor.translateAlternateColorCodes('&', text); return ChatColor.translateAlternateColorCodes('&', text);
} }
// ─────────────────────────── Getter ──────────────────────────────────── // ─────────────────────────── Getter ────────────────────────────────────
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 TicketGUI getTicketGUI() { return ticketGUI; } public TicketGUI getTicketGUI() { return ticketGUI; }
public DiscordWebhook getDiscordWebhook() { return discordWebhook; }
/** public boolean isDebug() { return debug; }
* Gibt zurück, ob der Debug-Modus aktiv ist (aus config.yml) }
*/
public boolean isDebug() { return debug; }
}

View File

@@ -26,7 +26,11 @@ public class UpdateChecker {
Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> {
try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) { try (InputStream is = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + this.resourceId).openStream(); Scanner scann = new Scanner(is)) {
if (scann.hasNext()) { if (scann.hasNext()) {
consumer.accept(scann.next()); String latest = scann.next();
plugin.getLogger().info("[UpdateChecker] Spigot-API Rückgabe: '" + latest + "'");
consumer.accept(latest);
} else {
plugin.getLogger().warning("[UpdateChecker] Keine Version von Spigot erhalten!");
} }
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().info("Unable to check for updates: " + e.getMessage()); plugin.getLogger().info("Unable to check for updates: " + e.getMessage());

View File

@@ -1,4 +1,3 @@
package de.ticketsystem.commands; package de.ticketsystem.commands;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
@@ -9,19 +8,259 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import java.io.File; import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class TicketCommand implements CommandExecutor, TabCompleter { public class TicketCommand implements CommandExecutor, TabCompleter {
// Platzhalter für Admin-Kommandos
private final TicketPlugin plugin;
public TicketCommand(TicketPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden.");
return true;
}
if (args.length == 0) {
plugin.getTicketManager().sendHelpMessage(player);
return true;
}
switch (args[0].toLowerCase()) {
case "create" -> handleCreate(player, args);
case "list" -> handleList(player);
case "claim" -> handleClaim(player, args);
case "close" -> handleClose(player, args);
case "forward" -> handleForward(player, args);
case "reload" -> handleReload(player);
case "migrate" -> handleMigrate(player, args);
case "export" -> handleExport(player, args);
case "import" -> handleImport(player, args);
case "stats" -> handleStats(player);
case "archive" -> handleArchive(player);
default -> plugin.getTicketManager().sendHelpMessage(player);
}
return true;
}
// ─────────────────────────── /ticket create ────────────────────────────
private void handleCreate(Player player, String[] args) {
if (!player.hasPermission("ticket.create")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket create <Beschreibung>"));
return;
}
if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) {
long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId());
player.sendMessage(plugin.formatMessage("messages.cooldown")
.replace("{seconds}", String.valueOf(remaining)));
return;
}
if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
player.sendMessage(plugin.color("&cDu hast bereits &e" + max
+ " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde."));
return;
}
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
int maxLen = plugin.getConfig().getInt("max-description-length", 100);
if (message.length() > maxLen) {
player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen."));
return;
}
Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int id = plugin.getDatabaseManager().createTicket(ticket);
if (id == -1) {
player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets!"));
return;
}
ticket.setId(id);
plugin.getTicketManager().setCooldown(player.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-created")
.replace("{id}", String.valueOf(id)));
plugin.getTicketManager().notifyTeam(ticket); // ruft auch Discord-Webhook auf
});
});
}
// ─────────────────────────── /ticket list ──────────────────────────────
private void handleList(Player player) {
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
Bukkit.getScheduler().runTask(plugin, () ->
plugin.getTicketGUI().openGUI(player)));
} else {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
Bukkit.getScheduler().runTask(plugin, () ->
plugin.getTicketGUI().openPlayerGUI(player)));
}
}
// ─────────────────────────── /ticket claim ─────────────────────────────
private void handleClaim(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket claim <ID>"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
final int ticketId = id;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().claimTicket(ticketId, player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> {
if (!success) { player.sendMessage(plugin.formatMessage("messages.already-claimed")); return; }
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) return;
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticketId))
.replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket);
if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
});
});
}
// ─────────────────────────── /ticket close ─────────────────────────────
private void handleClose(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket close <ID> [Kommentar]"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
String closeComment = args.length > 2
? String.join(" ", Arrays.copyOfRange(args, 2, args.length)) : "";
final int ticketId = id;
final String comment = closeComment;
final String closer = player.getName();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-closed")
.replace("{id}", String.valueOf(ticketId)));
if (ticket != null) {
ticket.setCloseComment(comment);
// closerName für Discord-Nachricht mitgeben
plugin.getTicketManager().notifyCreatorClosed(ticket, closer);
}
});
} else {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
}
});
}
// ─────────────────────────── /ticket forward ───────────────────────────
private void handleForward(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket forward <ID> <Spieler>"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) { player.sendMessage(plugin.color("&cUngültige ID!")); return; }
Player target = Bukkit.getPlayer(args[2]);
if (target == null || !target.isOnline()) {
player.sendMessage(plugin.color("&cSpieler &e" + args[2] + " &cist nicht online!"));
return;
}
final int ticketId = id;
final Player finalTarget = target;
final String fromName = player.getName();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager()
.forwardTicket(ticketId, finalTarget.getUniqueId(), finalTarget.getName());
if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-forwarded")
.replace("{id}", String.valueOf(ticketId))
.replace("{player}", finalTarget.getName()));
if (ticket != null) {
// fromName für Discord mitgeben
plugin.getTicketManager().notifyForwardedTo(ticket, fromName);
plugin.getTicketManager().notifyCreatorForwarded(ticket);
}
});
} else {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
}
});
}
// ─────────────────────────── /ticket reload ────────────────────────────
private void handleReload(Player player) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
plugin.reloadConfig();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen."));
}
// ─────────────────────────── /ticket archive ───────────────────────────
private void handleArchive(Player player) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().archiveClosedTickets();
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.archive-success")
.replace("{count}", String.valueOf(count)));
} else {
player.sendMessage(plugin.formatMessage("messages.archive-fail"));
}
});
});
}
// ─────────────────────────── /ticket migrate ───────────────────────────
private void handleMigrate(Player player, String[] args) { private void handleMigrate(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -34,19 +273,14 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int migrated = 0; int migrated = 0;
String mode = args[1].toLowerCase(); String mode = args[1].toLowerCase();
if (mode.equals("tomysql")) { if (mode.equals("tomysql")) migrated = plugin.getDatabaseManager().migrateToMySQL();
migrated = plugin.getDatabaseManager().migrateToMySQL(); else if (mode.equals("tofile")) migrated = plugin.getDatabaseManager().migrateToFile();
} else if (mode.equals("tofile")) { else { player.sendMessage(plugin.formatMessage("messages.unknown-mode")); return; }
migrated = plugin.getDatabaseManager().migrateToFile();
} else {
player.sendMessage(plugin.formatMessage("messages.unknown-mode"));
return;
}
int finalMigrated = migrated; int finalMigrated = migrated;
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (finalMigrated > 0) { if (finalMigrated > 0) {
player.sendMessage(plugin.formatMessage("messages.migration-success") player.sendMessage(plugin.formatMessage("messages.migration-success")
.replace("{count}", String.valueOf(finalMigrated))); .replace("{count}", String.valueOf(finalMigrated)));
} else { } else {
player.sendMessage(plugin.formatMessage("messages.migration-fail")); player.sendMessage(plugin.formatMessage("messages.migration-fail"));
} }
@@ -54,6 +288,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket export ────────────────────────────
private void handleExport(Player player, String[] args) { private void handleExport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -70,7 +306,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) { if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.export-success") player.sendMessage(plugin.formatMessage("messages.export-success")
.replace("{count}", String.valueOf(count)).replace("{file}", filename)); .replace("{count}", String.valueOf(count)).replace("{file}", filename));
} else { } else {
player.sendMessage(plugin.formatMessage("messages.export-fail")); player.sendMessage(plugin.formatMessage("messages.export-fail"));
} }
@@ -78,6 +314,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket import ────────────────────────────
private void handleImport(Player player, String[] args) { private void handleImport(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -98,7 +336,7 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) { if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.import-success") player.sendMessage(plugin.formatMessage("messages.import-success")
.replace("{count}", String.valueOf(count))); .replace("{count}", String.valueOf(count)));
} else { } else {
player.sendMessage(plugin.formatMessage("messages.import-fail")); player.sendMessage(plugin.formatMessage("messages.import-fail"));
} }
@@ -106,6 +344,8 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
}); });
} }
// ─────────────────────────── /ticket stats ─────────────────────────────
private void handleStats(Player player) { private void handleStats(Player player) {
if (!player.hasPermission("ticket.admin")) { if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission")); player.sendMessage(plugin.formatMessage("messages.no-permission"));
@@ -114,309 +354,38 @@ public class TicketCommand implements CommandExecutor, TabCompleter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
var stats = plugin.getDatabaseManager().getTicketStats(); var stats = plugin.getDatabaseManager().getTicketStats();
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage("§6--- Ticket Statistik ---"); player.sendMessage(plugin.color("&6--- Ticket Statistik ---"));
player.sendMessage("§eGesamt: §a" + stats.total + " §7| §eOffen: §a" + stats.open + " §7| §eGeschlossen: §a" + stats.closed + " §7| §eWeitergeleitet: §a" + stats.forwarded); player.sendMessage(plugin.color("&eGesamt: &a" + stats.total
player.sendMessage("§6Top Ersteller:"); + " &7| &eOffen: &a" + stats.open
stats.byPlayer.entrySet().stream().sorted((a,b)->b.getValue()-a.getValue()).limit(5).forEach(e -> + " &7| &eGeschlossen: &a" + stats.closed
player.sendMessage("§e" + e.getKey() + ": §a" + e.getValue()) + " &7| &eWeitergeleitet: &a" + stats.forwarded));
); player.sendMessage(plugin.color("&6Top Ersteller:"));
stats.byPlayer.entrySet().stream()
.sorted((a, b) -> b.getValue() - a.getValue())
.limit(5)
.forEach(e -> player.sendMessage(plugin.color("&e" + e.getKey() + ": &a" + e.getValue())));
}); });
}); });
} }
// ─────────────────────────── /ticket archive ────────────────────────────
private void handleArchive(Player player) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().archiveClosedTickets();
Bukkit.getScheduler().runTask(plugin, () -> {
if (count > 0) {
player.sendMessage(plugin.formatMessage("messages.archive-success")
.replace("{count}", String.valueOf(count)));
} else {
player.sendMessage(plugin.formatMessage("messages.archive-fail"));
}
});
});
}
private final TicketPlugin plugin;
public TicketCommand(TicketPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command,
String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Dieser Befehl kann nur von Spielern ausgeführt werden.");
return true;
}
if (args.length == 0) {
plugin.getTicketManager().sendHelpMessage(player);
return true;
}
switch (args[0].toLowerCase()) {
case "create" -> handleCreate(player, args);
case "list" -> handleList(player);
case "claim" -> handleClaim(player, args);
case "close" -> handleClose(player, args);
case "forward" -> handleForward(player, args);
case "reload" -> handleReload(player);
case "migrate" -> handleMigrate(player, args);
case "export" -> handleExport(player, args);
case "import" -> handleImport(player, args);
case "stats" -> handleStats(player);
case "archive" -> handleArchive(player);
default -> plugin.getTicketManager().sendHelpMessage(player);
}
return true;
}
// Methoden wie handleMigrate, handleCreate, handleList, handleClaim, handleClose, handleForward, handleReload, handleStats müssen auf Klassenebene stehen und dürfen nicht innerhalb von onCommand oder anderen Methoden verschachtelt sein.
// Entferne alle verschachtelten Methoden und stelle sicher, dass jede Methode nur einmal und auf Klassenebene existiert.
// ─────────────────────────── /ticket create ────────────────────────────
private void handleCreate(Player player, String[] args) {
if (!player.hasPermission("ticket.create")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket create <Beschreibung>"));
return;
}
// Cooldown-Check
if (plugin.getTicketManager().hasCooldown(player.getUniqueId())) {
long remaining = plugin.getTicketManager().getRemainingCooldown(player.getUniqueId());
player.sendMessage(plugin.formatMessage("messages.cooldown")
.replace("{seconds}", String.valueOf(remaining)));
return;
}
// Ticket-Limit-Check
if (plugin.getTicketManager().hasReachedTicketLimit(player.getUniqueId())) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
player.sendMessage(plugin.color("&cDu hast bereits &e" + max + " &coffene Ticket(s). Bitte warte, bis dein Ticket bearbeitet wurde."));
return;
}
// Nachricht zusammenbauen
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
int maxLen = plugin.getConfig().getInt("max-description-length", 100);
if (message.length() > maxLen) {
player.sendMessage(plugin.color("&cDeine Beschreibung ist zu lang! Maximal " + maxLen + " Zeichen."));
return;
}
// Ticket asynchron in DB speichern
Ticket ticket = new Ticket(player.getUniqueId(), player.getName(), message, player.getLocation());
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int id = plugin.getDatabaseManager().createTicket(ticket);
if (id == -1) {
player.sendMessage(plugin.color("&cFehler beim Erstellen des Tickets! Bitte wende dich an einen Admin."));
return;
}
ticket.setId(id);
plugin.getTicketManager().setCooldown(player.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-created")
.replace("{id}", String.valueOf(id)));
// Team benachrichtigen
plugin.getTicketManager().notifyTeam(ticket);
});
});
}
// ─────────────────────────── /ticket list ──────────────────────────────
private void handleList(Player player) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
// GUI öffnen (synchron, Datenbankabfrage läuft darin async)
Bukkit.getScheduler().runTaskAsynchronously(plugin, () ->
Bukkit.getScheduler().runTask(plugin, () ->
plugin.getTicketGUI().openGUI(player)));
}
// ─────────────────────────── /ticket claim ─────────────────────────────
private void handleClaim(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket claim <ID>"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige ID!"));
return;
}
final int ticketId = id;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().claimTicket(
ticketId, player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> {
if (!success) {
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
return;
}
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
if (ticket == null) return;
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticketId))
.replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket);
// Zur Ticket-Position teleportieren
if (ticket.getLocation() != null) {
player.teleport(ticket.getLocation());
}
});
});
}
// ─────────────────────────── /ticket close ─────────────────────────────
private void handleClose(Player player, String[] args) {
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 2) {
player.sendMessage(plugin.color("&cBenutzung: /ticket close <ID>"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige ID!"));
return;
}
final int ticketId = id;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
player.sendMessage(plugin.formatMessage("messages.ticket-closed")
.replace("{id}", String.valueOf(ticketId)));
} else {
player.sendMessage(plugin.formatMessage("messages.ticket-not-found"));
}
});
});
}
// ─────────────────────────── /ticket forward ───────────────────────────
private void handleForward(Player player, String[] args) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
if (args.length < 3) {
player.sendMessage(plugin.color("&cBenutzung: /ticket forward <ID> <Spieler>"));
return;
}
int id;
try { id = Integer.parseInt(args[1]); }
catch (NumberFormatException e) {
player.sendMessage(plugin.color("&cUngültige ID!"));
return;
}
Player target = Bukkit.getPlayer(args[2]);
if (target == null || !target.isOnline()) {
player.sendMessage(plugin.color("&cSpieler &e" + args[2] + " &cist nicht online!"));
return;
}
final int ticketId = id;
final Player finalTarget = target;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().forwardTicket(
ticketId, finalTarget.getUniqueId(), finalTarget.getName());
if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-forwarded")
.replace("{id}", String.valueOf(ticketId))
.replace("{player}", finalTarget.getName()));
if (ticket != null) plugin.getTicketManager().notifyForwardedTo(ticket);
});
} else {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
}
});
}
// ─────────────────────────── /ticket reload ────────────────────────────
private void handleReload(Player player) {
if (!player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.formatMessage("messages.no-permission"));
return;
}
plugin.reloadConfig();
player.sendMessage(plugin.color("&aKonfiguration wurde neu geladen."));
}
// ─────────────────────────── Tab-Completion ──────────────────────────── // ─────────────────────────── Tab-Completion ────────────────────────────
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
String label, String[] args) {
List<String> completions = new ArrayList<>(); List<String> completions = new ArrayList<>();
if (!(sender instanceof Player player)) return completions; if (!(sender instanceof Player player)) return completions;
if (args.length == 1) { if (args.length == 1) {
List<String> subs = new ArrayList<>(); List<String> subs = new ArrayList<>(List.of("create", "list"));
subs.add("create"); if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin"))
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { subs.addAll(List.of("claim", "close"));
subs.addAll(List.of("list", "claim", "close")); if (player.hasPermission("ticket.admin"))
} subs.addAll(List.of("forward", "reload", "stats", "archive", "migrate", "export", "import"));
if (player.hasPermission("ticket.admin")) { for (String s : subs)
subs.addAll(List.of("forward", "reload"));
}
for (String s : subs) {
if (s.startsWith(args[0].toLowerCase())) completions.add(s); if (s.startsWith(args[0].toLowerCase())) completions.add(s);
}
} else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) { } else if (args.length == 3 && args[0].equalsIgnoreCase("forward")) {
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers())
if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) if (p.getName().toLowerCase().startsWith(args[2].toLowerCase())) completions.add(p.getName());
completions.add(p.getName());
}
} }
return completions; return completions;
} }
} }

View File

@@ -1,4 +1,3 @@
package de.ticketsystem.database; package de.ticketsystem.database;
import java.io.File; import java.io.File;
@@ -14,7 +13,6 @@ import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus; import de.ticketsystem.model.TicketStatus;
import java.sql.*; import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
@@ -23,295 +21,9 @@ import java.io.FileWriter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
public class DatabaseManager { public class DatabaseManager {
// Test-Konstruktor für Unit-Tests (ohne Bukkit/Plugin)
public DatabaseManager(File dataFile, YamlConfiguration dataConfig) {
this.plugin = null;
this.useMySQL = false;
this.useJson = false;
this.dataFileName = dataFile.getName();
this.archiveFileName = "archive.json";
this.dataFile = dataFile;
this.dataConfig = dataConfig;
validateLoadedTickets();
}
/**
* Archiviert alle geschlossenen Tickets in eine separate Datei und entfernt sie aus dem aktiven Speicher.
* @return Anzahl archivierter Tickets
*/
public int archiveClosedTickets() {
List<Ticket> all = getAllTickets();
List<Ticket> toArchive = new ArrayList<>();
for (Ticket t : all) {
if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t);
}
if (toArchive.isEmpty()) return 0;
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
JSONArray arr = new JSONArray();
// Bestehendes Archiv laden
if (archiveFile.exists()) {
try (FileReader fr = new FileReader(archiveFile)) {
JSONParser parser = new JSONParser();
Object parsed = parser.parse(fr);
if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr);
} catch (Exception ignored) {}
}
for (Ticket t : toArchive) arr.add(ticketToJson(t));
try (FileWriter fw = new FileWriter(archiveFile)) {
fw.write(arr.toJSONString());
} catch (Exception e) {
sendError("Fehler beim Archivieren: " + e.getMessage());
return 0;
}
// Entferne archivierte Tickets aus aktivem Speicher
int removed = 0;
if (useMySQL) {
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) {
for (Ticket t : toArchive) {
ps.setInt(1, t.getId());
ps.executeUpdate();
removed++;
}
} catch (Exception e) {
sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage());
}
} else {
for (Ticket t : toArchive) {
dataConfig.set("tickets." + t.getId(), null);
removed++;
}
try { dataConfig.save(dataFile); } catch (Exception e) { sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage()); }
}
return removed;
}
/**
* Liefert Statistiken über Tickets.
*/
public TicketStats getTicketStats() {
List<Ticket> all = getAllTickets();
int open = 0, closed = 0, forwarded = 0;
java.util.Map<String, Integer> byPlayer = new java.util.HashMap<>();
for (Ticket t : all) {
switch (t.getStatus()) {
case OPEN -> open++;
case CLOSED -> closed++;
case FORWARDED -> forwarded++;
}
byPlayer.merge(t.getCreatorName(), 1, Integer::sum);
}
return new TicketStats(all.size(), open, closed, forwarded, byPlayer);
}
public static class TicketStats { // ─────────────────────────── Felder ────────────────────────────────────
public final int total, open, closed, forwarded;
public final java.util.Map<String, Integer> byPlayer;
public TicketStats(int total, int open, int closed, int forwarded, java.util.Map<String, Integer> byPlayer) {
this.total = total; this.open = open; this.closed = closed; this.forwarded = forwarded; this.byPlayer = byPlayer;
}
}
/**
* Exportiert alle Tickets als JSON-Datei.
* @param exportFile Ziel-Datei
* @return Anzahl exportierter Tickets
*/
public int exportTickets(File exportFile) {
List<Ticket> tickets = getAllTickets();
JSONArray arr = new JSONArray();
for (Ticket t : tickets) {
arr.add(ticketToJson(t));
}
try (FileWriter fw = new FileWriter(exportFile)) {
fw.write(arr.toJSONString());
return tickets.size();
} catch (IOException e) {
sendError("Fehler beim Export: " + e.getMessage());
return 0;
}
}
/**
* Importiert Tickets aus einer JSON-Datei.
* @param importFile Quell-Datei
* @return Anzahl importierter Tickets
*/
public int importTickets(File importFile) {
int imported = 0;
try (FileReader fr = new FileReader(importFile)) {
JSONParser parser = new JSONParser();
JSONArray arr = (JSONArray) parser.parse(fr);
for (Object o : arr) {
JSONObject obj = (JSONObject) o;
Ticket t = ticketFromJson(obj);
if (t != null) {
int id = createTicket(t);
if (id != -1) imported++;
}
}
} catch (Exception e) {
sendError("Fehler beim Import: " + e.getMessage());
}
return imported;
}
/**
* Gibt alle Tickets (egal welcher Status) zurück.
*/
public List<Ticket> getAllTickets() {
List<Ticket> list = new ArrayList<>();
if (useMySQL) {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
while (rs.next()) list.add(mapRow(rs));
} catch (SQLException e) {
sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage());
}
} else {
if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null) list.add(t);
}
}
}
return list;
}
// Hilfsmethoden für JSON-Konvertierung
private JSONObject ticketToJson(Ticket t) {
JSONObject obj = new JSONObject();
obj.put("id", t.getId());
obj.put("creatorUUID", t.getCreatorUUID().toString());
obj.put("creatorName", t.getCreatorName());
obj.put("message", t.getMessage());
obj.put("world", t.getWorldName());
obj.put("x", t.getX());
obj.put("y", t.getY());
obj.put("z", t.getZ());
obj.put("yaw", t.getYaw());
obj.put("pitch", t.getPitch());
obj.put("status", t.getStatus().name());
obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null);
obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null);
obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null);
if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString());
if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName());
if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString());
if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName());
return obj;
}
private Ticket ticketFromJson(JSONObject obj) {
try {
Ticket t = new Ticket();
t.setId(((Long)obj.get("id")).intValue());
t.setCreatorUUID(UUID.fromString((String)obj.get("creatorUUID")));
t.setCreatorName((String)obj.get("creatorName"));
t.setMessage((String)obj.get("message"));
t.setWorldName((String)obj.get("world"));
t.setX((Double)obj.get("x"));
t.setY((Double)obj.get("y"));
t.setZ((Double)obj.get("z"));
t.setYaw(((Double)obj.get("yaw")).floatValue());
t.setPitch(((Double)obj.get("pitch")).floatValue());
t.setStatus(TicketStatus.valueOf((String)obj.get("status")));
if (obj.get("createdAt") != null) t.setCreatedAt(new java.sql.Timestamp((Long)obj.get("createdAt")));
if (obj.get("claimedAt") != null) t.setClaimedAt(new java.sql.Timestamp((Long)obj.get("claimedAt")));
if (obj.get("closedAt") != null) t.setClosedAt(new java.sql.Timestamp((Long)obj.get("closedAt")));
if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String)obj.get("claimerUUID")));
if (obj.get("claimerName") != null) t.setClaimerName((String)obj.get("claimerName"));
if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String)obj.get("forwardedToUUID")));
if (obj.get("forwardedToName") != null) t.setForwardedToName((String)obj.get("forwardedToName"));
return t;
} catch (Exception e) {
plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());
return null;
}
}
/**
* Migriert alle Tickets aus data.yml nach MySQL.
*/
public int migrateToMySQL() {
if (useMySQL || dataConfig == null) return 0;
int migrated = 0;
try {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null) {
// Ticket in MySQL speichern
useMySQL = true;
int id = createTicket(t);
useMySQL = false;
if (id != -1) migrated++;
}
}
} catch (Exception e) {
plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage());
}
return migrated;
}
/**
* Migriert alle Tickets aus MySQL nach data.yml.
*/
public int migrateToFile() {
if (!useMySQL) return 0;
int migrated = 0;
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
while (rs.next()) {
Ticket t = mapRow(rs);
if (t != null) {
useMySQL = false;
int id = createTicket(t);
useMySQL = true;
if (id != -1) migrated++;
}
}
} catch (Exception e) {
plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage());
}
return migrated;
}
private String dataFileName;
private String archiveFileName;
// Prüft geladene Tickets auf Korrektheit (Platzhalter)
private void validateLoadedTickets() {
if (dataConfig == null || !dataConfig.contains("tickets")) return;
int invalid = 0;
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Object obj = dataConfig.get("tickets." + key);
if (!(obj instanceof Ticket t)) {
sendError("Ungültiges Ticket-Objekt bei ID: " + key);
invalid++;
continue;
}
if (t.getCreatorUUID() == null || t.getCreatorName() == null || t.getMessage() == null || t.getStatus() == null) {
sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key);
invalid++;
}
try { UUID.fromString(t.getCreatorUUID().toString()); } catch (Exception e) {
sendError("Ungültige UUID bei Ticket ID: " + key);
invalid++;
}
try { TicketStatus.valueOf(t.getStatus().name()); } catch (Exception e) {
sendError("Ungültiger Status bei Ticket ID: " + key);
invalid++;
}
}
if (invalid > 0) {
String msg = plugin != null ? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid)) : (invalid + " ungültige Tickets beim Laden gefunden.");
sendError(msg);
}
}
// Backup der MySQL-Datenbank (Platzhalter)
private void backupMySQL() {
// TODO: Implementiere Backup-Logik für MySQL
}
// Backup der Datei-basierten Daten (Platzhalter)
private void backupDataFile() {
// TODO: Implementiere Backup-Logik für data.yml/data.json
}
private final TicketPlugin plugin; private final TicketPlugin plugin;
private HikariDataSource dataSource; private HikariDataSource dataSource;
private boolean useMySQL; private boolean useMySQL;
@@ -319,22 +31,26 @@ public class DatabaseManager {
private File dataFile; private File dataFile;
private YamlConfiguration dataConfig; private YamlConfiguration dataConfig;
private JSONArray dataJson; private JSONArray dataJson;
private String dataFileName;
private String archiveFileName;
// ─────────────────────────── Konstruktoren ─────────────────────────────
public DatabaseManager(TicketPlugin plugin) { public DatabaseManager(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true); this.useMySQL = plugin.getConfig().getBoolean("use-mysql", true);
this.useJson = plugin.getConfig().getBoolean("use-json", false); this.useJson = plugin.getConfig().getBoolean("use-json", false);
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] DatabaseManager initialisiert. useMySQL=" + useMySQL + ", useJson=" + useJson);
// Speicherpfade aus config.yml (absolut oder relativ zum Plugin-Ordner)
String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml"); String dataPath = plugin.getConfig().getString("data-file", useJson ? "data.json" : "data.yml");
String archivePath = plugin.getConfig().getString("archive-file", "archive.json"); String archivePath = plugin.getConfig().getString("archive-file", "archive.json");
this.dataFileName = dataPath; this.dataFileName = dataPath;
this.archiveFileName = archivePath; this.archiveFileName = archivePath;
if (!useMySQL) { if (!useMySQL) {
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath); if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] Datei-Speicher wird verwendet: " + dataPath);
if (useJson) { if (useJson) {
dataFile = resolvePath(dataPath); dataFile = resolvePath(dataPath);
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] JSON-Datei: " + dataFile.getAbsolutePath());
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
dataFile.getParentFile().mkdirs(); dataFile.getParentFile().mkdirs();
@@ -346,7 +62,7 @@ public class DatabaseManager {
} else { } else {
try { try {
JSONParser parser = new JSONParser(); JSONParser parser = new JSONParser();
dataJson = (JSONArray) parser.parse(new java.io.FileReader(dataFile)); dataJson = (JSONArray) parser.parse(new FileReader(dataFile));
} catch (Exception e) { } catch (Exception e) {
sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage()); sendError("Konnte " + dataPath + " nicht laden: " + e.getMessage());
dataJson = new JSONArray(); dataJson = new JSONArray();
@@ -354,7 +70,6 @@ public class DatabaseManager {
} }
} else { } else {
dataFile = resolvePath(dataPath); dataFile = resolvePath(dataPath);
if (plugin.isDebug()) plugin.getLogger().info("[DEBUG] YAML-Datei: " + dataFile.getAbsolutePath());
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
dataFile.getParentFile().mkdirs(); dataFile.getParentFile().mkdirs();
@@ -369,18 +84,33 @@ public class DatabaseManager {
} }
} }
// Hilfsfunktion: Absoluten oder relativen Pfad auflösen // Konstruktor für Tests
public DatabaseManager(File dataFile, YamlConfiguration dataConfig) {
this.plugin = null;
this.useMySQL = false;
this.useJson = false;
this.dataFileName = dataFile.getName();
this.archiveFileName = "archive.json";
this.dataFile = dataFile;
this.dataConfig = dataConfig;
validateLoadedTickets();
}
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
private File resolvePath(String path) { private File resolvePath(String path) {
File f = new File(path); File f = new File(path);
if (f.isAbsolute()) return f; if (f.isAbsolute()) return f;
return new File(plugin.getDataFolder(), path); return new File(plugin != null ? plugin.getDataFolder() : new File("."), path);
} }
// Fehlerausgabe im Chat und Log
private void sendError(String msg) { private void sendError(String msg) {
if (plugin != null) plugin.getLogger().severe(msg); if (plugin != null) plugin.getLogger().severe(msg);
// Fehler an alle Admins im Chat senden if (Bukkit.getServer() != null) {
Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] " + msg)); Bukkit.getOnlinePlayers().stream()
.filter(p -> p.hasPermission("ticket.admin"))
.forEach(p -> p.sendMessage("§c[TicketSystem] " + msg));
}
} }
// ─────────────────────────── Verbindung ──────────────────────────────── // ─────────────────────────── Verbindung ────────────────────────────────
@@ -389,7 +119,8 @@ public class DatabaseManager {
if (useMySQL) { if (useMySQL) {
try { try {
HikariConfig config = new HikariConfig(); HikariConfig config = new HikariConfig();
config.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8", config.setJdbcUrl(String.format(
"jdbc:mysql://%s:%d/%s?useSSL=false&autoReconnect=true&characterEncoding=UTF-8",
plugin.getConfig().getString("mysql.host"), plugin.getConfig().getString("mysql.host"),
plugin.getConfig().getInt("mysql.port"), plugin.getConfig().getInt("mysql.port"),
plugin.getConfig().getString("mysql.database"))); plugin.getConfig().getString("mysql.database")));
@@ -403,14 +134,17 @@ public class DatabaseManager {
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config); dataSource = new HikariDataSource(config);
// Tabellen anlegen & fehlende Spalten ergänzen
createTables(); createTables();
ensureColumns();
plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt."); plugin.getLogger().info("MySQL-Verbindung erfolgreich hergestellt.");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e); plugin.getLogger().log(Level.SEVERE, "Fehler beim Verbinden mit MySQL: " + e.getMessage(), e);
plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!"); plugin.getLogger().warning("Weiche auf Datei-Speicherung (data.yml) aus!");
useMySQL = false; useMySQL = false;
// Datei-Storage initialisieren
dataFile = new File(plugin.getDataFolder(), "data.yml"); dataFile = new File(plugin.getDataFolder(), "data.yml");
if (!dataFile.exists()) { if (!dataFile.exists()) {
try { try {
@@ -434,7 +168,6 @@ public class DatabaseManager {
dataSource.close(); dataSource.close();
plugin.getLogger().info("MySQL-Verbindung getrennt."); plugin.getLogger().info("MySQL-Verbindung getrennt.");
} }
// Bei Datei-Storage nichts zu tun
} }
private Connection getConnection() throws SQLException { private Connection getConnection() throws SQLException {
@@ -444,26 +177,28 @@ public class DatabaseManager {
// ─────────────────────────── Tabellen erstellen ──────────────────────── // ─────────────────────────── Tabellen erstellen ────────────────────────
private void createTables() { private void createTables() {
// close_comment ist jetzt von Anfang an in der CREATE-Anweisung enthalten
String sql = """ String sql = """
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
creator_uuid VARCHAR(36) NOT NULL, creator_uuid VARCHAR(36) NOT NULL,
creator_name VARCHAR(16) NOT NULL, creator_name VARCHAR(16) NOT NULL,
message VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL,
world VARCHAR(64) NOT NULL, world VARCHAR(64) NOT NULL,
x DOUBLE NOT NULL, x DOUBLE NOT NULL,
y DOUBLE NOT NULL, y DOUBLE NOT NULL,
z DOUBLE NOT NULL, z DOUBLE NOT NULL,
yaw FLOAT NOT NULL DEFAULT 0, yaw FLOAT NOT NULL DEFAULT 0,
pitch FLOAT NOT NULL DEFAULT 0, pitch FLOAT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'OPEN', status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
claimer_uuid VARCHAR(36), claimer_uuid VARCHAR(36),
claimer_name VARCHAR(16), claimer_name VARCHAR(16),
forwarded_to_uuid VARCHAR(36), forwarded_to_uuid VARCHAR(36),
forwarded_to_name VARCHAR(16), forwarded_to_name VARCHAR(16),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
claimed_at TIMESTAMP, claimed_at TIMESTAMP NULL,
closed_at TIMESTAMP closed_at TIMESTAMP NULL,
close_comment VARCHAR(500) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""; """;
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
@@ -473,6 +208,32 @@ public class DatabaseManager {
} }
} }
/**
* Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch.
* Wichtig für Server, die das Plugin bereits installiert hatten bevor
* close_comment existierte.
*/
private void ensureColumns() {
// close_comment hinzufügen, falls nicht vorhanden
String checkSql = """
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tickets'
AND COLUMN_NAME = 'close_comment'
""";
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(checkSql);
if (rs.next() && rs.getInt(1) == 0) {
// Spalte existiert nicht → hinzufügen
stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL");
plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt.");
}
} catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e);
}
}
// ─────────────────────────── CRUD ────────────────────────────────────── // ─────────────────────────── CRUD ──────────────────────────────────────
/** /**
@@ -507,7 +268,6 @@ public class DatabaseManager {
} }
return -1; return -1;
} else { } else {
// Datei-Storage: Ticket-ID generieren
int id = dataConfig.getInt("lastId", 0) + 1; int id = dataConfig.getInt("lastId", 0) + 1;
ticket.setId(id); ticket.setId(id);
dataConfig.set("lastId", id); dataConfig.set("lastId", id);
@@ -517,7 +277,6 @@ public class DatabaseManager {
backupDataFile(); backupDataFile();
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage()); plugin.getLogger().severe("Fehler beim Speichern von data.yml: " + e.getMessage());
Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission("ticket.admin")).forEach(p -> p.sendMessage("§c[TicketSystem] Fehler beim Speichern von data.yml: " + e.getMessage()));
} }
return id; return id;
} }
@@ -547,7 +306,7 @@ public class DatabaseManager {
t.setStatus(TicketStatus.CLAIMED); t.setStatus(TicketStatus.CLAIMED);
t.setClaimerUUID(claimerUUID); t.setClaimerUUID(claimerUUID);
t.setClaimerName(claimerName); t.setClaimerName(claimerName);
t.setClaimedAt(new java.sql.Timestamp(System.currentTimeMillis())); t.setClaimedAt(new Timestamp(System.currentTimeMillis()));
dataConfig.set("tickets." + ticketId, t); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); dataConfig.save(dataFile);
@@ -562,11 +321,15 @@ public class DatabaseManager {
/** /**
* Schließt ein Ticket (Status → CLOSED). * Schließt ein Ticket (Status → CLOSED).
*/ */
public boolean closeTicket(int ticketId) { public boolean closeTicket(int ticketId, String closeComment) {
if (useMySQL) { if (useMySQL) {
String sql = "UPDATE tickets SET status = 'CLOSED', closed_at = NOW() WHERE id = ? AND status != 'CLOSED'"; String sql = """
UPDATE tickets SET status = 'CLOSED', closed_at = NOW(), close_comment = ?
WHERE id = ? AND status != 'CLOSED'
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, ticketId); ps.setString(1, closeComment != null ? closeComment : "");
ps.setInt(2, ticketId);
return ps.executeUpdate() > 0; return ps.executeUpdate() > 0;
} catch (SQLException e) { } catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e); plugin.getLogger().log(Level.SEVERE, "Fehler beim Schließen des Tickets: " + e.getMessage(), e);
@@ -576,7 +339,8 @@ public class DatabaseManager {
Ticket t = getTicketById(ticketId); Ticket t = getTicketById(ticketId);
if (t == null || t.getStatus() == TicketStatus.CLOSED) return false; if (t == null || t.getStatus() == TicketStatus.CLOSED) return false;
t.setStatus(TicketStatus.CLOSED); t.setStatus(TicketStatus.CLOSED);
t.setClosedAt(new java.sql.Timestamp(System.currentTimeMillis())); t.setClosedAt(new Timestamp(System.currentTimeMillis()));
t.setCloseComment(closeComment != null ? closeComment : "");
dataConfig.set("tickets." + ticketId, t); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); dataConfig.save(dataFile);
@@ -588,6 +352,38 @@ public class DatabaseManager {
} }
} }
/**
* Löscht ein Ticket anhand der ID.
*/
public boolean deleteTicket(int id) {
if (useMySQL) {
String sql = "DELETE FROM tickets WHERE id = ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
int rows = ps.executeUpdate();
if (rows > 0) {
backupMySQL();
return true;
}
} catch (SQLException e) {
sendError("Fehler beim Löschen des Tickets: " + e.getMessage());
}
return false;
} else {
if (dataConfig.contains("tickets." + id)) {
dataConfig.set("tickets." + id, null);
try {
dataConfig.save(dataFile);
backupDataFile();
return true;
} catch (IOException e) {
sendError("Fehler beim Löschen des Tickets: " + e.getMessage());
}
}
return false;
}
}
/** /**
* Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). * Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED).
*/ */
@@ -623,6 +419,8 @@ public class DatabaseManager {
} }
} }
// ─────────────────────────── Abfragen ──────────────────────────────────
/** /**
* Gibt alle Tickets mit einem bestimmten Status zurück. * Gibt alle Tickets mit einem bestimmten Status zurück.
*/ */
@@ -642,7 +440,6 @@ public class DatabaseManager {
} }
return list; return list;
} else { } else {
// Datei-Storage: Alle Tickets filtern
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); Ticket t = (Ticket) dataConfig.get("tickets." + key);
@@ -655,6 +452,29 @@ public class DatabaseManager {
} }
} }
/**
* Gibt alle Tickets zurück (alle Status).
*/
public List<Ticket> getAllTickets() {
List<Ticket> list = new ArrayList<>();
if (useMySQL) {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
while (rs.next()) list.add(mapRow(rs));
} catch (SQLException e) {
sendError("Fehler beim Abrufen aller Tickets: " + e.getMessage());
}
} else {
if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null) list.add(t);
}
}
}
return list;
}
/** /**
* Gibt ein einzelnes Ticket anhand der ID zurück. * Gibt ein einzelnes Ticket anhand der ID zurück.
*/ */
@@ -678,11 +498,11 @@ public class DatabaseManager {
} }
/** /**
* Anzahl offener Tickets (OPEN + FORWARDED) für Join-Benachrichtigung. * Anzahl offener Tickets (OPEN) für Join-Benachrichtigung.
*/ */
public int countOpenTickets() { public int countOpenTickets() {
if (useMySQL) { if (useMySQL) {
String sql = "SELECT COUNT(*) FROM tickets WHERE status IN ('OPEN', 'FORWARDED', 'CLAIMED')"; String sql = "SELECT COUNT(*) FROM tickets WHERE status = 'OPEN'";
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(sql); ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) return rs.getInt(1); if (rs.next()) return rs.getInt(1);
@@ -695,7 +515,7 @@ public class DatabaseManager {
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.FORWARDED || t.getStatus() == TicketStatus.CLAIMED)) count++; if (t != null && t.getStatus() == TicketStatus.OPEN) count++;
} }
} }
return count; return count;
@@ -721,17 +541,176 @@ public class DatabaseManager {
if (dataConfig.contains("tickets")) { if (dataConfig.contains("tickets")) {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) { for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key); Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null && uuid.equals(t.getCreatorUUID()) && (t.getStatus() == TicketStatus.OPEN || t.getStatus() == TicketStatus.CLAIMED || t.getStatus() == TicketStatus.FORWARDED)) count++; if (t != null && uuid.equals(t.getCreatorUUID())
&& (t.getStatus() == TicketStatus.OPEN
|| t.getStatus() == TicketStatus.CLAIMED
|| t.getStatus() == TicketStatus.FORWARDED)) count++;
} }
} }
return count; return count;
} }
} }
// ─────────────────────────── Archivierung ──────────────────────────────
/**
* Archiviert alle geschlossenen Tickets in eine separate Datei.
*/
public int archiveClosedTickets() {
List<Ticket> all = getAllTickets();
List<Ticket> toArchive = new ArrayList<>();
for (Ticket t : all) {
if (t.getStatus() == TicketStatus.CLOSED) toArchive.add(t);
}
if (toArchive.isEmpty()) return 0;
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
JSONArray arr = new JSONArray();
if (archiveFile.exists()) {
try (FileReader fr = new FileReader(archiveFile)) {
JSONParser parser = new JSONParser();
Object parsed = parser.parse(fr);
if (parsed instanceof JSONArray oldArr) arr.addAll(oldArr);
} catch (Exception ignored) {}
}
for (Ticket t : toArchive) arr.add(ticketToJson(t));
try (FileWriter fw = new FileWriter(archiveFile)) {
fw.write(arr.toJSONString());
} catch (Exception e) {
sendError("Fehler beim Archivieren: " + e.getMessage());
return 0;
}
int removed = 0;
if (useMySQL) {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement("DELETE FROM tickets WHERE id = ?")) {
for (Ticket t : toArchive) {
ps.setInt(1, t.getId());
ps.executeUpdate();
removed++;
}
} catch (Exception e) {
sendError("Fehler beim Entfernen archivierter Tickets: " + e.getMessage());
}
} else {
for (Ticket t : toArchive) {
dataConfig.set("tickets." + t.getId(), null);
removed++;
}
try { dataConfig.save(dataFile); } catch (Exception e) {
sendError("Fehler beim Speichern nach Archivierung: " + e.getMessage());
}
}
return removed;
}
// ─────────────────────────── Statistiken ───────────────────────────────
public TicketStats getTicketStats() {
List<Ticket> all = getAllTickets();
int open = 0, claimed = 0, forwarded = 0, closed = 0;
java.util.Map<String, Integer> byPlayer = new java.util.HashMap<>();
for (Ticket t : all) {
switch (t.getStatus()) {
case OPEN -> open++;
case CLAIMED -> claimed++;
case FORWARDED -> forwarded++;
case CLOSED -> closed++;
}
byPlayer.merge(t.getCreatorName(), 1, Integer::sum);
}
return new TicketStats(all.size(), open, closed, forwarded, byPlayer);
}
public static class TicketStats {
public final int total, open, closed, forwarded;
public final java.util.Map<String, Integer> byPlayer;
public TicketStats(int total, int open, int closed, int forwarded, java.util.Map<String, Integer> byPlayer) {
this.total = total; this.open = open; this.closed = closed;
this.forwarded = forwarded; this.byPlayer = byPlayer;
}
}
// ─────────────────────────── Export / Import ────────────────────────────
public int exportTickets(File exportFile) {
List<Ticket> tickets = getAllTickets();
JSONArray arr = new JSONArray();
for (Ticket t : tickets) arr.add(ticketToJson(t));
try (FileWriter fw = new FileWriter(exportFile)) {
fw.write(arr.toJSONString());
return tickets.size();
} catch (IOException e) {
sendError("Fehler beim Export: " + e.getMessage());
return 0;
}
}
public int importTickets(File importFile) {
int imported = 0;
try (FileReader fr = new FileReader(importFile)) {
JSONParser parser = new JSONParser();
JSONArray arr = (JSONArray) parser.parse(fr);
for (Object o : arr) {
Ticket t = ticketFromJson((JSONObject) o);
if (t != null && createTicket(t) != -1) imported++;
}
} catch (Exception e) {
sendError("Fehler beim Import: " + e.getMessage());
}
return imported;
}
// ─────────────────────────── Migration ─────────────────────────────────
public int migrateToMySQL() {
if (useMySQL || dataConfig == null) return 0;
int migrated = 0;
try {
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Ticket t = (Ticket) dataConfig.get("tickets." + key);
if (t != null) {
useMySQL = true;
int id = createTicket(t);
useMySQL = false;
if (id != -1) migrated++;
}
}
} catch (Exception e) {
plugin.getLogger().severe("Fehler bei Migration zu MySQL: " + e.getMessage());
}
return migrated;
}
public int migrateToFile() {
if (!useMySQL) return 0;
int migrated = 0;
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM tickets");
while (rs.next()) {
Ticket t = mapRow(rs);
if (t != null) {
useMySQL = false;
int id = createTicket(t);
useMySQL = true;
if (id != -1) migrated++;
}
}
} catch (Exception e) {
plugin.getLogger().severe("Fehler bei Migration zu Datei: " + e.getMessage());
}
return migrated;
}
// ─────────────────────────── Mapping ─────────────────────────────────── // ─────────────────────────── Mapping ───────────────────────────────────
/**
* Liest eine Zeile aus dem ResultSet und erstellt ein Ticket-Objekt.
* close_comment wird mit try-catch abgesichert, damit ältere Datenbanken
* ohne diese Spalte nicht abstürzen.
*/
private Ticket mapRow(ResultSet rs) throws SQLException { private Ticket mapRow(ResultSet rs) throws SQLException {
File archiveFile = new File(plugin.getDataFolder(), archiveFileName);
Ticket t = new Ticket(); Ticket t = new Ticket();
t.setId(rs.getInt("id")); t.setId(rs.getInt("id"));
t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid"))); t.setCreatorUUID(UUID.fromString(rs.getString("creator_uuid")));
@@ -748,6 +727,16 @@ public class DatabaseManager {
t.setClaimedAt(rs.getTimestamp("claimed_at")); t.setClaimedAt(rs.getTimestamp("claimed_at"));
t.setClosedAt(rs.getTimestamp("closed_at")); t.setClosedAt(rs.getTimestamp("closed_at"));
// ── BUGFIX: close_comment mit try-catch absichern ──────────────────
// Wenn die Spalte in einer alten DB noch nicht existiert, wird der
// Fehler ignoriert statt die gesamte Ticket-Liste leer zu lassen.
try {
String closeComment = rs.getString("close_comment");
if (closeComment != null) t.setCloseComment(closeComment);
} catch (SQLException ignored) {
// Spalte existiert noch nicht ensureColumns() ergänzt sie beim nächsten Start
}
String claimerUUID = rs.getString("claimer_uuid"); String claimerUUID = rs.getString("claimer_uuid");
if (claimerUUID != null) { if (claimerUUID != null) {
t.setClaimerUUID(UUID.fromString(claimerUUID)); t.setClaimerUUID(UUID.fromString(claimerUUID));
@@ -760,4 +749,100 @@ public class DatabaseManager {
} }
return t; return t;
} }
}
// ─────────────────────────── JSON-Hilfsmethoden ─────────────────────────
private JSONObject ticketToJson(Ticket t) {
JSONObject obj = new JSONObject();
obj.put("id", t.getId());
obj.put("creatorUUID", t.getCreatorUUID().toString());
obj.put("creatorName", t.getCreatorName());
obj.put("message", t.getMessage());
obj.put("world", t.getWorldName());
obj.put("x", t.getX());
obj.put("y", t.getY());
obj.put("z", t.getZ());
obj.put("yaw", t.getYaw());
obj.put("pitch", t.getPitch());
obj.put("status", t.getStatus().name());
obj.put("createdAt", t.getCreatedAt() != null ? t.getCreatedAt().getTime() : null);
obj.put("claimedAt", t.getClaimedAt() != null ? t.getClaimedAt().getTime() : null);
obj.put("closedAt", t.getClosedAt() != null ? t.getClosedAt().getTime() : null);
if (t.getClaimerUUID() != null) obj.put("claimerUUID", t.getClaimerUUID().toString());
if (t.getClaimerName() != null) obj.put("claimerName", t.getClaimerName());
if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString());
if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName());
if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment());
return obj;
}
private Ticket ticketFromJson(JSONObject obj) {
try {
Ticket t = new Ticket();
t.setId(((Long) obj.get("id")).intValue());
t.setCreatorUUID(UUID.fromString((String) obj.get("creatorUUID")));
t.setCreatorName((String) obj.get("creatorName"));
t.setMessage((String) obj.get("message"));
t.setWorldName((String) obj.get("world"));
t.setX((Double) obj.get("x"));
t.setY((Double) obj.get("y"));
t.setZ((Double) obj.get("z"));
t.setYaw(((Double) obj.get("yaw")).floatValue());
t.setPitch(((Double) obj.get("pitch")).floatValue());
t.setStatus(TicketStatus.valueOf((String) obj.get("status")));
if (obj.get("createdAt") != null) t.setCreatedAt(new Timestamp((Long) obj.get("createdAt")));
if (obj.get("claimedAt") != null) t.setClaimedAt(new Timestamp((Long) obj.get("claimedAt")));
if (obj.get("closedAt") != null) t.setClosedAt(new Timestamp((Long) obj.get("closedAt")));
if (obj.get("claimerUUID") != null) t.setClaimerUUID(UUID.fromString((String) obj.get("claimerUUID")));
if (obj.get("claimerName") != null) t.setClaimerName((String) obj.get("claimerName"));
if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID")));
if (obj.get("forwardedToName") != null) t.setForwardedToName((String) obj.get("forwardedToName"));
if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment"));
return t;
} catch (Exception e) {
if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());
return null;
}
}
// ─────────────────────────── Validierung ───────────────────────────────
private void validateLoadedTickets() {
if (dataConfig == null || !dataConfig.contains("tickets")) return;
int invalid = 0;
for (String key : dataConfig.getConfigurationSection("tickets").getKeys(false)) {
Object obj = dataConfig.get("tickets." + key);
if (!(obj instanceof Ticket t)) {
sendError("Ungültiges Ticket-Objekt bei ID: " + key);
invalid++;
continue;
}
if (t.getCreatorUUID() == null || t.getCreatorName() == null
|| t.getMessage() == null || t.getStatus() == null) {
sendError("Ticket mit fehlenden Pflichtfeldern: ID " + key);
invalid++;
}
try { UUID.fromString(t.getCreatorUUID().toString()); }
catch (Exception e) { sendError("Ungültige UUID bei Ticket ID: " + key); invalid++; }
try { TicketStatus.valueOf(t.getStatus().name()); }
catch (Exception e) { sendError("Ungültiger Status bei Ticket ID: " + key); invalid++; }
}
if (invalid > 0) {
String msg = plugin != null
? plugin.formatMessage("messages.validation-warning").replace("{count}", String.valueOf(invalid))
: invalid + " ungültige Tickets beim Laden gefunden.";
sendError(msg);
}
}
// ─────────────────────────── Backup (Platzhalter) ──────────────────────
private void backupMySQL() {
// TODO: MySQL-Backup implementieren
}
private void backupDataFile() {
// TODO: Datei-Backup implementieren
}
}

View File

@@ -0,0 +1,195 @@
package de.ticketsystem.discord;
import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket;
import org.bukkit.Bukkit;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
/**
* Sendet Benachrichtigungen an einen Discord-Webhook.
* Unterstützt Embeds mit Farbe, Feldern und Timestamp.
*/
public class DiscordWebhook {
private final TicketPlugin plugin;
public DiscordWebhook(TicketPlugin plugin) {
this.plugin = plugin;
}
// ─────────────────────────── Öffentliche Methoden ──────────────────────
/**
* Sendet eine Benachrichtigung wenn ein neues Ticket erstellt wurde.
*/
public void sendNewTicket(Ticket ticket) {
if (!isEnabled()) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
if (webhookUrl.isEmpty()) return;
// Felder aus Config lesen
String title = plugin.getConfig().getString("discord.messages.new-ticket.title", "🎫 Neues Ticket erstellt");
String color = plugin.getConfig().getString("discord.messages.new-ticket.color", "3066993"); // Grün
String footer = plugin.getConfig().getString("discord.messages.new-ticket.footer", "TicketSystem");
boolean showPos = plugin.getConfig().getBoolean("discord.messages.new-ticket.show-position", true);
// JSON-Embed aufbauen
StringBuilder fields = new StringBuilder();
fields.append(field("Spieler", ticket.getCreatorName(), true));
fields.append(",");
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(",");
fields.append(field("Anliegen", ticket.getMessage(), false));
if (showPos) {
fields.append(",");
fields.append(field("Welt", ticket.getWorldName(), true));
fields.append(",");
fields.append(field("Position",
String.format("%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()), true));
}
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json);
}
/**
* Sendet eine Benachrichtigung wenn ein Ticket geschlossen wurde.
*/
public void sendTicketClosed(Ticket ticket, String closerName) {
if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-closed.enabled", false)) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
if (webhookUrl.isEmpty()) return;
String title = plugin.getConfig().getString("discord.messages.ticket-closed.title", "🔒 Ticket geschlossen");
String color = plugin.getConfig().getString("discord.messages.ticket-closed.color", "15158332"); // Rot
String footer = plugin.getConfig().getString("discord.messages.ticket-closed.footer", "TicketSystem");
StringBuilder fields = new StringBuilder();
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(",");
fields.append(field("Ersteller", ticket.getCreatorName(), true));
fields.append(",");
fields.append(field("Geschlossen von", closerName, true));
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
fields.append(",");
fields.append(field("Kommentar", ticket.getCloseComment(), false));
}
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json);
}
/**
* Sendet eine Benachrichtigung wenn ein Ticket weitergeleitet wurde.
*/
public void sendTicketForwarded(Ticket ticket, String fromName) {
if (!isEnabled()) return;
if (!plugin.getConfig().getBoolean("discord.messages.ticket-forwarded.enabled", false)) return;
String webhookUrl = plugin.getConfig().getString("discord.webhook-url", "");
if (webhookUrl.isEmpty()) return;
String title = plugin.getConfig().getString("discord.messages.ticket-forwarded.title", "🔀 Ticket weitergeleitet");
String color = plugin.getConfig().getString("discord.messages.ticket-forwarded.color", "15105570"); // Orange
String footer = plugin.getConfig().getString("discord.messages.ticket-forwarded.footer", "TicketSystem");
StringBuilder fields = new StringBuilder();
fields.append(field("Ticket ID", "#" + ticket.getId(), true));
fields.append(",");
fields.append(field("Ersteller", ticket.getCreatorName(), true));
fields.append(",");
fields.append(field("Weitergeleitet von", fromName, true));
fields.append(",");
fields.append(field("Weitergeleitet an", ticket.getForwardedToName(), true));
String json = buildPayload(title, Integer.parseInt(color), fields.toString(), footer);
sendAsync(webhookUrl, json);
}
// ─────────────────────────── Private Hilfsmethoden ─────────────────────
private boolean isEnabled() {
return plugin.getConfig().getBoolean("discord.enabled", false);
}
/**
* Baut einen einzelnen Embed-Field als JSON-String.
*/
private String field(String name, String value, boolean inline) {
// Anführungszeichen und Backslashes im Wert escapen
String safeValue = value != null
? value.replace("\\", "\\\\").replace("\"", "\\\"")
: "";
String safeName = name.replace("\\", "\\\\").replace("\"", "\\\"");
return String.format("{\"name\":\"%s\",\"value\":\"%s\",\"inline\":%b}",
safeName, safeValue, inline);
}
/**
* Baut den kompletten Webhook-Payload als JSON.
*/
private String buildPayload(String title, int color, String fieldsJson, String footer) {
String timestamp = Instant.now().toString(); // ISO-8601
return String.format("""
{
"embeds": [{
"title": "%s",
"color": %d,
"fields": [%s],
"footer": { "text": "%s" },
"timestamp": "%s"
}]
}""",
title.replace("\"", "\\\""),
color,
fieldsJson,
footer.replace("\"", "\\\""),
timestamp);
}
/**
* Sendet den JSON-Payload asynchron an den Webhook.
*/
private void sendAsync(String webhookUrl, String json) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
URL url = new URL(webhookUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "TicketSystem-Plugin");
conn.setDoOutput(true);
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes(StandardCharsets.UTF_8));
}
int responseCode = conn.getResponseCode();
if (plugin.isDebug()) {
plugin.getLogger().info("[DEBUG] Discord Webhook Response: " + responseCode);
}
// 204 = No Content → Erfolg bei Discord
if (responseCode != 200 && responseCode != 204) {
plugin.getLogger().warning("[DiscordWebhook] Unerwarteter Response-Code: " + responseCode);
}
conn.disconnect();
} catch (Exception e) {
plugin.getLogger().warning("[DiscordWebhook] Fehler beim Senden: " + e.getMessage());
if (plugin.isDebug()) e.printStackTrace();
}
});
}
}

View File

@@ -4,12 +4,13 @@ import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket; import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus; import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
@@ -23,19 +24,35 @@ import java.util.UUID;
public class TicketGUI implements Listener { public class TicketGUI implements Listener {
private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // ─────────────────────────── Titel-Konstanten ──────────────────────────
private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht
private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; // Spieler: eigene Tickets
private static final String DETAIL_GUI_TITLE = "§8§lTicket-Details"; // Admin: Detail-Ansicht
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm");
private final TicketPlugin plugin; private final TicketPlugin plugin;
// Speichert welcher Spieler welches Ticket an welchem Slot hat /** Admin-Übersicht: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>();
/** Spieler-GUI: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>();
/** Detail-Ansicht: Player-UUID → Ticket */
private final Map<UUID, Ticket> detailTicketMap = new HashMap<>();
/** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */
private final Map<UUID, Integer> awaitingComment = new HashMap<>();
public TicketGUI(TicketPlugin plugin) { public TicketGUI(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
// ─────────────────────────── GUI öffnen ──────────────────────────────── // ═══════════════════════════════════════════════════════════════════════
// ADMIN / SUPPORTER GUI (Übersicht aller Tickets)
// ═══════════════════════════════════════════════════════════════════════
public void openGUI(Player player) { public void openGUI(Player player) {
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus( List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(
@@ -46,47 +63,326 @@ public class TicketGUI implements Listener {
return; return;
} }
// Inventar-Größe: nächste Vielfaches von 9 (max. 54 Slots) int size = calcSize(tickets.size());
int size = Math.min(54, (int) (Math.ceil(tickets.size() / 9.0) * 9));
if (size < 9) size = 9;
Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>(); Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 54; i++) { for (int i = 0; i < tickets.size() && i < 54; i++) {
Ticket ticket = tickets.get(i); Ticket ticket = tickets.get(i);
ItemStack item = buildTicketItem(ticket); inv.setItem(i, buildAdminListItem(ticket));
inv.setItem(i, item);
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
// Trennlinie am Ende, wenn Platz
fillEmpty(inv); fillEmpty(inv);
playerSlotMap.put(player.getUniqueId(), slotMap); playerSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
// ─────────────────────────── Item bauen ──────────────────────────────── // ═══════════════════════════════════════════════════════════════════════
// SPIELER-GUI (nur eigene Tickets, mit Lösch-Option bei OPEN)
// ═══════════════════════════════════════════════════════════════════════
private ItemStack buildTicketItem(Ticket ticket) { public void openPlayerGUI(Player player) {
// Material je nach Status List<Ticket> all = plugin.getDatabaseManager().getTicketsByStatus(
Material mat; TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED, TicketStatus.CLOSED);
switch (ticket.getStatus()) {
case OPEN -> mat = Material.PAPER; List<Ticket> tickets = new ArrayList<>();
case CLAIMED -> mat = Material.YELLOW_DYE; for (Ticket t : all) {
case FORWARDED -> mat = Material.ORANGE_DYE; if (t.getCreatorUUID().equals(player.getUniqueId())) tickets.add(t);
default -> mat = Material.PAPER;
} }
if (tickets.isEmpty()) {
player.sendMessage(plugin.color("&aDu hast aktuell keine Tickets."));
return;
}
int size = calcSize(tickets.size());
Inventory inv = Bukkit.createInventory(null, size, PLAYER_GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 54; i++) {
Ticket ticket = tickets.get(i);
inv.setItem(i, buildPlayerTicketItem(ticket));
slotMap.put(i, ticket);
}
fillEmpty(inv);
playerOwnSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv);
}
// ═══════════════════════════════════════════════════════════════════════
// ADMIN DETAIL-GUI (Aktionen für ein einzelnes Ticket)
// ═══════════════════════════════════════════════════════════════════════
public void openDetailGUI(Player player, Ticket ticket) {
Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE);
// Slot 4: Ticket-Info (Mitte oben)
inv.setItem(4, buildDetailInfoItem(ticket));
// Slot 10: Teleportieren (immer verfügbar)
inv.setItem(10, buildActionItem(
Material.ENDER_PEARL,
"§b§lTeleportieren",
List.of("§7Teleportiert dich zur", "§7Position des Tickets.")));
// Slot 12: Claimen (nur wenn OPEN), sonst grauer Platzhalter
if (ticket.getStatus() == TicketStatus.OPEN) {
inv.setItem(12, buildActionItem(
Material.LIME_WOOL,
"§a§lTicket annehmen",
List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet.")));
} else {
inv.setItem(12, buildActionItem(
Material.GRAY_WOOL,
"§8Bereits angenommen",
List.of("§7Dieses Ticket wurde bereits", "§7angenommen.")));
}
// Slot 14: Schließen — für OPEN, CLAIMED und FORWARDED; grauer Block wenn bereits CLOSED
if (ticket.getStatus() != TicketStatus.CLOSED) {
inv.setItem(14, buildActionItem(
Material.RED_WOOL,
"§c§lTicket schließen",
List.of(
"§7Schließt das Ticket.",
"§8§m ",
"§eNach dem Klick kannst du im",
"§eChat einen Kommentar eingeben.",
"§7Tippe §c- §7für keinen Kommentar.",
"§7Tippe §ccancel §7zum Abbrechen.")));
} else {
inv.setItem(14, buildActionItem(
Material.GRAY_WOOL,
"§8Bereits geschlossen",
List.of("§7Dieses Ticket ist bereits", "§7geschlossen.")));
}
// Slot 16: Zurück zur Übersicht
inv.setItem(16, buildActionItem(
Material.ARROW,
"§7§lZurück",
List.of("§7Zurück zur Ticket-Übersicht.")));
fillEmpty(inv);
detailTicketMap.put(player.getUniqueId(), ticket);
player.openInventory(inv);
}
// ═══════════════════════════════════════════════════════════════════════
// CLICK-EVENTS
// ═══════════════════════════════════════════════════════════════════════
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
String title = event.getView().getTitle();
if (!title.equals(GUI_TITLE) && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return;
event.setCancelled(true);
int slot = event.getRawSlot();
if (slot < 0) return;
// ── Admin-Übersicht ──────────────────────────────────────────────
if (title.equals(GUI_TITLE)) {
Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId());
if (slotMap == null) return;
Ticket ticket = slotMap.get(slot);
if (ticket == null) return;
player.closeInventory();
// Frische Daten aus DB holen, dann Detail-GUI öffnen
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (fresh == null) {
player.sendMessage(plugin.formatMessage("messages.ticket-not-found"));
return;
}
openDetailGUI(player, fresh);
});
});
return;
}
// ── Spieler-GUI ──────────────────────────────────────────────────
if (title.equals(PLAYER_GUI_TITLE)) {
Map<Integer, Ticket> slotMap = playerOwnSlotMap.get(player.getUniqueId());
if (slotMap == null) return;
Ticket ticket = slotMap.get(slot);
if (ticket == null) return;
player.closeInventory();
if (ticket.getStatus() == TicketStatus.OPEN) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean deleted = plugin.getDatabaseManager().deleteTicket(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (deleted) {
player.sendMessage(plugin.color(
"&aDein Ticket &e#" + ticket.getId() + " &awurde gelöscht."));
openPlayerGUI(player);
} else {
player.sendMessage(plugin.color("&cFehler beim Löschen des Tickets."));
}
});
});
} else {
player.sendMessage(plugin.color(
"&cDieses Ticket kann nicht mehr gelöscht werden, " +
"da es bereits angenommen oder geschlossen wurde."));
}
return;
}
// ── Admin Detail-GUI ─────────────────────────────────────────────
if (title.equals(DETAIL_GUI_TITLE)) {
Ticket ticket = detailTicketMap.get(player.getUniqueId());
if (ticket == null) return;
player.closeInventory();
switch (slot) {
case 10 -> handleDetailTeleport(player, ticket);
case 12 -> handleDetailClaim(player, ticket);
case 14 -> handleDetailClose(player, ticket);
case 16 -> openGUI(player);
// Glasscheiben und andere Slots → nichts tun
}
}
}
// ─────────────────────────── Detail-Aktionen ───────────────────────────
private void handleDetailTeleport(Player player, Ticket ticket) {
if (ticket.getLocation() != null) {
player.teleport(ticket.getLocation());
player.sendMessage(plugin.color(
"&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
} else {
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
}
}
private void handleDetailClaim(Player player, Ticket ticket) {
if (ticket.getStatus() != TicketStatus.OPEN) {
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().claimTicket(
ticket.getId(), player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket);
if (ticket.getLocation() != null) player.teleport(ticket.getLocation());
// ── BUGFIX: Detail-GUI mit frischen DB-Daten neu öffnen ──
// Dadurch verschwindet der Claim-Button und der Schließen-Button
// ist sofort korrekt sichtbar, ohne dass der Admin die GUI
// erst schließen und neu öffnen muss.
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (fresh != null) openDetailGUI(player, fresh);
});
});
} else {
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
}
});
});
}
private void handleDetailClose(Player player, Ticket ticket) {
if (ticket.getStatus() == TicketStatus.CLOSED) {
player.sendMessage(plugin.color("&cDieses Ticket ist bereits geschlossen."));
return;
}
awaitingComment.put(player.getUniqueId(), ticket.getId());
player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Ticket #" + ticket.getId() + " schließen"));
player.sendMessage(plugin.color("&7Gib einen Kommentar für den Spieler ein."));
player.sendMessage(plugin.color("&7Kein Kommentar? Tippe: &e-"));
player.sendMessage(plugin.color("&7Abbrechen? Tippe: &ccancel"));
player.sendMessage(plugin.color("&8&m "));
}
// ─────────────────────────── Chat-Listener (Kommentar-Eingabe) ─────────
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
if (!awaitingComment.containsKey(player.getUniqueId())) return;
event.setCancelled(true);
int ticketId = awaitingComment.remove(player.getUniqueId());
String input = event.getMessage().trim();
if (input.equalsIgnoreCase("cancel")) {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.color("&cSchließen abgebrochen.")));
return;
}
// "-" = bewusst kein Kommentar
final String comment = input.equals("-") ? "" : input;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-closed")
.replace("{id}", String.valueOf(ticketId)));
if (!comment.isEmpty()) {
player.sendMessage(plugin.color("&7Kommentar gespeichert: &f" + comment));
}
if (ticket != null) {
ticket.setCloseComment(comment);
plugin.getTicketManager().notifyCreatorClosed(ticket);
}
});
} else {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
}
});
}
// ═══════════════════════════════════════════════════════════════════════
// ITEM-BUILDER
// ═══════════════════════════════════════════════════════════════════════
private ItemStack buildAdminListItem(Ticket ticket) {
Material mat = switch (ticket.getStatus()) {
case OPEN -> Material.PAPER;
case CLAIMED -> Material.YELLOW_DYE;
case FORWARDED -> Material.ORANGE_DYE;
default -> Material.PAPER;
};
ItemStack item = new ItemStack(mat); ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta(); ItemMeta meta = item.getItemMeta();
if (meta == null) return item; if (meta == null) return item;
// Display-Name
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
// Lore aufbauen
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Ersteller: §e" + ticket.getCreatorName()); lore.add("§7Ersteller: §e" + ticket.getCreatorName());
@@ -94,22 +390,97 @@ public class TicketGUI implements Listener {
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName()); lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
if (ticket.getClaimerName() != null)
lore.add("§7Angenommen: §a" + ticket.getClaimerName());
if (ticket.getForwardedToName() != null)
lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName());
lore.add("§8§m ");
lore.add("§e§l» KLICKEN für Details & Aktionen");
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}
private ItemStack buildDetailInfoItem(Ticket ticket) {
Material mat = switch (ticket.getStatus()) {
case OPEN -> Material.PAPER;
case CLAIMED -> Material.YELLOW_DYE;
case FORWARDED -> Material.ORANGE_DYE;
case CLOSED -> Material.GRAY_DYE;
};
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta == null) return item;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<String> lore = new ArrayList<>();
lore.add("§8§m ");
lore.add("§7Ersteller: §e" + ticket.getCreatorName());
lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
if (ticket.getClaimerName() != null) { if (ticket.getClaimerName() != null) {
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Geclaimt von: §a" + ticket.getClaimerName()); lore.add("§7Angenommen von: §a" + ticket.getClaimerName());
if (ticket.getClaimedAt() != null) if (ticket.getClaimedAt() != null)
lore.add("§7Geclaimt am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt()));
} }
if (ticket.getForwardedToName() != null) { if (ticket.getForwardedToName() != null)
lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName());
if (ticket.getStatus() == TicketStatus.CLOSED) {
if (ticket.getClosedAt() != null)
lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt()));
if (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
lore.add("§7Kommentar: §f" + ticket.getCloseComment());
} }
lore.add("§8§m "); lore.add("§8§m ");
if (ticket.getStatus() == TicketStatus.OPEN) {
lore.add("§a§l» KLICKEN zum Claimen & Teleportieren"); meta.setLore(lore);
} else { item.setItemMeta(meta);
lore.add("§e§l» KLICKEN zum Teleportieren"); return item;
}
private ItemStack buildPlayerTicketItem(Ticket ticket) {
Material mat = switch (ticket.getStatus()) {
case OPEN -> Material.PAPER;
case CLAIMED -> Material.YELLOW_DYE;
case FORWARDED -> Material.ORANGE_DYE;
case CLOSED -> Material.GRAY_DYE;
};
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta == null) return item;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<String> lore = new ArrayList<>();
lore.add("§8§m ");
lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
if (ticket.getStatus() == TicketStatus.CLAIMED && ticket.getClaimerName() != null)
lore.add("§7Angenommen von: §a" + ticket.getClaimerName());
if (ticket.getStatus() == TicketStatus.FORWARDED && ticket.getForwardedToName() != null)
lore.add("§7Bearbeiter: §6" + ticket.getForwardedToName());
if (ticket.getStatus() == TicketStatus.CLOSED
&& ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
lore.add("§8§m ");
lore.add("§7Kommentar des Supports:");
lore.add("§f" + ticket.getCloseComment());
}
lore.add("§8§m ");
switch (ticket.getStatus()) {
case OPEN -> { lore.add("§c§l» KLICKEN zum Löschen");
lore.add("§7Nur möglich solange noch nicht angenommen."); }
case CLOSED -> lore.add("§8» Dieses Ticket ist abgeschlossen.");
default -> { lore.add("§e» Ticket wird bearbeitet...");
lore.add("§7Kann nicht mehr gelöscht werden."); }
} }
meta.setLore(lore); meta.setLore(lore);
@@ -117,72 +488,32 @@ public class TicketGUI implements Listener {
return item; return item;
} }
private ItemStack buildActionItem(Material material, String displayName, List<String> lore) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
if (meta == null) return item;
meta.setDisplayName(displayName);
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
private int calcSize(int ticketCount) {
int size = (int) Math.ceil(ticketCount / 9.0) * 9;
return Math.max(9, Math.min(54, size));
}
private void fillEmpty(Inventory inv) { private void fillEmpty(Inventory inv) {
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = glass.getItemMeta(); ItemMeta meta = glass.getItemMeta();
if (meta != null) { meta.setDisplayName(" "); glass.setItemMeta(meta); } if (meta != null) {
meta.setDisplayName(" ");
glass.setItemMeta(meta);
}
for (int i = 0; i < inv.getSize(); i++) { for (int i = 0; i < inv.getSize(); i++) {
if (inv.getItem(i) == null) inv.setItem(i, glass); if (inv.getItem(i) == null) inv.setItem(i, glass);
} }
} }
}
// ─────────────────────────── Klick-Event ───────────────────────────────
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
if (!event.getView().getTitle().equals(GUI_TITLE)) return;
event.setCancelled(true);
Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId());
if (slotMap == null) return;
int slot = event.getRawSlot();
Ticket ticket = slotMap.get(slot);
if (ticket == null) return;
player.closeInventory();
// Asynchron aus DB neu laden (aktuelle Daten)
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
if (fresh == null) {
player.sendMessage(plugin.formatMessage("messages.ticket-not-found"));
return;
}
Bukkit.getScheduler().runTask(plugin, () -> handleTicketClick(player, fresh));
});
}
private void handleTicketClick(Player player, Ticket ticket) {
// Versuche zu claimen, wenn noch OPEN
if (ticket.getStatus() == TicketStatus.OPEN) {
boolean success = plugin.getDatabaseManager().claimTicket(
ticket.getId(), player.getUniqueId(), player.getName());
if (success) {
ticket.setStatus(TicketStatus.CLAIMED);
ticket.setClaimerUUID(player.getUniqueId());
ticket.setClaimerName(player.getName());
player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket);
} else {
player.sendMessage(plugin.formatMessage("messages.already-claimed"));
}
}
// Teleportation zur Ticket-Position
if (ticket.getLocation() != null) {
player.teleport(ticket.getLocation());
player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
} else {
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
}
}
}

View File

@@ -1,7 +1,12 @@
package de.ticketsystem.listeners; package de.ticketsystem.listeners;
import java.util.List;
import de.ticketsystem.TicketPlugin; import de.ticketsystem.TicketPlugin;
import de.ticketsystem.model.Ticket;
import de.ticketsystem.model.TicketStatus;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -19,21 +24,56 @@ public class PlayerJoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
// Nur Supporter und Admins erhalten die Join-Benachrichtigung // ── Supporter/Admin: offene Tickets anzeigen ──────────────────────
if (!player.hasPermission("ticket.support") && !player.hasPermission("ticket.admin")) return; if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().countOpenTickets();
if (count > 0) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
String msg = plugin.formatMessage("messages.join-open-tickets")
.replace("{count}", String.valueOf(count));
player.sendMessage(msg);
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht."));
}, 40L); // 2 Sekunden Verzögerung
}
});
}
// Verzögerung von 2 Sekunden damit die Join-Sequenz abgeschlossen ist // ── Spieler: über geschlossene Tickets mit Kommentar informieren ──
// Nur wenn der Ersteller noch nicht live benachrichtigt wurde
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = plugin.getDatabaseManager().countOpenTickets(); List<Ticket> closed = plugin.getDatabaseManager()
.getTicketsByStatus(TicketStatus.CLOSED);
if (count > 0) { for (Ticket t : closed) {
Bukkit.getScheduler().runTaskLater(plugin, () -> { if (!t.getCreatorUUID().equals(player.getUniqueId())) continue;
String msg = plugin.formatMessage("messages.join-open-tickets") if (t.getCloseComment() == null || t.getCloseComment().isEmpty()) continue;
.replace("{count}", String.valueOf(count));
player.sendMessage(msg); // Nicht erneut senden, wenn bereits live benachrichtigt (In-Memory-Set)
player.sendMessage(plugin.color("&7» Tippe &e/ticket list &7für die Übersicht.")); if (plugin.getTicketManager().wasClosedNotificationSent(t.getId())) continue;
}, 40L); // 40 Ticks = 2 Sekunden
Bukkit.getScheduler().runTask(plugin, () ->
plugin.getTicketManager().notifyCreatorClosed(t));
} }
}); });
// ── Update-Hinweis für OPs/Admins ────────────────────────────────
if (player.isOp() || player.hasPermission("ticket.admin")) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
int resourceId = 132757;
new de.ticketsystem.UpdateChecker(plugin, resourceId).getVersion(version -> {
String current = plugin.getDescription().getVersion();
if (!current.equals(version)) {
String bar = ChatColor.GOLD + "====================================================";
player.sendMessage(bar);
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
+ ChatColor.YELLOW + "NEUES UPDATE VERFÜGBAR: v" + version);
player.sendMessage(ChatColor.GOLD + "[TicketSystem] "
+ ChatColor.YELLOW + "Download: https://www.spigotmc.org/resources/132757");
player.sendMessage(bar);
}
});
}, 20L); // 1 Sekunde
}
} }
} }

View File

@@ -7,16 +7,21 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
public class TicketManager { public class TicketManager {
private final TicketPlugin plugin; private final TicketPlugin plugin;
// Cooldown Map: UUID → Zeit in Millis, wann das letzte Ticket erstellt wurde /** Cooldown Map: UUID → Zeitstempel letztes Ticket */
private final Map<UUID, Long> cooldowns = new HashMap<>(); private final Map<UUID, Long> cooldowns = new HashMap<>();
/** Ticket-IDs für die der Ersteller bereits über Schließung informiert wurde */
private final Set<Integer> notifiedClosedTickets = new HashSet<>();
public TicketManager(TicketPlugin plugin) { public TicketManager(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@@ -42,7 +47,8 @@ public class TicketManager {
// ─────────────────────────── Benachrichtigungen ──────────────────────── // ─────────────────────────── Benachrichtigungen ────────────────────────
/** /**
* Benachrichtigt alle Online-Supporter und Admins über ein neues Ticket. * Benachrichtigt alle Online-Supporter/Admins über ein neues Ticket
* und sendet optional eine Discord-Webhook-Nachricht.
*/ */
public void notifyTeam(Ticket ticket) { public void notifyTeam(Ticket ticket) {
String msg = plugin.formatMessage("messages.new-ticket-notify") String msg = plugin.formatMessage("messages.new-ticket-notify")
@@ -53,15 +59,16 @@ public class TicketManager {
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) { if (p.hasPermission("ticket.support") || p.hasPermission("ticket.admin")) {
p.sendMessage(msg); p.sendMessage(msg);
// Klickbaren Hinweis senden (Bukkit Chat-Component)
p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen.")); p.sendMessage(plugin.color("&7» Klicke &e/ticket list &7um die GUI zu öffnen."));
} }
} }
// Discord-Webhook (asynchron, kein Einfluss auf Server-Performance)
plugin.getDiscordWebhook().sendNewTicket(ticket);
} }
/** /**
* Benachrichtigt den Ersteller des Tickets, wenn es geclaimt wurde. * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
*/ */
public void notifyCreatorClaimed(Ticket ticket) { public void notifyCreatorClaimed(Ticket ticket) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
@@ -74,9 +81,24 @@ public class TicketManager {
} }
/** /**
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung. * Benachrichtigt den Ersteller, wenn sein Ticket weitergeleitet wurde.
*/ */
public void notifyForwardedTo(Ticket ticket) { public void notifyCreatorForwarded(Ticket ticket) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) {
String forwardedTo = ticket.getForwardedToName() != null ? ticket.getForwardedToName() : "einen Supporter";
String msg = plugin.formatMessage("messages.ticket-forwarded-creator-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{supporter}", forwardedTo);
creator.sendMessage(msg);
}
}
/**
* Sendet dem weitergeleiteten Supporter eine Benachrichtigung
* und informiert optional Discord.
*/
public void notifyForwardedTo(Ticket ticket, String fromName) {
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
if (target != null && target.isOnline()) { if (target != null && target.isOnline()) {
String msg = plugin.formatMessage("messages.ticket-forwarded-notify") String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
@@ -84,13 +106,55 @@ public class TicketManager {
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()));
target.sendMessage(msg); target.sendMessage(msg);
} }
// Discord
plugin.getDiscordWebhook().sendTicketForwarded(ticket, fromName);
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde,
* und informiert optional Discord.
*/
public void notifyCreatorClosed(Ticket ticket) {
notifyCreatorClosed(ticket, null);
}
/**
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde.
* @param closerName Name des Admins/Supporters der es geschlossen hat (für Discord, kann null sein)
*/
public void notifyCreatorClosed(Ticket ticket, String closerName) {
notifiedClosedTickets.add(ticket.getId());
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) {
String comment = (ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty())
? ticket.getCloseComment() : "";
String msg = plugin.formatMessage("messages.ticket-closed-notify")
.replace("{id}", String.valueOf(ticket.getId()))
.replace("{comment}", comment);
creator.sendMessage(msg);
if (!comment.isEmpty()) {
creator.sendMessage(plugin.color("&7Kommentar des Supports: &f" + comment));
}
}
// Discord
String closer = closerName != null ? closerName : "Unbekannt";
plugin.getDiscordWebhook().sendTicketClosed(ticket, closer);
}
/**
* Prüft ob der Ersteller für dieses Ticket bereits über die Schließung informiert wurde.
*/
public boolean wasClosedNotificationSent(int ticketId) {
return notifiedClosedTickets.contains(ticketId);
} }
// ─────────────────────────── Hilfsmethoden ───────────────────────────── // ─────────────────────────── Hilfsmethoden ─────────────────────────────
/**
* Prüft, ob ein Spieler zu viele offene Tickets hat.
*/
public boolean hasReachedTicketLimit(UUID uuid) { public boolean hasReachedTicketLimit(UUID uuid) {
int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2); int max = plugin.getConfig().getInt("max-open-tickets-per-player", 2);
if (max <= 0) return false; if (max <= 0) return false;
@@ -102,10 +166,10 @@ public class TicketManager {
player.sendMessage(plugin.color("&6TicketSystem &7 Befehle")); player.sendMessage(plugin.color("&6TicketSystem &7 Befehle"));
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&e/ticket create <Text> &7 Neues Ticket erstellen")); player.sendMessage(plugin.color("&e/ticket create <Text> &7 Neues Ticket erstellen"));
player.sendMessage(plugin.color("&e/ticket list &7 Deine Tickets ansehen (GUI)"));
if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) { if (player.hasPermission("ticket.support") || player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&e/ticket list &7 Ticket-Übersicht (GUI)"));
player.sendMessage(plugin.color("&e/ticket claim <ID> &7 Ticket annehmen")); player.sendMessage(plugin.color("&e/ticket claim <ID> &7 Ticket annehmen"));
player.sendMessage(plugin.color("&e/ticket close <ID> &7 Ticket schließen")); player.sendMessage(plugin.color("&e/ticket close <ID> [Kommentar] &7 Ticket schließen"));
} }
if (player.hasPermission("ticket.admin")) { if (player.hasPermission("ticket.admin")) {
player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7 Ticket weiterleiten")); player.sendMessage(plugin.color("&e/ticket forward <ID> <Spieler> &7 Ticket weiterleiten"));
@@ -113,4 +177,4 @@ public class TicketManager {
} }
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
} }
} }

View File

@@ -3,18 +3,25 @@ package de.ticketsystem.model;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.SerializableAs;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class Ticket {
@SerializableAs("Ticket")
public class Ticket implements ConfigurationSerializable {
private int id; private int id;
private UUID creatorUUID; private UUID creatorUUID;
private String creatorName; private String creatorName;
private String message; private String message;
// Location-Felder (werden separat gespeichert)
private String worldName; private String worldName;
private double x, y, z; private double x, y, z;
private float yaw, pitch; private float yaw, pitch;
@@ -27,9 +34,12 @@ public class Ticket {
private Timestamp createdAt; private Timestamp createdAt;
private Timestamp claimedAt; private Timestamp claimedAt;
private Timestamp closedAt; private Timestamp closedAt;
private String closeComment;
public Ticket() {} public Ticket() {}
public Ticket(UUID creatorUUID, String creatorName, String message, Location location) { public Ticket(UUID creatorUUID, String creatorName, String message, Location location) {
this.creatorUUID = creatorUUID; this.creatorUUID = creatorUUID;
this.creatorName = creatorName; this.creatorName = creatorName;
@@ -44,6 +54,101 @@ public class Ticket {
this.createdAt = new Timestamp(System.currentTimeMillis()); this.createdAt = new Timestamp(System.currentTimeMillis());
} }
// --- NEU: Konstruktor zum Laden aus der YAML (Deserialisierung) ---
public Ticket(Map<String, Object> map) {
this.id = (int) map.get("id");
// UUIDs sicher aus String konvertieren
Object creatorObj = map.get("creatorUUID");
this.creatorUUID = creatorObj instanceof UUID ? (UUID) creatorObj : UUID.fromString((String) creatorObj);
this.creatorName = (String) map.get("creatorName");
this.message = (String) map.get("message");
this.worldName = (String) map.get("world");
// Koordinaten sicher parsen
this.x = map.get("x") instanceof Double ? (Double) map.get("x") : ((Number) map.get("x")).doubleValue();
this.y = map.get("y") instanceof Double ? (Double) map.get("y") : ((Number) map.get("y")).doubleValue();
this.z = map.get("z") instanceof Double ? (Double) map.get("z") : ((Number) map.get("z")).doubleValue();
this.yaw = map.get("yaw") instanceof Float ? (Float) map.get("yaw") : ((Number) map.get("yaw")).floatValue();
this.pitch = map.get("pitch") instanceof Float ? (Float) map.get("pitch") : ((Number) map.get("pitch")).floatValue();
this.status = TicketStatus.valueOf((String) map.get("status"));
// Timestamps aus Long (Millis) wieder zu Timestamp machen
if (map.get("createdAt") != null) {
this.createdAt = new Timestamp(((Number) map.get("createdAt")).longValue());
}
if (map.get("claimedAt") != null) {
this.claimedAt = new Timestamp(((Number) map.get("claimedAt")).longValue());
}
if (map.get("closedAt") != null) {
this.closedAt = new Timestamp(((Number) map.get("closedAt")).longValue());
}
this.closeComment = (String) map.get("closeComment");
// Optionale Felder
if (map.containsKey("claimerUUID") && map.get("claimerUUID") != null) {
Object claimerObj = map.get("claimerUUID");
this.claimerUUID = claimerObj instanceof UUID ? (UUID) claimerObj : UUID.fromString((String) claimerObj);
this.claimerName = (String) map.get("claimerName");
}
if (map.containsKey("forwardedToUUID") && map.get("forwardedToUUID") != null) {
Object fwdObj = map.get("forwardedToUUID");
this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj);
this.forwardedToName = (String) map.get("forwardedToName");
}
}
// --- NEU: Methode zum Speichern in die YAML (Serialisierung) ---
@Override
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
// WICHTIG: UUID als String speichern, um !!java.util.UUID Tag zu vermeiden
map.put("creatorUUID", creatorUUID.toString());
map.put("creatorName", creatorName);
map.put("message", message);
map.put("world", worldName);
map.put("x", x);
map.put("y", y);
map.put("z", z);
map.put("yaw", yaw);
map.put("pitch", pitch);
map.put("status", status.name());
// Timestamps als Long speichern
if (createdAt != null) map.put("createdAt", createdAt.getTime());
if (claimedAt != null) map.put("claimedAt", claimedAt.getTime());
if (closedAt != null) map.put("closedAt", closedAt.getTime());
if (closeComment != null) map.put("closeComment", closeComment);
if (claimerUUID != null) {
map.put("claimerUUID", claimerUUID.toString());
map.put("claimerName", claimerName);
}
if (forwardedToUUID != null) {
map.put("forwardedToUUID", forwardedToUUID.toString());
map.put("forwardedToName", forwardedToName);
}
return map;
}
// --- NEU: Registrierung ---
public static void register() {
ConfigurationSerialization.registerClass(Ticket.class, "Ticket");
}
// --- Deine ursprüngliche getLocation Methode (beibehalten) ---
public Location getLocation() { public Location getLocation() {
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
if (world == null) return null; if (world == null) return null;
@@ -105,4 +210,7 @@ public class Ticket {
public Timestamp getClosedAt() { return closedAt; } public Timestamp getClosedAt() { return closedAt; }
public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; } public void setClosedAt(Timestamp closedAt) { this.closedAt = closedAt; }
}
public String getCloseComment() { return closeComment; }
public void setCloseComment(String closeComment) { this.closeComment = closeComment; }
}

View File

@@ -59,6 +59,16 @@ max-open-tickets-per-player: 2 # Maximale offene Tickets pro Spieler (0 = unbeg
# ---------------------------------------------------- # ----------------------------------------------------
auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus) auto-archive-interval-hours: 24 # Intervall in Stunden (0 = aus)
# ----------------------------------------------------
# DISCORD WEBHOOK (Optional)
# ----------------------------------------------------
discord:
# Auf true setzen um Discord-Benachrichtigungen zu aktivieren
enabled: false
# Webhook-URL aus Discord (Kanaleinstellungen → Integrationen → Webhook erstellen)
webhook-url: ""
# ---------------------------------------------------- # ----------------------------------------------------
# SYSTEM-NACHRICHTEN (mit &-Farbcodes) # SYSTEM-NACHRICHTEN (mit &-Farbcodes)
# ---------------------------------------------------- # ----------------------------------------------------
@@ -82,13 +92,19 @@ messages:
ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen." ticket-claimed-notify: "&aDein Ticket &e#{id} &awurde von &e{claimer} &aangenommen."
ticket-closed: "&aTicket &e#{id} &awurde geschlossen." ticket-closed: "&aTicket &e#{id} &awurde geschlossen."
ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet." ticket-forwarded: "&aTicket &e#{id} &awurde an &e{player} &aweitergeleitet."
ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen." ticket-forwarded-notify: "&eDu hast ein Ticket von &6{player} &eweitergeleitet bekommen. &7(ID: {id})"
# --- NEU: Benachrichtigungen für den Ticket-Ersteller ---
# Wird gesendet, wenn das eigene Ticket geschlossen wurde
ticket-closed-notify: "&aDein Ticket &e#{id} &awurde geschlossen."
# Wird gesendet, wenn das eigene Ticket an einen anderen Supporter weitergeleitet wurde
ticket-forwarded-creator-notify: "&eDein Ticket &6#{id} &ewurde an &b{supporter} &eweitergeleitet."
# --- FEHLER & HINWEISE --- # --- FEHLER & HINWEISE ---
no-permission: "&cDu hast keine Berechtigung!" no-permission: "&cDu hast keine Berechtigung!"
no-open-tickets: "&aAktuell gibt es keine offenen Tickets." no-open-tickets: "&aAktuell gibt es keine offenen Tickets."
join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!" join-open-tickets: "&eEs gibt noch &6{count} &eoffene Ticket(s)!"
new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message}" new-ticket-notify: "&e{player} &ahat ein neues Ticket erstellt: &7{message} &7(ID: &e{id}&7)"
already-claimed: "&cDieses Ticket wurde bereits geclaimt!" already-claimed: "&cDieses Ticket wurde bereits geclaimt!"
ticket-not-found: "&cTicket nicht gefunden!" ticket-not-found: "&cTicket nicht gefunden!"
cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst." cooldown: "&cBitte warte &e{seconds} Sekunden &cbevor du ein neues Ticket erstellst."

View File

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