cleanup
This commit is contained in:
parent
faa59f6a3c
commit
69495ce00f
9 changed files with 337 additions and 1306 deletions
329
docs/code-analysis-report.md
Normal file
329
docs/code-analysis-report.md
Normal file
|
|
@ -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
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* Base interface for all managers
|
||||
*/
|
||||
export interface IManager {
|
||||
/**
|
||||
* Initialize the manager
|
||||
*/
|
||||
initialize?(): Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Refresh the manager's state
|
||||
*/
|
||||
refresh?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for managers that handle events
|
||||
*/
|
||||
export interface IEventManager extends IManager {
|
||||
loadData(): Promise<void>;
|
||||
getEvents(): CalendarEvent[];
|
||||
getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for managers that handle rendering
|
||||
*/
|
||||
export interface IRenderingManager extends IManager {
|
||||
render(): Promise<void> | 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;
|
||||
}
|
||||
|
|
@ -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<HTMLElement>();
|
||||
|
||||
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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CalendarEvent>;
|
||||
};
|
||||
[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<T extends keyof CalendarEventPayloadMap> = CalendarEventPayloadMap[T];
|
||||
|
||||
// Type guard to check if an event has a payload
|
||||
export function hasPayload<T extends keyof CalendarEventPayloadMap>(
|
||||
eventType: T,
|
||||
payload: unknown
|
||||
): payload is CalendarEventPayloadMap[T] {
|
||||
return payload !== undefined;
|
||||
}
|
||||
|
|
@ -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> | void;
|
||||
refresh?(): void;
|
||||
}
|
||||
|
||||
export interface EventManager extends IManager {
|
||||
loadData(): Promise<void>;
|
||||
getEvents(): CalendarEvent[];
|
||||
|
|
|
|||
|
|
@ -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<EventId, StackLink>;
|
||||
};
|
||||
|
||||
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<EventId, StackLink>();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue