fix permissions

This commit is contained in:
Matthias Hochmeister
2026-03-25 07:54:40 +01:00
parent 59140939df
commit 5ceae7c364
6 changed files with 3 additions and 334 deletions

View File

@@ -464,69 +464,6 @@ class IssueController {
res.status(500).json({ success: false, message: 'Priorität konnte nicht deaktiviert werden' }); res.status(500).json({ success: false, message: 'Priorität konnte nicht deaktiviert werden' });
} }
} }
async getStatusmeldungen(_req: Request, res: Response): Promise<void> {
try {
const items = await issueService.getStatusmeldungen();
res.status(200).json({ success: true, data: items });
} catch (error) {
logger.error('IssueController.getStatusmeldungen error', { error });
res.status(500).json({ success: false, message: 'Statusmeldungen konnten nicht geladen werden' });
}
}
async createStatusmeldung(req: Request, res: Response): Promise<void> {
const { titel } = req.body;
if (!titel || typeof titel !== 'string' || titel.trim().length === 0) {
res.status(400).json({ success: false, message: 'Titel ist erforderlich' });
return;
}
try {
const item = await issueService.createStatusmeldung(req.body, req.user!.id);
res.status(201).json({ success: true, data: item });
} catch (error) {
logger.error('IssueController.createStatusmeldung error', { error });
res.status(500).json({ success: false, message: 'Statusmeldung konnte nicht erstellt werden' });
}
}
async updateStatusmeldung(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 item = await issueService.updateStatusmeldung(id, req.body);
if (!item) {
res.status(404).json({ success: false, message: 'Statusmeldung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: item });
} catch (error) {
logger.error('IssueController.updateStatusmeldung error', { error });
res.status(500).json({ success: false, message: 'Statusmeldung konnte nicht aktualisiert werden' });
}
}
async deleteStatusmeldung(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 deleted = await issueService.deleteStatusmeldung(id);
if (!deleted) {
res.status(404).json({ success: false, message: 'Statusmeldung nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Statusmeldung gelöscht' });
} catch (error) {
logger.error('IssueController.deleteStatusmeldung error', { error });
res.status(500).json({ success: false, message: 'Statusmeldung konnte nicht gelöscht werden' });
}
}
} }
export default new IssueController(); export default new IssueController();

View File

