import { randomUUID } from 'crypto'; import db from '../../db.js'; import { badRequest, UUID } from '../../lib/http.js'; import { mergeAttachmentEventsForApi } from '../../lib/ticket-events-merge.js'; import { mapTicket, ticketJoinSelect, ticketLastActivityExpr, ticketSlaDueExpr, } from '../../lib/mappers.js'; function parseSlaDaysForCreate(v) { if (v === undefined || v === null || v === '') return null; const n = Number(v); if (!Number.isInteger(n) || n < 1 || n > 5) return undefined; return n; } /** Nur wenn Key gesetzt: null = Standard (2 Tage), 1–5 = Tage. */ function parseSlaDaysForUpdate(v) { if (v === null || v === '') return null; const n = Number(v); if (!Number.isInteger(n) || n < 1 || n > 5) return undefined; return n; } function slaDaysEqual(a, b) { const na = a == null || a === '' ? null : Number(a); const nb = b == null || b === '' ? null : Number(b); return na === nb; } const ticketListOrderBy = ` ORDER BY CASE WHEN t.status IN ('OPEN','WAITING') AND datetime('now') > ${ticketSlaDueExpr} THEN 0 ELSE 1 END ASC, CASE WHEN t.status IN ('OPEN','WAITING') AND datetime('now') > ${ticketSlaDueExpr} THEN ${ticketSlaDueExpr} ELSE '9999-12-31' END ASC, ${ticketLastActivityExpr} DESC`; export function registerTicketRoutes(api) { api.get('/tickets', (req, res) => { const { status, priority, machineId, open } = req.query; const cond = ['1=1']; const params = []; const openFilter = open === '1' || open === 'true'; if (openFilter) { cond.push("t.status IN ('OPEN', 'WAITING')"); } else if (status) { cond.push('t.status = ?'); params.push(status); } if (priority) { cond.push('t.priority = ?'); params.push(priority); } if (machineId) { cond.push('t.machine_id = ?'); params.push(machineId); } const sql = `${ticketJoinSelect} WHERE ${cond.join(' AND ')} ${ticketListOrderBy}`; const rows = db.prepare(sql).all(...params); res.json(rows.map(mapTicket)); }); api.post('/tickets', (req, res) => { const { machineId, title, description, status, priority, slaDays } = req.body || {}; if (!machineId || !title || !description) { return badRequest(res, 'Pflichtfelder fehlen.'); } const m = db .prepare('SELECT 1 AS ok FROM machines WHERE id = ?') .get(machineId); if (!m) return res.status(404).json({ message: 'Nicht gefunden' }); const st = status || 'OPEN'; const pr = priority || 'MEDIUM'; const sd = parseSlaDaysForCreate(slaDays); if (sd === undefined) { return badRequest(res, 'slaDays ungültig (1–5 oder weglassen für Standard).'); } const tid = randomUUID(); db.prepare( `INSERT INTO tickets (id, machine_id, title, description, status, priority, sla_days, sla_anchor_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, ).run(tid, machineId, title, description, st, pr, sd); const full = db .prepare(`${ticketJoinSelect} WHERE t.id = ?`) .get(tid); res.status(201).json(mapTicket(full)); }); api.get('/tickets/:id/events', (req, res) => { const { id } = req.params; if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' }); const ex = db.prepare('SELECT 1 AS ok FROM tickets WHERE id = ?').get(id); if (!ex) return res.status(404).json({ message: 'Nicht gefunden' }); const rows = db .prepare( `SELECT * FROM events WHERE ticket_id = ? ORDER BY CASE WHEN type = 'ATTACHMENT' THEN 1 ELSE 0 END ASC, created_at DESC`, ) .all(id); const eventIds = rows.map((r) => r.id); const byEvent = new Map(); if (eventIds.length > 0) { const ph = eventIds.map(() => '?').join(','); const attRows = db .prepare( `SELECT * FROM ticket_attachments WHERE event_id IN (${ph}) ORDER BY created_at ASC`, ) .all(...eventIds); for (const a of attRows) { if (!byEvent.has(a.event_id)) byEvent.set(a.event_id, []); byEvent.get(a.event_id).push(a); } } res.json(mergeAttachmentEventsForApi(rows, byEvent, db)); }); api.get('/tickets/:id', (req, res) => { const { id } = req.params; if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' }); const row = db.prepare(`${ticketJoinSelect} WHERE t.id = ?`).get(id); if (!row) return res.status(404).json({ message: 'Nicht gefunden' }); res.json(mapTicket(row)); }); api.put('/tickets/:id', (req, res) => { const { id } = req.params; if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' }); const cur = db.prepare('SELECT * FROM tickets WHERE id = ?').get(id); if (!cur) return res.status(404).json({ message: 'Nicht gefunden' }); const b = req.body || {}; const next = { title: b.title ?? cur.title, description: b.description ?? cur.description, status: b.status ?? cur.status, priority: b.priority ?? cur.priority, }; let nextSlaDays = cur.sla_days != null ? cur.sla_days : null; let resetSlaAnchor = false; if (Object.prototype.hasOwnProperty.call(b, 'slaDays')) { const parsed = parseSlaDaysForUpdate(b.slaDays); if (parsed === undefined) { return badRequest( res, 'slaDays ungültig (1–5 oder leer für Standard).', ); } nextSlaDays = parsed; resetSlaAnchor = !slaDaysEqual(parsed, cur.sla_days); } const lines = []; if (b.status !== undefined && b.status !== cur.status) { lines.push(`Status: ${cur.status} → ${b.status}`); } if (b.priority !== undefined && b.priority !== cur.priority) { lines.push(`Priorität: ${cur.priority} → ${b.priority}`); } if (b.title !== undefined && b.title !== cur.title) lines.push('Titel geändert'); if (b.description !== undefined && b.description !== cur.description) { lines.push('Beschreibung geändert'); } if ( Object.prototype.hasOwnProperty.call(b, 'slaDays') && !slaDaysEqual(nextSlaDays, cur.sla_days) ) { const label = (d) => d == null ? 'Standard (2 Tage)' : `${d} Tag(e)`; lines.push( `Fälligkeit: ${label(cur.sla_days)} → ${label(nextSlaDays)}`, ); } if (lines.length > 0) { const eid = randomUUID(); db.prepare( `INSERT INTO events (id, ticket_id, type, description, callback_number, teamviewer_id, article_number, remote_duration_seconds, teamviewer_notes) VALUES (?, ?, 'SYSTEM', ?, NULL, NULL, NULL, NULL, NULL)`, ).run(eid, id, lines.join('; ')); } db.prepare( `UPDATE tickets SET title = ?, description = ?, status = ?, priority = ?, sla_days = ?, sla_anchor_at = CASE WHEN ? THEN datetime('now') ELSE sla_anchor_at END, updated_at = datetime('now') WHERE id = ?`, ).run( next.title, next.description, next.status, next.priority, nextSlaDays, resetSlaAnchor ? 1 : 0, id, ); const row = db.prepare(`${ticketJoinSelect} WHERE t.id = ?`).get(id); res.json(mapTicket(row)); }); }