2988a70d8f
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>
217 lines
7.2 KiB
TypeScript
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);
|
|
}
|
|
});
|