@@ -5,34 +5,6 @@ import { requirePermission } from '../middleware/rbac.middleware';
const router = Router(); const router = Router();
// --- Statusmeldungen routes (BEFORE /:id) ---
router.get(
'/statusmeldungen',
authenticate,
issueController.getStatusmeldungen.bind(issueController)
);
router.post(
'/statusmeldungen',
authenticate,
requirePermission('issues:edit_settings'),
issueController.createStatusmeldung.bind(issueController)
);
router.patch(
'/statusmeldungen/:id',
authenticate,
requirePermission('issues:edit_settings'),
issueController.updateStatusmeldung.bind(issueController)
);
router.delete(
'/statusmeldungen/:id',
authenticate,
requirePermission('issues:edit_settings'),
issueController.deleteStatusmeldung.bind(issueController)
);
// --- Widget summary route (BEFORE /:id) --- // --- Widget summary route (BEFORE /:id) ---
router.get( router.get(
'/widget-summary', '/widget-summary',

View File

@@ -559,79 +559,6 @@ async function deleteIssuePriority(id: number) {
} }
} }
async function getStatusmeldungen() {
try {
const result = await pool.query(
`SELECT * FROM issue_statusmeldungen WHERE aktiv = true ORDER BY created_at DESC`
);
return result.rows;
} catch (error) {
logger.error('IssueService.getStatusmeldungen failed', { error });
throw new Error('Statusmeldungen konnten nicht geladen werden');
}
}
async function createStatusmeldung(
data: { titel: string; inhalt?: string; schwere?: string },
userId: string
) {
try {
const result = await pool.query(
`INSERT INTO issue_statusmeldungen (titel, inhalt, schwere, erstellt_von)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[data.titel, data.inhalt ?? null, data.schwere ?? 'info', userId]
);
return result.rows[0];
} catch (error) {
logger.error('IssueService.createStatusmeldung failed', { error });
throw new Error('Statusmeldung konnte nicht erstellt werden');
}
}
async function updateStatusmeldung(
id: number,
data: { titel?: string; inhalt?: string; schwere?: string; aktiv?: boolean }
) {
try {
const setClauses: string[] = [];
const values: any[] = [];
let idx = 1;
if (data.titel !== undefined) { setClauses.push(`titel = $${idx}`); values.push(data.titel); idx++; }
if ('inhalt' in data) { setClauses.push(`inhalt = $${idx}`); values.push(data.inhalt ?? null); idx++; }
if (data.schwere !== undefined) { setClauses.push(`schwere = $${idx}`); values.push(data.schwere); 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_statusmeldungen WHERE id = $1`, [id]);
return r.rows[0] || null;
}
values.push(id);
const result = await pool.query(
`UPDATE issue_statusmeldungen SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
values
);
return result.rows[0] || null;
} catch (error) {
logger.error('IssueService.updateStatusmeldung failed', { error, id });
throw new Error('Statusmeldung konnte nicht aktualisiert werden');
}
}
async function deleteStatusmeldung(id: number) {
try {
const result = await pool.query(
`DELETE FROM issue_statusmeldungen WHERE id = $1 RETURNING id`,
[id]
);
return result.rows.length > 0;
} catch (error) {
logger.error('IssueService.deleteStatusmeldung failed', { error, id });
throw new Error('Statusmeldung konnte nicht gelöscht werden');
}
}
export default { export default {
getIssues, getIssues,
@@ -655,9 +582,5 @@ export default {
createIssuePriority, createIssuePriority,
updateIssuePriority, updateIssuePriority,
deleteIssuePriority, deleteIssuePriority,
getStatusmeldungen,
createStatusmeldung,
updateStatusmeldung,
deleteStatusmeldung,
UNASSIGN, UNASSIGN,
}; };

View File

@@ -20,7 +20,7 @@ import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { issuesApi } from '../services/issues'; import { issuesApi } from '../services/issues';
import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayload, IssueFilters, AssignableMember, IssueStatusmeldung, IssueStatusDef, IssuePriorityDef } from '../types/issue.types'; import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayload, IssueFilters, AssignableMember, IssueStatusDef, IssuePriorityDef } from '../types/issue.types';
// ── Helpers ── // ── Helpers ──
@@ -755,12 +755,6 @@ function IssueSettings() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification(); const { showSuccess, showError } = useNotification();
// ── Statusmeldungen state ──
const [createOpen, setCreateOpen] = useState(false);
const [createData, setCreateData] = useState<{ titel: string; inhalt: string; schwere: 'info' | 'warnung' | 'fehler' }>({ titel: '', inhalt: '', schwere: 'info' });
const [editId, setEditId] = useState<number | null>(null);
const [editData, setEditData] = useState<Partial<IssueStatusmeldung>>({});
// ── Status state ── // ── Status state ──
const [statusCreateOpen, setStatusCreateOpen] = useState(false); const [statusCreateOpen, setStatusCreateOpen] = useState(false);
const [statusCreateData, setStatusCreateData] = useState<Partial<IssueStatusDef>>({ schluessel: '', bezeichnung: '', farbe: 'default', ist_abschluss: false, ist_initial: false, benoetigt_typ_freigabe: false, sort_order: 0 }); const [statusCreateData, setStatusCreateData] = useState<Partial<IssueStatusDef>>({ schluessel: '', bezeichnung: '', farbe: 'default', ist_abschluss: false, ist_initial: false, benoetigt_typ_freigabe: false, sort_order: 0 });
@@ -773,11 +767,6 @@ function IssueSettings() {
const [prioEditId, setPrioEditId] = useState<number | null>(null); const [prioEditId, setPrioEditId] = useState<number | null>(null);
const [prioEditData, setPrioEditData] = useState<Partial<IssuePriorityDef>>({}); const [prioEditData, setPrioEditData] = useState<Partial<IssuePriorityDef>>({});
const { data: statusmeldungen = [], isLoading: smLoading } = useQuery({
queryKey: ['issue-statusmeldungen'],
queryFn: issuesApi.getStatusmeldungen,
});
const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({ const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({
queryKey: ['issue-statuses'], queryKey: ['issue-statuses'],
queryFn: issuesApi.getStatuses, queryFn: issuesApi.getStatuses,
@@ -788,40 +777,6 @@ function IssueSettings() {
queryFn: issuesApi.getPriorities, queryFn: issuesApi.getPriorities,
}); });
// Statusmeldungen mutations
const createSmMut = useMutation({
mutationFn: (data: { titel: string; inhalt?: string; schwere?: string }) => issuesApi.createStatusmeldung(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung erstellt');
setCreateOpen(false);
setCreateData({ titel: '', inhalt: '', schwere: 'info' });
},
onError: () => showError('Fehler beim Erstellen'),
});
const updateSmMut = useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<IssueStatusmeldung> }) => issuesApi.updateStatusmeldung(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung aktualisiert');
setEditId(null);
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const deleteSmMut = useMutation({
mutationFn: (id: number) => issuesApi.deleteStatusmeldung(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung gelöscht');
},
onError: () => showError('Fehler beim Löschen'),
});
const schwereColors: Record<string, 'info' | 'warning' | 'error'> = { info: 'info', warnung: 'warning', fehler: 'error' };
const schwereLabels: Record<string, string> = { info: 'Info', warnung: 'Warnung', fehler: 'Fehler' };
// Status mutations // Status mutations
const createStatusMut = useMutation({ const createStatusMut = useMutation({
mutationFn: (data: Partial<IssueStatusDef>) => issuesApi.createStatus(data), mutationFn: (data: Partial<IssueStatusDef>) => issuesApi.createStatus(data),
@@ -988,76 +943,7 @@ function IssueSettings() {
)} )}
</Box> </Box>
{/* Section 3: Statusmeldungen */} {/* Section 3: Kategorien */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Statusmeldungen</Typography>
<Button startIcon={<AddIcon />} variant="contained" size="small" onClick={() => setCreateOpen(true)}>
Neue Meldung
</Button>
</Box>
{smLoading ? <CircularProgress /> : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Titel</TableCell>
<TableCell>Schwere</TableCell>
<TableCell>Aktiv</TableCell>
<TableCell>Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{statusmeldungen.length === 0 ? (
<TableRow>
<TableCell colSpan={4} sx={{ textAlign: 'center', color: 'text.secondary' }}>
Keine Statusmeldungen
</TableCell>
</TableRow>
) : statusmeldungen.map((sm) => (
<TableRow key={sm.id}>
{editId === sm.id ? (
<>
<TableCell>
<TextField size="small" value={editData.titel ?? sm.titel} onChange={(e) => setEditData({ ...editData, titel: e.target.value })} />
</TableCell>
<TableCell>
<Select size="small" value={editData.schwere ?? sm.schwere} onChange={(e) => setEditData({ ...editData, schwere: e.target.value as 'info' | 'warnung' | 'fehler' })}>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="warnung">Warnung</MenuItem>
<MenuItem value="fehler">Fehler</MenuItem>
</Select>
</TableCell>
<TableCell>
<Switch checked={editData.aktiv ?? sm.aktiv} onChange={(e) => setEditData({ ...editData, aktiv: e.target.checked })} size="small" />
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => updateSmMut.mutate({ id: sm.id, data: editData })}><CheckIcon /></IconButton>
<IconButton size="small" onClick={() => setEditId(null)}><CloseIcon /></IconButton>
</TableCell>
</>
) : (
<>
<TableCell>{sm.titel}</TableCell>
<TableCell><Chip label={schwereLabels[sm.schwere]} color={schwereColors[sm.schwere]} size="small" /></TableCell>
<TableCell>
<Switch checked={sm.aktiv} onChange={(e) => updateSmMut.mutate({ id: sm.id, data: { aktiv: e.target.checked } })} size="small" />
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => { setEditId(sm.id); setEditData({ titel: sm.titel, schwere: sm.schwere, aktiv: sm.aktiv, inhalt: sm.inhalt ?? '' }); }}><EditIcon /></IconButton>
<IconButton size="small" onClick={() => deleteSmMut.mutate(sm.id)}><DeleteIcon /></IconButton>
</TableCell>
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
{/* Section 4: Kategorien */}
<Box> <Box>
<IssueTypeAdmin /> <IssueTypeAdmin />
</Box> </Box>
@@ -1100,28 +986,6 @@ function IssueSettings() {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{/* Create Statusmeldung Dialog */}
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Neue Statusmeldung</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField label="Titel" required fullWidth value={createData.titel} onChange={(e) => setCreateData({ ...createData, titel: e.target.value })} autoFocus />
<TextField label="Inhalt" fullWidth multiline rows={3} value={createData.inhalt} onChange={(e) => setCreateData({ ...createData, inhalt: e.target.value })} />
<FormControl fullWidth size="small">
<InputLabel>Schwere</InputLabel>
<Select label="Schwere" value={createData.schwere} onChange={(e) => setCreateData({ ...createData, schwere: e.target.value as 'info' | 'warnung' | 'fehler' })}>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="warnung">Warnung</MenuItem>
<MenuItem value="fehler">Fehler</MenuItem>
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setCreateOpen(false)}>Abbrechen</Button>
<Button variant="contained" onClick={() => createSmMut.mutate(createData)} disabled={!createData.titel.trim() || createSmMut.isPending}>
Erstellen
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,5 @@
import { api } from './api'; import { api } from './api';
import type { Issue, IssueComment, CreateIssuePayload, UpdateIssuePayload, IssueTyp, IssueFilters, AssignableMember, IssueStatusmeldung, IssueStatusDef, IssuePriorityDef, IssueWidgetSummary } from '../types/issue.types'; import type { Issue, IssueComment, CreateIssuePayload, UpdateIssuePayload, IssueTyp, IssueFilters, AssignableMember, IssueStatusDef, IssuePriorityDef, IssueWidgetSummary } from '../types/issue.types';
export const issuesApi = { export const issuesApi = {
getIssues: async (filters?: IssueFilters): Promise<Issue[]> => { getIssues: async (filters?: IssueFilters): Promise<Issue[]> => {
@@ -57,22 +57,6 @@ export const issuesApi = {
const r = await api.get('/api/issues/members'); const r = await api.get('/api/issues/members');
return r.data.data; return r.data.data;
}, },
// Statusmeldungen CRUD
getStatusmeldungen: async (): Promise<IssueStatusmeldung[]> => {
const r = await api.get('/api/issues/statusmeldungen');
return r.data.data;
},
createStatusmeldung: async (data: { titel: string; inhalt?: string; schwere?: string }): Promise<IssueStatusmeldung> => {
const r = await api.post('/api/issues/statusmeldungen', data);
return r.data.data;
},
updateStatusmeldung: async (id: number, data: Partial<IssueStatusmeldung>): Promise<IssueStatusmeldung> => {
const r = await api.patch(`/api/issues/statusmeldungen/${id}`, data);
return r.data.data;
},
deleteStatusmeldung: async (id: number): Promise<void> => {
await api.delete(`/api/issues/statusmeldungen/${id}`);
},
// Widget summary // Widget summary
getWidgetSummary: async (): Promise<IssueWidgetSummary> => { getWidgetSummary: async (): Promise<IssueWidgetSummary> => {
const r = await api.get('/api/issues/widget-summary'); const r = await api.get('/api/issues/widget-summary');

View File

@@ -68,17 +68,6 @@ export interface AssignableMember {
name: string; name: string;
} }
export interface IssueStatusmeldung {
id: number;
titel: string;
inhalt: string | null;
schwere: 'info' | 'warnung' | 'fehler';
aktiv: boolean;
erstellt_von: string | null;
created_at: string;
updated_at: string;
}
export interface IssueStatusDef { export interface IssueStatusDef {
id: number; id: number;
schluessel: string; schluessel: string;