diff --git a/src/main/java/de/ticketsystem/database/DatabaseManager.java b/src/main/java/de/ticketsystem/database/DatabaseManager.java index 0948c97..d848870 100644 --- a/src/main/java/de/ticketsystem/database/DatabaseManager.java +++ b/src/main/java/de/ticketsystem/database/DatabaseManager.java @@ -177,7 +177,6 @@ public class DatabaseManager { // ─────────────────────────── Tabellen erstellen ──────────────────────── private void createTables() { - // close_comment ist jetzt von Anfang an in der CREATE-Anweisung enthalten String sql = """ CREATE TABLE IF NOT EXISTS tickets ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -198,7 +197,8 @@ public class DatabaseManager { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, claimed_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; """; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { @@ -210,11 +210,9 @@ 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 + // close_comment hinzufügen String checkSql = """ SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() @@ -225,20 +223,34 @@ public class DatabaseManager { 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); } + + // 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 ────────────────────────────────────── - /** - * Speichert ein neues Ticket in der DB und gibt die generierte ID zurück. - */ public int createTicket(Ticket ticket) { if (useMySQL) { String sql = """ @@ -282,13 +294,14 @@ public class DatabaseManager { } } - /** - * Claimt ein Ticket (Status → CLAIMED). - */ + // ─── FIX: player_deleted wird beim Claimen zurückgesetzt, damit der Spieler + // sein Ticket wieder sieht, sobald ein Supporter es annimmt. ─────── public boolean claimTicket(int ticketId, UUID claimerUUID, String claimerName) { if (useMySQL) { 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' """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { @@ -307,6 +320,7 @@ public class DatabaseManager { t.setClaimerUUID(claimerUUID); t.setClaimerName(claimerName); t.setClaimedAt(new Timestamp(System.currentTimeMillis())); + t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); @@ -318,9 +332,6 @@ public class DatabaseManager { } } - /** - * Schließt ein Ticket (Status → CLOSED). - */ public boolean closeTicket(int ticketId, String closeComment) { if (useMySQL) { String sql = """ @@ -352,9 +363,36 @@ public class DatabaseManager { } } - /** - * Löscht ein Ticket anhand der ID. - */ + // ─── Soft Delete Methode ──────────────────────────────────────────────── + 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) { if (useMySQL) { String sql = "DELETE FROM tickets WHERE id = ?"; @@ -384,13 +422,14 @@ public class DatabaseManager { } } - /** - * Leitet ein Ticket an einen anderen Supporter weiter (Status → FORWARDED). - */ + // ─── FIX: player_deleted wird beim Weiterleiten zurückgesetzt, damit der + // Spieler sein Ticket wieder sieht, sobald es weitergeleitet wird. ── public boolean forwardTicket(int ticketId, UUID toUUID, String toName) { if (useMySQL) { 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' """; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { @@ -408,6 +447,7 @@ public class DatabaseManager { t.setStatus(TicketStatus.FORWARDED); t.setForwardedToUUID(toUUID); t.setForwardedToName(toName); + t.setPlayerDeleted(false); // FIX: Sichtbarkeit für den Spieler wiederherstellen dataConfig.set("tickets." + ticketId, t); try { dataConfig.save(dataFile); @@ -421,9 +461,6 @@ public class DatabaseManager { // ─────────────────────────── Abfragen ────────────────────────────────── - /** - * Gibt alle Tickets mit einem bestimmten Status zurück. - */ public List getTicketsByStatus(TicketStatus... statuses) { List list = new ArrayList<>(); if (statuses.length == 0) return list; @@ -452,9 +489,6 @@ public class DatabaseManager { } } - /** - * Gibt alle Tickets zurück (alle Status). - */ public List getAllTickets() { List list = new ArrayList<>(); if (useMySQL) { @@ -475,9 +509,6 @@ public class DatabaseManager { return list; } - /** - * Gibt ein einzelnes Ticket anhand der ID zurück. - */ public Ticket getTicketById(int id) { if (useMySQL) { 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() { if (useMySQL) { 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) { if (useMySQL) { String sql = "SELECT COUNT(*) FROM tickets WHERE creator_uuid = ? AND status IN ('OPEN', 'CLAIMED', 'FORWARDED')"; @@ -553,9 +578,6 @@ public class DatabaseManager { // ─────────────────────────── Archivierung ────────────────────────────── - /** - * Archiviert alle geschlossenen Tickets in eine separate Datei. - */ public int archiveClosedTickets() { List all = getAllTickets(); List toArchive = new ArrayList<>(); @@ -705,11 +727,6 @@ public class DatabaseManager { // ─────────────────────────── 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 { Ticket t = new Ticket(); t.setId(rs.getInt("id")); @@ -727,15 +744,10 @@ public class DatabaseManager { t.setClaimedAt(rs.getTimestamp("claimed_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 - } + } catch (SQLException ignored) { } String claimerUUID = rs.getString("claimer_uuid"); if (claimerUUID != null) { @@ -747,6 +759,10 @@ public class DatabaseManager { t.setForwardedToUUID(UUID.fromString(fwdUUID)); t.setForwardedToName(rs.getString("forwarded_to_name")); } + + // Mapping des Soft Delete Flags + t.setPlayerDeleted(rs.getBoolean("player_deleted")); + return t; } @@ -773,6 +789,7 @@ public class DatabaseManager { 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()); + obj.put("playerDeleted", t.isPlayerDeleted()); 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("forwardedToName") != null) t.setForwardedToName((String) obj.get("forwardedToName")); if (obj.get("closeComment") != null) t.setCloseComment((String) obj.get("closeComment")); + if (obj.containsKey("playerDeleted")) t.setPlayerDeleted((Boolean) obj.get("playerDeleted")); return t; } catch (Exception e) { if (plugin != null) plugin.getLogger().severe("Fehler beim Parsen eines Tickets: " + e.getMessage()); diff --git a/src/main/java/de/ticketsystem/gui/TicketGUI.java b/src/main/java/de/ticketsystem/gui/TicketGUI.java index 023c0e2..e18344d 100644 --- a/src/main/java/de/ticketsystem/gui/TicketGUI.java +++ b/src/main/java/de/ticketsystem/gui/TicketGUI.java @@ -18,8 +18,10 @@ import org.bukkit.inventory.meta.ItemMeta; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; public class TicketGUI implements Listener { @@ -27,9 +29,13 @@ public class TicketGUI implements Listener { // ─────────────────────────── Titel-Konstanten ────────────────────────── 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 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 final TicketPlugin plugin; @@ -37,6 +43,9 @@ public class TicketGUI implements Listener { /** Admin-Übersicht: Slot → Ticket */ private final Map> playerSlotMap = new HashMap<>(); + /** Admin-Archiv: Slot → Ticket */ + private final Map> playerClosedSlotMap = new HashMap<>(); + /** Spieler-GUI: Slot → Ticket */ private final Map> playerOwnSlotMap = new HashMap<>(); @@ -46,40 +55,76 @@ public class TicketGUI implements Listener { /** Wartet auf Chat-Eingabe für Close-Kommentar: Player-UUID → Ticket-ID */ private final Map 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 viewingFromArchive = new HashSet<>(); + public TicketGUI(TicketPlugin plugin) { this.plugin = plugin; } // ═══════════════════════════════════════════════════════════════════════ - // ADMIN / SUPPORTER GUI (Übersicht aller Tickets) + // ADMIN / SUPPORTER GUI (Feste 54 Slots mit Archiv-Button) // ═══════════════════════════════════════════════════════════════════════ public void openGUI(Player player) { + // Lade nur offene/aktive Tickets List tickets = plugin.getDatabaseManager().getTicketsByStatus( TicketStatus.OPEN, TicketStatus.CLAIMED, TicketStatus.FORWARDED); - if (tickets.isEmpty()) { - player.sendMessage(plugin.formatMessage("messages.no-open-tickets")); - return; - } - - int size = calcSize(tickets.size()); - Inventory inv = Bukkit.createInventory(null, size, GUI_TITLE); + // Admin GUI hat immer 54 Slots (6 Reihen) für feste Buttons + Inventory inv = Bukkit.createInventory(null, 54, GUI_TITLE); Map 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); inv.setItem(i, buildAdminListItem(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); 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 tickets = plugin.getDatabaseManager().getTicketsByStatus(TicketStatus.CLOSED); + + Inventory inv = Bukkit.createInventory(null, 54, CLOSED_GUI_TITLE); + Map 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) { @@ -88,7 +133,10 @@ public class TicketGUI implements Listener { List tickets = new ArrayList<>(); 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()) { @@ -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) { Inventory inv = Bukkit.createInventory(null, 27, DETAIL_GUI_TITLE); - // Slot 4: Ticket-Info (Mitte oben) + // Slot 4: Ticket-Info inv.setItem(4, buildDetailInfoItem(ticket)); - // Slot 10: Teleportieren (immer verfügbar) + // Slot 10: Teleportieren 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 + // Slot 12: Claimen (nur wenn OPEN) / Permanent löschen (wenn CLOSED + ticket.archive) / Grau 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 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 { inv.setItem(12, buildActionItem( Material.GRAY_WOOL, @@ -140,18 +198,12 @@ public class TicketGUI implements Listener { 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) { 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."))); + List.of("§7Schließt das Ticket.", "§8§m ", "§eKlick für Kommentar-Eingabe."))); } else { inv.setItem(14, buildActionItem( Material.GRAY_WOOL, @@ -159,7 +211,7 @@ public class TicketGUI implements Listener { List.of("§7Dieses Ticket ist bereits", "§7geschlossen."))); } - // Slot 16: Zurück zur Übersicht + // Slot 16: Zurück inv.setItem(16, buildActionItem( Material.ARROW, "§7§lZurück", @@ -179,36 +231,58 @@ public class TicketGUI implements Listener { 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; + if (!title.equals(GUI_TITLE) && !title.equals(CLOSED_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 ────────────────────────────────────────────── + // ── Admin Haupt-Übersicht ────────────────────────────────────────────── 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 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); - }); - }); + if (ticket != null) { + viewingFromArchive.remove(player.getUniqueId()); // Kommt aus Hauptübersicht + player.closeInventory(); + openTicketDetailAsync(player, ticket); + } 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 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)) { Map slotMap = playerOwnSlotMap.get(player.getUniqueId()); if (slotMap == null) return; @@ -217,28 +291,26 @@ public class TicketGUI implements Listener { 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.")); - } - }); + // Nur löschen wenn OFFEN oder GESCHLOSSEN + if (ticket.getStatus() == TicketStatus.OPEN || ticket.getStatus() == TicketStatus.CLOSED) { + boolean success = plugin.getDatabaseManager().markAsPlayerDeleted(ticket.getId()); + + Bukkit.getScheduler().runTask(plugin, () -> { + if (success) { + player.sendMessage(plugin.color("&aDein Ticket &e#" + ticket.getId() + " &awurde aus deiner Übersicht entfernt.")); + openPlayerGUI(player); + } else { + player.sendMessage(plugin.color("&cFehler beim Entfernen des Tickets.")); + } }); } else { - player.sendMessage(plugin.color( - "&cDieses Ticket kann nicht mehr gelöscht werden, " + - "da es bereits angenommen oder geschlossen wurde.")); + // Ticket wird bearbeitet (Claimed oder Forwarded) -> Löschen verweigern + player.sendMessage(plugin.color("&cDu kannst dieses Ticket nicht löschen, da es bereits von einem Supporter bearbeitet wird.")); } return; } - // ── Admin Detail-GUI ───────────────────────────────────────────── + // ── Admin Detail-GUI ───────────────────────────────────────────────── if (title.equals(DETAIL_GUI_TITLE)) { Ticket ticket = detailTicketMap.get(player.getUniqueId()); if (ticket == null) return; @@ -247,21 +319,46 @@ public class TicketGUI implements Listener { switch (slot) { 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 16 -> openGUI(player); - // Glasscheiben und andere Slots → nichts tun + case 16 -> { + // 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) { if (ticket.getLocation() != null) { player.teleport(ticket.getLocation()); - player.sendMessage(plugin.color( - "&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); + player.sendMessage(plugin.color("&7Du wurdest zu Ticket &e#" + ticket.getId() + " &7teleportiert.")); } else { 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")); return; } - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean success = plugin.getDatabaseManager().claimTicket( - ticket.getId(), player.getUniqueId(), player.getName()); - + boolean success = plugin.getDatabaseManager().claimTicket(ticket.getId(), player.getUniqueId(), player.getName()); Bukkit.getScheduler().runTask(plugin, () -> { if (success) { player.sendMessage(plugin.formatMessage("messages.ticket-claimed") .replace("{id}", String.valueOf(ticket.getId())) .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()); - // ── 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")); } @@ -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) { 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("&7Gib einen Kommentar ein (&e- &7für keinen).")); + player.sendMessage(plugin.color("&7Abbrechen mit &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."))); + Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(plugin.color("&cAbgebrochen."))); 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)); - } - + player.sendMessage(plugin.formatMessage("messages.ticket-closed").replace("{id}", String.valueOf(ticketId))); + if (!comment.isEmpty()) player.sendMessage(plugin.color("&7Kommentar: &f" + comment)); if (ticket != null) { 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 - // ═══════════════════════════════════════════════════════════════════════ + // ─────────────────────────── Item-Builder & Füll-Methoden ───────────── + + /** + * 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) { Material mat = switch (ticket.getStatus()) { case OPEN -> Material.PAPER; case CLAIMED -> Material.YELLOW_DYE; case FORWARDED -> Material.ORANGE_DYE; - default -> Material.PAPER; + case CLOSED -> Material.GRAY_DYE; }; ItemStack item = new ItemStack(mat); @@ -382,20 +522,19 @@ public class TicketGUI implements Listener { if (meta == null) return item; meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); - List 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) - lore.add("§7Angenommen: §a" + ticket.getClaimerName()); - if (ticket.getForwardedToName() != null) - lore.add("§7Weitergeleitet an: §6" + ticket.getForwardedToName()); + if (ticket.getStatus() == TicketStatus.CLOSED && ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) { + lore.add("§7Kommentar: §f" + ticket.getCloseComment()); + } + if (ticket.isPlayerDeleted()) { + lore.add("§cSpieler hat Ticket gelöscht."); + } lore.add("§8§m "); - lore.add("§e§l» KLICKEN für Details & Aktionen"); + lore.add("§e§l» KLICKEN für Details"); meta.setLore(lore); item.setItemMeta(meta); @@ -415,7 +554,6 @@ public class TicketGUI implements Listener { if (meta == null) return item; meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); - List lore = new ArrayList<>(); lore.add("§8§m "); lore.add("§7Ersteller: §e" + ticket.getCreatorName()); @@ -429,8 +567,6 @@ public class TicketGUI implements Listener { if (ticket.getClaimedAt() != null) 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.getClosedAt() != null) 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("§8§m "); - meta.setLore(lore); item.setItemMeta(meta); return item; @@ -457,17 +592,12 @@ public class TicketGUI implements Listener { if (meta == null) return item; meta.setDisplayName("§6§lTicket #" + ticket.getId() + " §r" + ticket.getStatus().getColored()); - List 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 "); @@ -475,14 +605,17 @@ public class TicketGUI implements Listener { 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."); } - } + 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); item.setItemMeta(meta); return item; @@ -498,8 +631,6 @@ public class TicketGUI implements Listener { 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)); diff --git a/src/main/java/de/ticketsystem/manager/TicketManager.java b/src/main/java/de/ticketsystem/manager/TicketManager.java index 6bdda61..f8ddb9a 100644 --- a/src/main/java/de/ticketsystem/manager/TicketManager.java +++ b/src/main/java/de/ticketsystem/manager/TicketManager.java @@ -51,9 +51,13 @@ public class TicketManager { * und sendet optional eine Discord-Webhook-Nachricht. */ 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") - .replace("{player}", ticket.getCreatorName()) - .replace("{message}", ticket.getMessage()) + .replace("{player}", creatorName) + .replace("{message}", message) .replace("{id}", String.valueOf(ticket.getId())); 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); } /** * Benachrichtigt den Ersteller, wenn sein Ticket angenommen wurde. + * --- FIX PROBLEMK 1: NIE "UNBEKANNT" --- */ public void notifyCreatorClaimed(Ticket ticket) { Player creator = Bukkit.getPlayer(ticket.getCreatorUUID()); 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") .replace("{id}", String.valueOf(ticket.getId())) - .replace("{claimer}", ticket.getClaimerName()); + .replace("{claimer}", claimerName); creator.sendMessage(msg); } } @@ -101,8 +118,10 @@ public class TicketManager { public void notifyForwardedTo(Ticket ticket, String fromName) { Player target = Bukkit.getPlayer(ticket.getForwardedToUUID()); if (target != null && target.isOnline()) { + String creatorName = ticket.getCreatorName() != null ? ticket.getCreatorName() : "Unbekannt"; + String msg = plugin.formatMessage("messages.ticket-forwarded-notify") - .replace("{player}", ticket.getCreatorName()) + .replace("{player}", creatorName) .replace("{id}", String.valueOf(ticket.getId())); target.sendMessage(msg); } @@ -121,7 +140,6 @@ public class TicketManager { /** * 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()); diff --git a/src/main/java/de/ticketsystem/model/Ticket.java b/src/main/java/de/ticketsystem/model/Ticket.java index f5a7a16..792d717 100644 --- a/src/main/java/de/ticketsystem/model/Ticket.java +++ b/src/main/java/de/ticketsystem/model/Ticket.java @@ -36,6 +36,9 @@ public class Ticket implements ConfigurationSerializable { private Timestamp closedAt; private String closeComment; + // ─── NEU: Soft Delete Flag ─── + private boolean playerDeleted = false; + public Ticket() {} @@ -101,6 +104,11 @@ public class Ticket implements ConfigurationSerializable { this.forwardedToUUID = fwdObj instanceof UUID ? (UUID) fwdObj : UUID.fromString((String) fwdObj); 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) --- @@ -140,6 +148,9 @@ public class Ticket implements ConfigurationSerializable { map.put("forwardedToName", forwardedToName); } + // ─── NEU: Speichern des Soft Delete Flags ─── + map.put("playerDeleted", playerDeleted); + return map; } @@ -213,4 +224,8 @@ public class Ticket implements ConfigurationSerializable { public String getCloseComment() { return 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; } } \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4f3422e..6e37320 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketSystem -version: 1.0.2 +version: 1.0.3 main: de.ticketsystem.TicketPlugin api-version: 1.20 author: M_Viper @@ -20,6 +20,12 @@ permissions: description: Supporter kann Tickets einsehen und claimen default: false + ticket.archive: + description: Zugriff auf das Ticket-Archiv (öffnen, einsehen, permanent löschen) + default: false + ticket.admin: description: Admin hat vollen Zugriff inkl. Weiterleitung und Reload default: op + children: + ticket.support: true \ No newline at end of file