From 01a914f31e6853f470715eb18c68d2107c561b0f Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 13 Aug 2026 21:49:00 +0200 Subject: [PATCH] Paginate and add search to the newsletter signups endpoint Unlike guest book and free-text, newsletter signups had no LIMIT at all and the frontend rendered every row in one plain table. Newsletter opt-in is a single checkbox rather than typed text, so it's plausibly the largest per-event list - fix it the same way as guest book: paginated (page/pageSize, capped at 200/page) plus an optional ?search= over first name, last name, and email. Co-Authored-By: Claude Sonnet 5 --- .../feedback/admin/reports.admin.router.ts | 21 +++++++++++-- .../feedback/admin/reports.admin.service.ts | 30 ++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/models/feedback/admin/reports.admin.router.ts b/src/models/feedback/admin/reports.admin.router.ts index a048405..eed0244 100644 --- a/src/models/feedback/admin/reports.admin.router.ts +++ b/src/models/feedback/admin/reports.admin.router.ts @@ -110,7 +110,7 @@ reportsAdminRouter.get('/:eventId/guestbook', async (req: Request, res: Response * /feedback/admin/events/{eventId}/newsletter: * get: * summary: Newsletter signups for one event - * description: Includes sync_status, so failures can be handled manually. + * description: Includes sync_status, so failures can be handled manually. Newest first, paginated. * tags: [feedback-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' @@ -120,6 +120,19 @@ reportsAdminRouter.get('/:eventId/guestbook', async (req: Request, res: Response * 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 @@ -128,7 +141,11 @@ reportsAdminRouter.get('/:eventId/guestbook', async (req: Request, res: Response */ reportsAdminRouter.get('/:eventId/newsletter', async (req: Request, res: Response) => { try { - res.status(200).send(await ReportsAdminService.getNewsletterSignups(Number(req.params.eventId))); + 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); } diff --git a/src/models/feedback/admin/reports.admin.service.ts b/src/models/feedback/admin/reports.admin.service.ts index 32f92da..5210e21 100644 --- a/src/models/feedback/admin/reports.admin.service.ts +++ b/src/models/feedback/admin/reports.admin.service.ts @@ -248,17 +248,33 @@ export const deleteSubmission = async (submissionId: number): Promise = } }; -export const getNewsletterSignups = async (eventId: number): Promise => { +export const getNewsletterSignups = async ( + eventId: number, + page: number, + pageSize: number, + search?: string +): Promise<{entries: NewsletterSignupRow[]; total: number}> => { let conn = await NachklangFeedbackDB.getConnection(); try { + const trimmedSearch = search?.trim(); + const whereClause = trimmedSearch + ? 'WHERE event_id = ? AND (first_name LIKE ? ESCAPE \'\\\\\' OR last_name LIKE ? ESCAPE \'\\\\\' OR email LIKE ? ESCAPE \'\\\\\')' + : 'WHERE event_id = ?'; + const likeParam = trimmedSearch ? `%${escapeLikeTerm(trimmedSearch)}%` : undefined; + const whereParams = trimmedSearch ? [eventId, likeParam, likeParam, likeParam] : [eventId]; + + const totalRows = await conn.query(`SELECT COUNT(*) as cnt FROM newsletter_signups ${whereClause}`, whereParams); const rows = await conn.query( - 'SELECT signup_id, first_name, last_name, email, consent_at, sync_status, last_error FROM newsletter_signups WHERE event_id = ? ORDER BY consent_at DESC', - [eventId] + `SELECT signup_id, first_name, last_name, email, consent_at, sync_status, last_error FROM newsletter_signups ${whereClause} ORDER BY consent_at DESC LIMIT ? OFFSET ?`, + [...whereParams, pageSize, (page - 1) * pageSize] ); - return rows.map((r: any) => ({ - signupId: r.signup_id, firstName: r.first_name, lastName: r.last_name, email: r.email, - consentAt: r.consent_at, syncStatus: r.sync_status, lastError: r.last_error - })); + return { + total: Number(totalRows[0].cnt), + entries: rows.map((r: any) => ({ + signupId: r.signup_id, firstName: r.first_name, lastName: r.last_name, email: r.email, + consentAt: r.consent_at, syncStatus: r.sync_status, lastError: r.last_error + })) + }; } finally { await conn.end(); }