Files
API/src/models/feedback/admin/reports.admin.router.ts
T
Paddy 2988a70d8f Add search to the admin guest book endpoint
At scale (many submissions after a concert), paging through the guest book 20 entries at a time with no way to find a specific person is impractical. Add an optional ?search= query param that filters entries whose name or message contains the term (case-insensitive), with LIKE wildcards escaped so a literal % or _ in a search term can't be misinterpreted as a pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:32:32 +02:00

217 lines
7.2 KiB
TypeScript

/**
* 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);
}
});