Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
01a914f31e
|
|||
|
2988a70d8f
|
|||
|
07d757e9af
|
@@ -6,8 +6,10 @@
|
||||
--
|
||||
-- Apply with e.g.:
|
||||
-- mysql -h <DB_HOST> -u <DB_USER> -p <FEEDBACK_DB> < 001_init.sql
|
||||
|
||||
USE `nachklang-feedback`;
|
||||
--
|
||||
-- Deliberately no USE statement here: the target database is selected via
|
||||
-- the mysql command line above (whatever FEEDBACK_DB is actually named in
|
||||
-- .env), not hardcoded to a literal schema name.
|
||||
|
||||
-- 1. events -------------------------------------------------------------
|
||||
CREATE TABLE events (
|
||||
|
||||
@@ -82,6 +82,11 @@ reportsAdminRouter.get('/:eventId/report', async (req: Request, res: Response) =
|
||||
* 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
|
||||
@@ -92,7 +97,8 @@ 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 result = await ReportsAdminService.getGuestBookEntries(Number(req.params.eventId), page, pageSize);
|
||||
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);
|
||||
@@ -104,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'
|
||||
@@ -114,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
|
||||
@@ -122,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);
|
||||
}
|
||||
|
||||
@@ -166,13 +166,29 @@ export interface GuestBookEntry {
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export const getGuestBookEntries = async (eventId: number, page: number, pageSize: number): Promise<{entries: GuestBookEntry[]; total: number}> => {
|
||||
// Escapes LIKE wildcards (% and _) so a search term is matched literally,
|
||||
// not interpreted as a pattern - a search for "50%" must not match everything.
|
||||
const escapeLikeTerm = (term: string) => term.replace(/[\\%_]/g, (c) => `\\${c}`);
|
||||
|
||||
export const getGuestBookEntries = async (
|
||||
eventId: number,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
search?: string
|
||||
): Promise<{entries: GuestBookEntry[]; total: number}> => {
|
||||
let conn = await NachklangFeedbackDB.getConnection();
|
||||
try {
|
||||
const totalRows = await conn.query('SELECT COUNT(*) as cnt FROM guest_book_entries WHERE event_id = ?', [eventId]);
|
||||
const trimmedSearch = search?.trim();
|
||||
const whereClause = trimmedSearch
|
||||
? 'WHERE event_id = ? AND (display_name LIKE ? ESCAPE \'\\\\\' OR message LIKE ? ESCAPE \'\\\\\')'
|
||||
: 'WHERE event_id = ?';
|
||||
const likeParam = trimmedSearch ? `%${escapeLikeTerm(trimmedSearch)}%` : undefined;
|
||||
const whereParams = trimmedSearch ? [eventId, likeParam, likeParam] : [eventId];
|
||||
|
||||
const totalRows = await conn.query(`SELECT COUNT(*) as cnt FROM guest_book_entries ${whereClause}`, whereParams);
|
||||
const rows = await conn.query(
|
||||
'SELECT entry_id, submission_id, created_at, display_name, message FROM guest_book_entries WHERE event_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?',
|
||||
[eventId, pageSize, (page - 1) * pageSize]
|
||||
`SELECT entry_id, submission_id, created_at, display_name, message FROM guest_book_entries ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
||||
[...whereParams, pageSize, (page - 1) * pageSize]
|
||||
);
|
||||
return {
|
||||
total: Number(totalRows[0].cnt),
|
||||
@@ -232,17 +248,33 @@ export const deleteSubmission = async (submissionId: number): Promise<boolean> =
|
||||
}
|
||||
};
|
||||
|
||||
export const getNewsletterSignups = async (eventId: number): Promise<NewsletterSignupRow[]> => {
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user