Rename all workspace packages to reflect the actual project name: - @caldav/client -> @calchat/client - @caldav/server -> @calchat/server - @caldav/shared -> @calchat/shared - Root package: caldav-mono -> calchat-mono Update all import statements across client and server to use the new package names. Update default MongoDB database name and logging service identifier accordingly.
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import { RRule, rrulestr } from "rrule";
|
|
import { CalendarEvent, ExpandedEvent } from "@calchat/shared";
|
|
|
|
// Convert local time to "fake UTC" for rrule
|
|
// rrule interprets all dates as UTC internally, so we need to trick it
|
|
function toRRuleDate(date: Date): Date {
|
|
return new Date(
|
|
Date.UTC(
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate(),
|
|
date.getHours(),
|
|
date.getMinutes(),
|
|
date.getSeconds(),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Convert rrule result back to local time
|
|
function fromRRuleDate(date: Date): Date {
|
|
return new Date(
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth(),
|
|
date.getUTCDate(),
|
|
date.getUTCHours(),
|
|
date.getUTCMinutes(),
|
|
date.getUTCSeconds(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Expand recurring events into individual occurrences within a date range.
|
|
* Non-recurring events are returned as-is with occurrenceStart/End = startTime/endTime.
|
|
*/
|
|
export function expandRecurringEvents(
|
|
events: CalendarEvent[],
|
|
rangeStart: Date,
|
|
rangeEnd: Date,
|
|
): ExpandedEvent[] {
|
|
const expanded: ExpandedEvent[] = [];
|
|
|
|
for (const event of events) {
|
|
const startTime = new Date(event.startTime);
|
|
const endTime = new Date(event.endTime);
|
|
const duration = endTime.getTime() - startTime.getTime();
|
|
|
|
if (!event.isRecurring || !event.recurrenceRule) {
|
|
// Non-recurring event: add as-is if within range
|
|
if (startTime >= rangeStart && startTime <= rangeEnd) {
|
|
expanded.push({
|
|
...event,
|
|
occurrenceStart: startTime,
|
|
occurrenceEnd: endTime,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Recurring event: parse RRULE and expand
|
|
try {
|
|
// Strip RRULE: prefix if present (AI may include it)
|
|
const ruleString = event.recurrenceRule.replace(/^RRULE:/i, "");
|
|
const rule = rrulestr(
|
|
`DTSTART:${formatRRuleDateString(startTime)}\nRRULE:${ruleString}`,
|
|
);
|
|
|
|
// Get occurrences within the range (using fake UTC dates)
|
|
const occurrences = rule.between(
|
|
toRRuleDate(rangeStart),
|
|
toRRuleDate(rangeEnd),
|
|
true, // inclusive
|
|
);
|
|
|
|
for (const occurrence of occurrences) {
|
|
const occurrenceStart = fromRRuleDate(occurrence);
|
|
const occurrenceEnd = new Date(occurrenceStart.getTime() + duration);
|
|
|
|
expanded.push({
|
|
...event,
|
|
occurrenceStart,
|
|
occurrenceEnd,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// If RRULE parsing fails, include the event as a single occurrence
|
|
console.error(
|
|
`Failed to parse recurrence rule for event ${event.id}:`,
|
|
error,
|
|
);
|
|
expanded.push({
|
|
...event,
|
|
occurrenceStart: startTime,
|
|
occurrenceEnd: endTime,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by occurrence start time
|
|
expanded.sort(
|
|
(a, b) => a.occurrenceStart.getTime() - b.occurrenceStart.getTime(),
|
|
);
|
|
|
|
return expanded;
|
|
}
|
|
|
|
// Format date as RRULE DTSTART string (YYYYMMDDTHHMMSS)
|
|
function formatRRuleDateString(date: Date): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const hours = String(date.getHours()).padStart(2, "0");
|
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
|
|
}
|