Tickets: Anliegen zur Auswahl, und das Team kann uebernehmen
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Bisher sah jedes Ticket gleich aus: ein Knopf, ein Thread, und im Thread stand
als Erstes die Frage, worum es ueberhaupt geht. Und wer sich darum kuemmert,
stand nirgends — man sah hinterher nur, wer zufaellig auf "Schliessen" geklickt
hat.

Anliegen sind Kategorien, die man in der Config anlegt: Name, Emoji, eine
Zeile Beschreibung, ein Einstiegstext und optional eine Rolle, die beim
Oeffnen erfaehrt, dass etwas anliegt. Der Ticket-Post wird dadurch vom Knopf
zum Auswahlmenue; das gewaehlte Anliegen steht im Thread-Namen und der
Einstiegstext gleich im ersten Beitrag — gute Stelle fuer "was wir wissen
muessen".

Wer keine anlegt, merkt nichts: ohne Eintrag bleibt es beim einen Knopf. Wer
nur eine Sorte Tickets hat, soll nichts einrichten muessen.

Uebernehmen ist ein Knopf im Thread, sichtbar fuers Team (wer aufraeumen darf,
darf auch uebernehmen). Danach steht im Thread, wer sich kuemmert, und der
Knopf wird zu "Freigeben". Die Bedingung "nur wenn noch niemand dran ist"
steckt im UPDATE und nicht davor — zwei gleichzeitige Klicks wuerden sonst
beide gewinnen. Beim Schliessen landen Anliegen und Bearbeiter im Transcript
und im Mod-Log-Eintrag.

Die Erstellung ist dabei aus client.js nach tickets.js gewandert. Knopf und
Menue gehen jetzt denselben Weg, und in client.js sind drei Importzeilen
weggefallen, die nur noch dort standen.

Geprueft: alsEmoji gegen 20 Eingaben (Unicode, Hautfarbe, ZWJ-Ketten,
Server-Emoji, Murks) und das fertige Panel gegen discord.js selbst — ein
ungueltiges Emoji oder ein zu langes Label laesst Discord sonst die ganze
Nachricht fallen, und das faellt erst beim Senden auf. Im Browser: Liste,
Bearbeiten-Formular und der leere Zustand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 09:14:49 +02:00
co-authored by Claude Opus 5
parent 2292dedeaa
commit bb2aee7818
8 changed files with 496 additions and 77 deletions
+56 -2
View File
@@ -1012,16 +1012,70 @@ db.exec(`
opened_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
const insertTicket = db.prepare('INSERT INTO tickets (thread_id, user_id) VALUES (?, ?)');
// Migration: Anliegen (Kategorie) und wer sich darum kümmert
{
const cols = db.prepare('PRAGMA table_info(tickets)').all().map((c) => c.name);
if (!cols.includes('category_id')) db.exec('ALTER TABLE tickets ADD COLUMN category_id INTEGER');
if (!cols.includes('claimed_by')) db.exec('ALTER TABLE tickets ADD COLUMN claimed_by TEXT');
if (!cols.includes('claimed_at')) db.exec('ALTER TABLE tickets ADD COLUMN claimed_at TEXT');
}
const insertTicket = db.prepare(
'INSERT INTO tickets (thread_id, user_id, category_id) VALUES (?, ?, ?)'
);
const getTicketStmt = db.prepare('SELECT * FROM tickets WHERE thread_id = ?');
const closeTicketStmt = db.prepare(`UPDATE tickets SET status = 'closed' WHERE thread_id = ?`);
const openTicketByUserStmt = db.prepare(
`SELECT * FROM tickets WHERE user_id = ? AND status = 'open'`
);
export const saveTicket = (threadId, userId) => insertTicket.run(threadId, userId);
// Übernehmen nur, solange niemand anders dran ist — die Bedingung gehört in die
// Abfrage und nicht davor: zwei gleichzeitige Klicks würden sonst beide gewinnen.
const claimTicketStmt = db.prepare(`
UPDATE tickets SET claimed_by = ?, claimed_at = datetime('now')
WHERE thread_id = ? AND claimed_by IS NULL
`);
const unclaimTicketStmt = db.prepare(
'UPDATE tickets SET claimed_by = NULL, claimed_at = NULL WHERE thread_id = ?'
);
export const saveTicket = (threadId, userId, categoryId = null) =>
insertTicket.run(threadId, userId, categoryId ?? null);
export const getTicket = (threadId) => getTicketStmt.get(threadId) ?? null;
export const closeTicket = (threadId) => closeTicketStmt.run(threadId);
export const openTicketOf = (userId) => openTicketByUserStmt.get(userId) ?? null;
/** @returns {boolean} true, wenn dieser Aufruf das Ticket übernommen hat */
export const claimTicket = (threadId, userId) => claimTicketStmt.run(userId, threadId).changes > 0;
export const unclaimTicket = (threadId) => unclaimTicketStmt.run(threadId);
// Anliegen: „Bug melden", „Bewerbung", „Sonstiges" … Ohne Eintrag bleibt es beim
// einen Knopf — wer nur eine Sorte Tickets hat, soll nichts einrichten müssen.
db.exec(`
CREATE TABLE IF NOT EXISTS ticket_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
emoji TEXT NOT NULL DEFAULT '🎫',
beschreibung TEXT NOT NULL DEFAULT '',
ping_role_id TEXT,
intro TEXT NOT NULL DEFAULT '',
sort INTEGER NOT NULL DEFAULT 0
);
`);
const listTicketCatsStmt = db.prepare('SELECT * FROM ticket_categories ORDER BY sort, id');
const getTicketCatStmt = db.prepare('SELECT * FROM ticket_categories WHERE id = ?');
const insertTicketCatStmt = db.prepare(`
INSERT INTO ticket_categories (name, emoji, beschreibung, ping_role_id, intro, sort)
VALUES (@name, @emoji, @beschreibung, @ping_role_id, @intro, @sort)
`);
const updateTicketCatStmt = db.prepare(`
UPDATE ticket_categories SET
name = @name, emoji = @emoji, beschreibung = @beschreibung,
ping_role_id = @ping_role_id, intro = @intro, sort = @sort
WHERE id = @id
`);
const deleteTicketCatStmt = db.prepare('DELETE FROM ticket_categories WHERE id = ?');
export const listTicketCategories = () => listTicketCatsStmt.all();
export const getTicketCategory = (id) => getTicketCatStmt.get(id) ?? null;
export const createTicketCategory = (c) => insertTicketCatStmt.run(c).lastInsertRowid;
export const updateTicketCategory = (c) => updateTicketCatStmt.run(c);
export const deleteTicketCategory = (id) => deleteTicketCatStmt.run(id).changes > 0;
// Modmail: DM-Konversationen ↔ Staff-Threads
db.exec(`