Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a91e17a097 | |||
| 12c9379797 | |||
| 535b0aa2f3 |
141
README.md
141
README.md
@@ -16,9 +16,15 @@
|
||||
- **Bessere Fehlerausgaben** – Alle Fehler erscheinen im Log und für Admins im Chat, inkl. Validierungs- und Speicherfehler
|
||||
- **Debug-Modus & Versionsprüfung** – Für Entwickler und Admins, erkennt veraltete config.yml automatisch
|
||||
- **Komplett anpassbar** – Nachrichten, Farben, Limits, Speicherpfade, Archiv-Intervall, Cooldowns, Rechte
|
||||
- **Unit-Tests** – Getestete Speicher-Logik für maximale Zuverlässigkeit
|
||||
- **Dynamische GUI** – Die Ticket-GUI passt sich automatisch der Ticketanzahl an (bis zu 54 Tickets pro Seite)
|
||||
- **Dynamische GUI** – Die Ticket-GUI passt sich automatisch der Ticketanzahl an (bis zu 45 Tickets pro Seite), Item-Material richtet sich nach der konfigurierten Kategorie
|
||||
- **Seiten-System** – Bei sehr vielen Tickets wird automatisch geblättert
|
||||
- **Kategorie-System** – Frei konfigurierbare Kategorien (Name, Farbe, Material, Aliases) in der config.yml
|
||||
- **Prioritäten-System** – Vier Stufen (LOW / NORMAL / HIGH / URGENT), beim Erstellen wählbar und nachträglich via GUI oder Befehl änderbar
|
||||
- **Bewertungs-System** – Spieler können nach Ticket-Schließung den Support bewerten (`good` / `bad`), Ergebnisse in `/ticket stats`
|
||||
- **Kommentar-System** – Spieler und Support können Nachrichten direkt am Ticket hinterlassen
|
||||
- **Offline-Benachrichtigungen** – Verpasste Kommentar-, Schließ- und Status-Benachrichtigungen werden gespeichert und beim nächsten Login angezeigt
|
||||
- **Discord-Webhook** – Benachrichtigungen mit Embeds, konfigurierbarem Rollen-Ping und Kategorie/Priorität-Anzeige
|
||||
- **Blacklist** – Spieler vom Ticket-System ausschließen
|
||||
- **Performance** – Optimiert für große Server, alle Operationen laufen asynchron und ressourcenschonend
|
||||
- **Support & Erweiterbarkeit** – Sauberer Code, viele Hooks für eigene Erweiterungen
|
||||
|
||||
@@ -26,59 +32,38 @@
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
1. **config.yml** anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
||||
2. **TicketSystem.jar** in den plugins-Ordner legen und Server starten
|
||||
1. **TicketSystem.jar** in den `plugins`-Ordner legen und Server starten
|
||||
2. **config.yml** anpassen (Speicherorte, Nachrichten, Limits, Farben, MySQL-Daten etc.)
|
||||
3. **/ticket**-Befehle nutzen (siehe unten)
|
||||
|
||||
---
|
||||
|
||||
## Beispiel-Konfiguration
|
||||
|
||||
```yaml
|
||||
config-version: 1
|
||||
debug: false
|
||||
storage:
|
||||
type: "file" # "file" oder "mysql"
|
||||
data-file: "data.yml"
|
||||
archive-file: "archive.yml"
|
||||
mysql:
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
database: "tickets"
|
||||
user: "root"
|
||||
password: "password"
|
||||
useSSL: false
|
||||
auto-archive-interval-hours: 24
|
||||
messages:
|
||||
prefix: "&7[&eTicket&7]"
|
||||
ticket-created: "&aDein Ticket wurde erstellt!"
|
||||
error: "&cEin Fehler ist aufgetreten!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Befehle & Rechte
|
||||
|
||||
### Spieler-Befehle
|
||||
|
||||
```
|
||||
/ticket - Ticket erstellen, verwalten, Status abfragen
|
||||
/ticket create <Nachricht> - Neues Ticket erstellen
|
||||
/ticket list - Zeigt alle Tickets des Spielers
|
||||
/ticket close <ID> - Ticket schließen
|
||||
/ticket - Hilfe & Befehlsübersicht
|
||||
/ticket create [Kategorie] [Priorität] <Text> - Neues Ticket erstellen
|
||||
/ticket list - Eigene Tickets in der GUI anzeigen
|
||||
/ticket comment <ID> <Nachricht> - Kommentar zu einem Ticket hinzufügen
|
||||
/ticket rate <ID> <good|bad> - Abgeschlossenes Ticket bewerten
|
||||
```
|
||||
|
||||
### Admin-Befehle
|
||||
### Support/Admin-Befehle
|
||||
|
||||
```
|
||||
/ticket reload - Plugin neu laden
|
||||
/ticket migrate - Speicherart wechseln (Migration)
|
||||
/ticket export/import - Tickets exportieren/importieren
|
||||
/ticket stats - Statistiken anzeigen
|
||||
/ticket archive - Tickets archivieren
|
||||
/ticket claim <ID> - Ticket übernehmen
|
||||
/ticket claim <ID> - Ticket annehmen
|
||||
/ticket close <ID> [Kommentar] - Ticket schließen
|
||||
/ticket forward <ID> <Spieler> - Ticket weiterleiten
|
||||
/ticket close <ID> - Ticket schließen
|
||||
/ticket setpriority <ID> <low|normal|high|urgent> - Priorität eines Tickets ändern
|
||||
/ticket reload - Konfiguration neu laden (inkl. Kategorien)
|
||||
/ticket stats - Statistiken anzeigen
|
||||
/ticket archive - Geschlossene Tickets archivieren
|
||||
/ticket blacklist <add|remove|list> [Spieler] [Grund] - Blacklist verwalten
|
||||
/ticket migrate <tomysql|tofile> - Speicherart wechseln
|
||||
/ticket export <Dateiname> - Tickets exportieren
|
||||
/ticket import <Dateiname> - Tickets importieren
|
||||
```
|
||||
|
||||
### Permissions
|
||||
@@ -86,9 +71,9 @@ messages:
|
||||
| Permission | Beschreibung | Standard |
|
||||
|---|---|---|
|
||||
| `ticket.create` | Ticket erstellen | ✅ alle Spieler |
|
||||
| `ticket.support` | Tickets einsehen, claimen & schließen | ❌ manuell vergeben |
|
||||
| `ticket.support` | Tickets einsehen, claimen, schließen & Priorität ändern | ❌ manuell vergeben |
|
||||
| `ticket.archive` | Archiv öffnen, einsehen & Tickets permanent löschen | ❌ manuell vergeben |
|
||||
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung & Reload (beinhaltet `ticket.support`) | OP |
|
||||
| `ticket.admin` | Voller Zugriff inkl. Weiterleitung, Reload & Blacklist (beinhaltet `ticket.support`) | OP |
|
||||
|
||||
> ⚠️ **Wichtig:** `ticket.archive` ist bewusst **nicht** in `ticket.admin` enthalten und wird auch **nicht automatisch an OPs vergeben**. Das Archiv-Recht muss explizit zugewiesen werden:
|
||||
> ```
|
||||
@@ -97,23 +82,68 @@ messages:
|
||||
|
||||
---
|
||||
|
||||
## Kategorie & Priorität beim Erstellen
|
||||
|
||||
Beim Erstellen eines Tickets können Kategorie und Priorität optional angegeben werden:
|
||||
|
||||
```
|
||||
/ticket create <Text> → Kategorie: Standard, Priorität: NORMAL
|
||||
/ticket create bug <Text> → Kategorie: Bug, Priorität: NORMAL
|
||||
/ticket create high <Text> → Kategorie: Standard, Priorität: HIGH
|
||||
/ticket create bug high <Text> → Kategorie: Bug, Priorität: HIGH
|
||||
/ticket create question urgent <Text> → Kategorie: Frage, Priorität: URGENT
|
||||
```
|
||||
|
||||
Verfügbare Prioritäten: `low`, `normal`, `high`, `urgent` (auch auf Deutsch: `niedrig`, `hoch`, `dringend`)
|
||||
|
||||
Kategorien und ihre Aliases sind frei in der `config.yml` konfigurierbar.
|
||||
|
||||
---
|
||||
|
||||
## Discord-Webhook
|
||||
|
||||
Der integrierte Discord-Webhook unterstützt:
|
||||
|
||||
- **Embeds** mit Kategorie und Priorität als eigene Felder
|
||||
- **Rollen-Ping** (`@role`) pro Nachrichtentyp einzeln konfigurierbar
|
||||
- **Drei Ereignisse:** Neues Ticket, Ticket geschlossen, Ticket weitergeleitet
|
||||
|
||||
Konfiguration in der `config.yml`:
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
enabled: true
|
||||
webhook-url: "https://discord.com/api/webhooks/..."
|
||||
role-ping-id: "123456789012345678" # Discord-Rollen-ID (leer = kein Ping)
|
||||
messages:
|
||||
new-ticket:
|
||||
role-ping: true
|
||||
show-category: true
|
||||
show-priority: true
|
||||
ticket-closed:
|
||||
enabled: true
|
||||
role-ping: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Kann ich zwischen MySQL und Datei-Speicherung wechseln?**
|
||||
> Ja! Das Plugin migriert alle Daten automatisch und sicher.
|
||||
> Ja! Mit `/ticket migrate tomysql` oder `/ticket migrate tofile` werden alle Daten automatisch migriert.
|
||||
|
||||
**Wie viele Tickets passen in die GUI?**
|
||||
> Bis zu 54 pro Seite, bei mehr Tickets wird automatisch geblättert.
|
||||
**Wie konfiguriere ich eigene Kategorien?**
|
||||
> In der `config.yml` unter `categories:` — Name, Farbe, Material (für die GUI) und Aliases frei wählbar. Änderungen werden mit `/ticket reload` übernommen.
|
||||
|
||||
**Wie kann ich Nachrichten und Limits anpassen?**
|
||||
> Alle Texte, Farben und Limits findest du in der `config.yml`.
|
||||
**Was passiert mit Benachrichtigungen wenn ein Spieler offline ist?**
|
||||
> Alle Kommentar-, Schließ- und Status-Benachrichtigungen werden gespeichert und beim nächsten Login gebündelt angezeigt.
|
||||
|
||||
**Wie ändere ich die Priorität eines bestehenden Tickets?**
|
||||
> Als Support/Admin entweder per Befehl `/ticket setpriority <ID> <Priorität>` oder direkt in der Detail-GUI per Klick.
|
||||
|
||||
**Wie aktiviere ich den Debug-Modus?**
|
||||
> Setze `debug: true` in der `config.yml`.
|
||||
|
||||
**Wie kann ich Tickets exportieren/importieren?**
|
||||
> Mit `/ticket export` und `/ticket import` – ideal für Server-Umzüge.
|
||||
|
||||
**Wer darf das Ticket-Archiv sehen?**
|
||||
> Nur Spieler mit der Permission `ticket.archive`. Diese wird weder automatisch an OPs noch an Admins vergeben und muss explizit zugewiesen werden.
|
||||
|
||||
@@ -125,9 +155,14 @@ messages:
|
||||
|----------------------------------|:------------:|:-------------:|:---------------:|
|
||||
| Speicher-Migration | ✔️ | ⚠️ | ✖️ |
|
||||
| Automatische Backups | ✔️ | ⚠️ | ✖️ |
|
||||
| GUI | ✔️ | ⚠️ | ✖️ |
|
||||
| GUI mit Kategorie-Materialien | ✔️ | ⚠️ | ✖️ |
|
||||
| Archivierung | ✔️ | ⚠️ | ✖️ |
|
||||
| Rollenbasierter Archiv-Zugriff | ✔️ | ✖️ | ✖️ |
|
||||
| Kategorie-System (konfigurierbar)| ✔️ | ✖️ | ✖️ |
|
||||
| Prioritäten-System | ✔️ | ✖️ | ✖️ |
|
||||
| Offline-Benachrichtigungen | ✔️ | ✖️ | ✖️ |
|
||||
| Discord-Webhook mit Rollen-Ping | ✔️ | ✖️ | ✖️ |
|
||||
| Bewertungs-System | ✔️ | ✖️ | ✖️ |
|
||||
| Update-Checker | ✔️ | ✖️ | ✖️ |
|
||||
|
||||
---
|
||||
@@ -143,5 +178,5 @@ Wir antworten in der Regel innerhalb von 24 Stunden!
|
||||
|
||||
## ⭐ Unterstütze das Projekt
|
||||
|
||||
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine **5-Sterne Bewertung auf spigotmc**!
|
||||
Wenn TicketSystem deinen Server bereichert hat, freuen wir uns über eine **5-Sterne Bewertung auf SpigotMC**!
|
||||
Dein Feedback hilft uns, das Plugin weiter zu verbessern.
|
||||
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.ticketsystem</groupId>
|
||||
<artifactId>TicketSystem</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<version>1.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>TicketSystem</name>
|
||||
|
||||
@@ -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<Ticket> getTicketsByStatus(TicketStatus... statuses) {
|
||||
List<Ticket> 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<Ticket> getAllTickets() {
|
||||
List<Ticket> 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<Ticket> all = getAllTickets();
|
||||
List<Ticket> 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());
|
||||
|
||||
@@ -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<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 */
|
||||
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 */
|
||||
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) {
|
||||
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<Ticket> 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<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);
|
||||
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<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) {
|
||||
@@ -88,7 +133,10 @@ public class TicketGUI implements Listener {
|
||||
|
||||
List<Ticket> 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<Integer, Ticket> slotMap = playerSlotMap.get(player.getUniqueId());
|
||||
if (slotMap == null) return;
|
||||
Ticket ticket = slotMap.get(slot);
|
||||
if (ticket == null) return;
|
||||
|
||||
if (ticket != null) {
|
||||
viewingFromArchive.remove(player.getUniqueId()); // Kommt aus Hauptübersicht
|
||||
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;
|
||||
openTicketDetailAsync(player, ticket);
|
||||
}
|
||||
openDetailGUI(player, fresh);
|
||||
});
|
||||
});
|
||||
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)) {
|
||||
Map<Integer, Ticket> 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());
|
||||
// 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 (deleted) {
|
||||
player.sendMessage(plugin.color(
|
||||
"&aDein Ticket &e#" + ticket.getId() + " &awurde gelöscht."));
|
||||
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 Löschen des Tickets."));
|
||||
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<String> lore = new ArrayList<>();
|
||||
lore.add("§8§m ");
|
||||
lore.add("§7Ersteller: §e" + ticket.getCreatorName());
|
||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
||||
lore.add("§7Welt: §e" + ticket.getWorldName());
|
||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||
if (ticket.getClaimerName() != null)
|
||||
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<String> 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<String> lore = new ArrayList<>();
|
||||
lore.add("§8§m ");
|
||||
lore.add("§7Anliegen: §f" + ticket.getMessage());
|
||||
lore.add("§7Erstellt: §e" + DATE_FORMAT.format(ticket.getCreatedAt()));
|
||||
lore.add("§7Welt: §e" + ticket.getWorldName());
|
||||
lore.add(String.format("§7Position: §e%.0f, %.0f, %.0f", ticket.getX(), ticket.getY(), ticket.getZ()));
|
||||
if (ticket.getStatus() == TicketStatus.CLAIMED && ticket.getClaimerName() != null)
|
||||
lore.add("§7Angenommen von: §a" + ticket.getClaimerName());
|
||||
if (ticket.getStatus() == TicketStatus.FORWARDED && ticket.getForwardedToName() != null)
|
||||
lore.add("§7Bearbeiter: §6" + ticket.getForwardedToName());
|
||||
if (ticket.getStatus() == TicketStatus.CLOSED
|
||||
&& ticket.getCloseComment() != null && !ticket.getCloseComment().isEmpty()) {
|
||||
lore.add("§8§m ");
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user