587 lines
17 KiB
TypeScript
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,
|
|
};
|