rework from modal to page

This commit is contained in:
Matthias Hochmeister
2026-03-25 10:23:28 +01:00
parent 4ad260ce66
commit feb39d234f
14 changed files with 698 additions and 280 deletions

View File

@@ -167,6 +167,30 @@ class IssueController {
return;
}
// Log history entries for detected changes
const fieldLabels: Record<string, string> = {
status: 'Status geändert',
prioritaet: 'Priorität geändert',
zugewiesen_an: 'Zuweisung geändert',
titel: 'Titel geändert',
beschreibung: 'Beschreibung geändert',
typ_id: 'Typ geändert',
};
for (const [field, label] of Object.entries(fieldLabels)) {
if (field in updateData && updateData[field] !== existing[field]) {
const details: Record<string, unknown> = { von: existing[field], zu: updateData[field] };
if (field === 'zugewiesen_an') {
details.von_name = existing.zugewiesen_an_name || null;
details.zu_name = issue.zugewiesen_an_name || null;
}
if (field === 'status') {
details.von_label = existing.status;
details.zu_label = issue.status;
}
issueService.addHistoryEntry(id, label, details, userId);
}
}
// 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()}`);
@@ -280,6 +304,21 @@ class IssueController {
// --- Type management ---
async getHistory(req: Request, res: Response): Promise<void> {
const issueId = parseInt(param(req, 'id'), 10);
if (isNaN(issueId)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const history = await issueService.getHistory(issueId);
res.status(200).json({ success: true, data: history });
} catch (error) {
logger.error('IssueController.getHistory error', { error });
res.status(500).json({ success: false, message: 'Historie konnte nicht geladen werden' });
}
}
async getTypes(_req: Request, res: Response): Promise<void> {
try {
const types = await issueService.getTypes();

View File

@@ -0,0 +1,11 @@
-- Issue change history
CREATE TABLE IF NOT EXISTS issue_historie (
id SERIAL PRIMARY KEY,
issue_id INT NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
aktion VARCHAR(100) NOT NULL,
details JSONB,
erstellt_von UUID REFERENCES users(id),
erstellt_am TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_issue_historie_issue_id ON issue_historie(issue_id);

View File

@@ -80,6 +80,12 @@ router.get(
issueController.getComments.bind(issueController)
);
router.get(
'/:id/history',
authenticate,
issueController.getHistory.bind(issueController)
);
router.post(
'/:id/comments',
authenticate,

View File

@@ -318,7 +318,8 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
async function getMyRequests(userId: string) {
const result = await pool.query(
`SELECT a.*,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
FROM ausruestung_anfragen a
WHERE a.anfrager_id = $1
ORDER BY a.erstellt_am DESC`,

View File

@@ -188,6 +188,40 @@ async function updateIssue(
}
}
async function addHistoryEntry(
issueId: number,
aktion: string,
details: Record<string, unknown> | null,
userId?: string,
) {
try {
await pool.query(
`INSERT INTO issue_historie (issue_id, aktion, details, erstellt_von)
VALUES ($1, $2, $3, $4)`,
[issueId, aktion, details ? JSON.stringify(details) : null, userId || null],
);
} catch (error) {
logger.error('IssueService.addHistoryEntry failed', { error, issueId });
}
}
async function getHistory(issueId: number) {
try {
const result = await pool.query(
`SELECT h.*, u.name AS erstellt_von_name
FROM issue_historie h
LEFT JOIN users u ON u.id = h.erstellt_von
WHERE h.issue_id = $1
ORDER BY h.erstellt_am DESC`,
[issueId],
);
return result.rows;
} catch (error) {
logger.error('IssueService.getHistory failed', { error, issueId });
return [];
}
}
async function deleteIssue(id: number) {
try {
const result = await pool.query(
@@ -575,6 +609,8 @@ export default {
getAssignableMembers,
getIssueCounts,
getIssueStatuses,
addHistoryEntry,
getHistory,
createIssueStatus,
updateIssueStatus,
deleteIssueStatus,