Update from Git Manager GUI

This commit is contained in:
2026-02-20 18:38:42 +01:00
parent d14646c5ae
commit 535b0aa2f3
5 changed files with 375 additions and 187 deletions

View File

@@ -177,7 +177,6 @@ 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,
@@ -198,7 +197,8 @@ public class DatabaseManager {
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
claimed_at TIMESTAMP NULL, claimed_at TIMESTAMP NULL,
closed_at TIMESTAMP NULL, closed_at TIMESTAMP NULL,
close_comment VARCHAR(500) NULL close_comment VARCHAR(500) NULL,
player_deleted BOOLEAN DEFAULT FALSE
) 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()) {
@@ -210,11 +210,9 @@ public class DatabaseManager {
/** /**
* Ergänzt fehlende Spalten in bestehenden Datenbanken automatisch. * 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() { private void ensureColumns() {
// close_comment hinzufügen, falls nicht vorhanden // close_comment hinzufügen
String checkSql = """ String checkSql = """
SELECT COUNT(*) FROM information_schema.COLUMNS SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() WHERE TABLE_SCHEMA = DATABASE()
@@ -225,20 +223,34 @@ public class DatabaseManager {
Statement stmt = conn.createStatement()) { Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(checkSql); ResultSet rs = stmt.executeQuery(checkSql);
if (rs.next() && rs.getInt(1) == 0) { if (rs.next() && rs.getInt(1) == 0) {
// Spalte existiert nicht → hinzufügen
stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL"); stmt.execute("ALTER TABLE tickets ADD COLUMN close_comment VARCHAR(500) NULL");
plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt."); plugin.getLogger().info("[TicketSystem] Spalte 'close_comment' wurde zur Datenbank hinzugefügt.");
} }
} catch (SQLException e) { } catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e); plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(): " + e.getMessage(), e);
} }
// player_deleted Spalte prüfen
String checkSqlDel = """
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tickets'
AND COLUMN_NAME = 'player_deleted'
""";
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(checkSqlDel);
if (rs.next() && rs.getInt(1) == 0) {
stmt.execute("ALTER TABLE tickets ADD COLUMN player_deleted BOOLEAN DEFAULT FALSE");
plugin.getLogger().info("[TicketSystem] Spalte 'player_deleted' wurde hinzugefügt.");
}
} catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler bei ensureColumns(player_deleted): " + e.getMessage(), e);
}
} }
// ─────────────────────────── CRUD ────────────────────────────────────── // ─────────────────────────── CRUD ──────────────────────────────────────
/**
* Speichert ein neues Ticket in der DB und gibt die generierte ID zurück.
*/
public int createTicket(Ticket ticket) { public int createTicket(Ticket ticket) {
if (useMySQL) { if (useMySQL) {
String sql = """ String sql = """
@@ -282,13 +294,14 @@ public class DatabaseManager {
} }
} }
/** // ─── FIX: player_deleted wird beim Claimen zurückgesetzt, damit der Spieler
* Claimt ein Ticket (Status → CLAIMED). // sein Ticket wieder sieht, sobald ein Supporter es annimmt. ───────
*/
public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) { public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) {
if (useMySQL) { if (useMySQL) {
String sql = """ String sql = """
UPDATE tickets SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?, claimed_at = NOW() UPDATE tickets
SET status = 'CLAIMED', claimer_uuid = ?, claimer_name = ?,
claimed_at = NOW(), player_deleted = FALSE
WHERE id = ? AND status = 'OPEN' WHERE id = ? AND status = 'OPEN'
"""; """;
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
@@ -307,6 +320,7 @@ public class DatabaseManager {
t.setClaimerUUID(claimerUUID); t.setClaimerUUID(claimerUUID);
t.setClaimerName(claimerName); t.setClaimerName(claimerName);
t.setClaimedAt(new Timestamp(System.currentTimeMillis())); t.setClaimedAt(new Timestamp(System.currentTimeMillis()));
t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen
dataConfig.set("tickets." + ticketId, t); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); dataConfig.save(dataFile);
@@ -318,9 +332,6 @@ public class DatabaseManager {
} }
} }
/**
* Schließt ein Ticket (Status → CLOSED).
*/
public boolean closeTicket(int ticketId, String closeComment) { public boolean closeTicket(int ticketId, String closeComment) {
if (useMySQL) { if (useMySQL) {
String sql = """ String sql = """
@@ -352,9 +363,36 @@ public class DatabaseManager {
} }
} }
/** // ─── Soft Delete Methode ────────────────────────────────────────────────
* Löscht ein Ticket anhand der ID. public boolean markAsPlayerDeleted(int id) {
*/ if (useMySQL) {
String sql = "UPDATE tickets SET player_deleted = TRUE WHERE id = ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Markieren als gelöscht: " + e.getMessage(), e);
}
return false;
} else {
if (dataConfig.contains("tickets." + id)) {
Ticket t = (Ticket) dataConfig.get("tickets." + id);
if (t != null) {
t.setPlayerDeleted(true);
dataConfig.set("tickets." + id, t);
try {
dataConfig.save(dataFile);
backupDataFile();
return true;
} catch (IOException e) {
plugin.getLogger().severe("Fehler beim Speichern (Soft Delete): " + e.getMessage());
}
}
}
return false;
}
}
public boolean deleteTicket(int id) { public boolean deleteTicket(int id) {
if (useMySQL) { if (useMySQL) {
String sql = "DELETE FROM tickets WHERE id = ?"; String sql = "DELETE FROM tickets WHERE id = ?";
@@ -384,13 +422,14 @@ public class DatabaseManager {
} }
} }
/** // ─── FIX: player_deleted wird beim Weiterleiten zurückgesetzt, damit der
* Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). // Spieler sein Ticket wieder sieht, sobald es weitergeleitet wird. ──
*/
public boolean forwardTicket(int ticketId, UUID toUUID, String toName) { public boolean forwardTicket(int ticketId, UUID toUUID, String toName) {
if (useMySQL) { if (useMySQL) {
String sql = """ String sql = """
UPDATE tickets SET status = 'FORWARDED', forwarded_to_uuid = ?, forwarded_to_name = ? UPDATE tickets
SET status = 'FORWARDED', forwarded_to_uuid = ?, forwarded_to_name = ?,
player_deleted = FALSE
WHERE id = ? AND status != 'CLOSED' WHERE id = ? AND status != 'CLOSED'
"""; """;
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
@@ -408,6 +447,7 @@ public class DatabaseManager {
t.setStatus(TicketStatus.FORWARDED); t.setStatus(TicketStatus.FORWARDED);
t.setForwardedToUUID(toUUID); t.setForwardedToUUID(toUUID);
t.setForwardedToName(toName); t.setForwardedToName(toName);
t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen
dataConfig.set("tickets." + ticketId, t); dataConfig.set("tickets." + ticketId, t);
try { try {
dataConfig.save(dataFile); dataConfig.save(dataFile);
@@ -421,9 +461,6 @@ public class DatabaseManager {
// ─────────────────────────── Abfragen ────────────────────────────────── // ─────────────────────────── Abfragen ──────────────────────────────────
/**
* Gibt alle Tickets mit einem bestimmten Status zurück.
*/
public List<Ticket> getTicketsByStatus(TicketStatus... statuses) { public List<Ticket> getTicketsByStatus(TicketStatus... statuses) {
List<Ticket> list = new ArrayList<>(); List<Ticket> list = new ArrayList<>();
if (statuses.length == 0) return list; if (statuses.length == 0) return list;
@@ -452,9 +489,6 @@ public class DatabaseManager {
} }
} }
/**
* Gibt alle Tickets zurück (alle Status).
*/
public List<Ticket> getAllTickets() { public List<Ticket> getAllTickets() {
List<Ticket> list = new ArrayList<>(); List<Ticket> list = new ArrayList<>();
if (useMySQL) { if (useMySQL) {
@@ -475,9 +509,6 @@ public class DatabaseManager {
return list; return list;
} }
/**
* Gibt ein einzelnes Ticket anhand der ID zurück.
*/
public Ticket getTicketById(int id) { public Ticket getTicketById(int id) {
if (useMySQL) { if (useMySQL) {
String sql = "SELECT * FROM tickets WHERE id = ?"; String sql = "SELECT * FROM tickets WHERE id = ?";
@@ -497,9 +528,6 @@ public class DatabaseManager {
} }
} }
/**
* 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 = 'OPEN'"; String sql = "SELECT COUNT(*) FROM tickets WHERE status = 'OPEN'";
@@ -522,9 +550,6 @@ public class DatabaseManager {
} }
} }
/**
* Anzahl offener Tickets eines bestimmten Spielers.
*/
public int countOpenTicketsByPlayer(UUID uuid) { public int countOpenTicketsByPlayer(UUID uuid) {
if (useMySQL) { if (useMySQL) {
String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')"; String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')";
@@ -553,9 +578,6 @@ public class DatabaseManager {
// ─────────────────────────── Archivierung ────────────────────────────── // ─────────────────────────── Archivierung ──────────────────────────────
/**
* Archiviert alle geschlossenen Tickets in eine separate Datei.
*/
public int archiveClosedTickets() { public int archiveClosedTickets() {
List<Ticket> all = getAllTickets(); List<Ticket> all = getAllTickets();
List<Ticket> toArchive = new ArrayList<>(); List<Ticket> toArchive = new ArrayList<>();
@@ -705,11 +727,6 @@ public class DatabaseManager {
// ─────────────────────────── 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 {
Ticket t = new Ticket(); Ticket t = new Ticket();
t.setId(rs.getInt("id")); t.setId(rs.getInt("id"));
@@ -727,15 +744,10 @@ 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 { try {
String closeComment = rs.getString("close_comment"); String closeComment = rs.getString("close_comment");
if (closeComment != null) t.setCloseComment(closeComment); if (closeComment != null) t.setCloseComment(closeComment);
} catch (SQLException ignored) { } 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) {
@@ -747,6 +759,10 @@ public class DatabaseManager {
t.setForwardedToUUID(UUID.fromString(fwdUUID)); t.setForwardedToUUID(UUID.fromString(fwdUUID));
t.setForwardedToName(rs.getString("forwarded_to_name")); t.setForwardedToName(rs.getString("forwarded_to_name"));
} }
// Mapping des Soft Delete Flags
t.setPlayerDeleted(rs.getBoolean("player_deleted"));
return t; return t;
} }
@@ -773,6 +789,7 @@ public class DatabaseManager {
if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString()); if (t.getForwardedToUUID() != null) obj.put("forwardedToUUID", t.getForwardedToUUID().toString());
if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName()); if (t.getForwardedToName() != null) obj.put("forwardedToName", t.getForwardedToName());
if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment()); if (t.getCloseComment() != null) obj.put("closeComment", t.getCloseComment());
obj.put("playerDeleted", t.isPlayerDeleted());
return obj; return obj;
} }
@@ -798,6 +815,7 @@ public class DatabaseManager {
if (obj.get("forwardedToUUID") != null) t.setForwardedToUUID(UUID.fromString((String) obj.get("forwardedToUUID"))); 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("forwardedToName") != null) t.setForwardedToName((String) obj.get("forwardedToName"));
if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment")); if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment"));
if (obj.containsKey("playerDeleted")) t.setPlayerDeleted((Boolean) obj.get("playerDeleted"));
return t; return t;
} catch (Exception e) { } catch (Exception e) {
if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage());

View File

@@ -18,8 +18,10 @@ import org.bukkit.inventory.meta.ItemMeta;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
public class TicketGUI implements Listener { public class TicketGUI implements Listener {
@@ -27,9 +29,13 @@ public class TicketGUI implements Listener {
// ─────────────────────────── Titel-Konstanten ────────────────────────── // ─────────────────────────── Titel-Konstanten ──────────────────────────
private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht private static final String GUI_TITLE = "§8§lTicket-Übersicht"; // Admin/Supporter Übersicht
private static final String CLOSED_GUI_TITLE = "§8§lTicket-Archiv"; // Admin: Geschlossene Tickets
private static final String PLAYER_GUI_TITLE = "§8§lMeine Tickets"; // Spieler: eigene Tickets 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 String DETAIL_GUI_TITLE = "§8§lTicket-Details"; // Admin: Detail-Ansicht
/** Permission für den Zugriff auf das Archiv */
private static final String ARCHIVE_PERMISSION = "ticket.archive";
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;
@@ -37,6 +43,9 @@ public class TicketGUI implements Listener {
/** Admin-Übersicht: Slot → Ticket */ /** Admin-Übersicht: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerSlotMap = new HashMap<>();
/** Admin-Archiv: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerClosedSlotMap = new HashMap<>();
/** Spieler-GUI: Slot → Ticket */ /** Spieler-GUI: Slot → Ticket */
private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>(); private final Map<UUID, Map<Integer, Ticket>> playerOwnSlotMap = new HashMap<>();
@@ -46,40 +55,76 @@ public class TicketGUI implements Listener {
/** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */ /** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */
private final Map<UUID, Integer> awaitingComment = new HashMap<>(); private final Map<UUID, Integer> awaitingComment = new HashMap<>();
/**
* Merkt, welche Spieler die Detail-Ansicht aus dem Archiv heraus geöffnet haben,
* damit der Zurück-Button wieder ins Archiv führt (statt in die Hauptübersicht).
*/
private final Set<UUID> viewingFromArchive = new HashSet<>();
public TicketGUI(TicketPlugin plugin) { public TicketGUI(TicketPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ADMIN / SUPPORTER GUI (Übersicht aller Tickets) // ADMIN / SUPPORTER GUI (Feste 54 Slots mit Archiv-Button)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openGUI(Player player) { public void openGUI(Player player) {
// Lade nur offene/aktive Tickets
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus( List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(
TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED);
if (tickets.isEmpty()) { // Admin GUI hat immer 54 Slots (6 Reihen) für feste Buttons
player.sendMessage(plugin.formatMessage("messages.no-open-tickets")); Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE);
return;
}
int size = calcSize(tickets.size());
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++) { // Tickets in die ersten 5 Reihen (0-44) füllen
for (int i = 0; i < tickets.size() && i < 45; i++) {
Ticket ticket = tickets.get(i); Ticket ticket = tickets.get(i);
inv.setItem(i, buildAdminListItem(ticket)); inv.setItem(i, buildAdminListItem(ticket));
slotMap.put(i, ticket); slotMap.put(i, ticket);
} }
fillEmpty(inv); // Letzte Reihe (45-53) mit Navigations-Items füllen
// Archiv-Button nur anzeigen wenn der Spieler die Archiv-Permission hat
fillAdminNavigation(inv, false, player);
playerSlotMap.put(player.getUniqueId(), slotMap); playerSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv); player.openInventory(inv);
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// SPIELER-GUI (nur eigene Tickets, mit Lösch-Option bei OPEN) // ADMIN ARCHIV GUI (Geschlossene Tickets) nur mit ticket.archive
// ═══════════════════════════════════════════════════════════════════════
public void openClosedGUI(Player player) {
// ── Permission-Check ──────────────────────────────────────────────
if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
return;
}
// Lade nur geschlossene Tickets
List<Ticket> tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED);
Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE);
Map<Integer, Ticket> slotMap = new HashMap<>();
for (int i = 0; i < tickets.size() && i < 45; i++) {
Ticket ticket = tickets.get(i);
inv.setItem(i, buildAdminListItem(ticket));
slotMap.put(i, ticket);
}
// Navigation (Zurück-Button statt Archiv-Button)
fillAdminNavigation(inv, true, player);
playerClosedSlotMap.put(player.getUniqueId(), slotMap);
player.openInventory(inv);
}
// ═══════════════════════════════════════════════════════════════════════
// SPIELER-GUI (Filtert 'playerDeleted' Tickets)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openPlayerGUI(Player player) { public void openPlayerGUI(Player player) {
@@ -88,7 +133,10 @@ public class TicketGUI implements Listener {
List<Ticket> tickets = new ArrayList<>(); List<Ticket> tickets = new ArrayList<>();
for (Ticket t : all) { for (Ticket t : all) {
if (t.getCreatorUUID().equals(player.getUniqueId())) tickets.add(t); // Verstecke Tickets, die der Spieler als gelöscht markiert hat
if (t.getCreatorUUID().equals(player.getUniqueId()) && !t.isPlayerDeleted()) {
tickets.add(t);
}
} }
if (tickets.isEmpty()) { if (tickets.isEmpty()) {
@@ -112,27 +160,37 @@ public class TicketGUI implements Listener {
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ADMIN DETAIL-GUI (Aktionen für ein einzelnes Ticket) // ADMIN DETAIL-GUI
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
public void openDetailGUI(Player player, Ticket ticket) { public void openDetailGUI(Player player, Ticket ticket) {
Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE); Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE);
// Slot 4: Ticket-Info (Mitte oben) // Slot 4: Ticket-Info
inv.setItem(4, buildDetailInfoItem(ticket)); inv.setItem(4, buildDetailInfoItem(ticket));
// Slot 10: Teleportieren (immer verfügbar) // Slot 10: Teleportieren
inv.setItem(10, buildActionItem( inv.setItem(10, buildActionItem(
Material.ENDER_PEARL, Material.ENDER_PEARL,
"§b§lTeleportieren", "§b§lTeleportieren",
List.of("§7Teleportiert dich zur", "§7Position des Tickets."))); List.of("§7Teleportiert dich zur", "§7Position des Tickets.")));
// Slot 12: Claimen (nur wenn OPEN), sonst grauer Platzhalter // Slot 12: Claimen (nur wenn OPEN) / Permanent löschen (wenn CLOSED + ticket.archive) / Grau
if (ticket.getStatus() == TicketStatus.OPEN) { if (ticket.getStatus() == TicketStatus.OPEN) {
inv.setItem(12, buildActionItem( inv.setItem(12, buildActionItem(
Material.LIME_WOOL, Material.LIME_WOOL,
"§a§lTicket annehmen", "§a§lTicket annehmen",
List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet."))); List.of("§7Nimmt dieses Ticket an", "§7und markiert es als bearbeitet.")));
} else if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) {
// ── NEU: Löschen-Button nur für Archiv-berechtigte Spieler ──
inv.setItem(12, buildActionItem(
Material.BARRIER,
"§4§lTicket permanent löschen",
List.of(
"§7Löscht dieses Ticket",
"§7unwiderruflich aus der Datenbank.",
"§8§m ",
"§c§lACHTUNG: §cNicht rückgängig zu machen!")));
} else { } else {
inv.setItem(12, buildActionItem( inv.setItem(12, buildActionItem(
Material.GRAY_WOOL, Material.GRAY_WOOL,
@@ -140,18 +198,12 @@ public class TicketGUI implements Listener {
List.of("§7Dieses Ticket wurde bereits", "§7angenommen."))); List.of("§7Dieses Ticket wurde bereits", "§7angenommen.")));
} }
// Slot 14: Schließen — für OPEN, CLAIMED und FORWARDED; grauer Block wenn bereits CLOSED // Slot 14: Schließen
if (ticket.getStatus() != TicketStatus.CLOSED) { if (ticket.getStatus() != TicketStatus.CLOSED) {
inv.setItem(14, buildActionItem( inv.setItem(14, buildActionItem(
Material.RED_WOOL, Material.RED_WOOL,
"§c§lTicket schließen", "§c§lTicket schließen",
List.of( List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe.")));
"§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 { } else {
inv.setItem(14, buildActionItem( inv.setItem(14, buildActionItem(
Material.GRAY_WOOL, Material.GRAY_WOOL,
@@ -159,7 +211,7 @@ public class TicketGUI implements Listener {
List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); List.of("§7Dieses Ticket ist bereits", "§7geschlossen.")));
} }
// Slot 16: Zurück zur Übersicht // Slot 16: Zurück
inv.setItem(16, buildActionItem( inv.setItem(16, buildActionItem(
Material.ARROW, Material.ARROW,
"§7§lZurück", "§7§lZurück",
@@ -179,36 +231,58 @@ public class TicketGUI implements Listener {
if (!(event.getWhoClicked() instanceof Player player)) return; if (!(event.getWhoClicked() instanceof Player player)) return;
String title = event.getView().getTitle(); String title = event.getView().getTitle();
if (!title.equals(GUI_TITLE) && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return; if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_GUI_TITLE) && !title.equals(PLAYER_GUI_TITLE) && !title.equals(DETAIL_GUI_TITLE)) return;
event.setCancelled(true); event.setCancelled(true);
int slot = event.getRawSlot(); int slot = event.getRawSlot();
if (slot < 0) return; if (slot < 0) return;
// ── Admin-Übersicht ────────────────────────────────────────────── // ── Admin Haupt-Übersicht ──────────────────────────────────────────────
if (title.equals(GUI_TITLE)) { if (title.equals(GUI_TITLE)) {
// Klick auf die Truhe (Archiv-Button) in Slot 49
if (slot == 49) {
// ── Permission-Check beim Klick ──
if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, das Archiv zu öffnen."));
return;
}
openClosedGUI(player);
return;
}
// Klick auf ein Ticket
Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId()); Map<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId());
if (slotMap == null) return; if (slotMap == null) return;
Ticket ticket = slotMap.get(slot); Ticket ticket = slotMap.get(slot);
if (ticket == null) return; if (ticket != null) {
viewingFromArchive.remove(player.getUniqueId()); // Kommt aus Hauptübersicht
player.closeInventory(); player.closeInventory();
openTicketDetailAsync(player, ticket);
// 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; return;
} }
// ── Spieler-GUI ────────────────────────────────────────────────── // ── Admin Archiv (Geschlossene Tickets) ─────────────────────────────────
if (title.equals(CLOSED_GUI_TITLE)) {
// Klick auf den Zurück-Pfeil (Slot 49)
if (slot == 49) {
openGUI(player);
return;
}
// Klick auf ein Ticket
Map<Integer, Ticket> slotMap = playerClosedSlotMap.get(player.getUniqueId());
if (slotMap == null) return;
Ticket ticket = slotMap.get(slot);
if (ticket != null) {
viewingFromArchive.add(player.getUniqueId()); // Kommt aus Archiv
player.closeInventory();
openTicketDetailAsync(player, ticket);
}
return;
}
// ── Spieler-GUI ──────────────────────────────────────────────────────
if (title.equals(PLAYER_GUI_TITLE)) { if (title.equals(PLAYER_GUI_TITLE)) {
Map<Integer, Ticket> slotMap = playerOwnSlotMap.get(player.getUniqueId()); Map<Integer, Ticket> slotMap = playerOwnSlotMap.get(player.getUniqueId());
if (slotMap == null) return; if (slotMap == null) return;
@@ -217,28 +291,26 @@ public class TicketGUI implements Listener {
player.closeInventory(); player.closeInventory();
if (ticket.getStatus() == TicketStatus.OPEN) { // Nur löschen wenn OFFEN oder GESCHLOSSEN
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) {
boolean deleted = plugin.getDatabaseManager().deleteTicket(ticket.getId()); boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (deleted) { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.color( if (success) {
"&aDein Ticket &e#" + ticket.getId() + " &awurde gelöscht.")); player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt."));
openPlayerGUI(player); openPlayerGUI(player);
} else { } else {
player.sendMessage(plugin.color("&cFehler beim Löschen des Tickets.")); player.sendMessage(plugin.color("&cFehler beim Entfernen des Tickets."));
} }
});
}); });
} else { } else {
player.sendMessage(plugin.color( // Ticket wird bearbeitet (Claimed oder Forwarded) -> Löschen verweigern
"&cDieses Ticket kann nicht mehr gelöscht werden, " + player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird."));
"da es bereits angenommen oder geschlossen wurde."));
} }
return; return;
} }
// ── Admin Detail-GUI ───────────────────────────────────────────── // ── Admin Detail-GUI ─────────────────────────────────────────────────
if (title.equals(DETAIL_GUI_TITLE)) { if (title.equals(DETAIL_GUI_TITLE)) {
Ticket ticket = detailTicketMap.get(player.getUniqueId()); Ticket ticket = detailTicketMap.get(player.getUniqueId());
if (ticket == null) return; if (ticket == null) return;
@@ -247,21 +319,46 @@ public class TicketGUI implements Listener {
switch (slot) { switch (slot) {
case 10 -> handleDetailTeleport(player, ticket); case 10 -> handleDetailTeleport(player, ticket);
case 12 -> handleDetailClaim(player, ticket); case 12 -> {
// Wenn CLOSED + archive-Permission → permanent löschen, sonst claimen
if (ticket.getStatus() == TicketStatus.CLOSED && player.hasPermission(ARCHIVE_PERMISSION)) {
handleDetailPermanentDelete(player, ticket);
} else {
handleDetailClaim(player, ticket);
}
}
case 14 -> handleDetailClose(player, ticket); case 14 -> handleDetailClose(player, ticket);
case 16 -> openGUI(player); case 16 -> {
// Glasscheiben und andere Slots → nichts tun // Zurück zur richtigen GUI je nach Herkunft
if (viewingFromArchive.remove(player.getUniqueId())) {
openClosedGUI(player);
} else {
openGUI(player);
}
}
} }
} }
} }
// ─────────────────────────── Detail-Aktionen ─────────────────────────── // ─────────────────────────── Detail-Aktionen & Helpers ──────────────────
private void openTicketDetailAsync(Player player, Ticket currentTicket) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(currentTicket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (fresh == null) {
player.sendMessage(plugin.formatMessage("messages.ticket-not-found"));
return;
}
openDetailGUI(player, fresh);
});
});
}
private void handleDetailTeleport(Player player, Ticket ticket) { private void handleDetailTeleport(Player player, Ticket ticket) {
if (ticket.getLocation() != null) { if (ticket.getLocation() != null) {
player.teleport(ticket.getLocation()); player.teleport(ticket.getLocation());
player.sendMessage(plugin.color( player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
"&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert."));
} else { } else {
player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!")); player.sendMessage(plugin.color("&cDie Welt des Tickets ist nicht geladen!"));
} }
@@ -272,32 +369,26 @@ public class TicketGUI implements Listener {
player.sendMessage(plugin.formatMessage("messages.already-claimed")); player.sendMessage(plugin.formatMessage("messages.already-claimed"));
return; return;
} }
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().claimTicket( boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName());
ticket.getId(), player.getUniqueId(), player.getName());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (success) { if (success) {
player.sendMessage(plugin.formatMessage("messages.ticket-claimed") player.sendMessage(plugin.formatMessage("messages.ticket-claimed")
.replace("{id}", String.valueOf(ticket.getId())) .replace("{id}", String.valueOf(ticket.getId()))
.replace("{player}", ticket.getCreatorName())); .replace("{player}", ticket.getCreatorName()));
plugin.getTicketManager().notifyCreatorClaimed(ticket); ticket.setClaimerUUID(player.getUniqueId());
ticket.setClaimerName(player.getName());
plugin.getTicketManager().notifyCreatorClaimed(ticket);
if (ticket.getLocation() != null) player.teleport(ticket.getLocation()); 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, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId()); Ticket fresh = plugin.getDatabaseManager().getTicketById(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
if (fresh != null) openDetailGUI(player, fresh); if (fresh != null) openDetailGUI(player, fresh);
}); });
}); });
} else { } else {
player.sendMessage(plugin.formatMessage("messages.already-claimed")); player.sendMessage(plugin.formatMessage("messages.already-claimed"));
} }
@@ -305,76 +396,125 @@ public class TicketGUI implements Listener {
}); });
} }
/**
* Löscht ein geschlossenes Ticket permanent aus der Datenbank.
* Nur für Spieler mit der Permission ticket.archive.
*/
private void handleDetailPermanentDelete(Player player, Ticket ticket) {
if (!player.hasPermission(ARCHIVE_PERMISSION)) {
player.sendMessage(plugin.color("&cDu hast keine Berechtigung, Tickets permanent zu löschen."));
return;
}
if (ticket.getStatus() != TicketStatus.CLOSED) {
player.sendMessage(plugin.color("&cNur geschlossene Tickets können permanent gelöscht werden."));
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().deleteTicket(ticket.getId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (success) {
player.sendMessage(plugin.color(
"&aTicket &e#" + ticket.getId() + " &awurde permanent aus der Datenbank gelöscht."));
viewingFromArchive.remove(player.getUniqueId());
openClosedGUI(player);
} else {
player.sendMessage(plugin.color("&cFehler beim Löschen des Tickets."));
openClosedGUI(player);
}
});
});
}
private void handleDetailClose(Player player, Ticket ticket) { private void handleDetailClose(Player player, Ticket ticket) {
if (ticket.getStatus() == TicketStatus.CLOSED) { if (ticket.getStatus() == TicketStatus.CLOSED) {
player.sendMessage(plugin.color("&cDieses Ticket ist bereits geschlossen.")); player.sendMessage(plugin.color("&cDieses Ticket ist bereits geschlossen."));
return; return;
} }
awaitingComment.put(player.getUniqueId(), ticket.getId()); awaitingComment.put(player.getUniqueId(), ticket.getId());
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
player.sendMessage(plugin.color("&6Ticket #" + ticket.getId() + " schließen")); 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("&7Gib einen Kommentar ein (&e- &7für keinen)."));
player.sendMessage(plugin.color("&7Kein Kommentar? Tippe: &e-")); player.sendMessage(plugin.color("&7Abbrechen mit &ccancel"));
player.sendMessage(plugin.color("&7Abbrechen? Tippe: &ccancel"));
player.sendMessage(plugin.color("&8&m ")); player.sendMessage(plugin.color("&8&m "));
} }
// ─────────────────────────── Chat-Listener (Kommentar-Eingabe) ─────────
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerChat(AsyncPlayerChatEvent event) { public void onPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
if (!awaitingComment.containsKey(player.getUniqueId())) return; if (!awaitingComment.containsKey(player.getUniqueId())) return;
event.setCancelled(true); event.setCancelled(true);
int ticketId = awaitingComment.remove(player.getUniqueId()); int ticketId = awaitingComment.remove(player.getUniqueId());
String input = event.getMessage().trim(); String input = event.getMessage().trim();
if (input.equalsIgnoreCase("cancel")) { if (input.equalsIgnoreCase("cancel")) {
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cAbgebrochen.")));
player.sendMessage(plugin.color("&cSchließen abgebrochen.")));
return; return;
} }
// "-" = bewusst kein Kommentar
final String comment = input.equals("-") ? "" : input; final String comment = input.equals("-") ? "" : input;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment); boolean success = plugin.getDatabaseManager().closeTicket(ticketId, comment);
if (success) { if (success) {
Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId); Ticket ticket = plugin.getDatabaseManager().getTicketById(ticketId);
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(plugin.formatMessage("messages.ticket-closed") player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId)));
.replace("{id}", String.valueOf(ticketId))); if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment));
if (!comment.isEmpty()) {
player.sendMessage(plugin.color("&7Kommentar gespeichert: &f" + comment));
}
if (ticket != null) { if (ticket != null) {
ticket.setCloseComment(comment); ticket.setCloseComment(comment);
plugin.getTicketManager().notifyCreatorClosed(ticket); plugin.getTicketManager().notifyCreatorClosed(ticket, player.getName());
} }
}); });
} else {
Bukkit.getScheduler().runTask(plugin, () ->
player.sendMessage(plugin.formatMessage("messages.ticket-not-found")));
} }
}); });
} }
// ═══════════════════════════════════════════════════════════════════════ // ─────────────────────────── Item-Builder & Füll-Methoden ─────────────
// ITEM-BUILDER
// ═══════════════════════════════════════════════════════════════════════ /**
* Füllt die Navigationsleiste (letzte Reihe) der Admin-GUIs.
* Der Archiv-Button (Truhe) wird nur angezeigt, wenn der Spieler ticket.archive besitzt.
*/
private void fillAdminNavigation(Inventory inv, boolean isArchiveView, Player player) {
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = glass.getItemMeta();
if (meta != null) {
meta.setDisplayName(" ");
glass.setItemMeta(meta);
}
// Letzte Reihe (45-53) füllen
for (int i = 45; i < 54; i++) {
if (i != 49) inv.setItem(i, glass);
}
if (isArchiveView) {
// Im Archiv: Zurück-Pfeil in Slot 49
inv.setItem(49, buildActionItem(
Material.ARROW,
"§7§lZurück zur Übersicht",
List.of("§7Zeigt alle offenen Tickets.")));
} else {
// In der Übersicht: Archiv-Truhe nur mit Permission
if (player.hasPermission(ARCHIVE_PERMISSION)) {
inv.setItem(49, buildActionItem(
Material.CHEST,
"§7§lGeschlossene Tickets",
List.of("§7Zeigt alle abgeschlossenen", "§7Tickets im Archiv an.")));
} else {
// Kein Archiv-Zugriff → Slot 49 bleibt Glas (kein Button)
inv.setItem(49, glass);
}
}
}
private ItemStack buildAdminListItem(Ticket ticket) { private ItemStack buildAdminListItem(Ticket ticket) {
Material mat = switch (ticket.getStatus()) { Material mat = switch (ticket.getStatus()) {
case OPEN -> Material.PAPER; case OPEN -> Material.PAPER;
case CLAIMED -> Material.YELLOW_DYE; case CLAIMED -> Material.YELLOW_DYE;
case FORWARDED -> Material.ORANGE_DYE; case FORWARDED -> Material.ORANGE_DYE;
default -> Material.PAPER; case CLOSED -> Material.GRAY_DYE;
}; };
ItemStack item = new ItemStack(mat); ItemStack item = new ItemStack(mat);
@@ -382,20 +522,19 @@ public class TicketGUI implements Listener {
if (meta == null) return item; if (meta == null) return item;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Ersteller: §e" + ticket.getCreatorName()); lore.add("§7Ersteller: §e" + ticket.getCreatorName());
lore.add("§7Anliegen: §f" + ticket.getMessage()); lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName()); if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); lore.add("§7Kommentar: §f" + ticket.getCloseComment());
if (ticket.getClaimerName() != null) }
lore.add("§7Angenommen: §a" + ticket.getClaimerName()); if (ticket.isPlayerDeleted()) {
if (ticket.getForwardedToName() != null) lore.add("§cSpieler hat Ticket gelöscht.");
lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); }
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§e§l» KLICKEN für Details & Aktionen"); lore.add("§e§l» KLICKEN für Details");
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); item.setItemMeta(meta);
@@ -415,7 +554,6 @@ public class TicketGUI implements Listener {
if (meta == null) return item; if (meta == null) return item;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Ersteller: §e" + ticket.getCreatorName()); lore.add("§7Ersteller: §e" + ticket.getCreatorName());
@@ -429,8 +567,6 @@ public class TicketGUI implements Listener {
if (ticket.getClaimedAt() != null) if (ticket.getClaimedAt() != null)
lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt())); lore.add("§7Angenommen am: §a" + DATE_FORMAT.format(ticket.getClaimedAt()));
} }
if (ticket.getForwardedToName() != null)
lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName());
if (ticket.getStatus() == TicketStatus.CLOSED) { if (ticket.getStatus() == TicketStatus.CLOSED) {
if (ticket.getClosedAt() != null) if (ticket.getClosedAt() != null)
lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt())); lore.add("§7Geschlossen am: §c" + DATE_FORMAT.format(ticket.getClosedAt()));
@@ -438,7 +574,6 @@ public class TicketGUI implements Listener {
lore.add("§7Kommentar: §f" + ticket.getCloseComment()); lore.add("§7Kommentar: §f" + ticket.getCloseComment());
} }
lore.add("§8§m "); lore.add("§8§m ");
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); item.setItemMeta(meta);
return item; return item;
@@ -457,17 +592,12 @@ public class TicketGUI implements Listener {
if (meta == null) return item; if (meta == null) return item;
meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored());
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§8§m "); lore.add("§8§m ");
lore.add("§7Anliegen: §f" + ticket.getMessage()); lore.add("§7Anliegen: §f" + ticket.getMessage());
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt())); lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
lore.add("§7Welt: §e" + ticket.getWorldName()); lore.add("§7Welt: §e" + ticket.getWorldName());
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ())); lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
if (ticket.getStatus() == TicketStatus.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 if (ticket.getStatus() == TicketStatus.CLOSED
&& ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
lore.add("§8§m "); lore.add("§8§m ");
@@ -475,14 +605,17 @@ public class TicketGUI implements Listener {
lore.add("§f" + ticket.getCloseComment()); lore.add("§f" + ticket.getCloseComment());
} }
lore.add("§8§m "); 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."); }
}
switch (ticket.getStatus()) {
case OPEN, CLOSED -> {
lore.add("§c§l» KLICKEN zum Löschen");
lore.add("§7Entferne dieses Ticket aus deiner Übersicht.");
}
default -> {
lore.add("§e» Ticket wird bearbeitet...");
lore.add("§7Kann nicht mehr gelöscht werden.");
}
}
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); item.setItemMeta(meta);
return item; return item;
@@ -498,8 +631,6 @@ public class TicketGUI implements Listener {
return item; return item;
} }
// ─────────────────────────── Hilfsmethoden ─────────────────────────────
private int calcSize(int ticketCount) { private int calcSize(int ticketCount) {
int size = (int) Math.ceil(ticketCount / 9.0) * 9; int size = (int) Math.ceil(ticketCount / 9.0) * 9;
return Math.max(9, Math.min(54, size)); return Math.max(9, Math.min(54, size));

View File

@@ -51,9 +51,13 @@ public class TicketManager {
* und sendet optional eine Discord-Webhook-Nachricht. * und sendet optional eine Discord-Webhook-Nachricht.
*/ */
public void notifyTeam(Ticket ticket) { public void notifyTeam(Ticket ticket) {
// Sicherheitschecks für null-Werte
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String message = ticket.getMessage() != null ? ticket.getMessage() : "";
String msg = plugin.formatMessage("messages.new-ticket-notify") String msg = plugin.formatMessage("messages.new-ticket-notify")
.replace("{player}", ticket.getCreatorName()) .replace("{player}", creatorName)
.replace("{message}", ticket.getMessage()) .replace("{message}", message)
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()));
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers()) {
@@ -63,19 +67,32 @@ public class TicketManager {
} }
} }
// Discord-Webhook (asynchron, kein Einfluss auf Server-Performance) // Discord-Webhook (asynchron)
plugin.getDiscordWebhook().sendNewTicket(ticket); plugin.getDiscordWebhook().sendNewTicket(ticket);
} }
/** /**
* Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde.
* --- FIX PROBLEMK 1: NIE "UNBEKANNT" ---
*/ */
public void notifyCreatorClaimed(Ticket ticket) { public void notifyCreatorClaimed(Ticket ticket) {
Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); Player creator = Bukkit.getPlayer(ticket.getCreatorUUID());
if (creator != null && creator.isOnline()) { if (creator != null && creator.isOnline()) {
// 1. Versuch: Name aus dem Ticket-Objekt
String claimerName = ticket.getClaimerName();
// 2. Versuch: Wenn Name fehlt, aber UUID vorhanden -> Namen über Bukkit holen
if (claimerName == null && ticket.getClaimerUUID() != null) {
claimerName = Bukkit.getOfflinePlayer(ticket.getClaimerUUID()).getName();
}
// 3. Fallback: Falls immer noch kein Name da ist, nimm "Support" (nie "Unbekannt")
if (claimerName == null) claimerName = "Support";
String msg = plugin.formatMessage("messages.ticket-claimed-notify") String msg = plugin.formatMessage("messages.ticket-claimed-notify")
.replace("{id}", String.valueOf(ticket.getId())) .replace("{id}", String.valueOf(ticket.getId()))
.replace("{claimer}", ticket.getClaimerName()); .replace("{claimer}", claimerName);
creator.sendMessage(msg); creator.sendMessage(msg);
} }
} }
@@ -101,8 +118,10 @@ public class TicketManager {
public void notifyForwardedTo(Ticket ticket, String fromName) { public void notifyForwardedTo(Ticket ticket, String fromName) {
Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); Player target = Bukkit.getPlayer(ticket.getForwardedToUUID());
if (target != null && target.isOnline()) { if (target != null && target.isOnline()) {
String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt";
String msg = plugin.formatMessage("messages.ticket-forwarded-notify") String msg = plugin.formatMessage("messages.ticket-forwarded-notify")
.replace("{player}", ticket.getCreatorName()) .replace("{player}", creatorName)
.replace("{id}", String.valueOf(ticket.getId())); .replace("{id}", String.valueOf(ticket.getId()));
target.sendMessage(msg); target.sendMessage(msg);
} }
@@ -121,7 +140,6 @@ public class TicketManager {
/** /**
* Benachrichtigt den Ersteller, wenn sein Ticket geschlossen wurde. * 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) { public void notifyCreatorClosed(Ticket ticket, String closerName) {
notifiedClosedTickets.add(ticket.getId()); notifiedClosedTickets.add(ticket.getId());

View File

@@ -36,6 +36,9 @@ public class Ticket implements ConfigurationSerializable {
private Timestamp closedAt; private Timestamp closedAt;
private String closeComment; private String closeComment;
// ─── NEU: Soft Delete Flag ───
private boolean playerDeleted = false;
public Ticket() {} public Ticket() {}
@@ -101,6 +104,11 @@ public class Ticket implements ConfigurationSerializable {
this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj); this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj);
this.forwardedToName = (String) map.get("forwardedToName"); this.forwardedToName = (String) map.get("forwardedToName");
} }
// ─── NEU: Laden des Soft Delete Flags ───
if (map.containsKey("playerDeleted")) {
this.playerDeleted = (boolean) map.get("playerDeleted");
}
} }
// --- NEU: Methode zum Speichern in die YAML (Serialisierung) --- // --- NEU: Methode zum Speichern in die YAML (Serialisierung) ---
@@ -140,6 +148,9 @@ public class Ticket implements ConfigurationSerializable {
map.put("forwardedToName", forwardedToName); map.put("forwardedToName", forwardedToName);
} }
// ─── NEU: Speichern des Soft Delete Flags ───
map.put("playerDeleted", playerDeleted);
return map; return map;
} }
@@ -213,4 +224,8 @@ public class Ticket implements ConfigurationSerializable {
public String getCloseComment() { return closeComment; } public String getCloseComment() { return closeComment; }
public void setCloseComment(String closeComment) { this.closeComment = closeComment; } public void setCloseComment(String closeComment) { this.closeComment = closeComment; }
// ─── NEU: Getter/Setter für Soft Delete ───
public boolean isPlayerDeleted() { return playerDeleted; }
public void setPlayerDeleted(boolean playerDeleted) { this.playerDeleted = playerDeleted; }
} }

View File

@@ -1,5 +1,5 @@
name: TicketSystem name: TicketSystem
version: 1.0.2 version: 1.0.3
main: de.ticketsystem.TicketPlugin main: de.ticketsystem.TicketPlugin
api-version: 1.20 api-version: 1.20
author: M_Viper author: M_Viper
@@ -20,6 +20,12 @@ permissions:
description: Supporter kann Tickets einsehen und claimen description: Supporter kann Tickets einsehen und claimen
default: false default: false
ticket.archive:
description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen)
default: false
ticket.admin: ticket.admin:
description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload
default: op default: op
children:
ticket.support: true