const FIXED_REFERENCE_EVENTS = { new_years_day: { label: 'Neujahr', month: 0, day: 1 }, epiphany: { label: 'Heilige Drei Könige', month: 0, day: 6 }, valentines_day: { label: 'Valentinstag', month: 1, day: 14 }, womens_day: { label: 'Internationaler Frauentag', month: 2, day: 8 }, may_day: { label: 'Tag der Arbeit', month: 4, day: 1 }, german_unity_day: { label: 'Tag der Deutschen Einheit', month: 9, day: 3 }, halloween: { label: 'Halloween', month: 9, day: 31 }, all_saints_day: { label: 'Allerheiligen', month: 10, day: 1 }, st_nicholas_day: { label: 'Nikolaus', month: 11, day: 6 }, christmas_eve: { label: 'Heiligabend', month: 11, day: 24 }, christmas_day: { label: '1. Weihnachtstag', month: 11, day: 25 }, boxing_day: { label: '2. Weihnachtstag', month: 11, day: 26 }, new_years_eve: { label: 'Silvester', month: 11, day: 31 } }; const EASTER_REFERENCE_EVENTS = { carnival_monday: { label: 'Rosenmontag', offsetDays: -48 }, ash_wednesday: { label: 'Aschermittwoch', offsetDays: -46 }, palm_sunday: { label: 'Palmsonntag', offsetDays: -7 }, maundy_thursday: { label: 'Gründonnerstag', offsetDays: -3 }, good_friday: { label: 'Karfreitag', offsetDays: -2 }, easter_sunday: { label: 'Ostersonntag', offsetDays: 0 }, easter_monday: { label: 'Ostermontag', offsetDays: 1 }, ascension_day: { label: 'Christi Himmelfahrt', offsetDays: 39 }, pentecost_sunday: { label: 'Pfingstsonntag', offsetDays: 49 }, pentecost_monday: { label: 'Pfingstmontag', offsetDays: 50 }, corpus_christi: { label: 'Fronleichnam', offsetDays: 60 } }; const REFERENCE_EVENT_DEFINITIONS = { ...FIXED_REFERENCE_EVENTS, ...EASTER_REFERENCE_EVENTS }; const REFERENCE_EVENTS = Object.entries(REFERENCE_EVENT_DEFINITIONS).map(([key, value]) => ({ key, label: value.label })); function startOfDay(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function addDays(date, days) { const copy = new Date(date.getTime()); copy.setDate(copy.getDate() + days); return copy; } function addMonths(date, months) { const copy = new Date(date.getTime()); copy.setMonth(copy.getMonth() + months); return copy; } function applyOffset(date, unit, value = 0) { const copy = new Date(date.getTime()); const numericValue = Number(value) || 0; if (unit === 'weeks') { copy.setDate(copy.getDate() + numericValue * 7); return copy; } if (unit === 'months') { copy.setMonth(copy.getMonth() + numericValue); return copy; } copy.setDate(copy.getDate() + numericValue); return copy; } function subtractOffset(date, unit, value = 0) { return applyOffset(date, unit, -(Number(value) || 0)); } function getIntervalMonths(interval) { if (interval === 'monthly') { return 1; } if (interval === 'quarterly') { return 3; } return 12; } function getEasterSunday(year) { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return new Date(year, month - 1, day); } function getReferenceEventDate(eventKey, year) { if (!eventKey || !Number.isFinite(year)) { return null; } const fixedEvent = FIXED_REFERENCE_EVENTS[eventKey]; if (fixedEvent) { return new Date(year, fixedEvent.month, fixedEvent.day); } const easterEvent = EASTER_REFERENCE_EVENTS[eventKey]; if (easterEvent) { return addDays(getEasterSunday(year), easterEvent.offsetDays); } return null; } function getNextOccurrence(baseDate, intervalMonths, todayStart) { if (!baseDate || Number.isNaN(baseDate.getTime())) { return null; } let candidate = startOfDay(baseDate); const guardYear = todayStart.getFullYear() + 200; while (candidate < todayStart && candidate.getFullYear() < guardYear) { candidate = startOfDay(addMonths(candidate, intervalMonths)); } return candidate; } function getReminderReference(reminder = {}) { const reference = reminder?.reference; if (!reference || reference.mode !== 'event') { return { mode: 'pickupDate' }; } const unit = ['days', 'weeks', 'months'].includes(reference.occurrenceOffsetUnit) ? reference.occurrenceOffsetUnit : 'days'; const value = Number.isFinite(Number(reference.occurrenceOffsetValue)) ? Number(reference.occurrenceOffsetValue) : 0; if (!REFERENCE_EVENT_DEFINITIONS[reference.eventKey]) { return { mode: 'pickupDate' }; } return { mode: 'event', eventKey: reference.eventKey, occurrenceOffsetUnit: unit, occurrenceOffsetValue: value }; } function normalizeReminderReference(reference = {}) { return getReminderReference({ reference }); } function computeReminderSchedule(entry, today = new Date()) { if (!entry?.reminder?.enabled) { return null; } const todayStart = startOfDay(today); const reminder = entry.reminder || {}; const beforeUnit = ['days', 'weeks', 'months'].includes(reminder.beforeUnit) ? reminder.beforeUnit : 'days'; const beforeValue = Number.isFinite(Number(reminder.beforeValue)) ? Math.max(0, Number(reminder.beforeValue)) : Number.isFinite(Number(reminder.daysBefore)) ? Math.max(0, Number(reminder.daysBefore)) : 6; const reference = getReminderReference(reminder); if (reference.mode === 'event') { for (let year = todayStart.getFullYear(); year <= todayStart.getFullYear() + 2; year += 1) { const eventDate = getReferenceEventDate(reference.eventKey, year); if (!eventDate) { return null; } const occurrence = startOfDay( applyOffset(eventDate, reference.occurrenceOffsetUnit, reference.occurrenceOffsetValue) ); const reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue)); if (reminderDate >= todayStart) { return { occurrence, reminderDate }; } } return null; } if (!entry.pickupDate) { return null; } const baseDate = new Date(`${entry.pickupDate}T00:00:00`); if (Number.isNaN(baseDate.getTime())) { return null; } const intervalMonths = getIntervalMonths(reminder.interval); let occurrence = getNextOccurrence(baseDate, intervalMonths, todayStart); if (!occurrence) { return null; } let reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue)); if (reminderDate < todayStart) { occurrence = startOfDay(addMonths(occurrence, intervalMonths)); reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue)); } return { occurrence, reminderDate }; } module.exports = { REFERENCE_EVENTS, REFERENCE_EVENT_DEFINITIONS, getReminderReference, getReminderSchedule: computeReminderSchedule, computeReminderSchedule, normalizeReminderReference };