Benutzer, Ticketzuweißungen
This commit is contained in:
@@ -12,6 +12,13 @@ import {
|
||||
import { hashPassword } from '../../password.js';
|
||||
import { requireAdmin, requireAuth } from '../../middleware/auth.js';
|
||||
|
||||
const CRM_ROLES = new Set(['admin', 'viewer', 'after_sales']);
|
||||
|
||||
function normalizeUserRole(role) {
|
||||
const r = String(role || '').trim();
|
||||
return CRM_ROLES.has(r) ? r : 'after_sales';
|
||||
}
|
||||
|
||||
export function createAdminRouter() {
|
||||
const admin = Router();
|
||||
admin.use(requireAuth, requireAdmin);
|
||||
@@ -31,7 +38,7 @@ export function createAdminRouter() {
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!un || !password) return badRequest(res, 'Benutzername und Passwort erforderlich.');
|
||||
const r = role === 'admin' ? 'admin' : 'user';
|
||||
const r = normalizeUserRole(role);
|
||||
const id = randomUUID();
|
||||
const ph = await hashPassword(password);
|
||||
try {
|
||||
@@ -66,7 +73,7 @@ export function createAdminRouter() {
|
||||
).run(ph, id);
|
||||
}
|
||||
if (b.role !== undefined) {
|
||||
if (b.role !== 'admin' && b.role !== 'user') {
|
||||
if (!CRM_ROLES.has(b.role)) {
|
||||
return badRequest(res, 'Ungültige Rolle.');
|
||||
}
|
||||
const admins = db
|
||||
@@ -74,7 +81,7 @@ export function createAdminRouter() {
|
||||
"SELECT COUNT(*) AS c FROM users WHERE role = 'admin' AND active = 1",
|
||||
)
|
||||
.get().c;
|
||||
if (cur.role === 'admin' && b.role === 'user' && admins <= 1) {
|
||||
if (cur.role === 'admin' && b.role !== 'admin' && admins <= 1) {
|
||||
return res.status(400).json({ message: 'Letzter Administrator kann nicht herabgestuft werden.' });
|
||||
}
|
||||
db.prepare('UPDATE users SET role = ?, updated_at = datetime(\'now\') WHERE id = ?').run(
|
||||
|
||||
27
server/routes/api/assignable-users.js
Normal file
27
server/routes/api/assignable-users.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import db from '../../db.js';
|
||||
|
||||
function mapAssignable(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
username: r.username,
|
||||
firstName: r.firstname ?? null,
|
||||
lastName: r.lastname ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Aktive CRM-Benutzer für Ticket-Zuweisung (Dropdown). */
|
||||
export function registerAssignableUserRoutes(api) {
|
||||
api.get('/assignable-users', (_req, res) => {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, username, firstname, lastname FROM users
|
||||
WHERE active = 1
|
||||
ORDER BY
|
||||
COALESCE(lastname, '') COLLATE NOCASE ASC,
|
||||
COALESCE(firstname, '') COLLATE NOCASE ASC,
|
||||
username COLLATE NOCASE ASC`,
|
||||
)
|
||||
.all();
|
||||
res.json(rows.map(mapAssignable));
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { randomUUID } from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
import multer from 'multer';
|
||||
import db from '../../db.js';
|
||||
import { requireCrmEdit } from '../../middleware/auth.js';
|
||||
import { badRequest, UUID } from '../../lib/http.js';
|
||||
import { mapEvent } from '../../lib/mappers.js';
|
||||
|
||||
@@ -106,6 +107,7 @@ function discardIncomingFiles(files) {
|
||||
export function registerAttachmentRoutes(api) {
|
||||
api.post(
|
||||
'/tickets/:ticketId/events/attachments',
|
||||
requireCrmEdit,
|
||||
uploadMiddleware,
|
||||
async (req, res) => {
|
||||
const { ticketId } = req.params;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import db from '../../db.js';
|
||||
import { requireCrmEdit } from '../../middleware/auth.js';
|
||||
import { badRequest } from '../../lib/http.js';
|
||||
import { mapEvent } from '../../lib/mappers.js';
|
||||
import { computeRemoteDurationSeconds } from '../../teamviewer.js';
|
||||
@@ -7,7 +8,7 @@ import { computeRemoteDurationSeconds } from '../../teamviewer.js';
|
||||
const EVENT_TYPES_USER = new Set(['NOTE', 'CALL', 'REMOTE', 'PART']);
|
||||
|
||||
export function registerEventRoutes(api) {
|
||||
api.post('/events', (req, res) => {
|
||||
api.post('/events', requireCrmEdit, (req, res) => {
|
||||
const b = req.body || {};
|
||||
const ticketId = b.ticketId;
|
||||
const type = b.type;
|
||||
|
||||
@@ -5,12 +5,14 @@ import { registerTeamViewerRoutes } from '../../teamviewer.js';
|
||||
import { registerAttachmentRoutes } from './attachments.js';
|
||||
import { registerEventRoutes } from './events.js';
|
||||
import { registerMachineRoutes } from './machines.js';
|
||||
import { registerAssignableUserRoutes } from './assignable-users.js';
|
||||
import { registerTicketRoutes } from './tickets.js';
|
||||
|
||||
export function createApiRouter() {
|
||||
const api = Router();
|
||||
api.use(requireAuth);
|
||||
registerMachineRoutes(api);
|
||||
registerAssignableUserRoutes(api);
|
||||
registerTicketRoutes(api);
|
||||
registerAttachmentRoutes(api);
|
||||
registerTeamViewerRoutes(api, loadIntegrations);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
|
||||
import db from '../../db.js';
|
||||
import { badRequest, UUID } from '../../lib/http.js';
|
||||
import { mapMachine } from '../../lib/mappers.js';
|
||||
import { requireCrmEdit } from '../../middleware/auth.js';
|
||||
|
||||
const ALLOWED_LIST_STATUS = new Set([
|
||||
'',
|
||||
@@ -49,7 +50,7 @@ export function registerMachineRoutes(api) {
|
||||
res.json(rows.map(mapMachine));
|
||||
});
|
||||
|
||||
api.post('/machines', (req, res) => {
|
||||
api.post('/machines', requireCrmEdit, (req, res) => {
|
||||
const b = req.body || {};
|
||||
const { name, typ, seriennummer, standort, listStatus } = b;
|
||||
if (!name || !typ || !seriennummer || !standort) {
|
||||
@@ -81,7 +82,7 @@ export function registerMachineRoutes(api) {
|
||||
res.json(mapMachine(row));
|
||||
});
|
||||
|
||||
api.put('/machines/:id', (req, res) => {
|
||||
api.put('/machines/:id', requireCrmEdit, (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' });
|
||||
const cur = db.prepare('SELECT * FROM machines WHERE id = ?').get(id);
|
||||
@@ -122,7 +123,7 @@ export function registerMachineRoutes(api) {
|
||||
res.json(mapMachine(row));
|
||||
});
|
||||
|
||||
api.delete('/machines/:id', (req, res) => {
|
||||
api.delete('/machines/:id', requireCrmEdit, (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' });
|
||||
const cur = db.prepare('SELECT * FROM machines WHERE id = ?').get(id);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import db from '../../db.js';
|
||||
import { requireCrmEdit } from '../../middleware/auth.js';
|
||||
import { badRequest, UUID } from '../../lib/http.js';
|
||||
import { mergeAttachmentEventsForApi } from '../../lib/ticket-events-merge.js';
|
||||
import {
|
||||
@@ -36,9 +37,15 @@ ORDER BY
|
||||
CASE WHEN t.status IN ('OPEN','WAITING') AND datetime('now') > ${ticketSlaDueExpr} THEN ${ticketSlaDueExpr} ELSE '9999-12-31' END ASC,
|
||||
${ticketLastActivityExpr} DESC`;
|
||||
|
||||
function userLabelFromRow(row) {
|
||||
if (!row) return '—';
|
||||
const fn = [row.firstname, row.lastname].filter(Boolean).join(' ').trim();
|
||||
return fn || row.username || row.id;
|
||||
}
|
||||
|
||||
export function registerTicketRoutes(api) {
|
||||
api.get('/tickets', (req, res) => {
|
||||
const { status, priority, machineId, open } = req.query;
|
||||
const { status, priority, machineId, open, assignedTo } = req.query;
|
||||
const cond = ['1=1'];
|
||||
const params = [];
|
||||
const openFilter = open === '1' || open === 'true';
|
||||
@@ -56,12 +63,21 @@ export function registerTicketRoutes(api) {
|
||||
cond.push('t.machine_id = ?');
|
||||
params.push(machineId);
|
||||
}
|
||||
if (assignedTo === 'me' && req.session?.userId) {
|
||||
cond.push('t.assigned_user_id = ?');
|
||||
params.push(req.session.userId);
|
||||
} else if (assignedTo === 'not_me' && req.session?.userId) {
|
||||
cond.push(
|
||||
'(t.assigned_user_id IS NULL OR t.assigned_user_id <> ?)',
|
||||
);
|
||||
params.push(req.session.userId);
|
||||
}
|
||||
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) => {
|
||||
api.post('/tickets', requireCrmEdit, (req, res) => {
|
||||
const { machineId, title, description, status, priority, slaDays } =
|
||||
req.body || {};
|
||||
if (!machineId || !title || !description) {
|
||||
@@ -125,7 +141,7 @@ export function registerTicketRoutes(api) {
|
||||
res.json(mapTicket(row));
|
||||
});
|
||||
|
||||
api.put('/tickets/:id', (req, res) => {
|
||||
api.put('/tickets/:id', requireCrmEdit, (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);
|
||||
@@ -173,6 +189,46 @@ export function registerTicketRoutes(api) {
|
||||
`Fälligkeit: ${label(cur.sla_days)} → ${label(nextSlaDays)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let nextAssignedUserId = cur.assigned_user_id ?? null;
|
||||
if (Object.prototype.hasOwnProperty.call(b, 'assignedUserId')) {
|
||||
const raw = b.assignedUserId;
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
nextAssignedUserId = null;
|
||||
} else if (!UUID.test(String(raw))) {
|
||||
return badRequest(res, 'assignedUserId ungültig.');
|
||||
} else {
|
||||
const u = db
|
||||
.prepare(
|
||||
'SELECT id, username, firstname, lastname FROM users WHERE id = ? AND active = 1',
|
||||
)
|
||||
.get(String(raw));
|
||||
if (!u) {
|
||||
return badRequest(res, 'Zugewiesener Benutzer nicht gefunden oder inaktiv.');
|
||||
}
|
||||
nextAssignedUserId = u.id;
|
||||
}
|
||||
}
|
||||
if (nextAssignedUserId !== (cur.assigned_user_id ?? null)) {
|
||||
const prevU = cur.assigned_user_id
|
||||
? db
|
||||
.prepare(
|
||||
'SELECT id, username, firstname, lastname FROM users WHERE id = ?',
|
||||
)
|
||||
.get(cur.assigned_user_id)
|
||||
: null;
|
||||
const nextU = nextAssignedUserId
|
||||
? db
|
||||
.prepare(
|
||||
'SELECT id, username, firstname, lastname FROM users WHERE id = ?',
|
||||
)
|
||||
.get(nextAssignedUserId)
|
||||
: null;
|
||||
const fromLabel = prevU ? userLabelFromRow(prevU) : 'nicht zugewiesen';
|
||||
const toLabel = nextU ? userLabelFromRow(nextU) : 'nicht zugewiesen';
|
||||
lines.push(`Zuweisung: ${fromLabel} → ${toLabel}`);
|
||||
}
|
||||
|
||||
if (lines.length > 0) {
|
||||
const eid = randomUUID();
|
||||
db.prepare(
|
||||
@@ -184,6 +240,7 @@ export function registerTicketRoutes(api) {
|
||||
db.prepare(
|
||||
`UPDATE tickets SET title = ?, description = ?, status = ?, priority = ?, sla_days = ?,
|
||||
sla_anchor_at = CASE WHEN ? THEN datetime('now') ELSE sla_anchor_at END,
|
||||
assigned_user_id = ?,
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
).run(
|
||||
@@ -193,6 +250,7 @@ export function registerTicketRoutes(api) {
|
||||
next.priority,
|
||||
nextSlaDays,
|
||||
resetSlaAnchor ? 1 : 0,
|
||||
nextAssignedUserId,
|
||||
id,
|
||||
);
|
||||
|
||||
|
||||
@@ -10,12 +10,17 @@ const router = Router();
|
||||
|
||||
function userPayload(row) {
|
||||
if (!row) return null;
|
||||
const role = row.role;
|
||||
const canAdmin = role === 'admin';
|
||||
const canEditCrm = canAdmin || role === 'after_sales';
|
||||
return {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
role: row.role,
|
||||
role,
|
||||
firstName: row.firstname ?? null,
|
||||
lastName: row.lastname ?? null,
|
||||
canAdmin,
|
||||
canEditCrm,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user