Tickets: Bezug zu Ressource oder Domain wählen, Verknüpfung direkt aus der Domain-Aufstellung

This commit is contained in:
Kundencenter 2026-09-27 16:40:14 +02:00
parent 43dc5bc827
commit a1472617b9
6 changed files with 62 additions and 18 deletions

View file

@ -19,11 +19,14 @@ async function nextNumber(c: PoolConnection): Promise<string> {
return `T-${(await one('SELECT LAST_INSERT_ID() AS n', [], c))!.n}`;
}
const ticketView = (t: any) => ({
id: t.id, number: t.number, orgId: t.org_id, orgName: t.org_name, customerNumber: t.customer_number, resourceId: t.resource_id, subject: t.subject,
id: t.id, number: t.number, orgId: t.org_id, orgName: t.org_name, customerNumber: t.customer_number, subject: t.subject,
resourceId: t.resource_id, resourceName: t.resource_name ?? null, resourceType: t.resource_type ?? null,
domainRecordId: t.domain_record_id, domain: t.domain ?? null,
status: t.status, priority: t.priority, createdBy: t.created_by, assignedTo: t.assigned_to, assignedName: t.assigned_name ?? null,
createdAt: t.created_at, updatedAt: t.updated_at, lastMessageAt: t.last_message_at, resolvedAt: t.resolved_at, closedAt: t.closed_at,
});
const TICKET_SQL = `SELECT t.*, g.name AS org_name, g.customer_number, u.name AS assigned_name FROM tickets t JOIN organizations g ON g.id = t.org_id LEFT JOIN users u ON u.id = t.assigned_to`;
const TICKET_SQL = `SELECT t.*, g.name AS org_name, g.customer_number, u.name AS assigned_name, r.name AS resource_name, r.type AS resource_type, d.domain AS domain
FROM tickets t JOIN organizations g ON g.id = t.org_id LEFT JOIN users u ON u.id = t.assigned_to LEFT JOIN resources r ON r.id = t.resource_id LEFT JOIN domain_records d ON d.id = t.domain_record_id`;
const messageView = (m: any, atts: any[] = []) => ({ id: m.id, authorId: m.author_id, authorName: m.author_name, authorKind: m.author_kind, body: m.body, internalNote: !!m.internal_note, createdAt: m.created_at,
attachments: atts.filter((a) => a.message_id === m.id).map((a) => ({ id: a.id, filename: a.filename, contentType: a.content_type, sizeBytes: a.size_bytes })) });
const notify = (event: string, extra: Record<string, unknown>, key: string, correlationId: string) => enqueue('discord.notify', { event, ...extra }, { idempotencyKey: key, correlationId });
@ -47,16 +50,18 @@ export const ticketsModule: KcModule = {
register(app: FastifyInstance) {
app.post('/tickets', async (req) => {
const a = requireAuth(req);
const b = z.object({ orgId: z.string().uuid(), subject: z.string().trim().min(3).max(200), body: z.string().trim().min(1).max(10000), resourceId: z.string().uuid().optional(), priority: z.enum(['low', 'normal', 'high', 'urgent']).default('normal') }).parse(req.body);
const b = z.object({ orgId: z.string().uuid(), subject: z.string().trim().min(3).max(200), body: z.string().trim().min(1).max(10000), resourceId: z.string().uuid().optional(), domainRecordId: z.string().uuid().optional(), priority: z.enum(['low', 'normal', 'high', 'urgent']).default('normal') }).parse(req.body);
const staff = can(a.principal, 'tickets.write');
if (!canInOrg(a.principal, b.orgId, 'tickets.create', 'tickets.write')) { if (!canInOrg(a.principal, b.orgId, 'tickets.read', 'tickets.read')) throw notFound(); throw forbidden(); }
if (!staff && b.priority !== 'normal' && b.priority !== 'high') throw forbidden('Diese Priorität kann nur vom Personal gesetzt werden', 'PRIORITY_FORBIDDEN');
if (b.resourceId && b.domainRecordId) throw badRequest('Ein Ticket kann sich nur auf eine Ressource oder eine Domain beziehen, nicht beides');
if (b.resourceId && !(await one('SELECT 1 AS x FROM resources WHERE id = ? AND org_id = ?', [b.resourceId, b.orgId]))) throw badRequest('Ressource gehört nicht zu diesem Kunden');
if (b.domainRecordId && !(await one('SELECT 1 AS x FROM domain_records WHERE id = ? AND org_id = ?', [b.domainRecordId, b.orgId]))) throw badRequest('Domain gehört nicht zu diesem Kunden');
const id = randomUUID(); const messageId = randomUUID();
const number = await tx(async (c) => {
const n = await nextNumber(c);
await run('INSERT INTO tickets (id, number, org_id, resource_id, subject, status, priority, created_by) VALUES (?,?,?,?,?,?,?,?)',
[id, n, b.orgId, b.resourceId ?? null, b.subject, staff ? 'pending_customer' : 'pending_staff', b.priority, a.user.id], c);
await run('INSERT INTO tickets (id, number, org_id, resource_id, domain_record_id, subject, status, priority, created_by) VALUES (?,?,?,?,?,?,?,?,?)',
[id, n, b.orgId, b.resourceId ?? null, b.domainRecordId ?? null, b.subject, staff ? 'pending_customer' : 'pending_staff', b.priority, a.user.id], c);
await run('INSERT INTO ticket_messages (id, ticket_id, author_id, author_kind, body) VALUES (?,?,?,?,?)', [messageId, id, a.user.id, staff ? 'staff' : 'customer', b.body], c);
return n;
});