bug fixes

This commit is contained in:
Matthias Hochmeister
2026-03-03 14:45:46 +01:00
parent 004b141cab
commit 5dfaf7db54
11 changed files with 166 additions and 35 deletions

View File

@@ -18,9 +18,11 @@ function getUserId(req: Request): string {
// ── Controller ────────────────────────────────────────────────────────────────
class AtemschutzController {
async list(_req: Request, res: Response): Promise<void> {
async list(req: Request, res: Response): Promise<void> {
try {
const records = await atemschutzService.getAll();
const userGroups: string[] = (req.user as any)?.groups ?? [];
const userId = getUserId(req);
const records = await atemschutzService.getAll(userGroups, userId);
res.status(200).json({ success: true, data: records });
} catch (error) {
logger.error('Atemschutz list error', { error });
@@ -47,9 +49,11 @@ class AtemschutzController {
}
}
async getStats(_req: Request, res: Response): Promise<void> {
async getStats(req: Request, res: Response): Promise<void> {
try {
const stats = await atemschutzService.getStats();
const userGroups: string[] = (req.user as any)?.groups ?? [];
const userId = getUserId(req);
const stats = await atemschutzService.getStats(userGroups, userId);
res.status(200).json({ success: true, data: stats });
} catch (error) {
logger.error('Atemschutz getStats error', { error });

View File

@@ -265,6 +265,24 @@ class EventsController {
}
};
// -------------------------------------------------------------------------
// POST /api/events/:id/delete (hard delete)
// -------------------------------------------------------------------------
deleteEvent = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
const deleted = await eventsService.deleteEvent(id);
if (!deleted) {
res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' });
return;
}
res.json({ success: true, message: 'Veranstaltung wurde gelöscht' });
} catch (error) {
logger.error('deleteEvent error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Löschen der Veranstaltung' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/calendar-token
// -------------------------------------------------------------------------

View File

@@ -3,8 +3,8 @@ import atemschutzController from '../controllers/atemschutz.controller';
import { authenticate } from '../middleware/auth.middleware';
import { requireGroups } from '../middleware/rbac.middleware';
const ADMIN_GROUPS = ['dashboard_admin'];
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_atemschutz'];
const ADMIN_GROUPS = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
const router = Router();

View File

@@ -143,4 +143,15 @@ router.delete(
eventsController.cancelEvent.bind(eventsController)
);
/**
* POST /api/events/:id/delete
* Hard-delete an event permanently. Requires admin or moderator.
*/
router.post(
'/:id/delete',
authenticate,
requireGroups(WRITE_GROUPS),
eventsController.deleteEvent.bind(eventsController)
);
export default router;

View File

@@ -8,19 +8,31 @@ import {
UpdateAtemschutzData,
} from '../models/atemschutz.model';
const ATEMSCHUTZ_PRIVILEGED = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
class AtemschutzService {
// =========================================================================
// ÜBERSICHT (ALL RECORDS)
// =========================================================================
async getAll(): Promise<AtemschutzUebersicht[]> {
async getAll(userGroups: string[], userId: string): Promise<AtemschutzUebersicht[]> {
const isPrivileged = userGroups.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g));
try {
const result = await pool.query(`
SELECT *
FROM atemschutz_uebersicht
WHERE mitglied_status IS NULL OR mitglied_status IN ('aktiv', 'anwärter')
ORDER BY user_family_name, user_given_name
`);
let result;
if (isPrivileged) {
result = await pool.query(`
SELECT *
FROM atemschutz_uebersicht
WHERE mitglied_status IS NULL OR mitglied_status IN ('aktiv', 'anwärter')
ORDER BY user_family_name, user_given_name
`);
} else {
result = await pool.query(`
SELECT *
FROM atemschutz_uebersicht
WHERE user_id = $1
`, [userId]);
}
return result.rows.map((row) => ({
...row,
@@ -208,7 +220,21 @@ class AtemschutzService {
// DASHBOARD KPI / STATISTIKEN
// =========================================================================
async getStats(): Promise<AtemschutzStats> {
async getStats(userGroups: string[], userId: string): Promise<AtemschutzStats> {
const isPrivileged = userGroups.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g));
if (!isPrivileged) {
return {
total: 0,
mitLehrgang: 0,
untersuchungGueltig: 0,
untersuchungAbgelaufen: 0,
untersuchungBaldFaellig: 0,
leistungstestGueltig: 0,
leistungstestAbgelaufen: 0,
leistungstestBaldFaellig: 0,
einsatzbereit: 0,
};
}
try {
const result = await pool.query(`
SELECT

View File

@@ -490,6 +490,24 @@ class EventsService {
}
}
/**
* Hard-deletes an event (and any recurrence children) from the database.
* Returns true if the event was found and deleted, false if not found.
*/
async deleteEvent(id: string): Promise<boolean> {
logger.info('Hard-deleting event', { id });
// Delete recurrence children first (wiederholung_parent_id references)
await pool.query(
`DELETE FROM veranstaltungen WHERE wiederholung_parent_id = $1`,
[id]
);
const result = await pool.query(
`DELETE FROM veranstaltungen WHERE id = $1`,
[id]
);
return (result.rowCount ?? 0) > 0;
}
// -------------------------------------------------------------------------
// ICAL TOKEN
// -------------------------------------------------------------------------