const BIRTHDAY_COLOR = '#E11D48'; const BIRTHDAY_RRULE = 'FREQ=YEARLY;INTERVAL=1'; function pad2(n) { return String(n).padStart(2, '0'); } function leapYear(year) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } function normalizedMonthDay(birthDate, year) { const [, monthStr, dayStr] = String(birthDate).split('-'); const month = parseInt(monthStr, 10); let day = parseInt(dayStr, 10); if (month === 2 && day === 29 && !leapYear(year)) day = 28; return `${year}-${pad2(month)}-${pad2(day)}`; } function nextBirthdayDate(birthDate, from = new Date()) { const now = from instanceof Date ? from : new Date(from); const thisYear = normalizedMonthDay(birthDate, now.getFullYear()); const today = now.toISOString().slice(0, 10); return thisYear >= today ? thisYear : normalizedMonthDay(birthDate, now.getFullYear() + 1); } function nextBirthdayAge(birthDate, from = new Date()) { const next = nextBirthdayDate(birthDate, from); return parseInt(next.slice(0, 4), 10) - parseInt(String(birthDate).slice(0, 4), 10); } function daysUntilBirthday(birthDate, from = new Date()) { const now = from instanceof Date ? from : new Date(from); const next = nextBirthdayDate(birthDate, now); const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); const nextUtc = Date.UTC( parseInt(next.slice(0, 4), 10), parseInt(next.slice(5, 7), 10) - 1, parseInt(next.slice(8, 10), 10), ); return Math.round((nextUtc - todayUtc) / 86400000); } function birthdayReminderAt(birthDate, from = new Date()) { const next = nextBirthdayDate(birthDate, from); return `${next}T12:00:00Z`; } function eventTitle(name) { return `Birthday: ${name}`; } function eventDescription(name, birthDate) { return `Birthday reminder for ${name} (${birthDate}).`; } function syncBirthdayCalendarEvent(database, birthday) { const payload = { title: eventTitle(birthday.name), description: eventDescription(birthday.name, birthday.birth_date), start_datetime: birthday.birth_date, end_datetime: null, all_day: 1, location: null, color: BIRTHDAY_COLOR, icon: 'cake', assigned_to: null, recurrence_rule: BIRTHDAY_RRULE, created_by: birthday.created_by, }; if (birthday.calendar_event_id) { const existing = database.prepare('SELECT id FROM calendar_events WHERE id = ?').get(birthday.calendar_event_id); if (existing) { database.prepare(` UPDATE calendar_events SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, all_day = ?, location = ?, color = ?, icon = ?, assigned_to = ?, recurrence_rule = ?, created_by = ?, external_source = 'local' WHERE id = ? `).run( payload.title, payload.description, payload.start_datetime, payload.end_datetime, payload.all_day, payload.location, payload.color, payload.icon, payload.assigned_to, payload.recurrence_rule, payload.created_by, birthday.calendar_event_id, ); return birthday.calendar_event_id; } } const result = database.prepare(` INSERT INTO calendar_events (title, description, start_datetime, end_datetime, all_day, location, color, icon, assigned_to, created_by, recurrence_rule, external_source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'local') `).run( payload.title, payload.description, payload.start_datetime, payload.end_datetime, payload.all_day, payload.location, payload.color, payload.icon, payload.assigned_to, payload.created_by, payload.recurrence_rule, ); database.prepare('UPDATE birthdays SET calendar_event_id = ? WHERE id = ?') .run(result.lastInsertRowid, birthday.id); return result.lastInsertRowid; } function syncBirthdayReminder(database, birthday, from = new Date()) { if (!birthday.calendar_event_id) return null; const desired = birthdayReminderAt(birthday.birth_date, from); const existing = database.prepare(` SELECT * FROM reminders WHERE entity_type = 'event' AND entity_id = ? AND created_by = ? ORDER BY created_at DESC `).all(birthday.calendar_event_id, birthday.created_by); const active = existing.find((row) => row.dismissed === 0); if (active && active.remind_at === desired) return active.id; database.prepare(` DELETE FROM reminders WHERE entity_type = 'event' AND entity_id = ? AND created_by = ? `).run(birthday.calendar_event_id, birthday.created_by); const result = database.prepare(` INSERT INTO reminders (entity_type, entity_id, remind_at, created_by) VALUES ('event', ?, ?, ?) `).run(birthday.calendar_event_id, desired, birthday.created_by); return result.lastInsertRowid; } function syncBirthdayArtifacts(database, birthday, from = new Date()) { const calendarEventId = syncBirthdayCalendarEvent(database, birthday); const refreshed = { ...birthday, calendar_event_id: calendarEventId }; syncBirthdayReminder(database, refreshed, from); return refreshed; } function deleteBirthdayArtifacts(database, birthday) { if (birthday.calendar_event_id) { database.prepare(` DELETE FROM reminders WHERE entity_type = 'event' AND entity_id = ? AND created_by = ? `).run(birthday.calendar_event_id, birthday.created_by); database.prepare('DELETE FROM calendar_events WHERE id = ?').run(birthday.calendar_event_id); } } function hydrateBirthday(row, from = new Date()) { const next_birthday = nextBirthdayDate(row.birth_date, from); return { ...row, next_birthday, next_age: nextBirthdayAge(row.birth_date, from), days_until: daysUntilBirthday(row.birth_date, from), }; } function syncAllBirthdayReminders(database, userId, from = new Date()) { const birthdays = database.prepare(` SELECT * FROM birthdays WHERE created_by = ? ORDER BY birth_date ASC `).all(userId); birthdays.forEach((birthday) => { const refreshed = birthday.calendar_event_id ? birthday : { ...birthday, calendar_event_id: syncBirthdayCalendarEvent(database, birthday), }; syncBirthdayReminder(database, refreshed, from); }); } export { BIRTHDAY_COLOR, BIRTHDAY_RRULE, birthdayReminderAt, daysUntilBirthday, deleteBirthdayArtifacts, eventDescription, eventTitle, hydrateBirthday, nextBirthdayAge, nextBirthdayDate, syncAllBirthdayReminders, syncBirthdayArtifacts, };