2025-09-25 23:38:17 +02:00
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
|
|
|
|
|
|
|
|
|
export interface EventLayout {
|
2025-09-27 15:01:22 +02:00
|
|
|
calenderEvent: CalendarEvent;
|
2025-09-25 23:38:17 +02:00
|
|
|
gridArea: string; // "row-start / col-start / row-end / col-end"
|
|
|
|
|
startColumn: number;
|
|
|
|
|
endColumn: number;
|
|
|
|
|
row: number;
|
|
|
|
|
columnSpan: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class AllDayLayoutEngine {
|
|
|
|
|
private weekDates: string[];
|
2025-09-26 17:47:02 +02:00
|
|
|
private tracks: boolean[][];
|
2025-09-25 23:38:17 +02:00
|
|
|
|
|
|
|
|
constructor(weekDates: string[]) {
|
|
|
|
|
this.weekDates = weekDates;
|
2025-09-26 17:47:02 +02:00
|
|
|
this.tracks = [];
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Calculate layout for all events using clean day-based logic
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-27 15:01:22 +02:00
|
|
|
public calculateLayout(events: CalendarEvent[]): EventLayout[] {
|
2025-09-26 17:47:02 +02:00
|
|
|
|
2025-09-27 15:01:22 +02:00
|
|
|
let layouts: EventLayout[] = [];
|
2025-09-26 17:47:02 +02:00
|
|
|
// Reset tracks for new calculation
|
|
|
|
|
this.tracks = [new Array(this.weekDates.length).fill(false)];
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// Filter to only visible events
|
|
|
|
|
const visibleEvents = events.filter(event => this.isEventVisible(event));
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// Process events in input order (no sorting)
|
|
|
|
|
for (const event of visibleEvents) {
|
|
|
|
|
const startDay = this.getEventStartDay(event);
|
|
|
|
|
const endDay = this.getEventEndDay(event);
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
if (startDay > 0 && endDay > 0) {
|
|
|
|
|
const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// Mark days as occupied
|
|
|
|
|
for (let day = startDay - 1; day <= endDay - 1; day++) {
|
|
|
|
|
this.tracks[track][day] = true;
|
|
|
|
|
}
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
const layout: EventLayout = {
|
2025-09-27 15:01:22 +02:00
|
|
|
calenderEvent: event,
|
2025-09-26 17:47:02 +02:00
|
|
|
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
|
|
|
|
|
startColumn: startDay,
|
|
|
|
|
endColumn: endDay,
|
|
|
|
|
row: track + 1,
|
|
|
|
|
columnSpan: endDay - startDay + 1
|
|
|
|
|
};
|
2025-09-27 15:01:22 +02:00
|
|
|
layouts.push(layout);
|
|
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:38:17 +02:00
|
|
|
return layouts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Find available track for event spanning from startDay to endDay (0-based indices)
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-26 17:47:02 +02:00
|
|
|
private findAvailableTrack(startDay: number, endDay: number): number {
|
|
|
|
|
for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) {
|
|
|
|
|
if (this.isTrackAvailable(trackIndex, startDay, endDay)) {
|
|
|
|
|
return trackIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// Create new track if none available
|
|
|
|
|
this.tracks.push(new Array(this.weekDates.length).fill(false));
|
|
|
|
|
return this.tracks.length - 1;
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Check if track is available for the given day range (0-based indices)
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-26 17:47:02 +02:00
|
|
|
private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
|
|
|
|
|
for (let day = startDay; day <= endDay; day++) {
|
|
|
|
|
if (this.tracks[trackIndex][day]) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Get start day index for event (1-based, 0 if not visible)
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-26 17:47:02 +02:00
|
|
|
private getEventStartDay(event: CalendarEvent): number {
|
|
|
|
|
const eventStartDate = this.formatDate(event.start);
|
|
|
|
|
const firstVisibleDate = this.weekDates[0];
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// If event starts before visible range, clip to first visible day
|
|
|
|
|
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
const dayIndex = this.weekDates.indexOf(clippedStartDate);
|
|
|
|
|
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get end day index for event (1-based, 0 if not visible)
|
|
|
|
|
*/
|
|
|
|
|
private getEventEndDay(event: CalendarEvent): number {
|
|
|
|
|
const eventEndDate = this.formatDate(event.end);
|
|
|
|
|
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// If event ends after visible range, clip to last visible day
|
|
|
|
|
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
const dayIndex = this.weekDates.indexOf(clippedEndDate);
|
|
|
|
|
return dayIndex >= 0 ? dayIndex + 1 : 0;
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Check if event is visible in the current date range
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-26 17:47:02 +02:00
|
|
|
private isEventVisible(event: CalendarEvent): boolean {
|
|
|
|
|
if (this.weekDates.length === 0) return false;
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
const eventStartDate = this.formatDate(event.start);
|
|
|
|
|
const eventEndDate = this.formatDate(event.end);
|
|
|
|
|
const firstVisibleDate = this.weekDates[0];
|
|
|
|
|
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
|
2025-09-27 15:01:22 +02:00
|
|
|
|
2025-09-26 17:47:02 +02:00
|
|
|
// Event overlaps if it doesn't end before visible range starts
|
|
|
|
|
// AND doesn't start after visible range ends
|
|
|
|
|
return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-26 17:47:02 +02:00
|
|
|
* Format date to YYYY-MM-DD string using local date
|
2025-09-25 23:38:17 +02:00
|
|
|
*/
|
2025-09-26 17:47:02 +02:00
|
|
|
private formatDate(date: Date): string {
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
return `${year}-${month}-${day}`;
|
2025-09-25 23:38:17 +02:00
|
|
|
}
|
|
|
|
|
}
|