feat: build the occurrence index

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-29 21:52:09 -04:00
parent 51bf195664
commit cd270e9e99
10 changed files with 344 additions and 167 deletions
+30 -30
View File
@@ -45,18 +45,18 @@
<div class="all-day-events-overlay">
<div
v-for="segment in allDaySegments.segments"
:key="segment.entity.identifier"
:key="segment.occurrence.key"
class="all-day-event"
:class="{
'is-start': segment.isStart,
'is-end': segment.isEnd,
}"
:style="getAllDayEventStyle(segment)"
@click="emit('event-click', segment.entity)"
@mouseenter="emit('event-hover', { event: $event, entity: segment.entity })"
@click="emit('event-click', segment.occurrence.entity)"
@mouseenter="emit('event-hover', { event: $event, entity: segment.occurrence.entity })"
@mouseleave="emit('event-hover-end')"
>
<span v-if="segment.isStart" class="event-label">{{ segment.entity.properties?.label || 'Untitled' }}</span>
<span v-if="segment.isStart" class="event-label">{{ segment.occurrence.entity.properties?.label || 'Untitled' }}</span>
</div>
</div>
</div>
@@ -73,22 +73,22 @@
<div v-for="day in visibleDates" :key="day.toISOString()" class="day-column" :class="{ 'single-day': daysCount === 1 }">
<div class="day-events" @click="handleDayClick($event, day)">
<div
v-for="entity in getTimedEvents(day)"
:key="entity.identifier"
v-for="occurrence in getTimedEvents(day)"
:key="occurrence.key"
class="day-event"
:style="getEventStyle(entity)"
@click.stop="emit('event-click', entity)"
@mouseenter="emit('event-hover', { event: $event, entity })"
:style="getEventStyle(occurrence)"
@click.stop="emit('event-click', occurrence.entity)"
@mouseenter="emit('event-hover', { event: $event, entity: occurrence.entity })"
@mouseleave="emit('event-hover-end')"
>
<template v-if="daysCount <= 3">
<div class="event-time">
{{ formatEventDateTime(getEventProperties(entity).startsOn) }} - {{ formatEventDateTime(getEventProperties(entity).endsOn) }}
{{ formatOccurrenceTime(occurrence.startMs) }} - {{ formatOccurrenceTime(occurrence.endMs) }}
</div>
<div class="event-title">{{ getEventProperties(entity).label || 'Untitled' }}</div>
<div class="event-title">{{ getEventProperties(occurrence.entity).label || 'Untitled' }}</div>
</template>
<template v-else>
<div class="event-title-compact">{{ getEventProperties(entity).label || 'Untitled' }}</div>
<div class="event-title-compact">{{ getEventProperties(occurrence.entity).label || 'Untitled' }}</div>
</template>
</div>
</div>
@@ -104,7 +104,6 @@ import { ref, computed, watch } from 'vue';
import {
startOfDay,
getMultiDaySegments,
getTimedEventsForDate,
type MultiDaySegment,
} from '@/utils/calendarHelpers';
import { shiftLocalDays } from '@/utils/dateRanges';
@@ -112,6 +111,8 @@ import { DAYS_VIEW_SPANS, spanToDays, type DaysViewSpan } from '@/types/spans';
import type { EntityObject } from '@ChronoManager/models/entity';
import type { CollectionObject } from '@ChronoManager/models/collection';
import { EventObject } from '@ChronoManager/models/event';
import { occurrencesForDay, occurrencesForRange } from '@/utils/occurrenceIndex';
import type { CalendarOccurrence, CalendarOccurrenceIndex } from '@/types/occurrence';
const ALL_DAY_EVENT_HEIGHT = 24;
@@ -120,7 +121,7 @@ type CalendarCollection = CollectionObject;
const props = defineProps<{
currentDate: Date;
events: CalendarEntity[];
occurrenceIndex: CalendarOccurrenceIndex;
calendars: CalendarCollection[];
initialSpan?: DaysViewSpan;
}>();
@@ -205,7 +206,12 @@ const allDaySegments = computed(() => {
return visibleDates.value.findIndex(d => startOfDay(d).getTime() === targetDay.getTime());
};
return getMultiDaySegments(props.events, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
const occurrences = occurrencesForRange(props.occurrenceIndex, {
startMs: rangeStart.getTime(),
endMs: shiftLocalDays(rangeEnd, 1).getTime(),
});
return getMultiDaySegments(occurrences, rangeStart, rangeEnd, daysCount.value, getColumnIndex);
});
function isToday(date: Date): boolean {
@@ -213,8 +219,8 @@ function isToday(date: Date): boolean {
return date.toDateString() === today.toDateString();
}
function getTimedEvents(date: Date): CalendarEntity[] {
return getTimedEventsForDate(props.events, date) as CalendarEntity[];
function getTimedEvents(date: Date): CalendarOccurrence[] {
return occurrencesForDay(props.occurrenceIndex, date).filter(occurrence => !occurrence.timeless);
}
function getEventProperties(entity: CalendarEntity): EventObject {
@@ -230,28 +236,22 @@ function getEventColor(entity: CalendarEntity): string {
function getAllDayEventStyle(segment: MultiDaySegment) {
return {
backgroundColor: getEventColor(segment.entity),
backgroundColor: getEventColor(segment.occurrence.entity),
left: `calc(${segment.startCol} / ${daysCount.value} * 100% + 4px)`,
width: `calc(${segment.span} / ${daysCount.value} * 100% - 8px)`,
top: `${segment.lane * ALL_DAY_EVENT_HEIGHT}px`,
};
}
function getEventStyle(entity: CalendarEntity) {
const event = getEventProperties(entity);
if (!event.startsOn || !event.endsOn) {
return { display: 'none' };
}
const startTime = new Date(event.startsOn);
const endTime = new Date(event.endsOn);
function getEventStyle(occurrence: CalendarOccurrence) {
const startTime = new Date(occurrence.startMs);
const start = startTime.getHours() * 60 + startTime.getMinutes();
const duration = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
const duration = occurrence.durationMs / (1000 * 60);
return {
top: `${(start / 1440) * 100}%`,
height: `${(duration / 1440) * 100}%`,
backgroundColor: getEventColor(entity),
backgroundColor: getEventColor(occurrence.entity),
};
}
@@ -265,8 +265,8 @@ function formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}
function formatEventDateTime(value: string | null): string {
return value ? formatTime(new Date(value)) : '';
function formatOccurrenceTime(timestamp: number): string {
return formatTime(new Date(timestamp));
}
function formatWeekDay(date: Date): string {