fix(discord): /ticket auch für Personal nutzbar, klarere Fehlermeldung

Bug gefunden: /ticket prüfte ausschließlich ein verknüpftes
Kundenkonto. Ein Discord-Konto kann aber immer nur mit einem
Kundencenter-Konto verknüpft sein – wer sein Konto (wie in der
Anleitung empfohlen) als Personal verknüpft hat, bekam die
irreführende Meldung "Bitte zuerst dein Konto verknüpfen", obwohl das
Konto sehr wohl verknüpft war (nur eben nicht als Kunde).

Jetzt: /ticket erkennt automatisch, ob das aufrufende Discord-Konto
mit einem Kunden- oder Personal-Konto verknüpft ist.
- Kunde: legt wie bisher ein eigenes Ticket samt Thread an.
- Personal: neue Option "kunde" (Kundennummer oder Name) legt ein
  Ticket für den jeweiligen Kunden an (Status "wartet auf Kunde", wie
  beim Anlegen über die Web-Oberfläche). Kein automatischer Thread,
  da der Kunde dafür selbst Discord verknüpft haben müsste.
- Fehlermeldung bei fehlender Verknüpfung präzisiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kundencenter 2026-09-29 00:17:55 +02:00
parent 078b1357fb
commit 4976fc529e
2 changed files with 35 additions and 13 deletions

View file

