fix(calendar): expand recurring multi-day events and support YEARLY frequency

Root causes:
1. parseRRule did not strip the "RRULE:" prefix stored by the ICS parser,
   causing all recurrence rules from CalDAV sync to silently fail parsing
2. YEARLY frequency (used by birthday events) was not supported
3. expandRecurringEvents filtered instances only by start date, missing
   multi-day events that start before the view window but span into it
4. All-day recurring instances got datetime end values instead of date-only

Fixes #5 (follow-up from @tschig)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-04-03 12:47:18 +02:00
parent 261dae5990
commit 678c896862
4 changed files with 44 additions and 7 deletions
+22 -4
View File
@@ -43,6 +43,9 @@ function expandRecurringEvents(events, from, to) {
const startMs = new Date(event.start_datetime).getTime();
const endMs = event.end_datetime ? new Date(event.end_datetime).getTime() : null;
const durationMs = endMs !== null ? endMs - startMs : null;
// Duration in days for all-day events (for date-only end calculation)
const isAllDay = !!event.all_day;
const durationDays = isAllDay && durationMs !== null ? Math.round(durationMs / 86400000) : 0;
// Original-Zeit-Teil erhalten (z.B. 'T14:30:00' oder '' bei All-Day)
const timeSuffix = event.start_datetime.slice(10);
@@ -54,13 +57,28 @@ function expandRecurringEvents(events, from, to) {
while (currentDate <= to && iterations < MAX_ITER) {
iterations++;
if (currentDate >= from) {
// For multi-day events, check if the instance end reaches into [from, to]
let instanceEnd = currentDate;
if (isAllDay && durationDays > 0) {
const d = new Date(currentDate + 'T00:00:00');
d.setDate(d.getDate() + durationDays);
instanceEnd = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
if (currentDate >= from || instanceEnd >= from) {
const newStart = currentDate + timeSuffix;
let newEnd = event.end_datetime;
if (durationMs !== null) {
newEnd = new Date(new Date(newStart).getTime() + durationMs)
.toISOString()
.replace('.000Z', 'Z');
if (isAllDay) {
// Keep date-only format for all-day events
const d = new Date(currentDate + 'T00:00:00');
d.setDate(d.getDate() + durationDays);
newEnd = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
} else {
newEnd = new Date(new Date(newStart).getTime() + durationMs)
.toISOString()
.replace('.000Z', 'Z');
}
}
result.push({