Initial commit

This commit is contained in:
root
2025-12-21 09:59:39 -05:00
committed by Sebastian Krupinski
commit cc4e467cef
36 changed files with 7133 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
/**
* 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.data?.startsOn) return false;
const start = startOfDay(new Date(entity.data.startsOn));
const end = entity.data?.endsOn ? startOfDay(new Date(entity.data.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.data?.startsOn) return false;
// Explicit all-day flag
if (entity.data?.allDay === 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.data?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.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.data?.startsOn) return false;
const eventStart = startOfDay(new Date(entity.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.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.data?.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.data.startsOn);
const aEnd = new Date(a.data.endsOn || a.data.startsOn);
const bStart = new Date(b.data.startsOn);
const bEnd = new Date(b.data.endsOn || b.data.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.data.startsOn));
const eventEnd = entity.data?.endsOn ? startOfDay(new Date(entity.data.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.data?.startsOn) return false;
if (isAllDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.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.data?.startsOn) return false;
if (isMultiDay(entity)) return false;
const eventDate = startOfDay(new Date(entity.data.startsOn));
return eventDate.toDateString() === date.toDateString();
});
}