368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> => {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> => {
|
|
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/calendar?from=<ISO>&to=<ISO>
|
|
// -------------------------------------------------------------------------
|
|
getCalendarRange = async (req: Request, res: Response): Promise<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> => {
|
|
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<void> => {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> => {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<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
|
|
// -------------------------------------------------------------------------
|
|
getCalendarToken = async (req: Request, res: Response): Promise<void> => {
|
|
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=<token>
|
|
// -------------------------------------------------------------------------
|
|
getIcalExport = async (req: Request, res: Response): Promise<void> => {
|
|
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<void> => {
|
|
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();
|