@ -36,30 +36,52 @@ const commands = [
new SlashCommandBuilder().setName('kc-status').setDescription('Zustand des Kundencenters (Aufträge, Datenbank)').setDefaultMemberPermissions(staffOnly),
new SlashCommandBuilder().setName('kc-kunde').setDescription('Kunde suchen (nur Kundennummer, Name, Status)').setDefaultMemberPermissions(staffOnly)
.addStringOption((o) => o.setName('suche').setDescription('Kundennummer oder Name').setRequired(true).setMaxLength(100)),
new SlashCommandBuilder().setName('ticket').setDescription('Neues Support-Ticket erstellen (eigenes verknüpftes Kundencenter-Konto)')
new SlashCommandBuilder().setName('ticket').setDescription('Neues Support-Ticket erstellen')
.addStringOption((o) => o.setName('betreff').setDescription('Kurzer Betreff').setRequired(true).setMaxLength(200))
.addStringOption((o) => o.setName('text').setDescription('Ihr Anliegen').setRequired(true).setMaxLength(2000)),
.addStringOption((o) => o.setName('text').setDescription('Anliegen').setRequired(true).setMaxLength(2000))
.addStringOption((o) => o.setName('kunde').setDescription('Nur für Personal: für welchen Kunden (Kundennummer oder Name)').setRequired(false).setMaxLength(100)),
new SlashCommandBuilder().setName('zuweisen').setDescription('Dieses Ticket einer Person zuweisen (nur im Ticket-Thread, nur Personal)').setDefaultMemberPermissions(staffOnly)
.addUserOption((o) => o.setName('mitarbeiter').setDescription('Zuständige Person (muss Discord verknüpft haben)').setRequired(true)),
new SlashCommandBuilder().setName('schliessen').setDescription('Dieses Ticket schließen (nur im Ticket-Thread, nur Personal)').setDefaultMemberPermissions(staffOnly),
].map((c) => c.toJSON());
/** Kunde eröffnet ein neues Ticket direkt aus Discord; legt Ticket + zugehörigen Thread an. */
/** Erstellt ein neues Ticket direkt aus Discord; legt bei verknüpften Kunden auch den zugehörigen Thread an.
* Kunden eröffnen ihr eigenes Ticket, Personal kann es für einen beliebigen Kunden anlegen (Option "kunde"). */
async function handleTicketCreate(i: ChatInputCommandInteraction): Promise<void> {
const u = await linkedUser(i.user.id, 'customer');
if (!u) { await i.reply({ content: 'Bitte zuerst dein Konto im Kundencenter verknüpfen (unter „Mein Konto“ → „Mit Discord verbinden“).', ephemeral: true }); return; }
const mem = await pool.query("SELECT org_id FROM memberships WHERE user_id = ? ORDER BY (role = 'owner') DESC LIMIT 1", [u.id]).then(([r]: any) => r[0]);
if (!mem) { await i.reply({ content: 'Für dein Konto ist keine Organisation hinterlegt.', ephemeral: true }); return; }
const subject = i.options.getString('betreff', true).slice(0, 200);
const body = i.options.getString('text', true).slice(0, 2000);
const kunde = i.options.getString('kunde')?.trim();
const customer = await linkedUser(i.user.id, 'customer');
if (customer) {
const mem = await pool.query("SELECT org_id FROM memberships WHERE user_id = ? ORDER BY (role = 'owner') DESC LIMIT 1", [customer.id]).then(([r]: any) => r[0]);
if (!mem) { await i.reply({ content: 'Für dein Konto ist keine Organisation hinterlegt.', ephemeral: true }); return; }
await i.deferReply({ ephemeral: true });
const ticketId = randomUUID();
const number = await nextTicketNumber();
await pool.query('INSERT INTO tickets (id, number, org_id, subject, status, priority, created_by) VALUES (?,?,?,?,?,?,?)', [ticketId, number, mem.org_id, subject, 'pending_staff', 'normal', customer.id]);
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), ticketId, customer.id, 'customer', body]);
await notify('ticket.created', { number, subject: subject.slice(0, 100) }, `discord-cmd:${ticketId}`);
await postTicketMessage(ticketId, body, 'Kunde');
await i.editReply({ content: `Ticket ${number} wurde erstellt. Den zugehörigen Thread findest du weiter unten im Kanal.` });
return;
}
const staff = await linkedUser(i.user.id, 'staff');
if (!staff) { await i.reply({ content: 'Dein Discord-Konto ist mit keinem Kundencenter-Konto verknüpft (unter „Mein Konto“ → „Mit Discord verbinden“).', ephemeral: true }); return; }
if (!kunde) { await i.reply({ content: 'Als Personal bitte die Option „kunde“ angeben (Kundennummer oder Name), für wen das Ticket ist.', ephemeral: true }); return; }
const orgs = await pool.query("SELECT id, name, customer_number FROM organizations WHERE customer_number = ? OR name LIKE CONCAT('%', ?, '%') LIMIT 2", [kunde, kunde]) as [any[], unknown];
const matches = orgs[0];
if (matches.length === 0) { await i.reply({ content: `Kein Kunde gefunden für „${kunde}“.`, ephemeral: true }); return; }
if (matches.length > 1) { await i.reply({ content: `Mehrdeutig – bitte die genaue Kundennummer angeben (z. B. gefunden: ${matches.map((m) => m.customer_number).join(', ')}).`, ephemeral: true }); return; }
const org = matches[0];
await i.deferReply({ ephemeral: true });
const ticketId = randomUUID();
const number = await nextTicketNumber();
await pool.query('INSERT INTO tickets (id, number, org_id, subject, status, priority, created_by) VALUES (?,?,?,?,?,?,?)', [ticketId, number, mem.org_id, subject, 'pending_staff', 'normal', u.id]);
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), ticketId, u.id, 'customer', body]);
await notify('ticket.created', { number, subject: subject.slice(0, 100) }, `discord-cmd:${ticketId}`);
await postTicketMessage(ticketId, body, 'Kunde');
await i.editReply({ content: `Ticket ${number} wurde erstellt. Den zugehörigen Thread findest du weiter unten im Kanal.` });
// Von Personal angelegt: wartet auf den Kunden, wie beim Anlegen über die Web-Oberfläche.
await pool.query('INSERT INTO tickets (id, number, org_id, subject, status, priority, created_by) VALUES (?,?,?,?,?,?,?)', [ticketId, number, org.id, subject, 'pending_customer', 'normal', staff.id]);
await pool.query('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [randomUUID(), ticketId, staff.id, 'staff', body]);
await i.editReply({ content: `Ticket ${number} für ${org.name} (${org.customer_number}) wurde angelegt.` });
}
/** Weist das Ticket des aktuellen Threads einer per Discord verknüpften Personal-Person zu. */
async function handleAssign(i: ChatInputCommandInteraction): Promise<void> {