/** * Required External Modules and Interfaces */ import express, {Request, Response} from 'express'; import {Guid} from 'guid-typescript'; import logger from '../../../middleware/logger'; import * as ReportsAdminService from './reports.admin.service'; import * as CsvService from './csv.service'; import * as EventsAdminService from './events.admin.service'; /** * Router Definition */ export const reportsAdminRouter = express.Router(); const sendServerError = (res: Response, e: any) => { let errorGuid = Guid.create().toString(); logger.error('Error handling a request: ' + e.message, {reference: errorGuid}); res.status(500).send({ 'status': 'PROCESSING_ERROR', 'message': 'Internal Server Error. Try again later.', 'reference': errorGuid }); }; /** * @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. * 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 * 401: * description: Unauthorized */ reportsAdminRouter.get('/:eventId/newsletter', async (req: Request, res: Response) => { try { res.status(200).send(await ReportsAdminService.getNewsletterSignups(Number(req.params.eventId))); } 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); } });