This commit is contained in:
2026-03-23 02:09:14 +01:00
parent 705329d3c2
commit d8d46ed8e9
61 changed files with 6054 additions and 3116 deletions

View File

@@ -0,0 +1,202 @@
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), 15 = 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 (15 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 (15 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));
});
}