- Supervisor -> Feedback (supervisors and admins, campus-scoped): every event's form with answered of asked, response rate, average rating and last reminder; per event the results per question (with the AI summary), each response, and on named forms who was asked and who has answered. - Remind those who haven't answered (or one person on a named form): a fresh link, plus the form to anyone eligible who hasn't had it; nobody gets the form or a reminder twice within 24 hours. The 3-day job is unchanged. - Anonymous forms show no names and no list of who has answered. - FeedbackInvite.reminderCount (migration 20261103120000_feedback_reminders); access log action feedback_responses; help page; dashboard and Ctrl+K links. - Roadmap: the AI helpers on the privacy pages are dropped; 2.1.0 is the feedback page. - Tests: tests/feedbackPage.test.ts, e2e/feedback.spec.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
2.8 KiB
JavaScript
66 lines
2.8 KiB
JavaScript
const prisma = require('../config/db');
|
|
const { getClientIp } = require('./requestUtils');
|
|
|
|
// What each DataAccessLog action means, for the admin page.
|
|
const ACCESS_ACTIONS = {
|
|
report_pdf: 'Report downloaded (PDF)',
|
|
report_excel: 'Report downloaded (Excel)',
|
|
report_email: 'Report emailed',
|
|
report_whatsapp: 'Report sent by WhatsApp',
|
|
attendance_export: 'Attendance register exported',
|
|
certificates_download: 'Certificates downloaded',
|
|
group_lists: 'Room/group lists opened',
|
|
form_responses: 'Attendee form answers viewed',
|
|
consent_list: 'Parental consent list viewed',
|
|
medical_list: 'Medical list viewed',
|
|
backup_download: 'Database backup downloaded',
|
|
impersonation: 'Viewed the site as another user',
|
|
kids_rooms: 'Kids check-in desk or room opened',
|
|
kids_offline: 'Kids check-in list downloaded for offline use',
|
|
volunteer_roster: 'Volunteer roster opened',
|
|
ai_feedback: 'Feedback comments summarised by the AI helper',
|
|
feedback_responses: 'Feedback answers viewed (named form)',
|
|
ai_report: 'Report summarised by the AI helper (anonymised totals)',
|
|
};
|
|
|
|
/**
|
|
* POPIA access log: someone exported or viewed personal information in bulk. Fire-and-
|
|
* forget like logAdminAction — a logging failure never blocks the export itself.
|
|
*/
|
|
async function logDataAccess(req, { action, what, eventId = null, records = null, sensitive }) {
|
|
try {
|
|
// Children's or special information: medical lists always; otherwise anything about a
|
|
// youth event (one that asks for parental consent).
|
|
let isSensitive = sensitive ?? (action === 'medical_list');
|
|
if (!isSensitive && eventId) {
|
|
const event = await prisma.event.findUnique({ where: { id: eventId }, select: { requiresParentalConsent: true } });
|
|
isSensitive = !!event?.requiresParentalConsent;
|
|
}
|
|
await prisma.dataAccessLog.create({
|
|
data: {
|
|
actorId: req.user?.id || null,
|
|
actorRole: req.user?.role || 'unknown',
|
|
action,
|
|
what: String(what || ACCESS_ACTIONS[action] || action).slice(0, 500),
|
|
eventId: eventId || null,
|
|
records: Number.isFinite(records) ? records : null,
|
|
sensitive: isSensitive,
|
|
ip: getClientIp(req),
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to log data access:', e?.message);
|
|
}
|
|
}
|
|
|
|
/** Rows in a report payload (main table or layered sections, plus extra tables). */
|
|
function reportRowCount(payload) {
|
|
const p = payload || {};
|
|
let n = Array.isArray(p.table?.rows) ? p.table.rows.length : 0;
|
|
if (Array.isArray(p.layered?.sections)) n += p.layered.sections.reduce((s, sec) => s + (Array.isArray(sec.items) ? sec.items.length : 0), 0);
|
|
if (Array.isArray(p.extraTables)) n += p.extraTables.reduce((s, t) => s + (Array.isArray(t.rows) ? t.rows.length : 0), 0);
|
|
return n;
|
|
}
|
|
|
|
module.exports = { ACCESS_ACTIONS, logDataAccess, reportRowCount };
|