import { Request, Response } from 'express'; import eventsService from '../services/events.service'; import { CreateKategorieSchema, UpdateKategorieSchema, CreateVeranstaltungSchema, UpdateVeranstaltungSchema, CancelVeranstaltungSchema, } from '../models/events.model'; import logger from '../utils/logger'; // --------------------------------------------------------------------------- // Helper — extract userGroups from request // --------------------------------------------------------------------------- function getUserGroups(req: Request): string[] { return (req.user as any)?.groups ?? []; } // --------------------------------------------------------------------------- // Controller // --------------------------------------------------------------------------- class EventsController { // ------------------------------------------------------------------------- // GET /api/events/kategorien // ------------------------------------------------------------------------- listKategorien = async (_req: Request, res: Response): Promise => { try { const data = await eventsService.getKategorien(); res.json({ success: true, data }); } catch (error) { logger.error('listKategorien error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Kategorien' }); } }; // ------------------------------------------------------------------------- // POST /api/events/kategorien // ------------------------------------------------------------------------- createKategorie = async (req: Request, res: Response): Promise => { try { const parsed = CreateKategorieSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const data = await eventsService.createKategorie(parsed.data, req.user!.id); res.status(201).json({ success: true, data }); } catch (error) { logger.error('createKategorie error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Erstellen der Kategorie' }); } }; // ------------------------------------------------------------------------- // PATCH /api/events/kategorien/:id // ------------------------------------------------------------------------- updateKategorie = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; const parsed = UpdateKategorieSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const data = await eventsService.updateKategorie(id, parsed.data); if (!data) { res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' }); return; } res.json({ success: true, data }); } catch (error) { logger.error('updateKategorie error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Kategorie' }); } }; // ------------------------------------------------------------------------- // DELETE /api/events/kategorien/:id // ------------------------------------------------------------------------- deleteKategorie = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; await eventsService.deleteKategorie(id); res.json({ success: true, message: 'Kategorie wurde gelöscht' }); } catch (error: any) { if ( error.message === 'Kategorie nicht gefunden' || error.message?.includes('noch Veranstaltungen') ) { res.status(409).json({ success: false, message: error.message }); return; } logger.error('deleteKategorie error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Löschen der Kategorie' }); } }; // ------------------------------------------------------------------------- // GET /api/events/groups // ------------------------------------------------------------------------- getAvailableGroups = async (_req: Request, res: Response): Promise => { try { const groups = await eventsService.getAvailableGroups(); res.json({ success: true, data: groups }); } catch (error) { logger.error('getAvailableGroups error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Gruppen' }); } }; // ------------------------------------------------------------------------- // GET /api/events/conflicts?from=&to=&excludeId= // ------------------------------------------------------------------------- checkConflicts = async (req: Request, res: Response): Promise => { try { const fromStr = req.query.from as string | undefined; const toStr = req.query.to as string | undefined; const excludeId = req.query.excludeId as string | undefined; if (!fromStr || !toStr) { res.status(400).json({ success: false, message: 'Query-Parameter "from" und "to" sind erforderlich (ISO-8601)', }); return; } const from = new Date(fromStr); const to = new Date(toStr); if (isNaN(from.getTime()) || isNaN(to.getTime())) { res.status(400).json({ success: false, message: 'Ungültiges Datumsformat' }); return; } const data = await eventsService.checkConflicts(from, to, excludeId); res.json({ success: true, data }); } catch (error) { logger.error('checkConflicts error', { error }); res.status(500).json({ success: false, message: 'Fehler bei der Konfliktprüfung' }); } }; // ------------------------------------------------------------------------- // GET /api/events/calendar?from=&to= // ------------------------------------------------------------------------- getCalendarRange = async (req: Request, res: Response): Promise => { try { const fromStr = req.query.from as string | undefined; const toStr = req.query.to as string | undefined; if (!fromStr || !toStr) { res.status(400).json({ success: false, message: 'Query-Parameter "from" und "to" sind erforderlich (ISO-8601)', }); return; } const from = new Date(fromStr); const to = new Date(toStr); if (isNaN(from.getTime()) || isNaN(to.getTime())) { res.status(400).json({ success: false, message: 'Ungültiges Datumsformat' }); return; } if (to < from) { res.status(400).json({ success: false, message: '"to" muss nach "from" liegen' }); return; } const userGroups = getUserGroups(req); const data = await eventsService.getEventsByDateRange(from, to, userGroups); res.json({ success: true, data }); } catch (error) { logger.error('getCalendarRange error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden des Kalenders' }); } }; // ------------------------------------------------------------------------- // GET /api/events/upcoming?limit=10 // ------------------------------------------------------------------------- getUpcoming = async (req: Request, res: Response): Promise => { try { const limit = Math.min(Number(req.query.limit) || 10, 50); const userGroups = getUserGroups(req); const data = await eventsService.getUpcomingEvents(limit, userGroups); res.json({ success: true, data }); } catch (error) { logger.error('getUpcoming error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Veranstaltungen' }); } }; // ------------------------------------------------------------------------- // GET /api/events/:id // ------------------------------------------------------------------------- getById = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; const event = await eventsService.getById(id); if (!event) { res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' }); return; } res.json({ success: true, data: event }); } catch (error) { logger.error('getById error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Veranstaltung' }); } }; // ------------------------------------------------------------------------- // POST /api/events // ------------------------------------------------------------------------- createEvent = async (req: Request, res: Response): Promise => { try { const parsed = CreateVeranstaltungSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const data = await eventsService.createEvent(parsed.data, req.user!.id); res.status(201).json({ success: true, data }); } catch (error) { logger.error('createEvent error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Erstellen der Veranstaltung' }); } }; // ------------------------------------------------------------------------- // PATCH /api/events/:id // ------------------------------------------------------------------------- updateEvent = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; const parsed = UpdateVeranstaltungSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const data = await eventsService.updateEvent(id, parsed.data); if (!data) { res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' }); return; } res.json({ success: true, data }); } catch (error) { logger.error('updateEvent error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Veranstaltung' }); } }; // ------------------------------------------------------------------------- // DELETE /api/events/:id (soft cancel) // ------------------------------------------------------------------------- cancelEvent = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; const parsed = CancelVeranstaltungSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } await eventsService.cancelEvent(id, parsed.data.abgesagt_grund, req.user!.id); res.json({ success: true, message: 'Veranstaltung wurde abgesagt' }); } catch (error: any) { if (error.message === 'Event not found') { res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' }); return; } logger.error('cancelEvent error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Absagen der Veranstaltung' }); } }; // ------------------------------------------------------------------------- // POST /api/events/:id/delete (hard delete) // ------------------------------------------------------------------------- deleteEvent = async (req: Request, res: Response): Promise => { try { const { id } = req.params as Record; const mode = (req.body?.mode as string) || 'all'; if (!['all', 'single', 'future'].includes(mode)) { res.status(400).json({ success: false, message: 'Ungültiger Löschmodus. Erlaubt: all, single, future' }); return; } const deleted = await eventsService.deleteEvent(id, mode as 'all' | 'single' | 'future'); 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 // ------------------------------------------------------------------------- getCalendarToken = async (req: Request, res: Response): Promise => { try { if (!req.user) { res.status(401).json({ success: false, message: 'Nicht authentifiziert' }); return; } const data = await eventsService.getOrCreateIcalToken(req.user.id); res.json({ success: true, data }); } catch (error) { logger.error('getCalendarToken error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden des Kalender-Tokens' }); } }; // ------------------------------------------------------------------------- // GET /api/events/calendar.ics?token= // ------------------------------------------------------------------------- getIcalExport = async (req: Request, res: Response): Promise => { try { const token = req.query.token as string | undefined; if (!token) { res.status(400).send('Token required'); return; } const ical = await eventsService.getIcalExport(token); if (!ical) { res.status(404).send('Invalid token'); return; } res.setHeader('Content-Type', 'text/calendar; charset=utf-8'); res.setHeader('Content-Disposition', 'attachment; filename="veranstaltungen.ics"'); // 30-minute cache — calendar clients typically re-fetch at this interval res.setHeader('Cache-Control', 'max-age=1800, public'); res.send(ical); } catch (error) { logger.error('getIcalExport error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Erstellen des Kalender-Exports' }); } }; // ------------------------------------------------------------------------- // POST /api/events/import // ------------------------------------------------------------------------- importEvents = async (req: Request, res: Response): Promise => { try { const { events } = req.body as { events: unknown[] }; if (!Array.isArray(events) || events.length === 0) { res.status(400).json({ success: false, message: 'Keine Ereignisse zum Importieren' }); return; } const userId = (req.user as any)?.id ?? 'unknown'; const created: number[] = []; const errors: string[] = []; for (let i = 0; i < events.length; i++) { try { const parsed = CreateVeranstaltungSchema.safeParse(events[i]); if (!parsed.success) { errors.push(`Zeile ${i + 2}: ${parsed.error.issues.map((e) => e.message).join(', ')}`); continue; } await eventsService.createEvent(parsed.data, userId); created.push(i); } catch (e) { errors.push(`Zeile ${i + 2}: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`); } } res.status(200).json({ success: true, data: { created: created.length, errors }, }); } catch (error) { logger.error('importEvents error', { error }); res.status(500).json({ success: false, message: 'Import fehlgeschlagen' }); } }; } export default new EventsController();