Files
dashboard/backend/src/services/issue.service.ts
Matthias Hochmeister 5ceae7c364 fix permissions
2026-03-25 07:54:40 +01:00

587 lines
17 KiB
TypeScript

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,
};