import pool from '../config/database'; import logger from '../utils/logger'; 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 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 }); throw new Error('Issues konnten nicht geladen werden'); } } async function getIssueById(id: number) { try { const result = await pool.query( `${BASE_SELECT} WHERE i.id = $1`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('IssueService.getIssueById failed', { error, id }); throw new Error('Issue konnte nicht geladen werden'); } } async function createIssue( data: { titel: string; beschreibung?: string; typ_id?: number; prioritaet?: string }, userId: string ) { try { const result = await pool.query( `INSERT INTO issues (titel, beschreibung, typ_id, prioritaet, erstellt_von) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [ data.titel, data.beschreibung || null, data.typ_id || 3, data.prioritaet || 'mittel', userId, ] ); 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_id?: number; prioritaet?: string; status?: string; zugewiesen_an?: string | null; } ) { try { 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'); } } async function deleteIssue(id: number) { try { const result = await pool.query( `DELETE FROM issues WHERE id = $1 RETURNING id`, [id] ); return result.rows.length > 0; } catch (error) { logger.error('IssueService.deleteIssue failed', { error, id }); throw new Error('Issue konnte nicht gelöscht werden'); } } async function getComments(issueId: number) { try { const result = await pool.query( `SELECT c.*, u.name AS autor_name FROM issue_kommentare c LEFT JOIN users u ON u.id = c.autor_id WHERE c.issue_id = $1 ORDER BY c.created_at ASC`, [issueId] ); return result.rows; } catch (error) { logger.error('IssueService.getComments failed', { error, issueId }); throw new Error('Kommentare konnten nicht geladen werden'); } } async function addComment(issueId: number, autorId: string, inhalt: string) { try { const result = await pool.query( `INSERT INTO issue_kommentare (issue_id, autor_id, inhalt) VALUES ($1, $2, $3) RETURNING *`, [issueId, autorId, inhalt] ); return result.rows[0]; } catch (error) { logger.error('IssueService.addComment failed', { error, issueId }); throw new Error('Kommentar konnte nicht erstellt werden'); } } 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'); } } async function getIssueCounts() { try { const result = await pool.query(` SELECT COALESCE(s.schluessel, i.status) AS schluessel, COALESCE(s.bezeichnung, i.status) AS bezeichnung, COALESCE(s.farbe, 'default') AS farbe, COALESCE(s.ist_abschluss, false) AS ist_abschluss, COALESCE(s.sort_order, 99) AS sort_order, COUNT(*)::int AS count FROM issues i LEFT JOIN issue_statuses s ON s.schluessel = i.status GROUP BY COALESCE(s.schluessel, i.status), COALESCE(s.bezeichnung, i.status), COALESCE(s.farbe, 'default'), COALESCE(s.ist_abschluss, false), COALESCE(s.sort_order, 99) ORDER BY COALESCE(s.sort_order, 99) `); return result.rows; } catch (error) { logger.error('IssueService.getIssueCounts failed', { error }); throw new Error('Issue-Counts konnten nicht geladen werden'); } } async function getIssueStatuses() { try { const result = await pool.query( `SELECT * FROM issue_statuses ORDER BY sort_order ASC, id ASC` ); return result.rows; } catch (error) { logger.error('IssueService.getIssueStatuses failed', { error }); throw new Error('Issue-Status konnten nicht geladen werden'); } } async function createIssueStatus(data: { schluessel: string; bezeichnung: string; farbe?: string; ist_abschluss?: boolean; ist_initial?: boolean; benoetigt_typ_freigabe?: boolean; sort_order?: number; }) { try { const result = await pool.query( `INSERT INTO issue_statuses (schluessel, bezeichnung, farbe, ist_abschluss, ist_initial, benoetigt_typ_freigabe, sort_order) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, [ data.schluessel, data.bezeichnung, data.farbe ?? 'default', data.ist_abschluss ?? false, data.ist_initial ?? false, data.benoetigt_typ_freigabe ?? false, data.sort_order ?? 0, ] ); return result.rows[0]; } catch (error) { logger.error('IssueService.createIssueStatus failed', { error }); throw new Error('Issue-Status konnte nicht erstellt werden'); } } async function updateIssueStatus(id: number, data: { bezeichnung?: string; farbe?: string; ist_abschluss?: boolean; ist_initial?: boolean; benoetigt_typ_freigabe?: boolean; sort_order?: number; aktiv?: boolean; }) { try { const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; } if (data.farbe !== undefined) { setClauses.push(`farbe = $${idx}`); values.push(data.farbe); idx++; } if (data.ist_abschluss !== undefined) { setClauses.push(`ist_abschluss = $${idx}`); values.push(data.ist_abschluss); idx++; } if (data.ist_initial !== undefined) { setClauses.push(`ist_initial = $${idx}`); values.push(data.ist_initial); idx++; } if (data.benoetigt_typ_freigabe !== undefined) { setClauses.push(`benoetigt_typ_freigabe = $${idx}`); values.push(data.benoetigt_typ_freigabe); idx++; } if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; } if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; } if (setClauses.length === 0) { const r = await pool.query(`SELECT * FROM issue_statuses WHERE id = $1`, [id]); return r.rows[0] || null; } values.push(id); const result = await pool.query( `UPDATE issue_statuses SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`, values ); return result.rows[0] || null; } catch (error) { logger.error('IssueService.updateIssueStatus failed', { error, id }); throw new Error('Issue-Status konnte nicht aktualisiert werden'); } } async function deleteIssueStatus(id: number) { try { const result = await pool.query( `UPDATE issue_statuses SET aktiv = false WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('IssueService.deleteIssueStatus failed', { error, id }); throw new Error('Issue-Status konnte nicht deaktiviert werden'); } } async function getIssuePriorities() { try { const result = await pool.query( `SELECT * FROM issue_prioritaeten ORDER BY sort_order ASC, id ASC` ); return result.rows; } catch (error) { logger.error('IssueService.getIssuePriorities failed', { error }); throw new Error('Issue-Prioritäten konnten nicht geladen werden'); } } async function createIssuePriority(data: { schluessel: string; bezeichnung: string; farbe?: string; sort_order?: number; }) { try { const result = await pool.query( `INSERT INTO issue_prioritaeten (schluessel, bezeichnung, farbe, sort_order) VALUES ($1, $2, $3, $4) RETURNING *`, [data.schluessel, data.bezeichnung, data.farbe ?? '#9e9e9e', data.sort_order ?? 0] ); return result.rows[0]; } catch (error) { logger.error('IssueService.createIssuePriority failed', { error }); throw new Error('Priorität konnte nicht erstellt werden'); } } async function updateIssuePriority(id: number, data: { bezeichnung?: string; farbe?: string; sort_order?: number; aktiv?: boolean; }) { try { const setClauses: string[] = []; const values: any[] = []; let idx = 1; if (data.bezeichnung !== undefined) { setClauses.push(`bezeichnung = $${idx}`); values.push(data.bezeichnung); idx++; } if (data.farbe !== undefined) { setClauses.push(`farbe = $${idx}`); values.push(data.farbe); idx++; } if (data.sort_order !== undefined) { setClauses.push(`sort_order = $${idx}`); values.push(data.sort_order); idx++; } if (data.aktiv !== undefined) { setClauses.push(`aktiv = $${idx}`); values.push(data.aktiv); idx++; } if (setClauses.length === 0) { const r = await pool.query(`SELECT * FROM issue_prioritaeten WHERE id = $1`, [id]); return r.rows[0] || null; } values.push(id); const result = await pool.query( `UPDATE issue_prioritaeten SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`, values ); return result.rows[0] || null; } catch (error) { logger.error('IssueService.updateIssuePriority failed', { error, id }); throw new Error('Priorität konnte nicht aktualisiert werden'); } } async function deleteIssuePriority(id: number) { try { const result = await pool.query( `UPDATE issue_prioritaeten SET aktiv = false WHERE id = $1 RETURNING *`, [id] ); return result.rows[0] || null; } catch (error) { logger.error('IssueService.deleteIssuePriority failed', { error, id }); throw new Error('Priorität konnte nicht deaktiviert werden'); } } export default { getIssues, getIssueById, createIssue, updateIssue, deleteIssue, getComments, addComment, getTypes, createType, updateType, deactivateType, getAssignableMembers, getIssueCounts, getIssueStatuses, createIssueStatus, updateIssueStatus, deleteIssueStatus, getIssuePriorities, createIssuePriority, updateIssuePriority, deleteIssuePriority, UNASSIGN, };