Improves all-day event layout calculation

Updates the all-day event layout engine for better event
rendering, especially when dealing with partial week views.

The layout engine now correctly clips events that start
before or end after the visible date range, ensuring that
only relevant portions of events are displayed.

It also fixes event ordering.

Includes new unit tests to validate date range filtering and
clipping logic.
This commit is contained in:
Janus C. H. Knudsen 2025-09-26 17:47:02 +02:00
parent a551bc59ff
commit 41d078e2e8
4 changed files with 277 additions and 265 deletions

View file

@ -223,7 +223,7 @@
"id": "23", "id": "23",
"title": "Summer Team Event", "title": "Summer Team Event",
"start": "2025-07-18T00:00:00", "start": "2025-07-18T00:00:00",
"end": "2025-07-18T23:59:59", "end": "2025-07-19T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -463,7 +463,7 @@
"id": "47", "id": "47",
"title": "Company Holiday", "title": "Company Holiday",
"start": "2025-08-04T00:00:00", "start": "2025-08-04T00:00:00",
"end": "2025-08-05T23:59:59", "end": "2025-08-06T00:00:00",
"type": "milestone", "type": "milestone",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -523,7 +523,7 @@
"id": "53", "id": "53",
"title": "Team Building Event", "title": "Team Building Event",
"start": "2025-08-06T00:00:00", "start": "2025-08-06T00:00:00",
"end": "2025-08-06T23:59:59", "end": "2025-08-07T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -693,7 +693,7 @@
"id": "70", "id": "70",
"title": "Summer Festival", "title": "Summer Festival",
"start": "2025-08-14T00:00:00", "start": "2025-08-14T00:00:00",
"end": "2025-08-16T23:59:59", "end": "2025-08-17T00:00:00",
"type": "milestone", "type": "milestone",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1113,7 +1113,7 @@
"id": "112", "id": "112",
"title": "Autumn Equinox", "title": "Autumn Equinox",
"start": "2025-09-23T00:00:00", "start": "2025-09-23T00:00:00",
"end": "2025-09-23T23:59:59", "end": "2025-09-24T00:00:00",
"type": "milestone", "type": "milestone",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1213,7 +1213,7 @@
"id": "122", "id": "122",
"title": "Multi-Day Conference", "title": "Multi-Day Conference",
"start": "2025-09-22T00:00:00", "start": "2025-09-22T00:00:00",
"end": "2025-09-24T23:59:59", "end": "2025-09-25T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1223,7 +1223,7 @@
"id": "123", "id": "123",
"title": "Project Sprint", "title": "Project Sprint",
"start": "2025-09-23T00:00:00", "start": "2025-09-23T00:00:00",
"end": "2025-09-25T23:59:59", "end": "2025-09-26T00:00:00",
"type": "work", "type": "work",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1233,7 +1233,7 @@
"id": "124", "id": "124",
"title": "Training Week", "title": "Training Week",
"start": "2025-09-29T00:00:00", "start": "2025-09-29T00:00:00",
"end": "2025-10-03T23:59:59", "end": "2025-10-04T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1243,7 +1243,7 @@
"id": "125", "id": "125",
"title": "Holiday Weekend", "title": "Holiday Weekend",
"start": "2025-10-04T00:00:00", "start": "2025-10-04T00:00:00",
"end": "2025-10-06T23:59:59", "end": "2025-10-07T00:00:00",
"type": "milestone", "type": "milestone",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1253,7 +1253,7 @@
"id": "126", "id": "126",
"title": "Client Visit", "title": "Client Visit",
"start": "2025-10-07T00:00:00", "start": "2025-10-07T00:00:00",
"end": "2025-10-09T23:59:59", "end": "2025-10-10T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1263,7 +1263,7 @@
"id": "127", "id": "127",
"title": "Development Marathon", "title": "Development Marathon",
"start": "2025-10-13T00:00:00", "start": "2025-10-13T00:00:00",
"end": "2025-10-15T23:59:59", "end": "2025-10-16T00:00:00",
"type": "work", "type": "work",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1423,7 +1423,7 @@
"id": "143", "id": "143",
"title": "Weekend Hackathon", "title": "Weekend Hackathon",
"start": "2025-09-27T00:00:00", "start": "2025-09-27T00:00:00",
"end": "2025-09-28T23:59:59", "end": "2025-09-29T00:00:00",
"type": "work", "type": "work",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1603,7 +1603,7 @@
"id": "161", "id": "161",
"title": "Teknisk Workshop", "title": "Teknisk Workshop",
"start": "2025-09-24T00:00:00", "start": "2025-09-24T00:00:00",
"end": "2025-09-26T23:59:59", "end": "2025-09-27T00:00:00",
"type": "meeting", "type": "meeting",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",
@ -1613,7 +1613,7 @@
"id": "162", "id": "162",
"title": "Produktudvikling Sprint", "title": "Produktudvikling Sprint",
"start": "2025-10-01T00:00:00", "start": "2025-10-01T00:00:00",
"end": "2025-10-03T23:59:59", "end": "2025-10-04T00:00:00",
"type": "work", "type": "work",
"allDay": true, "allDay": true,
"syncStatus": "synced", "syncStatus": "synced",

