Files
Pickup-Config/services/journalReminderUtils.js
Meik bd41e8c079 chore: sync running docker state
Commit the app code currently running in Docker, including scheduler, store status, journal reminder, maintenance mode, and Foodsharing API check updates. Remove generated runtime logs from version control and ignore local runtime captures.
2026-06-14 21:21:07 +02:00

216 lines
6.9 KiB
JavaScript

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
};