rework from modal to page
This commit is contained in:
@@ -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();
|
||||
|
||||
11
backend/src/database/migrations/059_issue_historie.sql
Normal file
11
backend/src/database/migrations/059_issue_historie.sql
Normal 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);
|
||||
@@ -80,6 +80,12 @@ router.get(
|
||||
issueController.getComments.bind(issueController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id/history',
|
||||
authenticate,
|
||||
issueController.getHistory.bind(issueController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/comments',
|
||||
authenticate,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user