f2f7bfc391
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
/**
|
|
* Calendar Helper Utilities
|
|
* Shared logic for multi-day event handling across calendar views
|
|
*/
|
|
|
|
export interface MultiDaySegment {
|
|
entity: any;
|
|
startCol: number;
|
|
span: number;
|
|
lane: number;
|
|
isStart: boolean;
|
|
isEnd: boolean;
|
|
}
|
|
|
|
export interface LaneAssignment {
|
|
segments: MultiDaySegment[];
|
|
laneCount: number;
|
|
}
|
|
|
|
/**
|
|
* Get the start of day (midnight) for a given date
|
|
*/
|
|
export function startOfDay(date: Date): Date {
|
|
const d = new Date(date);
|
|
d.setHours(0, 0, 0, 0);
|
|
return d;
|
|
}
|
|
|
|
/**
|
|
* Calculate the difference in days between two dates
|
|
*/
|
|
export function daysDiff(a: Date, b: Date): number {
|
|
const msPerDay = 24 * 60 * 60 * 1000;
|
|
return Math.round((startOfDay(b).getTime() - startOfDay(a).getTime()) / msPerDay);
|
|
}
|
|
|
|
/**
|
|
* Check if an event spans multiple days
|
|
*/
|
|
export function isMultiDay(entity: any): boolean {
|
|
if (!entity.properties?.startsOn) return false;
|
|
const start = startOfDay(new Date(entity.properties.startsOn));
|
|
const end = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : start;
|
|
return daysDiff(start, end) > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if an event is an all-day event (no specific time, or spans full day)
|
|
*/
|
|
export function isAllDay(entity: any): boolean {
|
|
if (!entity.properties?.startsOn) return false;
|
|
|
|
// Explicit all-day flag
|
|
if (entity.properties?.timeless === true) return true;
|
|
|
|
// Multi-day events are treated as all-day in the days view
|
|
if (isMultiDay(entity)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an event overlaps with a date range
|
|
*/
|
|
export function eventOverlapsRange(entity: any, rangeStart: Date, rangeEnd: Date): boolean {
|
|
if (!entity.properties?.startsOn) return false;
|
|
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
|
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
|
return eventStart <= rangeEnd && eventEnd >= rangeStart;
|
|
}
|
|
|
|
/**
|
|
* Check if an event occurs on a specific date
|
|
*/
|
|
export function eventOnDate(entity: any, date: Date): boolean {
|
|
if (!entity.properties?.startsOn) return false;
|
|
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
|
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
|
const targetDate = startOfDay(date);
|
|
return eventStart <= targetDate && eventEnd >= targetDate;
|
|
}
|
|
|
|
/**
|
|
* Get multi-day event segments for a given date range with lane assignments
|
|
* Used for rendering spanning events across columns
|
|
*/
|
|
export function getMultiDaySegments(
|
|
events: any[],
|
|
rangeStart: Date,
|
|
rangeEnd: Date,
|
|
columnCount: number,
|
|
getColumnIndex: (date: Date) => number
|
|
): LaneAssignment {
|
|
const segments: MultiDaySegment[] = [];
|
|
|
|
// Filter multi-day/all-day events that overlap this range
|
|
const multiDayEvents = events.filter(entity => {
|
|
if (!entity.properties?.startsOn) return false;
|
|
if (!isAllDay(entity)) return false;
|
|
return eventOverlapsRange(entity, rangeStart, rangeEnd);
|
|
});
|
|
|
|
// Sort: longer events first, then by start date
|
|
multiDayEvents.sort((a, b) => {
|
|
const aStart = new Date(a.properties.startsOn);
|
|
const aEnd = new Date(a.properties.endsOn || a.properties.startsOn);
|
|
const bStart = new Date(b.properties.startsOn);
|
|
const bEnd = new Date(b.properties.endsOn || b.properties.startsOn);
|
|
const aDuration = daysDiff(aStart, aEnd);
|
|
const bDuration = daysDiff(bStart, bEnd);
|
|
if (bDuration !== aDuration) return bDuration - aDuration;
|
|
return aStart.getTime() - bStart.getTime();
|
|
});
|
|
|
|
// Lane assignment - track which days are occupied in each lane
|
|
const lanes: boolean[][] = [];
|
|
|
|
for (const entity of multiDayEvents) {
|
|
const eventStart = startOfDay(new Date(entity.properties.startsOn));
|
|
const eventEnd = entity.properties?.endsOn ? startOfDay(new Date(entity.properties.endsOn)) : eventStart;
|
|
|
|
// Clamp to visible range
|
|
const segStart = eventStart < rangeStart ? rangeStart : eventStart;
|
|
const segEnd = eventEnd > rangeEnd ? rangeEnd : eventEnd;
|
|
|
|
const startCol = getColumnIndex(segStart);
|
|
const endCol = getColumnIndex(segEnd);
|
|
|
|
// Skip if outside visible columns
|
|
if (startCol < 0 || startCol >= columnCount) continue;
|
|
|
|
const span = Math.min(endCol, columnCount - 1) - startCol + 1;
|
|
|
|
// Find available lane
|
|
let lane = 0;
|
|
while (true) {
|
|
if (!lanes[lane]) {
|
|
lanes[lane] = new Array(columnCount).fill(false);
|
|
}
|
|
|
|
let isFree = true;
|
|
for (let d = startCol; d < startCol + span; d++) {
|
|
if (lanes[lane][d]) {
|
|
isFree = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isFree) {
|
|
for (let d = startCol; d < startCol + span; d++) {
|
|
lanes[lane][d] = true;
|
|
}
|
|
break;
|
|
}
|
|
lane++;
|
|
}
|
|
|
|
segments.push({
|
|
entity,
|
|
startCol,
|
|
span,
|
|
lane,
|
|
isStart: eventStart >= rangeStart,
|
|
isEnd: eventEnd <= rangeEnd,
|
|
});
|
|
}
|
|
|
|
return { segments, laneCount: lanes.length };
|
|
}
|
|
|
|
/**
|
|
* Get timed (non-all-day) events for a specific date
|
|
*/
|
|
export function getTimedEventsForDate(events: any[], date: Date): any[] {
|
|
return events.filter(entity => {
|
|
if (!entity.properties?.startsOn) return false;
|
|
if (isAllDay(entity)) return false;
|
|
const eventDate = startOfDay(new Date(entity.properties.startsOn));
|
|
return eventDate.toDateString() === date.toDateString();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get single-day events for a specific date (used in month view)
|
|
*/
|
|
export function getSingleDayEventsForDate(events: any[], date: Date): any[] {
|
|
return events.filter(entity => {
|
|
if (!entity.properties?.startsOn) return false;
|
|
if (isMultiDay(entity)) return false;
|
|
const eventDate = startOfDay(new Date(entity.properties.startsOn));
|
|
return eventDate.toDateString() === date.toDateString();
|
|
});
|
|
}
|