Add student birthday tracking with celebration banners and admin notification; v1.1.31
- Admin can set birthday (month/day) per student in Study Users panel - Summer birthdays (Jun–Aug) prompt admin to set an alternate school-year date - 🎂 badge on student card header during birthday week - Dismissible happy birthday banner shown to student on their study hub - Admin receives email notification on the celebration day, once per year Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { mkdir, stat, unlink, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName } from '../helpers.js'
|
||||
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName, isBirthdayThisWeek } from '../helpers.js'
|
||||
import { requireAdminAuth } from '../auth.js'
|
||||
import { UPLOADS_DIR } from '../config.js'
|
||||
import { state } from '../state.js'
|
||||
@@ -132,6 +132,11 @@ export function register(app) {
|
||||
noteCount,
|
||||
subscribeNewsletter: user.subscribeNewsletter !== false,
|
||||
studyRemindersEnabled: user.studyRemindersEnabled === true,
|
||||
birthdayMonth: user.birthdayMonth ?? null,
|
||||
birthdayDay: user.birthdayDay ?? null,
|
||||
birthdayAlternateMonth: user.birthdayAlternateMonth ?? null,
|
||||
birthdayAlternateDay: user.birthdayAlternateDay ?? null,
|
||||
isBirthdayWeek: isBirthdayThisWeek(user),
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -142,7 +147,7 @@ export function register(app) {
|
||||
const user = state.studyUsers.find(u => u.id === req.params.id)
|
||||
if (!user) { res.status(404).json({ message: 'User not found.' }); return }
|
||||
|
||||
const { displayName, newPassword, addEnrollment, removeEnrollment } = req.body ?? {}
|
||||
const { displayName, newPassword, addEnrollment, removeEnrollment, birthdayMonth, birthdayDay, birthdayAlternateMonth, birthdayAlternateDay } = req.body ?? {}
|
||||
|
||||
if (typeof displayName === 'string') {
|
||||
user.displayName = displayName.trim().slice(0, 80)
|
||||
@@ -172,6 +177,28 @@ export function register(app) {
|
||||
user.enrolledStudySlugs = (user.enrolledStudySlugs ?? []).filter(s => s !== slug)
|
||||
}
|
||||
|
||||
if (birthdayMonth !== undefined) {
|
||||
const m = parseInt(birthdayMonth, 10)
|
||||
user.birthdayMonth = (Number.isInteger(m) && m >= 1 && m <= 12) ? m : null
|
||||
}
|
||||
if (birthdayDay !== undefined) {
|
||||
const d = parseInt(birthdayDay, 10)
|
||||
user.birthdayDay = (Number.isInteger(d) && d >= 1 && d <= 31) ? d : null
|
||||
}
|
||||
if (birthdayAlternateMonth !== undefined) {
|
||||
const m = parseInt(birthdayAlternateMonth, 10)
|
||||
user.birthdayAlternateMonth = (Number.isInteger(m) && m >= 1 && m <= 12) ? m : null
|
||||
if (!user.birthdayAlternateMonth) user.birthdayAlternateDay = null
|
||||
}
|
||||
if (birthdayAlternateDay !== undefined) {
|
||||
const d = parseInt(birthdayAlternateDay, 10)
|
||||
user.birthdayAlternateDay = (Number.isInteger(d) && d >= 1 && d <= 31) ? d : null
|
||||
}
|
||||
// Clear the sent-year so a changed birthday can trigger a new notification
|
||||
if (birthdayMonth !== undefined || birthdayDay !== undefined || birthdayAlternateMonth !== undefined || birthdayAlternateDay !== undefined) {
|
||||
user.birthdayEmailSentYear = null
|
||||
}
|
||||
|
||||
user.updatedAt = new Date().toISOString()
|
||||
queueStudyUsersWrite()
|
||||
res.json({ ok: true, displayName: user.displayName, enrolledStudySlugs: user.enrolledStudySlugs })
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
verifyTotpCode,
|
||||
generateRecoveryCodes,
|
||||
} from '../auth.js'
|
||||
import { parseCookies } from '../helpers.js'
|
||||
import { parseCookies, isBirthdayThisWeek } from '../helpers.js'
|
||||
import { STUDY_SESSION_COOKIE, MAX_STUDY_USERS, MAX_CONTACT_SUBMISSIONS } from '../config.js'
|
||||
import { state } from '../state.js'
|
||||
import {
|
||||
@@ -57,6 +57,7 @@ export function register(app) {
|
||||
totpEnabled: Boolean(user && (user.twoFaMethod === 'app' || user.twoFaMethod === 'email') && (user.twoFaMethod === 'email' || (user.totpSecret && user.totpVerified))),
|
||||
twoFaMethod: user?.twoFaMethod ?? null,
|
||||
totpRecoveryCodesRemaining: user?.twoFaMethod === 'app' ? (user.totpRecoveryCodes?.length ?? 0) : 0,
|
||||
isBirthdayWeek: user ? isBirthdayThisWeek(user) : false,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user