/** * Required External Modules and Interfaces */ import express, {Request, Response} from 'express'; import * as ReportsAdminService from './reports.admin.service'; import * as CsvService from './csv.service'; import * as EventsAdminService from './events.admin.service'; import {sendServerError} from '../feedback.errors'; /** * Router Definition */ export const reportsAdminRouter = express.Router(); /** * @swagger * /feedback/admin/events/{eventId}/report: * get: * summary: Aggregated feedback report for one event * description: Song-pick vote counts, song-rating averages, capped free-text list, guest book count, and newsletter sync counts. * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: eventId * required: true * schema: * type: integer * responses: * 200: * description: Success * 404: * description: Unknown event * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/report', async (req: Request, res: Response) => { try { const report = await ReportsAdminService.getReport(Number(req.params.eventId)); if (!report) { res.status(404).send({status: 'NOT_FOUND'}); return; } res.status(200).send(report); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /feedback/admin/events/{eventId}/guestbook: * get: * summary: Guest Book entries for one event * description: Newest first, paginated. * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: eventId * required: true * schema: * type: integer * - in: query * name: page * schema: * type: integer * - in: query * name: pageSize * schema: * type: integer * - in: query * name: search * description: Filters entries whose name or message contains this text (case-insensitive). * schema: * type: string * responses: * 200: * description: Success * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/guestbook', async (req: Request, res: Response) => { try { const page = Math.max(1, Number(req.query.page) || 1); const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50)); const search = typeof req.query.search === 'string' ? req.query.search : undefined; const result = await ReportsAdminService.getGuestBookEntries(Number(req.params.eventId), page, pageSize, search); res.status(200).send(result); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /feedback/admin/events/{eventId}/newsletter: * get: * summary: Newsletter signups for one event * description: Includes sync_status, so failures can be handled manually. Newest first, paginated. * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: eventId * required: true * schema: * type: integer * - in: query * name: page * schema: * type: integer * - in: query * name: pageSize * schema: * type: integer * - in: query * name: search * description: Filters entries whose first name, last name, or email contains this text (case-insensitive). * schema: * type: string * responses: * 200: * description: Success * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/newsletter', async (req: Request, res: Response) => { try { const page = Math.max(1, Number(req.query.page) || 1); const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50)); const search = typeof req.query.search === 'string' ? req.query.search : undefined; const result = await ReportsAdminService.getNewsletterSignups(Number(req.params.eventId), page, pageSize, search); res.status(200).send(result); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /feedback/admin/events/{eventId}/export/responses.csv: * get: * summary: CSV export of all answers for one event * description: Long format, one row per answer. UTF-8 BOM, `;` separator, RFC 4180 escaping, formula-injection guard. * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: eventId * required: true * schema: * type: integer * responses: * 200: * description: CSV file * content: * text/csv: {} * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/export/responses.csv', async (req: Request, res: Response) => { try { const eventId = Number(req.params.eventId); const event = await EventsAdminService.getEventAdmin(eventId); if (!event) { res.status(404).send({status: 'NOT_FOUND'}); return; } const csv = await CsvService.buildResponsesCsv(eventId); res.status(200) .set('Content-Type', 'text/csv; charset=utf-8') .set('Content-Disposition', `attachment; filename="nachklang-feedback-${event.slug}.csv"`) .send(csv); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /feedback/admin/events/{eventId}/export/guestbook.csv: * get: * summary: CSV export of Guest Book entries for one event * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: eventId * required: true * schema: * type: integer * responses: * 200: * description: CSV file * content: * text/csv: {} * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/export/guestbook.csv', async (req: Request, res: Response) => { try { const eventId = Number(req.params.eventId); const event = await EventsAdminService.getEventAdmin(eventId); if (!event) { res.status(404).send({status: 'NOT_FOUND'}); return; } const csv = await CsvService.buildGuestBookCsv(eventId); res.status(200) .set('Content-Type', 'text/csv; charset=utf-8') .set('Content-Disposition', `attachment; filename="nachklang-feedback-guestbook-${event.slug}.csv"`) .send(csv); } catch (e: any) { sendServerError(res, e); } });