rspace-online/shared/markwhen/sources/rcal.ts

78 lines
2.7 KiB
TypeScript

/**
* rCal → markwhen projection.
*
* Maps `CalendarEvent` records to `MwEvent`. Handles:
* - all-day events (normalized to date-only via midnight-UTC)
* - per-event timezone (passes through to MwEvent.timezone)
* - virtual vs physical events (location shown in description)
* - tags: existing event tags + booking status + source name
*
* NOT handled yet (future work, flagged in markwhen_integration spec):
* - RRULE expansion — only stored master events are emitted.
* For recurring, expand the next N instances via rrule.js first.
*/
import type { MarkwhenSourceFactory, MwEvent, MwSource } from '../types';
import type { CalendarDoc, CalendarEvent } from '../../../modules/rcal/schemas';
import { calendarDocId } from '../../../modules/rcal/schemas';
import { loadDoc } from '../doc-loader'; // thin wrapper around local-first storage
const SECTION_LABEL = 'Calendar';
const SECTION_TAG = 'rcal';
function eventToMw(ev: CalendarEvent): MwEvent | null {
if (!ev.startTime) return null;
const start = ev.allDay ? Math.floor(ev.startTime / 86_400_000) * 86_400_000 : ev.startTime;
const end = ev.endTime && ev.endTime > ev.startTime
? (ev.allDay ? Math.floor(ev.endTime / 86_400_000) * 86_400_000 : ev.endTime)
: undefined;
const tags = [...(ev.tags ?? [])];
if (ev.bookingStatus) tags.push(ev.bookingStatus);
if (ev.sourceType && ev.sourceType !== 'local') tags.push(ev.sourceType);
if (ev.isVirtual) tags.push('virtual');
const descParts: string[] = [];
if (ev.description) descParts.push(ev.description.slice(0, 500));
if (ev.locationName) descParts.push(`📍 ${ev.locationName}`);
else if (ev.locationBreadcrumb) descParts.push(`📍 ${ev.locationBreadcrumb}`);
if (ev.isVirtual && ev.virtualUrl) descParts.push(`🔗 ${ev.virtualUrl}`);
if (ev.attendeeCount) descParts.push(`👥 ${ev.attendeeCount} attendee(s)`);
return {
start,
end,
title: ev.title || '(untitled)',
description: descParts.length > 0 ? descParts.join('\n') : undefined,
tags: tags.length > 0 ? tags : undefined,
timezone: ev.timezone ?? undefined,
sourceId: `rcal:ev:${ev.id}`,
};
}
export const rcalMarkwhenSource: MarkwhenSourceFactory = {
module: 'rcal',
label: 'Calendar',
icon: '📅',
async build({ space, from, to }): Promise<MwSource | null> {
const doc = await loadDoc<CalendarDoc>(calendarDocId(space));
if (!doc) return null;
const events: MwEvent[] = [];
for (const ev of Object.values(doc.events)) {
if (from !== undefined && ev.endTime && ev.endTime < from) continue;
if (to !== undefined && ev.startTime && ev.startTime > to) continue;
const mw = eventToMw(ev);
if (mw) events.push(mw);
}
return {
id: 'rcal',
label: SECTION_LABEL,
tag: SECTION_TAG,
color: 'blue',
events,
};
},
};