export function parseJsonField(v) { if (v == null) return undefined; if (typeof v === 'object') return v; return JSON.parse(v); } export function mapMachine(r) { const o = { id: r.id, name: r.name, typ: r.typ, seriennummer: r.seriennummer, standort: r.standort, listStatus: r.list_status != null && String(r.list_status).trim() !== '' ? String(r.list_status).trim() : '', createdAt: r.created_at, updatedAt: r.updated_at, }; if (r.extras != null && String(r.extras).trim() !== '') { try { o.extras = typeof r.extras === 'string' ? JSON.parse(r.extras) : r.extras; } catch { o.extras = null; } } return o; } export function mapTicket(r) { const machine_row = parseJsonField(r.machine_row); const t = { id: r.id, machineId: r.machine_id, title: r.title, description: r.description, status: r.status, priority: r.priority, slaDays: r.sla_days != null ? r.sla_days : null, slaAnchorAt: r.sla_anchor_at ?? null, dueAt: r.sla_due_at ?? null, isOverdue: Boolean(r.sla_is_overdue), createdAt: r.created_at, /** Letzte Änderung: neueres aus Ticket-Zeile oder letztem Event (für Anzeige „Aktualisiert“). */ updatedAt: r.ticket_last_activity_at ?? r.updated_at, }; if (machine_row) { t.machine = mapMachine(machine_row); } return t; } function mapAttachmentRow(a, ticketId) { return { id: a.id, originalName: a.original_name, mimeType: a.mime_type ?? null, sizeBytes: a.size_bytes, createdAt: a.created_at, url: `/tickets/${ticketId}/attachments/${a.id}/file`, }; } export function mapEvent(r, attachmentRows = []) { const list = Array.isArray(attachmentRows) ? attachmentRows : []; return { id: r.id, ticketId: r.ticket_id, type: r.type, description: r.description, createdAt: r.created_at, callbackNumber: r.callback_number ?? null, teamviewerId: r.teamviewer_id ?? null, articleNumber: r.article_number ?? null, remoteDurationSeconds: r.remote_duration_seconds != null ? r.remote_duration_seconds : null, teamviewerNotes: r.teamviewer_notes != null && String(r.teamviewer_notes).trim() !== '' ? String(r.teamviewer_notes).trim() : null, attachments: list.map((a) => mapAttachmentRow(a, r.ticket_id)), }; } export function mapPublicUser(r) { return { id: r.id, username: r.username, firstName: r.firstname ?? null, lastName: r.lastname ?? null, role: r.role, active: Boolean(r.active), source: r.source, ldapDn: r.ldap_dn || null, createdAt: r.created_at, updatedAt: r.updated_at, }; } /** Spätester Start der Bearbeitungszeit: gespeicherter Anker oder letztes Nutzer-Event (ohne SYSTEM). */ export const ticketSlaActivityAnchorExpr = `MAX( datetime(COALESCE(t.sla_anchor_at, t.created_at)), COALESCE( (SELECT MAX(datetime(e.created_at)) FROM events e WHERE e.ticket_id = t.id AND e.type IN ('NOTE','CALL','REMOTE','PART','ATTACHMENT')), datetime(COALESCE(t.sla_anchor_at, t.created_at)) ) )`; export const ticketSlaDueExpr = `datetime((${ticketSlaActivityAnchorExpr}), '+' || CAST(COALESCE(t.sla_days, 2) AS TEXT) || ' days')`; /** Spätester Zeitpunkt aus Ticket und Historie (alle Event-Typen). */ export const ticketLastActivityExpr = `MAX( datetime(t.updated_at), COALESCE( (SELECT MAX(datetime(e.created_at)) FROM events e WHERE e.ticket_id = t.id), datetime(t.updated_at) ) )`; export const ticketJoinSelect = ` SELECT t.*, ${ticketLastActivityExpr} AS ticket_last_activity_at, ${ticketSlaDueExpr} AS sla_due_at, (CASE WHEN t.status IN ('OPEN','WAITING') AND datetime('now') > ${ticketSlaDueExpr} THEN 1 ELSE 0 END) AS sla_is_overdue, json_object( 'id', m.id, 'name', m.name, 'typ', m.typ, 'seriennummer', m.seriennummer, 'standort', m.standort, 'list_status', m.list_status, 'extras', m.extras, 'created_at', m.created_at, 'updated_at', m.updated_at ) AS machine_row FROM tickets t JOIN machines m ON m.id = t.machine_id`;