rework issue system
This commit is contained in:
@@ -11,7 +11,34 @@ class IssueController {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
const issues = await issueService.getIssues(userId, canViewAll);
|
||||
|
||||
// Parse filter query params
|
||||
const filters: {
|
||||
typ_id?: number[];
|
||||
prioritaet?: string[];
|
||||
status?: string[];
|
||||
erstellt_von?: string;
|
||||
zugewiesen_an?: string;
|
||||
} = {};
|
||||
|
||||
if (req.query.typ_id) {
|
||||
filters.typ_id = String(req.query.typ_id).split(',').map(Number).filter((n) => !isNaN(n));
|
||||
}
|
||||
if (req.query.prioritaet) {
|
||||
filters.prioritaet = String(req.query.prioritaet).split(',');
|
||||
}
|
||||
if (req.query.status) {
|
||||
filters.status = String(req.query.status).split(',');
|
||||
}
|
||||
if (req.query.erstellt_von) {
|
||||
filters.erstellt_von = req.query.erstellt_von as string;
|
||||
}
|
||||
if (req.query.zugewiesen_an) {
|
||||
filters.zugewiesen_an =
|
||||
req.query.zugewiesen_an === 'me' ? userId : (req.query.zugewiesen_an as string);
|
||||
}
|
||||
|
||||
const issues = await issueService.getIssues({ userId, canViewAll, filters });
|
||||
res.status(200).json({ success: true, data: issues });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getIssues error', { error });
|
||||
@@ -34,7 +61,7 @@ class IssueController {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
if (!canViewAll && issue.erstellt_von !== userId) {
|
||||
if (!canViewAll && issue.erstellt_von !== userId && issue.zugewiesen_an !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
@@ -69,7 +96,8 @@ class IssueController {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
const canEdit = permissionService.hasPermission(groups, 'issues:edit');
|
||||
const canChangeStatus = permissionService.hasPermission(groups, 'issues:change_status');
|
||||
|
||||
const existing = await issueService.getIssueById(id);
|
||||
if (!existing) {
|
||||
@@ -78,19 +106,80 @@ class IssueController {
|
||||
}
|
||||
|
||||
const isOwner = existing.erstellt_von === userId;
|
||||
if (!canManage && !isOwner) {
|
||||
const isAssignee = existing.zugewiesen_an === userId;
|
||||
|
||||
// Determine what update data is allowed
|
||||
let updateData: Record<string, any>;
|
||||
|
||||
if (canEdit) {
|
||||
// Full edit access
|
||||
updateData = { ...req.body };
|
||||
// Explicit null for unassign is handled by 'zugewiesen_an' in data check in service
|
||||
} else if (canChangeStatus || isAssignee) {
|
||||
// Can only change status (+ kommentar is handled separately)
|
||||
updateData = {};
|
||||
if (req.body.status !== undefined) updateData.status = req.body.status;
|
||||
} else if (isOwner) {
|
||||
// Owner without change_status: can only close own issue or reopen from erledigt
|
||||
updateData = {};
|
||||
if (req.body.status !== undefined) {
|
||||
const newStatus = req.body.status;
|
||||
if (newStatus === 'erledigt') {
|
||||
updateData.status = 'erledigt';
|
||||
} else if (newStatus === 'offen' && existing.status === 'erledigt') {
|
||||
// Reopen: require kommentar
|
||||
if (!req.body.kommentar || typeof req.body.kommentar !== 'string' || req.body.kommentar.trim().length === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Beim Wiedereröffnen ist ein Kommentar erforderlich',
|
||||
});
|
||||
return;
|
||||
}
|
||||
updateData.status = 'offen';
|
||||
} else {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung für diese Statusänderung' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Owner trying to change non-status fields without edit permission
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Owners without manage permission can only change status
|
||||
const updateData = canManage ? req.body : { status: req.body.status };
|
||||
// Validate: if setting status to 'abgelehnt', check if type allows it
|
||||
if (updateData.status === 'abgelehnt' && existing.typ_id) {
|
||||
if (!existing.typ_erlaubt_abgelehnt) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Dieser Issue-Typ erlaubt den Status "Abgelehnt" nicht',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const issue = await issueService.updateIssue(id, updateData);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: issue });
|
||||
|
||||
// Handle reopen comment (owner reopen flow)
|
||||
if (isOwner && !canChangeStatus && req.body.status === 'offen' && existing.status === 'erledigt' && req.body.kommentar) {
|
||||
await issueService.addComment(id, userId, `[Wiedereröffnet] ${req.body.kommentar.trim()}`);
|
||||
}
|
||||
|
||||
// If kommentar was provided alongside a status change (not the reopen flow above)
|
||||
if (req.body.kommentar && updateData.status && !(isOwner && !canChangeStatus && req.body.status === 'offen' && existing.status === 'erledigt')) {
|
||||
await issueService.addComment(id, userId, req.body.kommentar.trim());
|
||||
}
|
||||
|
||||
// Re-fetch to include any new comment info
|
||||
const updated = await issueService.getIssueById(id);
|
||||
res.status(200).json({ success: true, data: updated });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.updateIssue error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue konnte nicht aktualisiert werden' });
|
||||
@@ -111,8 +200,8 @@ class IssueController {
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canManage && issue.erstellt_von !== userId) {
|
||||
const canDelete = permissionService.hasPermission(groups, 'issues:delete');
|
||||
if (!canDelete && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +228,7 @@ class IssueController {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
if (!canViewAll && issue.erstellt_von !== userId) {
|
||||
if (!canViewAll && issue.erstellt_von !== userId && issue.zugewiesen_an !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
@@ -170,12 +259,17 @@ class IssueController {
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canViewAll && !canManage && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
const isOwner = issue.erstellt_von === userId;
|
||||
const isAssignee = issue.zugewiesen_an === userId;
|
||||
const canChangeStatus = permissionService.hasPermission(groups, 'issues:change_status');
|
||||
const canEdit = permissionService.hasPermission(groups, 'issues:edit');
|
||||
|
||||
// Authorization: owner, assignee, change_status, or edit can comment
|
||||
if (!isOwner && !isAssignee && !canChangeStatus && !canEdit) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung zum Kommentieren' });
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = await issueService.addComment(issueId, userId, inhalt.trim());
|
||||
res.status(201).json({ success: true, data: comment });
|
||||
} catch (error) {
|
||||
@@ -183,6 +277,81 @@ class IssueController {
|
||||
res.status(500).json({ success: false, message: 'Kommentar konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Type management ---
|
||||
|
||||
async getTypes(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const types = await issueService.getTypes();
|
||||
res.status(200).json({ success: true, data: types });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getTypes error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue-Typen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async createType(req: Request, res: Response): Promise<void> {
|
||||
const { name } = req.body;
|
||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const type = await issueService.createType(req.body);
|
||||
res.status(201).json({ success: true, data: type });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.createType error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue-Typ konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateType(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const type = await issueService.updateType(id, req.body);
|
||||
if (!type) {
|
||||
res.status(404).json({ success: false, message: 'Issue-Typ nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: type });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.updateType error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue-Typ konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteType(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const type = await issueService.deactivateType(id);
|
||||
if (!type) {
|
||||
res.status(404).json({ success: false, message: 'Issue-Typ nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: type });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.deleteType error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue-Typ konnte nicht deaktiviert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getMembers(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const members = await issueService.getAssignableMembers();
|
||||
res.status(200).json({ success: true, data: members });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getMembers error', { error });
|
||||
res.status(500).json({ success: false, message: 'Mitglieder konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new IssueController();
|
||||
|
||||
Reference in New Issue
Block a user