/** * EventLayoutCoordinator - Coordinates event layout calculations * * Separates layout logic from rendering concerns. * Calculates stack levels, groups events, and determines rendering strategy. */ import { CalendarEvent } from '../types/CalendarTypes'; import { EventStackManager, EventGroup, StackLink } from './EventStackManager'; import { PositionUtils } from '../utils/PositionUtils'; export interface GridGroupLayout { events: CalendarEvent[]; stackLevel: number; position: { top: number }; } export interface StackedEventLayout { event: CalendarEvent; stackLink: StackLink; position: { top: number; height: number }; } export interface ColumnLayout { gridGroups: GridGroupLayout[]; stackedEvents: StackedEventLayout[]; } export class EventLayoutCoordinator { private stackManager: EventStackManager; constructor() { this.stackManager = new EventStackManager(); } /** * Calculate complete layout for a column of events */ public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout { if (columnEvents.length === 0) { return { gridGroups: [], stackedEvents: [] }; } // Step 1: Calculate stack levels for ALL events first (to understand overlaps) const allStackLinks = this.stackManager.createOptimizedStackLinks(columnEvents); // Step 2: Find grid candidates (start together ±15 min) const groups = this.stackManager.groupEventsByStartTime(columnEvents); const gridGroups = groups.filter(group => { if (group.events.length <= 1) return false; group.containerType = this.stackManager.decideContainerType(group); return group.containerType === 'GRID'; }); // Step 3: Build grid group layouts const gridGroupLayouts: GridGroupLayout[] = []; const renderedEventIds = new Set(); gridGroups.forEach(group => { const gridStackLevel = this.calculateGridGroupStackLevel(group, columnEvents, allStackLinks); const earliestEvent = group.events[0]; const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); gridGroupLayouts.push({ events: group.events, stackLevel: gridStackLevel, position: { top: position.top + 1 } }); group.events.forEach(e => renderedEventIds.add(e.id)); }); // Step 4: Build stacked event layouts for remaining events const remainingEvents = columnEvents.filter(e => !renderedEventIds.has(e.id)); const stackedEventLayouts: StackedEventLayout[] = remainingEvents.map(event => { const stackLink = allStackLinks.get(event.id)!; const position = PositionUtils.calculateEventPosition(event.start, event.end); return { event, stackLink, position: { top: position.top + 1, height: position.height - 3 } }; }); return { gridGroups: gridGroupLayouts, stackedEvents: stackedEventLayouts }; } /** * Calculate stack level for a grid group based on what it overlaps OUTSIDE the group */ private calculateGridGroupStackLevel( group: EventGroup, allEvents: CalendarEvent[], stackLinks: Map ): number { const groupEventIds = new Set(group.events.map(e => e.id)); // Find all events OUTSIDE this group const outsideEvents = allEvents.filter(e => !groupEventIds.has(e.id)); // Find the highest stackLevel of any event that overlaps with ANY event in the grid group let maxOverlappingLevel = -1; for (const gridEvent of group.events) { for (const outsideEvent of outsideEvents) { if (this.stackManager.doEventsOverlap(gridEvent, outsideEvent)) { const outsideLink = stackLinks.get(outsideEvent.id); if (outsideLink) { maxOverlappingLevel = Math.max(maxOverlappingLevel, outsideLink.stackLevel); } } } } // Grid group should be one level above the highest overlapping event return maxOverlappingLevel + 1; } }