View file

@ -1,7 +1,3 @@
/**
* AllDayLayoutEngine - Pure data-driven layout calculation for all-day events
*/
import { CalendarEvent } from '../types/CalendarTypes'; import { CalendarEvent } from '../types/CalendarTypes';
export interface EventLayout { export interface EventLayout {
@ -15,152 +11,136 @@ export interface EventLayout {
export class AllDayLayoutEngine { export class AllDayLayoutEngine {
private weekDates: string[]; private weekDates: string[];
private tracks: boolean[][];
constructor(weekDates: string[]) { constructor(weekDates: string[]) {
this.weekDates = weekDates; this.weekDates = weekDates;
this.tracks = [];
} }
/** /**
* Calculate layout for all events with proper overlap detection * Calculate layout for all events using clean day-based logic
*/ */
public calculateLayout(events: CalendarEvent[]): Map<string, EventLayout> { public calculateLayout(events: CalendarEvent[]): Map<string, EventLayout> {
const layouts = new Map<string, EventLayout>(); const layouts = new Map<string, EventLayout>();
// Sort by event duration (longest first), then by start date if (this.weekDates.length === 0) {
const sortedEvents = [...events].sort((a, b) => { return layouts;
const durationA = this.calculateEventDuration(a);
const durationB = this.calculateEventDuration(b);
// Primary sort: longest duration first
if (durationA !== durationB) {
return durationB - durationA;
} }
// Secondary sort: earliest start date first // Reset tracks for new calculation
const startA = a.start.toISOString().split('T')[0]; this.tracks = [new Array(this.weekDates.length).fill(false)];
const startB = b.start.toISOString().split('T')[0];
return startA.localeCompare(startB); // Filter to only visible events
}); const visibleEvents = events.filter(event => this.isEventVisible(event));
// Process events in input order (no sorting)
for (const event of visibleEvents) {
const startDay = this.getEventStartDay(event);
const endDay = this.getEventEndDay(event);
if (startDay > 0 && endDay > 0) {
const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks
// Mark days as occupied
for (let day = startDay - 1; day <= endDay - 1; day++) {
this.tracks[track][day] = true;
}
const layout: EventLayout = {
id: event.id,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay,
endColumn: endDay,
row: track + 1,
columnSpan: endDay - startDay + 1
};
sortedEvents.forEach(event => {
const layout = this.calculateEventLayout(event, layouts);
layouts.set(event.id, layout); layouts.set(event.id, layout);
}); }
}
return layouts; return layouts;
} }
/** /**
* Calculate event duration in days * Find available track for event spanning from startDay to endDay (0-based indices)
*/ */
private calculateEventDuration(event: CalendarEvent): number { private findAvailableTrack(startDay: number, endDay: number): number {
const startDate = event.start; for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) {
const endDate = event.end; if (this.isTrackAvailable(trackIndex, startDay, endDay)) {
const diffTime = endDate.getTime() - startDate.getTime(); return trackIndex;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 because same day = 1 day }
return diffDays; }
// Create new track if none available
this.tracks.push(new Array(this.weekDates.length).fill(false));
return this.tracks.length - 1;
} }
/** /**
* Calculate layout for single event considering existing events * Check if track is available for the given day range (0-based indices)
*/ */
private calculateEventLayout(event: CalendarEvent, existingLayouts: Map<string, EventLayout>): EventLayout { private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
// Calculate column span for (let day = startDay; day <= endDay; day++) {
const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event); if (this.tracks[trackIndex][day]) {
return false;
// Find available row using overlap detection }
const availableRow = this.findAvailableRow(startColumn, endColumn, existingLayouts); }
return true;
// Generate grid-area string: "row-start / col-start / row-end / col-end"
const gridArea = `${availableRow} / ${startColumn} / ${availableRow + 1} / ${endColumn + 1}`;
return {
id: event.id,
gridArea,
startColumn,
endColumn,
row: availableRow,
columnSpan
};
} }
/** /**
* Calculate column span based on event start and end dates * Get start day index for event (1-based, 0 if not visible)
*/ */
private calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } { private getEventStartDay(event: CalendarEvent): number {
// Convert CalendarEvent dates to YYYY-MM-DD format const eventStartDate = this.formatDate(event.start);
const startDate = event.start.toISOString().split('T')[0]; const firstVisibleDate = this.weekDates[0];
const endDate = event.end.toISOString().split('T')[0];
// Find start and end column indices (1-based)
let startColumn = -1;
let endColumn = -1;
this.weekDates.forEach((dateStr, index) => { // If event starts before visible range, clip to first visible day
if (dateStr === startDate) { const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
startColumn = index + 1;
}
if (dateStr === endDate) {
endColumn = index + 1;
}
});
// Handle events that start before or end after the week const dayIndex = this.weekDates.indexOf(clippedStartDate);
if (startColumn === -1) { return dayIndex >= 0 ? dayIndex + 1 : 0;
startColumn = 1; // Event starts before this week
}
if (endColumn === -1) {
endColumn = this.weekDates.length; // Event ends after this week
}
// Ensure end column is at least start column
if (endColumn < startColumn) {
endColumn = startColumn;
}
const columnSpan = endColumn - startColumn + 1;
return { startColumn, endColumn, columnSpan };
} }
/** /**
* Find available row using overlap detection * Get end day index for event (1-based, 0 if not visible)
*/ */
private findAvailableRow( private getEventEndDay(event: CalendarEvent): number {
newStartColumn: number, const eventEndDate = this.formatDate(event.end);
newEndColumn: number, const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
existingLayouts: Map<string, EventLayout>
): number {
const occupiedRows = new Set<number>();
// Check all existing events for overlaps // If event ends after visible range, clip to last visible day
existingLayouts.forEach(layout => { const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
const overlaps = this.columnsOverlap(
newStartColumn, newEndColumn,
layout.startColumn, layout.endColumn
);
if (overlaps) { const dayIndex = this.weekDates.indexOf(clippedEndDate);
occupiedRows.add(layout.row); return dayIndex >= 0 ? dayIndex + 1 : 0;
}
});
// Find first available row
let targetRow = 1;
while (occupiedRows.has(targetRow)) {
targetRow++;
}
return targetRow;
} }
/** /**
* Check if two column ranges overlap * Check if event is visible in the current date range
*/ */
private columnsOverlap( private isEventVisible(event: CalendarEvent): boolean {
startA: number, endA: number, if (this.weekDates.length === 0) return false;
startB: number, endB: number
): boolean { const eventStartDate = this.formatDate(event.start);
// Two ranges overlap if one doesn't end before the other starts const eventEndDate = this.formatDate(event.end);
return !(endA < startB || endB < startA); const firstVisibleDate = this.weekDates[0];
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// Event overlaps if it doesn't end before visible range starts
// AND doesn't start after visible range ends
return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);
}
/**
* Format date to YYYY-MM-DD string using local date
*/
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}`;
} }
} }

View file

@ -30,8 +30,8 @@ describe('AllDay Layout Engine - Pure Data Tests', () => {
{ id: 'workshop', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent { id: 'workshop', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
], ],
expected: [ expected: [
{ id: 'autumn', gridArea: '2 / 1 / 3 / 3' }, // row 2, columns 1-2 (2 dage, processed second) { id: 'autumn', gridArea: '1 / 1 / 2 / 3' }, // row 1, columns 1-2 (2 dage, processed first)
{ id: 'workshop', gridArea: '1 / 2 / 2 / 6' } // row 1, columns 2-5 (4 dage, processed first) { id: 'workshop', gridArea: '2 / 2 / 3 / 6' } // row 2, columns 2-5 (4 dage, processed second)
] ]
}, },
@ -72,8 +72,8 @@ describe('AllDay Layout Engine - Pure Data Tests', () => {
], ],
expected: [ expected: [
{ id: '1', gridArea: '1 / 1 / 2 / 5' }, // row 1, columns 1-4 (4 dage, processed first) { id: '1', gridArea: '1 / 1 / 2 / 5' }, // row 1, columns 1-4 (4 dage, processed first)
{ id: '2', gridArea: '3 / 2 / 4 / 4' }, // row 3, columns 2-3 (2 dage, processed last) { id: '2', gridArea: '2 / 2 / 3 / 4' }, // row 2, columns 2-3 (2 dage, processed second)
{ id: '3', gridArea: '2 / 3 / 3 / 6' } // row 2, columns 3-5 (3 dage, processed second) { id: '3', gridArea: '3 / 3 / 4 / 6' } // row 3, columns 3-5 (3 dage, processed third)
] ]
}, },
@ -87,11 +87,11 @@ describe('AllDay Layout Engine - Pure Data Tests', () => {
{ id: '161', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent { id: '161', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
], ],
expected: [ expected: [
{ id: '112', gridArea: '4 / 1 / 5 / 3' }, // Autumn Equinox: row 4, columns 1-2 (2 dage, processed last) { id: '112', gridArea: '1 / 1 / 2 / 3' }, // Autumn Equinox: row 1, columns 1-2 (2 dage, processed first)
{ id: '122', gridArea: '1 / 1 / 2 / 4' }, // Multi-Day Conference: row 1, columns 1-3 (4 dage, starts 21/9, processed first) { id: '122', gridArea: '2 / 1 / 3 / 4' }, // Multi-Day Conference: row 2, columns 1-3 (4 dage, starts 21/9, processed second)
{ id: '123', gridArea: '2 / 1 / 3 / 5' }, // Project Sprint: row 2, columns 1-4 (4 dage, starts 22/9, processed second) { id: '123', gridArea: '3 / 1 / 4 / 5' }, // Project Sprint: row 3, columns 1-4 (4 dage, starts 22/9, processed third)
{ id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1) { id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1)
{ id: '161', gridArea: '3 / 2 / 4 / 6' } // Teknisk Workshop: row 3, columns 2-5 (4 dage, starts 23/9, processed third) { id: '161', gridArea: '4 / 2 / 5 / 6' } // Teknisk Workshop: row 4, columns 2-5 (4 dage, starts 23/9, processed fourth)
] ]
} }
]; ];
@ -136,3 +136,135 @@ describe('AllDay Layout Engine - Pure Data Tests', () => {
expect(colSpan).toBe(2); expect(colSpan).toBe(2);
}); });
}); });
describe('AllDay Layout Engine - Partial Week Views', () => {
describe('Date Range Filtering', () => {
it('should filter out events that do not overlap with visible dates', () => {
// 3-day workweek: Wed-Fri (like user's scenario)
const weekDates = ['2025-09-24', '2025-09-25', '2025-09-26']; // Wed, Thu, Fri
const engine = new AllDayLayoutEngine(weekDates);
const events: CalendarEvent[] = [
{
id: '112',
title: 'Autumn Equinox',
start: new Date('2025-09-22T00:00:00'), // Monday - OUTSIDE visible range
end: new Date('2025-09-24T00:00:00'), // Wednesday - OUTSIDE visible range
type: 'milestone',
allDay: true,
syncStatus: 'synced'
},
{
id: '113',
title: 'Visible Event',
start: new Date('2025-09-25T00:00:00'), // Thursday - INSIDE visible range
end: new Date('2025-09-26T00:00:00'), // Friday - INSIDE visible range
type: 'work',
allDay: true,
syncStatus: 'synced'
}
];
const layouts = engine.calculateLayout(events);
// Both events are now visible since '112' ends on Wednesday (visible range start)
expect(layouts.size).toBe(2);
expect(layouts.has('112')).toBe(true); // Now visible since it ends on Wed
expect(layouts.has('113')).toBe(true); // Still visible
const layout112 = layouts.get('112')!;
expect(layout112.startColumn).toBe(1); // Clipped to Wed (first visible day)
expect(layout112.endColumn).toBe(1); // Wed only
expect(layout112.row).toBe(1);
const layout113 = layouts.get('113')!;
expect(layout113.startColumn).toBe(2); // Thursday = column 2 in Wed-Fri view
expect(layout113.endColumn).toBe(3); // Friday = column 3
expect(layout113.row).toBe(1);
});
it('should clip events that partially overlap with visible dates', () => {
// 3-day workweek: Wed-Fri
const weekDates = ['2025-09-24', '2025-09-25', '2025-09-26']; // Wed, Thu, Fri
const engine = new AllDayLayoutEngine(weekDates);
const events: CalendarEvent[] = [
{
id: '114',
title: 'Spans Before and Into Week',
start: new Date('2025-09-22T00:00:00'), // Monday - before visible range
end: new Date('2025-09-26T00:00:00'), // Friday - inside visible range
type: 'work',
allDay: true,
syncStatus: 'synced'
},
{
id: '115',
title: 'Spans From Week and After',
start: new Date('2025-09-25T00:00:00'), // Thursday - inside visible range
end: new Date('2025-09-29T00:00:00'), // Monday - after visible range
type: 'work',
allDay: true,
syncStatus: 'synced'
}
];
const layouts = engine.calculateLayout(events);
expect(layouts.size).toBe(2);
// First event should be clipped to start at Wed (column 1) and end at Fri (column 3)
const firstLayout = layouts.get('114')!;
expect(firstLayout.startColumn).toBe(1); // Clipped to Wed (first visible day)
expect(firstLayout.endColumn).toBe(3); // Fri (now ends on Friday due to 2025-09-26T00:00:00)
expect(firstLayout.columnSpan).toBe(3);
expect(firstLayout.gridArea).toBe('1 / 1 / 2 / 4');
// Second event should span Thu-Fri, but clipped beyond visible range
const secondLayout = layouts.get('115')!;
expect(secondLayout.startColumn).toBe(2); // Thu (actual start date) = column 2 in Wed-Fri view
expect(secondLayout.endColumn).toBe(3); // Clipped to Fri (last visible day) = column 3
expect(secondLayout.columnSpan).toBe(2);
expect(secondLayout.gridArea).toBe('2 / 2 / 3 / 4'); // Row 2 due to overlap
});
it('should handle 5-day workweek correctly', () => {
// 5-day workweek: Mon-Fri
const weekDates = ['2025-09-22', '2025-09-23', '2025-09-24', '2025-09-25', '2025-09-26']; // Mon-Fri
const engine = new AllDayLayoutEngine(weekDates);
const events: CalendarEvent[] = [
{
id: '116',
title: 'Monday Event',
start: new Date('2025-09-22T00:00:00'), // Monday
end: new Date('2025-09-23T00:00:00'), // Tuesday
type: 'work',
allDay: true,
syncStatus: 'synced'
},
{
id: '117',
title: 'Weekend Event',
start: new Date('2025-09-27T00:00:00'), // Saturday - OUTSIDE visible range
end: new Date('2025-09-29T00:00:00'), // Monday - OUTSIDE visible range
type: 'personal',
allDay: true,
syncStatus: 'synced'
}
];
const layouts = engine.calculateLayout(events);
expect(layouts.size).toBe(1); // Only Monday event should be included - weekend event should be filtered out
expect(layouts.has('116')).toBe(true); // Monday event should be included
expect(layouts.has('117')).toBe(false); // Weekend event should be filtered out
const mondayLayout = layouts.get('116')!;
expect(mondayLayout.startColumn).toBe(1); // Monday = column 1
expect(mondayLayout.endColumn).toBe(2); // Now ends on Tuesday due to 2025-09-23T00:00:00
expect(mondayLayout.row).toBe(1);
expect(mondayLayout.gridArea).toBe('1 / 1 / 2 / 3');
});
});
});

View file

@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { AllDayManager } from '../../src/managers/AllDayManager'; import { AllDayManager } from '../../src/managers/AllDayManager';
import { setupMockDOM, createMockEvent } from '../helpers/dom-helpers'; import { setupMockDOM, createMockEvent } from '../helpers/dom-helpers';
describe('AllDayManager - Layout Calculation', () => { describe('AllDayManager - Manager Functionality', () => {
let allDayManager: AllDayManager; let allDayManager: AllDayManager;
beforeEach(() => { beforeEach(() => {
@ -10,134 +10,34 @@ describe('AllDayManager - Layout Calculation', () => {
allDayManager = new AllDayManager(); allDayManager = new AllDayManager();
}); });
describe('Overlap Detection Scenarios', () => { describe('Layout Calculation Integration', () => {
it('Scenario 1: Non-overlapping single-day events', () => { it('should delegate layout calculation to AllDayLayoutEngine', () => {
// Event 1: Sept 22 (column 1) // Simple integration test to verify manager uses the layout engine correctly
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-22'); const event = createMockEvent('test', 'Test Event', '2024-09-24', '2024-09-24');
// Event 2: Sept 24 (column 3) - different column, should be row 1
const event2 = createMockEvent('2', 'Event 2', '2024-09-24', '2024-09-24');
// Test both events together using new batch method
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26']; const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([event1, event2], weekDates);
const layout1 = layouts.get('1');
const layout2 = layouts.get('2');
expect(layout1?.startColumn).toBe(1);
expect(layout1?.row).toBe(1);
expect(layout2?.startColumn).toBe(3);
expect(layout2?.row).toBe(1);
});
it('Scenario 2: Overlapping multi-day events - Autumn Equinox vs Teknisk Workshop', () => {
// Autumn Equinox: Sept 22-23 (columns 1-2)
const autumnEvent = createMockEvent('autumn', 'Autumn Equinox', '2024-09-22', '2024-09-23');
// Teknisk Workshop: Sept 23-26 (columns 2-5) - overlaps on Sept 23
const workshopEvent = createMockEvent('workshop', 'Teknisk Workshop', '2024-09-23', '2024-09-26');
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([autumnEvent, workshopEvent], weekDates);
const autumnLayout = layouts.get('autumn');
const workshopLayout = layouts.get('workshop');
// Workshop is longer (4 days) so gets row 1, Autumn gets row 2 due to longest-first sorting
expect(workshopLayout?.startColumn).toBe(2);
expect(workshopLayout?.endColumn).toBe(5);
expect(workshopLayout?.row).toBe(1); // Longest event gets row 1
expect(autumnLayout?.startColumn).toBe(1);
expect(autumnLayout?.endColumn).toBe(2);
expect(autumnLayout?.row).toBe(2); // Shorter overlapping event gets row 2
});
it('Scenario 3: Multiple events in same column', () => {
// All events on Sept 23 only
const event1 = createMockEvent('1', 'Event 1', '2024-09-23', '2024-09-23');
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-23');
const event3 = createMockEvent('3', 'Event 3', '2024-09-23', '2024-09-23');
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([event1, event2, event3], weekDates);
const layout1 = layouts.get('1');
const layout2 = layouts.get('2');
const layout3 = layouts.get('3');
expect(layout1?.startColumn).toBe(2);
expect(layout1?.row).toBe(1);
expect(layout2?.startColumn).toBe(2);
expect(layout2?.row).toBe(2);
expect(layout3?.startColumn).toBe(2);
expect(layout3?.row).toBe(3);
});
it('Scenario 4: Partial overlaps', () => {
// Event 1: Sept 22-23 (columns 1-2)
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-23');
// Event 2: Sept 23-24 (columns 2-3) - overlaps on Sept 23
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-24');
// Event 3: Sept 25-26 (columns 4-5) - no overlap, should be row 1
const event3 = createMockEvent('3', 'Event 3', '2024-09-25', '2024-09-26');
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([event1, event2, event3], weekDates);
const layout1 = layouts.get('1');
const layout2 = layouts.get('2');
const layout3 = layouts.get('3');
expect(layout1?.startColumn).toBe(1);
expect(layout1?.endColumn).toBe(2);
expect(layout1?.row).toBe(1);
expect(layout2?.startColumn).toBe(2);
expect(layout2?.endColumn).toBe(3);
expect(layout2?.row).toBe(2); // Should be row 2 due to overlap
expect(layout3?.startColumn).toBe(4);
expect(layout3?.endColumn).toBe(5);
expect(layout3?.row).toBe(1); // No overlap, back to row 1
});
it('Scenario 5: Complex overlapping pattern', () => {
// Event 1: Sept 22-25 (columns 1-4) - spans most of week (4 days)
const event1 = createMockEvent('1', 'Long Event', '2024-09-22', '2024-09-25');
// Event 2: Sept 23-24 (columns 2-3) - overlaps with Event 1 (2 days)
const event2 = createMockEvent('2', 'Short Event', '2024-09-23', '2024-09-24');
// Event 3: Sept 24-26 (columns 3-5) - overlaps with both (3 days)
const event3 = createMockEvent('3', 'Another Event', '2024-09-24', '2024-09-26');
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([event1, event2, event3], weekDates);
const layout1 = layouts.get('1');
const layout2 = layouts.get('2');
const layout3 = layouts.get('3');
// Longest-first sorting: Event1 (4 days) -> Event3 (3 days) -> Event2 (2 days)
expect(layout1?.startColumn).toBe(1);
expect(layout1?.endColumn).toBe(4);
expect(layout1?.row).toBe(1); // Longest event gets row 1
expect(layout3?.startColumn).toBe(3);
expect(layout3?.endColumn).toBe(5);
expect(layout3?.row).toBe(2); // Second longest, overlaps with Event1, gets row 2
expect(layout2?.startColumn).toBe(2);
expect(layout2?.endColumn).toBe(3);
expect(layout2?.row).toBe(3); // Shortest, overlaps with both, gets row 3
});
it('Scenario 6: Single event for drag-drop simulation', () => {
// Single event placed at specific date
const event = createMockEvent('drag', 'Dragged Event', '2024-09-24', '2024-09-24');
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([event], weekDates); const layouts = allDayManager.calculateAllDayEventsLayout([event], weekDates);
const layout = layouts.get('drag'); expect(layouts.size).toBe(1);
expect(layouts.has('test')).toBe(true);
const layout = layouts.get('test');
expect(layout?.startColumn).toBe(3); // Sept 24 is column 3 expect(layout?.startColumn).toBe(3); // Sept 24 is column 3
expect(layout?.endColumn).toBe(3); // Single column
expect(layout?.columnSpan).toBe(1);
expect(layout?.row).toBe(1); expect(layout?.row).toBe(1);
}); });
it('should handle empty event list', () => {
const weekDates = ['2024-09-22', '2024-09-23', '2024-09-24', '2024-09-25', '2024-09-26'];
const layouts = allDayManager.calculateAllDayEventsLayout([], weekDates);
expect(layouts.size).toBe(0);
});
it('should handle empty week dates', () => {
const event = createMockEvent('test', 'Test Event', '2024-09-24', '2024-09-24');
const layouts = allDayManager.calculateAllDayEventsLayout([event], []);
expect(layouts.size).toBe(0);
});
}); });
}); });