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:
nmemmert
2026-08-10 08:42:52 -04:00
parent 6a4399f7a6
commit c0a79b9ed0
7 changed files with 251 additions and 5 deletions
+45
View File
@@ -121,6 +121,51 @@ export function sanitizeRedirectRules(value) {
return out.length > 0 ? out : DEFAULT_REDIRECT_RULES
}
// Returns true if the user's celebration date falls within the current MonSun week.
// Uses birthdayAlternateMonth/Day if set (for summer birthday workarounds), otherwise
// falls back to birthdayMonth/Day. Checks both the current year and next year so
// year-wrap birthdays (e.g. Dec 30 checked in late December) work correctly.
export function isBirthdayThisWeek(user) {
const month = user.birthdayMonth
const day = user.birthdayDay
if (!month || !day) return false
const celebMonth = user.birthdayAlternateMonth ?? month
const celebDay = user.birthdayAlternateDay ?? day
const today = new Date()
const monday = new Date(today)
monday.setUTCHours(0, 0, 0, 0)
const dow = monday.getUTCDay()
monday.setUTCDate(monday.getUTCDate() - (dow === 0 ? 6 : dow - 1))
const sunday = new Date(monday)
sunday.setUTCDate(sunday.getUTCDate() + 6)
sunday.setUTCHours(23, 59, 59, 999)
const yr = today.getUTCFullYear()
for (const year of [yr, yr + 1]) {
const bday = new Date(Date.UTC(year, celebMonth - 1, celebDay))
if (bday >= monday && bday <= sunday) return true
}
return false
}
// June, July, August are considered summer months.
export function isSummerBirthday(month) {
return Number.isInteger(month) && month >= 6 && month <= 8
}
// Returns "YYYY-MM-DD" for the celebration date (alternate if set, else birthday)
// in the given year. Returns null if no birthday is set.
export function celebrationDateForYear(user, year) {
const month = user.birthdayMonth
const day = user.birthdayDay
if (!month || !day) return null
const m = user.birthdayAlternateMonth ?? month
const d = user.birthdayAlternateDay ?? day
return `${year}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
}
export function sanitizeFeaturedLinks(value) {
const source = Array.isArray(value) ? value : []
return source