From 69495ce00f3d40c9fb7653062442cde8206e9ff7 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 6 Oct 2025 21:39:57 +0200 Subject: [PATCH] cleanup --- docs/code-analysis-report.md | 329 +++++++++++++++ src/interfaces/IManager.ts | 50 --- src/managers/SimpleEventOverlapManager.ts | 473 ---------------------- src/strategies/MonthViewStrategy.ts | 167 -------- src/strategies/WeekViewStrategy.ts | 77 ---- src/types/EventPayloadMap.ts | 176 -------- src/types/ManagerTypes.ts | 9 +- src/utils/OverlapDetector.ts | 75 ---- test/utils/OverlapDetector.test.ts | 287 ------------- 9 files changed, 337 insertions(+), 1306 deletions(-) create mode 100644 docs/code-analysis-report.md delete mode 100644 src/interfaces/IManager.ts delete mode 100644 src/managers/SimpleEventOverlapManager.ts delete mode 100644 src/strategies/MonthViewStrategy.ts delete mode 100644 src/strategies/WeekViewStrategy.ts delete mode 100644 src/types/EventPayloadMap.ts delete mode 100644 src/utils/OverlapDetector.ts delete mode 100644 test/utils/OverlapDetector.test.ts diff --git a/docs/code-analysis-report.md b/docs/code-analysis-report.md new file mode 100644 index 0000000..232b309 --- /dev/null +++ b/docs/code-analysis-report.md @@ -0,0 +1,329 @@ +# Code Analysis Report +## Calendar Plantempus TypeScript Codebase + +**Generated:** 2025-10-06 +**Tool:** ts-unused-exports +**Total Files Analyzed:** 40+ TypeScript files + +--- + +## Executive Summary + +### Unused Exports Found +- **Total Modules with Unused Exports:** 14 +- **Total Unused Exports:** 40+ + +### Impact Assessment +- 🟢 **Low Risk:** Type definitions and interfaces (can be kept for future use) +- 🟡 **Medium Risk:** Unused classes and managers (should be reviewed) +- 🔴 **High Risk:** Duplicate implementations (should be removed) + +--- + +## Detailed Findings + +### 1. Constants & Core (`src/constants/`) + +#### `CoreEvents.ts` +**Unused Exports:** +- `CoreEventType` - Type definition +- `EVENT_MIGRATION_MAP` - Migration mapping + +**Analysis:** +- `CoreEventType` er en type definition - kan være nyttig for fremtidig type-sikkerhed +- `EVENT_MIGRATION_MAP` ser ud til at være til migration fra gamle events - kan muligvis fjernes hvis migration er færdig + +**Recommendation:** ⚠️ Review - Tjek om migration er færdig + +--- + +### 2. Factories (`src/factories/`) + +#### `CalendarTypeFactory.ts` +**Unused Exports:** +- `RendererConfig` - Interface + +**Analysis:** +- Interface der ikke bruges eksternt +- Kan være intern implementation detail + +**Recommendation:** ✅ Keep - Internal interface, no harm + +--- + +### 3. Interfaces (`src/interfaces/`) + +#### `IManager.ts` +**Unused Exports:** +- `IEventManager` +- `IRenderingManager` +- `INavigationManager` +- `IScrollManager` + +**Analysis:** +- Disse interfaces definerer kontrakter men bruges ikke +- Kan være planlagt til fremtidig dependency injection + +**Recommendation:** 🔴 **CRITICAL** - Enten brug interfaces eller fjern dem. Interfaces uden implementation er dead code. + +--- + +### 4. Managers (`src/managers/`) + +#### `EventLayoutCoordinator.ts` +**Unused Exports:** +- `ColumnLayout` - Interface + +**Analysis:** +- Return type interface der ikke bruges eksternt + +**Recommendation:** ✅ Keep - Part of public API + +#### `SimpleEventOverlapManager.ts` +**Unused Exports:** +- `OverlapType` +- `OverlapGroup` +- `StackLink` +- `SimpleEventOverlapManager` - **ENTIRE CLASS** + +**Analysis:** +- 🔴 **CRITICAL FINDING:** Hele klassen er ubrugt! +- Dette ser ud til at være en gammel implementation der er blevet erstattet af `EventStackManager` + +**Recommendation:** 🔴 **DELETE** - Remove entire file if not used + +#### `WorkHoursManager.ts` +**Unused Exports:** +- `DayWorkHours` +- `WorkScheduleConfig` + +**Analysis:** +- Interfaces for work hours functionality +- Kan være planlagt feature + +**Recommendation:** ⚠️ Review - Check if feature is planned or abandoned + +--- + +### 5. Strategies (`src/strategies/`) + +#### `MonthViewStrategy.ts` +**Unused Exports:** +- `MonthViewStrategy` - **ENTIRE CLASS** + +**Analysis:** +- 🔴 **CRITICAL:** Hele strategy-klassen er ubrugt +- Kan være planlagt feature eller gammel implementation + +**Recommendation:** 🔴 **DELETE or IMPLEMENT** - Either use it or remove it + +#### `WeekViewStrategy.ts` +**Unused Exports:** +- `WeekViewStrategy` - **ENTIRE CLASS** + +**Analysis:** +- 🔴 **CRITICAL:** Hele strategy-klassen er ubrugt +- Samme som MonthViewStrategy + +**Recommendation:** 🔴 **DELETE or IMPLEMENT** - Either use it or remove it + +--- + +### 6. Types (`src/types/`) + +#### `CalendarTypes.ts` +**Unused Exports:** +- `SyncStatus` +- `Resource` +- `GridPosition` +- `Period` +- `EventData` +- `DateModeContext` +- `ResourceModeContext` +- `CalendarModeContext` + +**Analysis:** +- Mange type definitions der ikke bruges +- Nogle kan være planlagt features (Resource, ResourceModeContext) +- Andre kan være legacy (SyncStatus, EventData) + +**Recommendation:** ⚠️ Review each type individually + +#### `DragDropTypes.ts` +**Unused Exports:** +- `DragState` +- `DragEndPosition` +- `StackLinkData` +- `DragEventHandlers` + +**Analysis:** +- Drag & drop types der ikke bruges eksternt +- Kan være internal types + +**Recommendation:** ✅ Keep - Part of drag-drop system + +#### `EventPayloadMap.ts` +**Unused Exports:** +- `CalendarEventPayloadMap` +- `EventPayload` +- `hasPayload` + +**Analysis:** +- Event payload system der ikke bruges +- Kan være planlagt type-safe event system + +**Recommendation:** ⚠️ Review - Check if this is planned feature + +#### `EventTypes.ts` +**Unused Exports:** +- `AllDayEvent` +- `TimeEvent` +- `CalendarEventData` +- `MousePosition` + +**Analysis:** +- Type definitions for events +- `MousePosition` bruges sandsynligvis internt + +**Recommendation:** ✅ Keep - Core types + +#### `ManagerTypes.ts` +**Unused Exports:** +- `EventManager` +- `EventRenderingService` +- `GridManager` +- `ScrollManager` +- `NavigationManager` +- `ViewManager` +- `CalendarManager` +- `DragDropManager` +- `AllDayManager` +- `Resource` +- `ResourceAssignment` + +**Analysis:** +- 🔴 **CRITICAL:** Mange manager types der ikke bruges +- Dette tyder på at type system ikke er implementeret korrekt + +**Recommendation:** 🔴 **REFACTOR** - Either use these types or remove them + +--- + +### 7. Utils (`src/utils/`) + +#### `OverlapDetector.ts` +**Unused Exports:** +- `EventId` +- `OverlapResult` +- `StackLink` +- `OverlapDetector` - **ENTIRE CLASS** + +**Analysis:** +- 🔴 **CRITICAL:** Hele utility-klassen er ubrugt +- Sandsynligvis erstattet af anden implementation + +**Recommendation:** 🔴 **DELETE** - Remove if not used + +--- + +## Summary Statistics + +### By Category + +| Category | Total Unused | Critical (Classes) | Medium (Interfaces) | Low (Types) | +|----------|--------------|-------------------|---------------------|-------------| +| Constants | 2 | 0 | 0 | 2 | +| Factories | 1 | 0 | 1 | 0 | +| Interfaces | 4 | 0 | 4 | 0 | +| Managers | 8 | 1 | 3 | 4 | +| Strategies | 2 | 2 | 0 | 0 | +| Types | 23 | 0 | 0 | 23 | +| Utils | 4 | 1 | 0 | 3 | +| **TOTAL** | **44** | **4** | **8** | **32** | + +--- + +## Critical Issues (Requires Immediate Action) + +### 🔴 Unused Classes (Dead Code) +1. **`SimpleEventOverlapManager`** - Entire class unused +2. **`MonthViewStrategy`** - Entire class unused +3. **`WeekViewStrategy`** - Entire class unused +4. **`OverlapDetector`** - Entire class unused + +### 🔴 Unused Interfaces (Architecture Issue) +1. **`IManager.ts`** - All 4 interfaces unused + - Suggests dependency injection pattern not implemented + - Either implement or remove + +--- + +## Recommendations + +### Immediate Actions (High Priority) + +1. **Delete Dead Code:** + ```bash + # Remove these files if confirmed unused: + rm src/managers/SimpleEventOverlapManager.ts + rm src/strategies/MonthViewStrategy.ts + rm src/strategies/WeekViewStrategy.ts + rm src/utils/OverlapDetector.ts + ``` + +2. **Review Interfaces:** + - Decide if `IManager.ts` interfaces should be implemented + - If not, remove them + +3. **Clean Up Types:** + - Review `ManagerTypes.ts` - many unused types + - Consider if these are planned features or legacy code + +### Medium Priority + +4. **Review Planned Features:** + - `Resource` and `ResourceModeContext` - Are these planned? + - `WorkHoursManager` types - Is this feature coming? + - `EventPayloadMap` - Is type-safe event system planned? + +5. **Document Decisions:** + - Add comments explaining why certain exports exist + - Mark planned features clearly + +### Low Priority + +6. **Type Definitions:** + - Most type definitions can stay (low cost) + - But consider if they add confusion + +--- + +## Estimated Impact + +### Code Reduction Potential +- **Files that can be deleted:** 4 (SimpleEventOverlapManager, MonthViewStrategy, WeekViewStrategy, OverlapDetector) +- **Lines of code reduction:** ~500-800 lines +- **Maintenance burden reduction:** Significant + +### Risk Assessment +- **Low Risk:** Removing unused classes (they're not imported anywhere) +- **Medium Risk:** Removing interfaces (might break future plans) +- **High Risk:** None (all findings are confirmed unused) + +--- + +## Next Steps + +1. ✅ **Review this report with team** +2. ⚠️ **Decide on each critical issue** +3. 🔴 **Create cleanup tasks** +4. ✅ **Run tests after cleanup** +5. ✅ **Update documentation** + +--- + +## Tools Used + +- **ts-unused-exports** v11.0.1 +- Analysis date: 2025-10-06 +- Project: Calendar Plantempus diff --git a/src/interfaces/IManager.ts b/src/interfaces/IManager.ts deleted file mode 100644 index caa3fd6..0000000 --- a/src/interfaces/IManager.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CalendarEvent } from '../types/CalendarTypes'; - -/** - * Base interface for all managers - */ -export interface IManager { - /** - * Initialize the manager - */ - initialize?(): Promise | void; - - /** - * Refresh the manager's state - */ - refresh?(): void; -} - -/** - * Interface for managers that handle events - */ -export interface IEventManager extends IManager { - loadData(): Promise; - getEvents(): CalendarEvent[]; - getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[]; -} - -/** - * Interface for managers that handle rendering - */ -export interface IRenderingManager extends IManager { - render(): Promise | void; -} - -/** - * Interface for managers that handle navigation - */ -export interface INavigationManager extends IManager { - getCurrentWeek(): Date; - navigateToToday(): void; - navigateToNextWeek(): void; - navigateToPreviousWeek(): void; -} - -/** - * Interface for managers that handle scrolling - */ -export interface IScrollManager extends IManager { - scrollTo(scrollTop: number): void; - scrollToHour(hour: number): void; -} \ No newline at end of file diff --git a/src/managers/SimpleEventOverlapManager.ts b/src/managers/SimpleEventOverlapManager.ts deleted file mode 100644 index 9b7a2ad..0000000 --- a/src/managers/SimpleEventOverlapManager.ts +++ /dev/null @@ -1,473 +0,0 @@ -/** - * SimpleEventOverlapManager - Clean, focused overlap management - * Eliminates complex state tracking in favor of direct DOM manipulation - */ - -import { CalendarEvent } from '../types/CalendarTypes'; -import { calendarConfig } from '../core/CalendarConfig'; - -export enum OverlapType { - NONE = 'none', - COLUMN_SHARING = 'column_sharing', - STACKING = 'stacking' -} - -export interface OverlapGroup { - type: OverlapType; - events: CalendarEvent[]; - position: { top: number; height: number }; -} - -export interface StackLink { - prev?: string; // Event ID of previous event in stack - next?: string; // Event ID of next event in stack - stackLevel: number; // 0 = base event, 1 = first stacked, etc -} - -export class SimpleEventOverlapManager { - private static readonly STACKING_WIDTH_REDUCTION_PX = 15; - - /** - * Detect overlap type between two DOM elements - pixel-based logic - */ - public resolveOverlapType(element1: HTMLElement, element2: HTMLElement): OverlapType { - const top1 = parseInt(element1.style.top) || 0; - const height1 = parseInt(element1.style.height) || 0; - const bottom1 = top1 + height1; - - const top2 = parseInt(element2.style.top) || 0; - const height2 = parseInt(element2.style.height) || 0; - const bottom2 = top2 + height2; - - // Check if events overlap in pixel space - const tolerance = 2; - if (bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance)) { - return OverlapType.NONE; - } - - // Events overlap - check start position difference for overlap type - const startDifference = Math.abs(top1 - top2); - - // Over 40px start difference = stacking - if (startDifference > 40) { - return OverlapType.STACKING; - } - - // Within 40px start difference = column sharing - return OverlapType.COLUMN_SHARING; - } - - - /** - * Group overlapping elements - pixel-based algorithm - */ - public groupOverlappingElements(elements: HTMLElement[]): HTMLElement[][] { - const groups: HTMLElement[][] = []; - const processed = new Set(); - - for (const element of elements) { - if (processed.has(element)) continue; - - // Find all elements that overlap with this one - const overlapping = elements.filter(other => { - if (processed.has(other)) return false; - return other === element || this.resolveOverlapType(element, other) !== OverlapType.NONE; - }); - - // Mark all as processed - overlapping.forEach(e => processed.add(e)); - - groups.push(overlapping); - } - - return groups; - } - - /** - * Create flexbox container for column sharing - clean and simple - */ - public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement { - const container = document.createElement('swp-event-group'); - return container; - } - - /** - * Add event to flexbox group - simple relative positioning - */ - public addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void { - // Set duration-based height - const duration = eventElement.dataset.duration; - if (duration) { - const durationMinutes = parseInt(duration); - const gridSettings = calendarConfig.getGridSettings(); - const height = (durationMinutes / 60) * gridSettings.hourHeight; - eventElement.style.height = `${height - 3}px`; - } - - // Flexbox styling - eventElement.style.position = 'relative'; - eventElement.style.flex = '1'; - eventElement.style.minWidth = '50px'; - - container.appendChild(eventElement); - } - - /** - * Create stacked event with data-attribute tracking - */ - public createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement, stackLevel: number): void { - const marginLeft = stackLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; - - // Apply visual styling - eventElement.style.marginLeft = `${marginLeft}px`; - eventElement.style.left = '2px'; - eventElement.style.right = '2px'; - eventElement.style.zIndex = `${100 + stackLevel}`; - - // Set up stack linking via data attributes - const eventId = eventElement.dataset.eventId; - const underlyingId = underlyingElement.dataset.eventId; - - if (!eventId || !underlyingId) { - console.warn('Missing event IDs for stack linking:', eventId, underlyingId); - return; - } - - // Find the last event in the stack chain - let lastElement = underlyingElement; - let lastLink = this.getStackLink(lastElement); - - // If underlying doesn't have stack link yet, create it - if (!lastLink) { - this.setStackLink(lastElement, { stackLevel: 0 }); - lastLink = { stackLevel: 0 }; - } - - // Traverse to find the end of the chain - while (lastLink?.next) { - const nextElement = this.findElementById(lastLink.next); - if (!nextElement) break; - lastElement = nextElement; - lastLink = this.getStackLink(lastElement); - } - - // Link the new event to the end of the chain - const lastElementId = lastElement.dataset.eventId!; - this.setStackLink(lastElement, { - ...lastLink!, - next: eventId - }); - - this.setStackLink(eventElement, { - prev: lastElementId, - stackLevel: stackLevel - }); - } - - /** - * Remove stacked styling with proper stack re-linking - */ - public removeStackedStyling(eventElement: HTMLElement): void { - // Clear visual styling - eventElement.style.marginLeft = ''; - eventElement.style.zIndex = ''; - eventElement.style.left = '2px'; - eventElement.style.right = '2px'; - - // Handle stack chain re-linking - const link = this.getStackLink(eventElement); - if (link) { - // Re-link prev and next events - if (link.prev && link.next) { - // Middle element - link prev to next - const prevElement = this.findElementById(link.prev); - const nextElement = this.findElementById(link.next); - - if (prevElement && nextElement) { - const prevLink = this.getStackLink(prevElement); - const nextLink = this.getStackLink(nextElement); - - // CRITICAL: Check if prev and next actually overlap without the middle element - const actuallyOverlap = this.resolveOverlapType(prevElement, nextElement); - - if (!actuallyOverlap) { - // CHAIN BREAKING: prev and next don't overlap - break the chain - console.log('Breaking stack chain - events do not overlap directly'); - - // Prev element: remove next link (becomes end of its own chain) - this.setStackLink(prevElement, { - ...prevLink!, - next: undefined - }); - - // Next element: becomes standalone (remove all stack links and styling) - this.setStackLink(nextElement, null); - nextElement.style.marginLeft = ''; - nextElement.style.zIndex = ''; - - // If next element had subsequent events, they also become standalone - if (nextLink?.next) { - let subsequentId: string | undefined = nextLink.next; - while (subsequentId) { - const subsequentElement = this.findElementById(subsequentId); - if (!subsequentElement) break; - - const subsequentLink = this.getStackLink(subsequentElement); - this.setStackLink(subsequentElement, null); - subsequentElement.style.marginLeft = ''; - subsequentElement.style.zIndex = ''; - - subsequentId = subsequentLink?.next; - } - } - } else { - // NORMAL STACKING: they overlap, maintain the chain - this.setStackLink(prevElement, { - ...prevLink!, - next: link.next - }); - - const correctStackLevel = (prevLink?.stackLevel ?? 0) + 1; - this.setStackLink(nextElement, { - ...nextLink!, - prev: link.prev, - stackLevel: correctStackLevel - }); - - // Update visual styling to match new stackLevel - const marginLeft = correctStackLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; - nextElement.style.marginLeft = `${marginLeft}px`; - nextElement.style.zIndex = `${100 + correctStackLevel}`; - } - } - } else if (link.prev) { - // Last element - remove next link from prev - const prevElement = this.findElementById(link.prev); - if (prevElement) { - const prevLink = this.getStackLink(prevElement); - this.setStackLink(prevElement, { - ...prevLink!, - next: undefined - }); - } - } else if (link.next) { - // First element - remove prev link from next - const nextElement = this.findElementById(link.next); - if (nextElement) { - const nextLink = this.getStackLink(nextElement); - this.setStackLink(nextElement, { - ...nextLink!, - prev: undefined, - stackLevel: 0 // Next becomes the base event - }); - } - } - - // Only update subsequent stack levels if we didn't break the chain - if (link.prev && link.next) { - const nextElement = this.findElementById(link.next); - const nextLink = nextElement ? this.getStackLink(nextElement) : null; - - // If next element still has a stack link, the chain wasn't broken - if (nextLink && nextLink.next) { - this.updateSubsequentStackLevels(nextLink.next, -1); - } - // If nextLink is null, chain was broken - no subsequent updates needed - } else { - // First or last removal - update all subsequent - this.updateSubsequentStackLevels(link.next, -1); - } - - // Clear this element's stack link - this.setStackLink(eventElement, null); - } - } - - /** - * Update stack levels for all events following a given event ID - */ - private updateSubsequentStackLevels(startEventId: string | undefined, levelDelta: number): void { - let currentId = startEventId; - - while (currentId) { - const currentElement = this.findElementById(currentId); - if (!currentElement) break; - - const currentLink = this.getStackLink(currentElement); - if (!currentLink) break; - - // Update stack level - const newLevel = Math.max(0, currentLink.stackLevel + levelDelta); - this.setStackLink(currentElement, { - ...currentLink, - stackLevel: newLevel - }); - - // Update visual styling - const marginLeft = newLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; - currentElement.style.marginLeft = `${marginLeft}px`; - currentElement.style.zIndex = `${100 + newLevel}`; - - currentId = currentLink.next; - } - } - - /** - * Check if element is stacked - check both style and data-stack-link - */ - public isStackedEvent(element: HTMLElement): boolean { - const marginLeft = element.style.marginLeft; - const hasMarginLeft = marginLeft !== '' && marginLeft !== '0px'; - const hasStackLink = this.getStackLink(element) !== null; - - return hasMarginLeft || hasStackLink; - } - - /** - * Remove event from group with proper cleanup - */ - public removeFromEventGroup(container: HTMLElement, eventId: string): boolean { - const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; - if (!eventElement) return false; - - // Simply remove the element - no position calculation needed since it's being removed - eventElement.remove(); - - // Handle remaining events - const remainingEvents = container.querySelectorAll('swp-event'); - const remainingCount = remainingEvents.length; - - if (remainingCount === 0) { - container.remove(); - return true; - } - - if (remainingCount === 1) { - const remainingEvent = remainingEvents[0] as HTMLElement; - - // Convert last event back to absolute positioning - use current pixel position - const currentTop = parseInt(remainingEvent.style.top) || 0; - - remainingEvent.style.position = 'absolute'; - remainingEvent.style.top = `${currentTop}px`; - remainingEvent.style.left = '2px'; - remainingEvent.style.right = '2px'; - remainingEvent.style.flex = ''; - remainingEvent.style.minWidth = ''; - - container.parentElement?.insertBefore(remainingEvent, container); - container.remove(); - return true; - } - - return false; - } - - /** - * Restack events in container - respects separate stack chains - */ - public restackEventsInContainer(container: HTMLElement): void { - const stackedEvents = Array.from(container.querySelectorAll('swp-event')) - .filter(el => this.isStackedEvent(el as HTMLElement)) as HTMLElement[]; - - if (stackedEvents.length === 0) return; - - // Group events by their stack chains - const processedEventIds = new Set(); - const stackChains: HTMLElement[][] = []; - - for (const element of stackedEvents) { - const eventId = element.dataset.eventId; - if (!eventId || processedEventIds.has(eventId)) continue; - - // Find the root of this stack chain (stackLevel 0 or no prev link) - let rootElement = element; - let rootLink = this.getStackLink(rootElement); - - while (rootLink?.prev) { - const prevElement = this.findElementById(rootLink.prev); - if (!prevElement) break; - rootElement = prevElement; - rootLink = this.getStackLink(rootElement); - } - - // Collect all elements in this chain - const chain: HTMLElement[] = []; - let currentElement = rootElement; - - while (currentElement) { - chain.push(currentElement); - processedEventIds.add(currentElement.dataset.eventId!); - - const currentLink = this.getStackLink(currentElement); - if (!currentLink?.next) break; - - const nextElement = this.findElementById(currentLink.next); - if (!nextElement) break; - currentElement = nextElement; - } - - if (chain.length > 1) { // Only add chains with multiple events - stackChains.push(chain); - } - } - - // Re-stack each chain separately - stackChains.forEach(chain => { - chain.forEach((element, index) => { - const marginLeft = index * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; - element.style.marginLeft = `${marginLeft}px`; - element.style.zIndex = `${100 + index}`; - - // Update the data-stack-link with correct stackLevel - const link = this.getStackLink(element); - if (link) { - this.setStackLink(element, { - ...link, - stackLevel: index - }); - } - }); - }); - } - - - /** - * Utility methods - simple DOM traversal - */ - public getEventGroup(eventElement: HTMLElement): HTMLElement | null { - return eventElement.closest('swp-event-group') as HTMLElement; - } - - public isInEventGroup(element: HTMLElement): boolean { - return this.getEventGroup(element) !== null; - } - - /** - * Helper methods for data-attribute based stack tracking - */ - public getStackLink(element: HTMLElement): StackLink | null { - const linkData = element.dataset.stackLink; - if (!linkData) return null; - - try { - return JSON.parse(linkData); - } catch (e) { - console.warn('Failed to parse stack link data:', linkData, e); - return null; - } - } - - private setStackLink(element: HTMLElement, link: StackLink | null): void { - if (link === null) { - delete element.dataset.stackLink; - } else { - element.dataset.stackLink = JSON.stringify(link); - } - } - - private findElementById(eventId: string): HTMLElement | null { - return document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; - } - -} \ No newline at end of file diff --git a/src/strategies/MonthViewStrategy.ts b/src/strategies/MonthViewStrategy.ts deleted file mode 100644 index 9ee61ea..0000000 --- a/src/strategies/MonthViewStrategy.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * MonthViewStrategy - Strategy for month view rendering - * Completely different from week view - no time axis, cell-based events - */ - -import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy'; -import { DateService } from '../utils/DateService'; -import { calendarConfig } from '../core/CalendarConfig'; -import { CalendarEvent } from '../types/CalendarTypes'; - -export class MonthViewStrategy implements ViewStrategy { - private dateService: DateService; - - constructor() { - this.dateService = new DateService('Europe/Copenhagen'); - } - - getLayoutConfig(): ViewLayoutConfig { - return { - needsTimeAxis: false, // No time axis in month view! - columnCount: 7, // Always 7 days (Mon-Sun) - scrollable: false, // Month fits in viewport - eventPositioning: 'cell-based' // Events go in day cells - }; - } - - renderGrid(context: ViewContext): void { - // Clear existing content - context.container.innerHTML = ''; - - // Create month grid (completely different from week!) - this.createMonthGrid(context); - } - - private createMonthGrid(context: ViewContext): void { - const monthGrid = document.createElement('div'); - monthGrid.className = 'month-grid'; - monthGrid.style.display = 'grid'; - monthGrid.style.gridTemplateColumns = 'repeat(7, 1fr)'; - monthGrid.style.gridTemplateRows = 'auto repeat(6, 1fr)'; - monthGrid.style.height = '100%'; - - // Add day headers (Mon, Tue, Wed, etc.) - this.createDayHeaders(monthGrid); - - // Add 6 weeks of day cells - this.createDayCells(monthGrid, context.currentDate); - - // Render events in day cells (will be handled by EventRendererManager) - // this.renderMonthEvents(monthGrid, context.allDayEvents); - - context.container.appendChild(monthGrid); - } - - private createDayHeaders(container: HTMLElement): void { - const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - - dayNames.forEach(dayName => { - const header = document.createElement('div'); - header.className = 'month-day-header'; - header.textContent = dayName; - header.style.padding = '8px'; - header.style.fontWeight = 'bold'; - header.style.textAlign = 'center'; - header.style.borderBottom = '1px solid #e0e0e0'; - container.appendChild(header); - }); - } - - private createDayCells(container: HTMLElement, monthDate: Date): void { - const dates = this.getMonthDates(monthDate); - - dates.forEach(date => { - const cell = document.createElement('div'); - cell.className = 'month-day-cell'; - cell.dataset.date = this.dateService.formatISODate(date); - cell.style.border = '1px solid #e0e0e0'; - cell.style.minHeight = '100px'; - cell.style.padding = '4px'; - cell.style.position = 'relative'; - - // Day number - const dayNumber = document.createElement('div'); - dayNumber.className = 'month-day-number'; - dayNumber.textContent = date.getDate().toString(); - dayNumber.style.fontWeight = 'bold'; - dayNumber.style.marginBottom = '4px'; - - // Check if today - if (this.dateService.isSameDay(date, new Date())) { - dayNumber.style.color = '#1976d2'; - cell.style.backgroundColor = '#f5f5f5'; - } - - cell.appendChild(dayNumber); - container.appendChild(cell); - }); - } - - private getMonthDates(monthDate: Date): Date[] { - // Get first day of month using DateService - const year = monthDate.getFullYear(); - const month = monthDate.getMonth(); - const firstOfMonth = this.dateService.startOfDay(new Date(year, month, 1)); - - // Get Monday of the week containing first day - const weekBounds = this.dateService.getWeekBounds(firstOfMonth); - const startDate = this.dateService.startOfDay(weekBounds.start); - - // Generate 42 days (6 weeks) - const dates: Date[] = []; - for (let i = 0; i < 42; i++) { - dates.push(this.dateService.addDays(startDate, i)); - } - - return dates; - } - - private renderMonthEvents(container: HTMLElement, events: CalendarEvent[]): void { - // TODO: Implement month event rendering - // Events will be small blocks in day cells - } - - getNextPeriod(currentDate: Date): Date { - const nextMonth = this.dateService.addMonths(currentDate, 1); - const year = nextMonth.getFullYear(); - const month = nextMonth.getMonth(); - return this.dateService.startOfDay(new Date(year, month, 1)); - } - - getPreviousPeriod(currentDate: Date): Date { - const prevMonth = this.dateService.addMonths(currentDate, -1); - const year = prevMonth.getFullYear(); - const month = prevMonth.getMonth(); - return this.dateService.startOfDay(new Date(year, month, 1)); - } - - getPeriodLabel(date: Date): string { - const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December']; - - return `${monthNames[date.getMonth()]} ${date.getFullYear()}`; - } - - getDisplayDates(baseDate: Date): Date[] { - return this.getMonthDates(baseDate); - } - - getPeriodRange(baseDate: Date): { startDate: Date; endDate: Date } { - // Month view shows events for the entire month grid (including partial weeks) - const year = baseDate.getFullYear(); - const month = baseDate.getMonth(); - const firstOfMonth = this.dateService.startOfDay(new Date(year, month, 1)); - - // Get Monday of the week containing first day - const weekBounds = this.dateService.getWeekBounds(firstOfMonth); - const startDate = this.dateService.startOfDay(weekBounds.start); - - // End date is 41 days after start (42 total days) - const endDate = this.dateService.addDays(startDate, 41); - - return { - startDate, - endDate - }; - } -} \ No newline at end of file diff --git a/src/strategies/WeekViewStrategy.ts b/src/strategies/WeekViewStrategy.ts deleted file mode 100644 index bd8e1db..0000000 --- a/src/strategies/WeekViewStrategy.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * WeekViewStrategy - Strategy for week/day view rendering - * Extracts the time-based grid logic from GridManager - */ - -import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy'; -import { DateService } from '../utils/DateService'; -import { calendarConfig } from '../core/CalendarConfig'; -import { GridRenderer } from '../renderers/GridRenderer'; -import { GridStyleManager } from '../renderers/GridStyleManager'; - -export class WeekViewStrategy implements ViewStrategy { - private dateService: DateService; - private gridRenderer: GridRenderer; - private styleManager: GridStyleManager; - - constructor() { - const timezone = calendarConfig.getTimezone?.(); - this.dateService = new DateService(timezone); - this.gridRenderer = new GridRenderer(); - this.styleManager = new GridStyleManager(); - } - - getLayoutConfig(): ViewLayoutConfig { - return { - needsTimeAxis: true, - columnCount: calendarConfig.getWorkWeekSettings().totalDays, - scrollable: true, - eventPositioning: 'time-based' - }; - } - - renderGrid(context: ViewContext): void { - // Update grid styles - this.styleManager.updateGridStyles(context.resourceData); - - // Render the grid structure (time axis + day columns) - this.gridRenderer.renderGrid( - context.container, - context.currentDate, - context.resourceData - ); - } - - getNextPeriod(currentDate: Date): Date { - return this.dateService.addWeeks(currentDate, 1); - } - - getPreviousPeriod(currentDate: Date): Date { - return this.dateService.addWeeks(currentDate, -1); - } - - getPeriodLabel(date: Date): string { - const weekBounds = this.dateService.getWeekBounds(date); - const weekStart = this.dateService.startOfDay(weekBounds.start); - const weekEnd = this.dateService.addDays(weekStart, 6); - const weekNumber = this.dateService.getWeekNumber(date); - - return `Week ${weekNumber}: ${this.dateService.formatDateRange(weekStart, weekEnd)}`; - } - - getDisplayDates(baseDate: Date): Date[] { - const workWeekSettings = calendarConfig.getWorkWeekSettings(); - return this.dateService.getWorkWeekDates(baseDate, workWeekSettings.workDays); - } - - getPeriodRange(baseDate: Date): { startDate: Date; endDate: Date } { - const weekBounds = this.dateService.getWeekBounds(baseDate); - const weekStart = this.dateService.startOfDay(weekBounds.start); - const weekEnd = this.dateService.addDays(weekStart, 6); - - return { - startDate: weekStart, - endDate: weekEnd - }; - } -} \ No newline at end of file diff --git a/src/types/EventPayloadMap.ts b/src/types/EventPayloadMap.ts deleted file mode 100644 index e2630d2..0000000 --- a/src/types/EventPayloadMap.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { CalendarEvent, CalendarView } from './CalendarTypes'; -import { - DragStartEventPayload, - DragMoveEventPayload, - DragEndEventPayload, - DragMouseEnterHeaderEventPayload, - DragMouseLeaveHeaderEventPayload, - HeaderReadyEventPayload -} from './EventTypes'; -import { CoreEvents } from '../constants/CoreEvents'; - -/** - * Complete type mapping for all calendar events - * This enables type-safe event emission and handling - */ -export interface CalendarEventPayloadMap { - // Lifecycle events - [CoreEvents.INITIALIZED]: { - initialized: boolean; - timestamp: number; - }; - [CoreEvents.READY]: undefined; - [CoreEvents.DESTROYED]: undefined; - - // View events - [CoreEvents.VIEW_CHANGED]: { - view: CalendarView; - previousView?: CalendarView; - }; - [CoreEvents.VIEW_RENDERED]: { - view: CalendarView; - }; - [CoreEvents.WORKWEEK_CHANGED]: { - settings: unknown; - }; - - // Navigation events - [CoreEvents.DATE_CHANGED]: { - date: Date; - view?: CalendarView; - }; - [CoreEvents.NAVIGATION_COMPLETED]: { - direction: 'previous' | 'next' | 'today'; - }; - [CoreEvents.PERIOD_INFO_UPDATE]: { - label: string; - startDate: Date; - endDate: Date; - }; - [CoreEvents.NAVIGATE_TO_EVENT]: { - eventId: string; - }; - - // Data events - [CoreEvents.DATA_LOADING]: undefined; - [CoreEvents.DATA_LOADED]: { - events: CalendarEvent[]; - count: number; - }; - [CoreEvents.DATA_ERROR]: { - error: Error; - }; - [CoreEvents.EVENTS_FILTERED]: { - filteredEvents: CalendarEvent[]; - }; - - // Grid events - [CoreEvents.GRID_RENDERED]: { - container: HTMLElement; - currentDate: Date; - startDate: Date; - endDate: Date; - columnCount: number; - }; - [CoreEvents.GRID_CLICKED]: { - column: string; - row: number; - }; - [CoreEvents.CELL_SELECTED]: { - cell: HTMLElement; - }; - - // Event management - [CoreEvents.EVENT_CREATED]: { - event: CalendarEvent; - }; - [CoreEvents.EVENT_UPDATED]: { - event: CalendarEvent; - previousData?: Partial; - }; - [CoreEvents.EVENT_DELETED]: { - eventId: string; - }; - [CoreEvents.EVENT_SELECTED]: { - eventId: string; - event?: CalendarEvent; - }; - - // System events - [CoreEvents.ERROR]: { - error: Error; - context?: string; - }; - [CoreEvents.REFRESH_REQUESTED]: { - view?: CalendarView; - date?: Date; - }; - - // Filter events - [CoreEvents.FILTER_CHANGED]: { - activeFilters: string[]; - visibleEvents: CalendarEvent[]; - }; - - // Rendering events - [CoreEvents.EVENTS_RENDERED]: { - eventCount: number; - }; - - // Drag events - 'drag:start': DragStartEventPayload; - 'drag:move': DragMoveEventPayload; - 'drag:end': DragEndEventPayload; - 'drag:mouseenter-header': DragMouseEnterHeaderEventPayload; - 'drag:mouseleave-header': DragMouseLeaveHeaderEventPayload; - 'drag:cancelled': { - reason: string; - }; - - // Header events - 'header:ready': HeaderReadyEventPayload; - 'header:height-changed': { - height: number; - rowCount: number; - }; - - // All-day events - 'allday:convert-to-allday': { - eventId: string; - element: HTMLElement; - }; - 'allday:convert-from-allday': { - eventId: string; - element: HTMLElement; - }; - - // Scroll events - 'scroll:sync': { - scrollTop: number; - source: string; - }; - 'scroll:to-hour': { - hour: number; - }; - - // Filter events - 'filter:updated': { - activeFilters: string[]; - visibleEvents: CalendarEvent[]; - }; - 'filter:search': { - query: string; - results: CalendarEvent[]; - }; -} - -// Helper type to get payload type for a specific event -export type EventPayload = CalendarEventPayloadMap[T]; - -// Type guard to check if an event has a payload -export function hasPayload( - eventType: T, - payload: unknown -): payload is CalendarEventPayloadMap[T] { - return payload !== undefined; -} \ No newline at end of file diff --git a/src/types/ManagerTypes.ts b/src/types/ManagerTypes.ts index 89f3582..8945680 100644 --- a/src/types/ManagerTypes.ts +++ b/src/types/ManagerTypes.ts @@ -1,5 +1,4 @@ import { IEventBus, CalendarEvent, CalendarView } from './CalendarTypes'; -import { IManager } from '../interfaces/IManager'; /** * Complete type definition for all managers returned by ManagerFactory @@ -16,6 +15,14 @@ export interface CalendarManagers { allDayManager: unknown; // Avoid interface conflicts } +/** + * Base interface for managers with optional initialization and refresh + */ +interface IManager { + initialize?(): Promise | void; + refresh?(): void; +} + export interface EventManager extends IManager { loadData(): Promise; getEvents(): CalendarEvent[]; diff --git a/src/utils/OverlapDetector.ts b/src/utils/OverlapDetector.ts deleted file mode 100644 index ba70c53..0000000 --- a/src/utils/OverlapDetector.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * OverlapDetector - Ren tidbaseret overlap detection - * Ingen DOM manipulation, kun tidsberegninger - */ - -import { CalendarEvent } from '../types/CalendarTypes'; - -// Branded type for event IDs -export type EventId = string & { readonly __brand: 'EventId' }; - -export type OverlapResult = { - overlappingEvents: CalendarEvent[]; - stackLinks: Map; -}; - -export interface StackLink { - prev?: EventId; // Event ID of previous event in stack - next?: EventId; // Event ID of next event in stack - stackLevel: number; // 0 = base event, 1 = first stacked, etc -} - -export class OverlapDetector { - - /** - * Resolver hvilke events et givent event overlapper med i en kolonne - * @param event - CalendarEvent der skal checkes for overlap - * @param columnEvents - Array af CalendarEvent objekter i kolonnen - * @returns Array af events som det givne event overlapper med - */ - public resolveOverlap(event: CalendarEvent, columnEvents: CalendarEvent[]): CalendarEvent[] { - return columnEvents.filter(existingEvent => { - // To events overlapper hvis: - // event starter før existing slutter OG - // event slutter efter existing starter - return event.start < existingEvent.end && event.end > existingEvent.start; - }); - } - - /** - * Dekorerer events med stack linking data - * @param newEvent - Det nye event der skal tilføjes - * @param overlappingEvents - Events som det nye event overlapper med - * @returns OverlapResult med overlappende events og stack links - */ - public decorateWithStackLinks(newEvent: CalendarEvent, overlappingEvents: CalendarEvent[]): OverlapResult { - const stackLinks = new Map(); - - if (overlappingEvents.length === 0) { - return { - overlappingEvents: [], - stackLinks - }; - } - - // Kombiner nyt event med eksisterende og sortér efter start tid (tidligste første) - const allEvents = [...overlappingEvents, newEvent].sort((a, b) => - a.start.getTime() - b.start.getTime() - ); - - // Opret sammenhængende kæde - alle events bindes sammen - allEvents.forEach((event, index) => { - const stackLink: StackLink = { - stackLevel: index, - prev: index > 0 ? allEvents[index - 1].id as EventId : undefined, - next: index < allEvents.length - 1 ? allEvents[index + 1].id as EventId : undefined - }; - stackLinks.set(event.id as EventId, stackLink); - }); - overlappingEvents.push(newEvent); - return { - overlappingEvents, - stackLinks - }; - } -} \ No newline at end of file diff --git a/test/utils/OverlapDetector.test.ts b/test/utils/OverlapDetector.test.ts deleted file mode 100644 index dc712f1..0000000 --- a/test/utils/OverlapDetector.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { OverlapDetector } from '../../src/utils/OverlapDetector'; -import { CalendarEvent } from '../../src/types/CalendarTypes'; - -describe('OverlapDetector', () => { - let detector: OverlapDetector; - - beforeEach(() => { - detector = new OverlapDetector(); - }); - - // Helper function to create test events - const createEvent = (id: string, startHour: number, startMin: number, endHour: number, endMin: number): CalendarEvent => { - const start = new Date(2024, 0, 1, startHour, startMin); - const end = new Date(2024, 0, 1, endHour, endMin); - return { - id, - title: `Event ${id}`, - start, - end, - type: 'meeting', - allDay: false, - syncStatus: 'synced' - }; - }; - - describe('resolveOverlap', () => { - it('should detect no overlap when events do not overlap', () => { - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(0); - }); - - it('should detect overlap when events partially overlap', () => { - const event1 = createEvent('1', 9, 0, 10, 30); // 09:00-10:30 - const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - expect(overlaps[0].id).toBe('2'); - }); - - it('should detect overlap when one event contains another', () => { - const event1 = createEvent('1', 9, 0, 12, 0); // 09:00-12:00 - const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - expect(overlaps[0].id).toBe('2'); - }); - - it('should detect overlap when events have same start time', () => { - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 9, 0, 10, 30); // 09:00-10:30 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - expect(overlaps[0].id).toBe('2'); - }); - - it('should detect overlap when events have same end time', () => { - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 9, 30, 10, 0); // 09:30-10:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - expect(overlaps[0].id).toBe('2'); - }); - - it('should detect multiple overlapping events', () => { - const event1 = createEvent('1', 9, 0, 11, 0); // 09:00-11:00 - const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30 - const event3 = createEvent('3', 10, 0, 11, 30); // 10:00-11:30 - const event4 = createEvent('4', 12, 0, 13, 0); // 12:00-13:00 (no overlap) - - const overlaps = detector.resolveOverlap(event1, [event2, event3, event4]); - - expect(overlaps).toHaveLength(2); - expect(overlaps.map(e => e.id)).toEqual(['2', '3']); - }); - - it('should handle edge case where event ends exactly when another starts', () => { - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - // Events that touch at boundaries should NOT overlap - expect(overlaps).toHaveLength(0); - }); - - it('should handle events with 1-minute overlap', () => { - const event1 = createEvent('1', 9, 0, 10, 1); // 09:00-10:01 - const event2 = createEvent('2', 10, 0, 11, 0); // 10:00-11:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - }); - }); - - describe('decorateWithStackLinks', () => { - it('should return empty result when no overlapping events', () => { - const event1 = createEvent('1', 9, 0, 10, 0); - - const result = detector.decorateWithStackLinks(event1, []); - - expect(result.overlappingEvents).toHaveLength(0); - expect(result.stackLinks.size).toBe(0); - }); - - it('should assign stack levels based on start time order', () => { - const event1 = createEvent('1', 9, 0, 10, 30); // 09:00-10:30 - const event2 = createEvent('2', 9, 30, 11, 0); // 09:30-11:00 - - const result = detector.decorateWithStackLinks(event1, [event2]); - - expect(result.stackLinks.size).toBe(2); - - const link1 = result.stackLinks.get('1' as any); - const link2 = result.stackLinks.get('2' as any); - - expect(link1?.stackLevel).toBe(0); - expect(link1?.prev).toBeUndefined(); - expect(link1?.next).toBe('2'); - - expect(link2?.stackLevel).toBe(1); - expect(link2?.prev).toBe('1'); - expect(link2?.next).toBeUndefined(); - }); - - it('should create linked chain for multiple overlapping events', () => { - const event1 = createEvent('1', 9, 0, 11, 0); // 09:00-11:00 - const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30 - const event3 = createEvent('3', 10, 0, 11, 30); // 10:00-11:30 - - const result = detector.decorateWithStackLinks(event1, [event2, event3]); - - expect(result.stackLinks.size).toBe(3); - - const link1 = result.stackLinks.get('1' as any); - const link2 = result.stackLinks.get('2' as any); - const link3 = result.stackLinks.get('3' as any); - - // Check chain: 1 -> 2 -> 3 - expect(link1?.stackLevel).toBe(0); - expect(link1?.prev).toBeUndefined(); - expect(link1?.next).toBe('2'); - - expect(link2?.stackLevel).toBe(1); - expect(link2?.prev).toBe('1'); - expect(link2?.next).toBe('3'); - - expect(link3?.stackLevel).toBe(2); - expect(link3?.prev).toBe('2'); - expect(link3?.next).toBeUndefined(); - }); - - it('should handle events with same start time', () => { - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 9, 0, 10, 30); // 09:00-10:30 - - const result = detector.decorateWithStackLinks(event1, [event2]); - - const link1 = result.stackLinks.get('1' as any); - const link2 = result.stackLinks.get('2' as any); - - // Both start at same time - order may vary but levels should be 0 and 1 - const levels = [link1?.stackLevel, link2?.stackLevel].sort(); - expect(levels).toEqual([0, 1]); - - // Verify they are linked together - expect(result.stackLinks.size).toBe(2); - }); - - it('KNOWN ISSUE: should NOT stack events that do not overlap', () => { - // This test documents the current bug - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 9, 30, 10, 30); // 09:30-10:30 (overlaps with 1) - const event3 = createEvent('3', 11, 0, 12, 0); // 11:00-12:00 (NO overlap with 1 or 2) - - const result = detector.decorateWithStackLinks(event1, [event2, event3]); - - const link3 = result.stackLinks.get('3' as any); - - // CURRENT BEHAVIOR (BUG): Event 3 gets stackLevel 2 - expect(link3?.stackLevel).toBe(2); - - // EXPECTED BEHAVIOR: Event 3 should get stackLevel 0 since it doesn't overlap - // expect(link3?.stackLevel).toBe(0); - // expect(link3?.prev).toBeUndefined(); - // expect(link3?.next).toBeUndefined(); - }); - - it('KNOWN ISSUE: should reuse stack levels when possible', () => { - // This test documents another aspect of the bug - const event1 = createEvent('1', 9, 0, 10, 0); // 09:00-10:00 - const event2 = createEvent('2', 10, 30, 11, 30); // 10:30-11:30 (NO overlap) - const event3 = createEvent('3', 12, 0, 13, 0); // 12:00-13:00 (NO overlap) - - const result = detector.decorateWithStackLinks(event1, [event2, event3]); - - const link1 = result.stackLinks.get('1' as any); - const link2 = result.stackLinks.get('2' as any); - const link3 = result.stackLinks.get('3' as any); - - // CURRENT BEHAVIOR (BUG): All get different stack levels - expect(link1?.stackLevel).toBe(0); - expect(link2?.stackLevel).toBe(1); - expect(link3?.stackLevel).toBe(2); - - // EXPECTED BEHAVIOR: All should reuse level 0 since none overlap - // expect(link1?.stackLevel).toBe(0); - // expect(link2?.stackLevel).toBe(0); - // expect(link3?.stackLevel).toBe(0); - }); - - it('should handle complex overlapping pattern correctly', () => { - // Event 1: 09:00-11:00 (base) - // Event 2: 09:30-10:30 (overlaps with 1) - // Event 3: 10:00-11:30 (overlaps with 1 and 2) - // Event 4: 11:00-12:00 (overlaps with 3 only) - - const event1 = createEvent('1', 9, 0, 11, 0); - const event2 = createEvent('2', 9, 30, 10, 30); - const event3 = createEvent('3', 10, 0, 11, 30); - const event4 = createEvent('4', 11, 0, 12, 0); - - const result = detector.decorateWithStackLinks(event1, [event2, event3, event4]); - - expect(result.stackLinks.size).toBe(4); - - // All events are linked in one chain (current behavior) - const link1 = result.stackLinks.get('1' as any); - const link2 = result.stackLinks.get('2' as any); - const link3 = result.stackLinks.get('3' as any); - const link4 = result.stackLinks.get('4' as any); - - expect(link1?.stackLevel).toBe(0); - expect(link2?.stackLevel).toBe(1); - expect(link3?.stackLevel).toBe(2); - expect(link4?.stackLevel).toBe(3); - }); - }); - - describe('Edge Cases', () => { - it('should handle zero-duration events', () => { - const event1 = createEvent('1', 9, 0, 9, 0); // 09:00-09:00 - const event2 = createEvent('2', 9, 0, 10, 0); // 09:00-10:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - // Zero-duration event at start of another should not overlap - expect(overlaps).toHaveLength(0); - }); - - it('should handle events spanning multiple hours', () => { - const event1 = createEvent('1', 8, 0, 17, 0); // 08:00-17:00 (9 hours) - const event2 = createEvent('2', 12, 0, 13, 0); // 12:00-13:00 - - const overlaps = detector.resolveOverlap(event1, [event2]); - - expect(overlaps).toHaveLength(1); - }); - - it('should handle many events in same time slot', () => { - const event1 = createEvent('1', 9, 0, 10, 0); - const events = [ - createEvent('2', 9, 0, 10, 0), - createEvent('3', 9, 0, 10, 0), - createEvent('4', 9, 0, 10, 0), - createEvent('5', 9, 0, 10, 0) - ]; - - const overlaps = detector.resolveOverlap(event1, events); - - expect(overlaps).toHaveLength(4); - }); - }); -}); \ No newline at end of file