rework issue system

This commit is contained in:
Matthias Hochmeister
2026-03-24 14:21:17 +01:00
parent abb337c683
commit 6c7531438e
9 changed files with 1260 additions and 189 deletions

View File

@@ -1,21 +1,76 @@
import pool from '../config/database';
import logger from '../utils/logger';
async function getIssues(userId: string, canViewAll: boolean) {
interface IssueFilters {
typ_id?: number[];
prioritaet?: string[];
status?: string[];
erstellt_von?: string;
zugewiesen_an?: string;
}
interface GetIssuesParams {
userId: string;
canViewAll: boolean;
filters?: IssueFilters;
}
const BASE_SELECT = `
SELECT i.*,
it.name AS typ_name, it.icon AS typ_icon, it.farbe AS typ_farbe, it.erlaubt_abgelehnt AS typ_erlaubt_abgelehnt,
u1.name AS erstellt_von_name,
u2.name AS zugewiesen_an_name
FROM issues i
LEFT JOIN issue_typen it ON it.id = i.typ_id
LEFT JOIN users u1 ON u1.id = i.erstellt_von
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
`;
async function getIssues({ userId, canViewAll, filters }: GetIssuesParams) {
try {
const query = `
SELECT i.*,
u1.name AS erstellt_von_name,
u2.name AS zugewiesen_an_name
FROM issues i
LEFT JOIN users u1 ON u1.id = i.erstellt_von
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
${canViewAll ? '' : 'WHERE i.erstellt_von = $1'}
ORDER BY i.created_at DESC
`;
const result = canViewAll
? await pool.query(query)
: await pool.query(query, [userId]);
const conditions: string[] = [];
const values: any[] = [];
let idx = 1;
if (!canViewAll) {
conditions.push(`(i.erstellt_von = $${idx} OR i.zugewiesen_an = $${idx})`);
values.push(userId);
idx++;
}
if (filters?.typ_id && filters.typ_id.length > 0) {
conditions.push(`i.typ_id = ANY($${idx}::int[])`);
values.push(filters.typ_id);
idx++;
}
if (filters?.prioritaet && filters.prioritaet.length > 0) {
conditions.push(`i.prioritaet = ANY($${idx}::text[])`);
values.push(filters.prioritaet);
idx++;
}
if (filters?.status && filters.status.length > 0) {
conditions.push(`i.status = ANY($${idx}::text[])`);
values.push(filters.status);
idx++;
}
if (filters?.erstellt_von) {
conditions.push(`i.erstellt_von = $${idx}`);
values.push(filters.erstellt_von);
idx++;
}
if (filters?.zugewiesen_an) {
conditions.push(`i.zugewiesen_an = $${idx}`);
values.push(filters.zugewiesen_an);
idx++;
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const query = `${BASE_SELECT} ${where} ORDER BY i.created_at DESC`;
const result = await pool.query(query, values);
return result.rows;
} catch (error) {
logger.error('IssueService.getIssues failed', { error });
@@ -26,13 +81,7 @@ async function getIssues(userId: string, canViewAll: boolean) {
async function getIssueById(id: number) {
try {
const result = await pool.query(
`SELECT i.*,
u1.name AS erstellt_von_name,
u2.name AS zugewiesen_an_name
FROM issues i
LEFT JOIN users u1 ON u1.id = i.erstellt_von
LEFT JOIN users u2 ON u2.id = i.zugewiesen_an
WHERE i.id = $1`,
`${BASE_SELECT} WHERE i.id = $1`,
[id]
);
return result.rows[0] || null;
@@ -43,63 +92,96 @@ async function getIssueById(id: number) {
}
async function createIssue(
data: { titel: string; beschreibung?: string; typ?: string; prioritaet?: string },
data: { titel: string; beschreibung?: string; typ_id?: number; prioritaet?: string },
userId: string
) {
try {
const result = await pool.query(
`INSERT INTO issues (titel, beschreibung, typ, prioritaet, erstellt_von)
`INSERT INTO issues (titel, beschreibung, typ_id, prioritaet, erstellt_von)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[
data.titel,
data.beschreibung || null,
data.typ || 'sonstiges',
data.typ_id || 3,
data.prioritaet || 'mittel',
userId,
]
);
return result.rows[0];
return getIssueById(result.rows[0].id);
} catch (error) {
logger.error('IssueService.createIssue failed', { error });
throw new Error('Issue konnte nicht erstellt werden');
}
}
// Sentinel value to explicitly set zugewiesen_an to NULL (not used currently but kept for reference)
const UNASSIGN = '__UNASSIGN__';
async function updateIssue(
id: number,
data: {
titel?: string;
beschreibung?: string;
typ?: string;
typ_id?: number;
prioritaet?: string;
status?: string;
zugewiesen_an?: string | null;
}
) {
try {
const result = await pool.query(
`UPDATE issues
SET titel = COALESCE($1, titel),
beschreibung = COALESCE($2, beschreibung),
typ = COALESCE($3, typ),
prioritaet = COALESCE($4, prioritaet),
status = COALESCE($5, status),
zugewiesen_an = COALESCE($6, zugewiesen_an),
updated_at = NOW()
WHERE id = $7
RETURNING *`,
[
data.titel,
data.beschreibung,
data.typ,
data.prioritaet,
data.status,
data.zugewiesen_an,
id,
]
);
return result.rows[0] || null;
const setClauses: string[] = [];
const values: any[] = [];
let idx = 1;
if (data.titel !== undefined) {
setClauses.push(`titel = $${idx}`);
values.push(data.titel);
idx++;
}
if (data.beschreibung !== undefined) {
setClauses.push(`beschreibung = $${idx}`);
values.push(data.beschreibung);
idx++;
}
if (data.typ_id !== undefined) {
setClauses.push(`typ_id = $${idx}`);
values.push(data.typ_id);
idx++;
}
if (data.prioritaet !== undefined) {
setClauses.push(`prioritaet = $${idx}`);
values.push(data.prioritaet);
idx++;
}
if (data.status !== undefined) {
setClauses.push(`status = $${idx}`);
values.push(data.status);
idx++;
}
if ('zugewiesen_an' in data) {
setClauses.push(`zugewiesen_an = $${idx}`);
values.push(data.zugewiesen_an ?? null);
idx++;
}
if (setClauses.length === 0) {
return getIssueById(id);
}
setClauses.push(`updated_at = NOW()`);
values.push(id);
const query = `
UPDATE issues
SET ${setClauses.join(', ')}
WHERE id = $${idx}
RETURNING id
`;
const result = await pool.query(query, values);
if (result.rows.length === 0) return null;
return getIssueById(id);
} catch (error) {
logger.error('IssueService.updateIssue failed', { error, id });
throw new Error('Issue konnte nicht aktualisiert werden');
@@ -151,6 +233,134 @@ async function addComment(issueId: number, autorId: string, inhalt: string) {
}
}
async function getTypes() {
try {
const result = await pool.query(
`SELECT * FROM issue_typen WHERE aktiv = true ORDER BY sort_order ASC, id ASC`
);
return result.rows;
} catch (error) {
logger.error('IssueService.getTypes failed', { error });
throw new Error('Issue-Typen konnten nicht geladen werden');
}
}
async function createType(data: {
name: string;
parent_id?: number | null;
icon?: string | null;
farbe?: string | null;
erlaubt_abgelehnt?: boolean;
sort_order?: number;
}) {
try {
const result = await pool.query(
`INSERT INTO issue_typen (name, parent_id, icon, farbe, erlaubt_abgelehnt, sort_order)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[
data.name,
data.parent_id ?? null,
data.icon ?? null,
data.farbe ?? null,
data.erlaubt_abgelehnt ?? true,
data.sort_order ?? 0,
]
);
return result.rows[0];
} catch (error) {
logger.error('IssueService.createType failed', { error });
throw new Error('Issue-Typ konnte nicht erstellt werden');
}
}
async function updateType(
id: number,
data: {
name?: string;
parent_id?: number | null;
icon?: string | null;
farbe?: string | null;
erlaubt_abgelehnt?: boolean;
sort_order?: number;
}
) {
try {
const setClauses: string[] = [];
const values: any[] = [];
let idx = 1;
if (data.name !== undefined) {
setClauses.push(`name = $${idx}`);
values.push(data.name);
idx++;
}
if ('parent_id' in data) {
setClauses.push(`parent_id = $${idx}`);
values.push(data.parent_id);
idx++;
}
if ('icon' in data) {
setClauses.push(`icon = $${idx}`);
values.push(data.icon);
idx++;
}
if ('farbe' in data) {
setClauses.push(`farbe = $${idx}`);
values.push(data.farbe);
idx++;
}
if (data.erlaubt_abgelehnt !== undefined) {
setClauses.push(`erlaubt_abgelehnt = $${idx}`);
values.push(data.erlaubt_abgelehnt);
idx++;
}
if (data.sort_order !== undefined) {
setClauses.push(`sort_order = $${idx}`);
values.push(data.sort_order);
idx++;
}
if (setClauses.length === 0) {
const result = await pool.query(`SELECT * FROM issue_typen WHERE id = $1`, [id]);
return result.rows[0] || null;
}
values.push(id);
const query = `UPDATE issue_typen SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`;
const result = await pool.query(query, values);
return result.rows[0] || null;
} catch (error) {
logger.error('IssueService.updateType failed', { error, id });
throw new Error('Issue-Typ konnte nicht aktualisiert werden');
}
}
async function deactivateType(id: number) {
try {
const result = await pool.query(
`UPDATE issue_typen SET aktiv = false WHERE id = $1 RETURNING *`,
[id]
);
return result.rows[0] || null;
} catch (error) {
logger.error('IssueService.deactivateType failed', { error, id });
throw new Error('Issue-Typ konnte nicht deaktiviert werden');
}
}
async function getAssignableMembers() {
try {
const result = await pool.query(
`SELECT id, name FROM users WHERE id IS NOT NULL ORDER BY name`
);
return result.rows;
} catch (error) {
logger.error('IssueService.getAssignableMembers failed', { error });
throw new Error('Mitglieder konnten nicht geladen werden');
}
}
export default {
getIssues,
getIssueById,
@@ -159,4 +369,10 @@ export default {
deleteIssue,
getComments,
addComment,
getTypes,
createType,
updateType,
deactivateType,
getAssignableMembers,
UNASSIGN,
};