Removes outdated documentation and code comments

Deletes a large set of architectural plans, code reviews, and implementation documents. This content is no longer relevant as the described features and refactorings are complete.

Streamlines the event renderer by removing legacy drag event listener setup and outdated comments, reflecting improved separation of concerns and rendering strategies.
This commit is contained in:
Janus C. H. Knudsen 2025-09-22 20:59:25 +02:00
parent 92463ef173
commit 996459f226
25 changed files with 4 additions and 4947 deletions

View file

@ -1,155 +0,0 @@
# Critical Code Review - Calendar Plantempus (UPDATED)
**Date:** January 2025
**Reviewer:** Code Analysis Assistant
**Scope:** Full TypeScript/JavaScript codebase
**Update:** Post-refactoring status
## Executive Summary
This code review identified 14+ critical issues. After immediate refactoring, 7 critical issues have been resolved, significantly improving code quality and maintainability.
## ✅ RESOLVED ISSUES (January 2025)
### 1. ~~Inconsistent File Structure~~ ✅ FIXED
**Resolution:**
- ✅ Deleted `src/utils/PositionUtils.js` (legacy JavaScript)
- ✅ Fixed `tsconfig.json` output directory to `./wwwroot/js`
- ✅ Build pipeline now consistent
### 2. ~~Event System Overcomplexity~~ ✅ PARTIALLY FIXED
**Resolution:**
- ✅ Deleted unused `CalendarState.ts` (170 lines of dead code)
- ✅ Created `CoreEvents.ts` with only 20 essential events
- ✅ Added migration map for gradual transition
- ⚠️ Still need to migrate all code to use CoreEvents
### 3. ~~Missing Error Handling~~ ✅ PARTIALLY FIXED
**Resolution:**
- ✅ Added `validateDate()` method to DateCalculator
- ✅ All date methods now validate inputs
- ⚠️ Still need error boundaries in UI components
### 4. ~~Memory Leak Potential~~ ✅ PARTIALLY FIXED
**Resolution:**
- ✅ ViewManager now tracks all listeners
- ✅ Proper `destroy()` method implementation
- ⚠️ Other managers still need cleanup methods
### 7. ~~Type Safety Issues~~ ✅ FIXED
**Resolution:**
- ✅ Replaced `any[]` with `AllDayEvent[]` type
- ✅ Created proper event type definitions
- ✅ No more type casting in fixed files
---
## 🚨 REMAINING CRITICAL ISSUES
### 5. Single Responsibility Violations
**Severity:** High
**Impact:** Unmaintainable code, difficult to test
**Still Present:**
- GridManager: 311 lines handling multiple responsibilities
- CalendarConfig: Config + state management mixed
**Recommendation:** Implement strategy pattern for different views
---
### 6. Dependency Injection Missing
**Severity:** Medium
**Impact:** Untestable code, tight coupling
**Still Present:**
- Singleton imports in 15+ files
- Circular dependencies through EventBus
**Recommendation:** Use constructor injection pattern
---
### 8. Performance Problems
**Severity:** Medium
**Impact:** Sluggish UI with many events
**Still Present:**
- DOM queries not cached
- Full re-renders on every change
**Recommendation:** Implement virtual scrolling and caching
---
## 📊 IMPROVEMENT METRICS
### Before Refactoring
- **Event Types:** 102 + StateEvents
- **Dead Code:** ~200 lines (CalendarState.ts)
- **Type Safety:** Multiple `any` types
- **Error Handling:** None
- **Memory Leaks:** All managers
### After Refactoring
- **Event Types:** 20 core events (80% reduction!)
- **Dead Code:** 0 lines removed
- **Type Safety:** Proper types defined
- **Error Handling:** Date validation added
- **Memory Leaks:** ViewManager fixed
### Code Quality Scores (Updated)
- **Maintainability:** ~~3/10~~**5/10** ⬆️
- **Testability:** ~~2/10~~**4/10** ⬆️
- **Performance:** 5/10 (unchanged)
- **Type Safety:** ~~4/10~~**7/10** ⬆️
- **Architecture:** ~~3/10~~**4/10** ⬆️
---
## 🎯 NEXT STEPS
### Phase 1: Architecture (Priority)
1. Implement ViewStrategy pattern for month view
2. Split GridManager using strategy pattern
3. Add dependency injection
### Phase 2: Performance
4. Cache DOM queries
5. Implement selective rendering
6. Add virtual scrolling for large datasets
### Phase 3: Testing
7. Add unit tests for DateCalculator
8. Add integration tests for event system
9. Add E2E tests for critical user flows
---
## Files Modified
### Deleted Files
- `src/utils/PositionUtils.js` - Legacy JavaScript removed
- `src/types/CalendarState.ts` - Unused state management
### Created Files
- `src/constants/CoreEvents.ts` - Consolidated event system
- `src/types/EventTypes.ts` - Proper type definitions
### Modified Files
- `tsconfig.json` - Fixed output directory
- `src/utils/DateCalculator.ts` - Added validation
- `src/managers/ViewManager.ts` - Added cleanup
- `src/managers/GridManager.ts` - Fixed types
- `src/renderers/GridRenderer.ts` - Fixed types
- 4 files - Removed StateEvents imports
---
## Conclusion
The immediate refactoring has addressed 50% of critical issues with minimal effort (~1 hour of work). The codebase is now:
- **Cleaner:** 200+ lines of dead code removed
- **Safer:** Type safety and validation improved
- **Simpler:** Event system reduced by 80%
- **More maintainable:** Clear separation emerging
The remaining issues require architectural changes but the foundation is now stronger for implementing month view and other features.

View file

@ -1,282 +0,0 @@
# Critical Code Review - Calendar Plantempus
**Date:** January 2025
**Reviewer:** Code Analysis Assistant
**Scope:** Full TypeScript/JavaScript codebase
## Executive Summary
This code review identifies 14+ critical issues that impact maintainability, performance, and the ability to add new features (especially month view). The codebase shows signs of rapid development without architectural planning, resulting in significant technical debt.
---
## 🚨 CRITICAL ISSUES
### 1. Inconsistent File Structure
**Severity:** High
**Impact:** Development confusion, build issues
**Problems:**
- Duplicate TypeScript/JavaScript files exist (`src/utils/PositionUtils.js` and `.ts`)
- Mixed compiled and source code in `wwwroot/js/`
- Legacy files in root directory (`calendar-*.js`)
**Evidence:**
```
src/utils/PositionUtils.js (JavaScript)
src/utils/PositionUtils.ts (TypeScript)
calendar-grid-manager.js (Root legacy file)
```
**Recommendation:** Delete all `.js` files in `src/`, remove legacy root files, keep only TypeScript sources.
---
### 2. Event System Overcomplexity
**Severity:** Critical
**Impact:** Impossible to maintain, performance degradation
**Problems:**
- Two overlapping event systems (`EventTypes.ts` with 102 events + `CalendarState.ts`)
- Unclear separation of concerns
- Legacy events marked as "removed" but still present
**Evidence:**
```typescript
// EventTypes.ts - 102 constants!
export const EventTypes = {
CONFIG_UPDATE: 'calendar:configupdate',
CALENDAR_TYPE_CHANGED: 'calendar:calendartypechanged',
// ... 100 more events
}
// CalendarState.ts - Another event system
export const StateEvents = {
CALENDAR_STATE_CHANGED: 'calendar:state:changed',
// ... more events
}
```
**Recommendation:** Consolidate to ~20 core events with clear ownership.
---
### 3. Missing Error Handling
**Severity:** High
**Impact:** Silent failures, poor user experience
**Problems:**
- No try-catch blocks in critical paths
- No error boundaries for component failures
- DateCalculator assumes all inputs are valid
**Evidence:**
```typescript
// DateCalculator.ts - No validation
getISOWeekStart(date: Date): Date {
const monday = new Date(date); // What if date is invalid?
const currentDay = monday.getDay();
// ... continues without checks
}
```
**Recommendation:** Add comprehensive error handling and validation.
---
### 4. Memory Leak Potential
**Severity:** Critical
**Impact:** Browser performance degradation over time
**Problems:**
- Event listeners never cleaned up
- DOM references held indefinitely
- Multiple DateCalculator instances created
**Evidence:**
```typescript
// ViewManager.ts - No cleanup
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
this.setupEventListeners(); // Never removed!
}
// No destroy() method exists
```
**Recommendation:** Implement proper cleanup in all managers.
---
### 5. Single Responsibility Violations
**Severity:** High
**Impact:** Unmaintainable code, difficult to test
**Problems:**
- `GridManager`: Handles rendering, events, styling, positioning (311 lines!)
- `CalendarConfig`: Config, state management, and factory logic mixed
- `NavigationRenderer`: DOM manipulation and event rendering
**Evidence:**
```typescript
// GridManager doing everything:
- subscribeToEvents()
- render()
- setupGridInteractions()
- getClickPosition()
- scrollToHour()
- minutesToTime()
```
**Recommendation:** Split into focused, single-purpose classes.
---
### 6. Dependency Injection Missing
**Severity:** Medium
**Impact:** Untestable code, tight coupling
**Problems:**
- Hard-coded singleton imports everywhere
- `calendarConfig` imported directly in 15+ files
- Circular dependencies through EventBus
**Evidence:**
```typescript
import { calendarConfig } from '../core/CalendarConfig'; // Singleton
import { eventBus } from '../core/EventBus'; // Another singleton
```
**Recommendation:** Use constructor injection pattern.
---
### 7. Performance Problems
**Severity:** Medium
**Impact:** Sluggish UI, especially with many events
**Problems:**
- `document.querySelector` called repeatedly
- No caching of DOM elements
- Full re-renders on every change
**Evidence:**
```typescript
// Called multiple times per render:
const scrollableContent = document.querySelector('swp-scrollable-content');
```
**Recommendation:** Cache DOM queries, implement selective rendering.
---
### 8. Type Safety Issues
**Severity:** High
**Impact:** Runtime errors, hidden bugs
**Problems:**
- `any` types used extensively
- Type casting to bypass checks
- Missing interfaces for data structures
**Evidence:**
```typescript
private allDayEvents: any[] = []; // No type safety
(header as any).dataset.today = 'true'; // Bypassing TypeScript
```
**Recommendation:** Define proper TypeScript interfaces for all data.
---
## 🔧 TECHNICAL DEBT
### 9. Redundant Code
- Duplicate date logic in DateCalculator and PositionUtils
- Headers rendered in multiple places
- Similar event handling patterns copy-pasted
### 10. Testability Issues
- No dependency injection makes mocking impossible
- Direct DOM manipulation prevents unit testing
- Global state makes tests brittle
### 11. Documentation Problems
- Mixed Danish/English comments
- Missing JSDoc for public APIs
- Outdated comments that don't match code
---
## ⚡ ARCHITECTURE ISSUES
### 12. Massive Interfaces
- `CalendarTypes.ts`: Too many interfaces in one file
- `EventTypes`: 102 constants is unmanageable
- Manager interfaces too broad
### 13. Coupling Problems
- High coupling between managers
- Everything communicates via events (performance hit)
- All components depend on global config
### 14. Naming Inconsistency
- Mixed language conventions
- Unclear event names (`REFRESH_REQUESTED` vs `CALENDAR_REFRESH_REQUESTED`)
- `swp-` prefix unexplained
---
## 📊 METRICS
### Code Quality Scores
- **Maintainability:** 3/10
- **Testability:** 2/10
- **Performance:** 5/10
- **Type Safety:** 4/10
- **Architecture:** 3/10
### File Statistics
- **Total TypeScript files:** 24
- **Total JavaScript files:** 8 (should be 0)
- **Average file size:** ~200 lines (acceptable)
- **Largest file:** GridManager.ts (311 lines)
- **Event types defined:** 102 (should be ~20)
---
## 🎯 RECOMMENDATIONS
### Immediate Actions (Week 1)
1. **Remove duplicate files** - Clean up `.js` duplicates
2. **Add error boundaries** - Prevent cascade failures
3. **Fix memory leaks** - Add cleanup methods
### Short Term (Month 1)
4. **Consolidate events** - Reduce to core 20 events
5. **Implement DI** - Remove singleton dependencies
6. **Split mega-classes** - Apply Single Responsibility
### Long Term (Quarter 1)
7. **Add comprehensive tests** - Aim for 80% coverage
8. **Performance optimization** - Virtual scrolling, caching
9. **Complete documentation** - JSDoc all public APIs
---
## Impact on Month View Implementation
**Without refactoring:**
- 🔴 ~2000 lines of new code
- 🔴 3-4 weeks implementation
- 🔴 High bug risk
**With minimal refactoring:**
- ✅ ~500 lines of new code
- ✅ 1 week implementation
- ✅ Reusable components
---
## Conclusion
The codebase requires significant refactoring to support new features efficiently. The identified issues, particularly the lack of strategy pattern and hardcoded week/day assumptions, make adding month view unnecessarily complex.
**Priority:** Focus on minimal refactoring that enables month view (Strategy pattern, config split, event consolidation) before attempting to add new features.

View file

@ -1,270 +0,0 @@
# Month View Implementation Plan (POST-REFACTORING)
**Updated:** January 2025
**Status:** Ready to implement - Foundation cleaned up
**Timeline:** 2 days (reduced from 3)
## Pre-Work Completed ✅
The following critical issues have been resolved, making month view implementation much easier:
### ✅ Foundation Improvements Done
- **Event system simplified**: 102 → 20 events with CoreEvents.ts
- **Dead code removed**: CalendarState.ts (170 lines) deleted
- **Type safety improved**: Proper event interfaces defined
- **Error handling added**: Date validation in DateCalculator
- **Build fixed**: tsconfig.json output directory corrected
### ✅ Impact on Month View
- **Clearer event system**: Know exactly which events to use
- **No confusing StateEvents**: Removed competing event system
- **Better types**: AllDayEvent interface ready for month events
- **Reliable dates**: DateCalculator won't crash on bad input
---
## Revised Implementation Plan
### Phase 1: Strategy Pattern (4 hours → 2 hours)
*Time saved: Dead code removed, events clarified*
#### 1.1 Create ViewStrategy Interface ✨
**New file:** `src/strategies/ViewStrategy.ts`
```typescript
import { CoreEvents } from '../constants/CoreEvents'; // Use new events!
export interface ViewStrategy {
renderGrid(container: HTMLElement, context: ViewContext): void;
renderEvents(events: AllDayEvent[], container: HTMLElement): void; // Use proper types!
getLayoutConfig(): ViewLayoutConfig;
handleNavigation(date: Date): Date; // Now validated!
}
```
#### 1.2 Extract WeekViewStrategy ✨
**New file:** `src/strategies/WeekViewStrategy.ts`
- Move existing logic from GridManager
- Use CoreEvents instead of EventTypes
- Leverage improved type safety
#### 1.3 Create MonthViewStrategy
**New file:** `src/strategies/MonthViewStrategy.ts`
```typescript
export class MonthViewStrategy implements ViewStrategy {
renderGrid(container: HTMLElement, context: ViewContext): void {
// 7x6 month grid - no time axis needed
this.createMonthGrid(container, context.currentDate);
}
renderEvents(events: AllDayEvent[], container: HTMLElement): void {
// Use proper AllDayEvent types (now defined!)
// Simple day cell rendering
}
}
```
#### 1.4 Update GridManager
**Modify:** `src/managers/GridManager.ts`
```typescript
export class GridManager {
private strategy: ViewStrategy;
setViewStrategy(strategy: ViewStrategy): void {
this.strategy = strategy;
// No memory leaks - cleanup is now handled!
}
render(): void {
// Emit CoreEvents.GRID_RENDERED instead of old events
this.eventBus.emit(CoreEvents.GRID_RENDERED, {...});
}
}
```
---
### Phase 2: Month Components (2 hours → 1.5 hours)
*Time saved: Better types, no conflicting events*
#### 2.1 MonthGridRenderer
**New file:** `src/renderers/MonthGridRenderer.ts`
```typescript
import { AllDayEvent } from '../types/EventTypes'; // Proper types!
import { CoreEvents } from '../constants/CoreEvents';
export class MonthGridRenderer {
renderMonth(container: HTMLElement, date: Date): void {
// DateCalculator.validateDate() prevents crashes
this.dateCalculator.validateDate(date, 'renderMonth');
// Create 7x6 grid with proper types
}
}
```
#### 2.2 MonthEventRenderer
**New file:** `src/renderers/MonthEventRenderer.ts`
```typescript
export class MonthEventRenderer {
render(events: AllDayEvent[], container: HTMLElement): void {
// Use AllDayEvent interface - no more any!
// Clean event filtering using proper types
}
}
```
---
### Phase 3: Integration (2 hours → 1 hour)
*Time saved: Clear events, no StateEvents confusion*
#### 3.1 Wire ViewManager
**Modify:** `src/managers/ViewManager.ts`
```typescript
private changeView(newView: CalendarView): void {
let strategy: ViewStrategy;
switch(newView) {
case 'month':
strategy = new MonthViewStrategy();
break;
// ... other views
}
this.gridManager.setViewStrategy(strategy);
// Use CoreEvents - no confusion about which events!
this.eventBus.emit(CoreEvents.VIEW_CHANGED, { view: newView });
}
```
#### 3.2 Update HTML & CSS
**Modify:** `wwwroot/index.html`
```html
<!-- Enable month view - no conflicting events to worry about -->
<swp-view-button data-view="month">Month</swp-view-button>
```
**New:** `wwwroot/css/calendar-month.css`
```css
.month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: auto repeat(6, 1fr);
}
.month-day-cell {
border: 1px solid var(--border-color);
min-height: 120px;
padding: 4px;
}
.month-event {
font-size: 0.75rem;
padding: 1px 4px;
margin: 1px 0;
border-radius: 2px;
background: var(--event-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
---
## Updated Timeline
### Day 1 (Reduced from full day)
**Morning (2 hours)**
- ✅ Foundation already clean
- Implement ViewStrategy interface
- Extract WeekViewStrategy
- Create MonthViewStrategy skeleton
**Afternoon (1 hour)**
- Wire strategies in GridManager
- Test view switching works
### Day 2
**Morning (1.5 hours)**
- Implement MonthGridRenderer
- Implement MonthEventRenderer
- Create month CSS
**Afternoon (1 hour)**
- Final integration
- Enable month button
- Test and polish
**Total: 5.5 hours instead of 16+ hours!**
---
## Benefits from Pre-Refactoring
### 🚀 **Development Speed**
- **No conflicting events**: Clear which events to use
- **No dead code confusion**: CalendarState removed
- **Proper types**: AllDayEvent interface ready
- **Reliable foundation**: DateCalculator validation prevents crashes
### 🎯 **Quality**
- **Consistent patterns**: Following established CoreEvents
- **Type safety**: No more `any` types to debug
- **Memory management**: Cleanup patterns established
- **Error handling**: Built-in date validation
### 🔧 **Maintainability**
- **Single event system**: No EventTypes vs StateEvents confusion
- **Clean codebase**: 200+ lines of cruft removed
- **Clear interfaces**: AllDayEvent, ViewStrategy defined
- **Proper separation**: Strategy pattern foundation laid
---
## Success Metrics (Updated)
### ✅ **Foundation Quality**
- [x] Event system consolidated (20 events)
- [x] Dead code removed
- [x] Types properly defined
- [x] Date validation added
- [x] Build configuration fixed
### 🎯 **Month View Goals**
- [ ] Month grid displays 6 weeks correctly
- [ ] Events show in day cells (max 3 + "more")
- [ ] Navigation works (prev/next month)
- [ ] View switching between week/month
- [ ] No regressions in existing views
- [ ] Under 400 lines of new code (down from 750!)
### 📊 **Expected Results**
- **Implementation time**: 5.5 hours (67% reduction)
- **Code quality**: Higher (proper types, clear events)
- **Maintainability**: Much improved (clean foundation)
- **Bug risk**: Lower (validation, proper cleanup)
---
## Risk Assessment (Much Improved)
### ✅ **Risks Eliminated**
- ~~Event system conflicts~~ → Single CoreEvents system
- ~~Type errors~~ → Proper AllDayEvent interface
- ~~Date crashes~~ → DateCalculator validation
- ~~Memory leaks~~ → Cleanup patterns established
- ~~Dead code confusion~~ → CalendarState removed
### ⚠️ **Remaining Risks (Low)**
1. **CSS conflicts**: Mitigated with namespaced `.month-view` classes
2. **Performance with many events**: Can implement virtualization later
3. **Browser compatibility**: CSS Grid widely supported
---
## Conclusion
The pre-refactoring work has transformed this from a difficult, error-prone implementation into a straightforward feature addition. The month view can now be implemented cleanly in ~5.5 hours with high confidence and low risk.
**Ready to proceed!** 🚀

View file

@ -1,456 +0,0 @@
# Month View Refactoring Plan
**Purpose:** Enable month view with minimal refactoring
**Timeline:** 3 days (6 hours of focused work)
**Priority:** High - Blocks new feature development
## Overview
This plan addresses only the critical architectural issues that prevent month view implementation. By focusing on the minimal necessary changes, we can add month view in ~500 lines instead of ~2000 lines.
---
## Current Blockers for Month View
### 🚫 Why Month View Can't Be Added Now
1. **GridManager is hardcoded for time-based views**
- Assumes everything is hours and columns
- Time axis doesn't make sense for months
- Hour-based scrolling irrelevant
2. **No strategy pattern for different view types**
- Would need entirely new managers
- Massive code duplication
- Inconsistent behavior
3. **Config assumes time-based views**
```typescript
hourHeight: 60,
dayStartHour: 0,
snapInterval: 15
// These are meaningless for month view!
```
4. **Event rendering tied to time positions**
- Events positioned by minutes
- No concept of day cells
- Can't handle multi-day spans properly
---
## Phase 1: View Strategy Pattern (2 hours)
### 1.1 Create ViewStrategy Interface
**New file:** `src/strategies/ViewStrategy.ts`
```typescript
export interface ViewStrategy {
// Core rendering methods
renderGrid(container: HTMLElement, context: ViewContext): void;
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
// Configuration
getLayoutConfig(): ViewLayoutConfig;
getRequiredConfig(): string[]; // Which config keys this view needs
// Navigation
getNextPeriod(currentDate: Date): Date;
getPreviousPeriod(currentDate: Date): Date;
getPeriodLabel(date: Date): string;
}
export interface ViewContext {
currentDate: Date;
config: CalendarConfig;
events: CalendarEvent[];
container: HTMLElement;
}
```
### 1.2 Extract WeekViewStrategy
**New file:** `src/strategies/WeekViewStrategy.ts`
- Move existing logic from GridRenderer
- Keep all time-based rendering
- Minimal changes to existing code
```typescript
export class WeekViewStrategy implements ViewStrategy {
renderGrid(container: HTMLElement, context: ViewContext): void {
// Move existing GridRenderer.renderGrid() here
this.createTimeAxis(container);
this.createDayColumns(container, context);
this.createTimeSlots(container);
}
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
// Move existing EventRenderer logic
// Position by time as before
}
}
```
### 1.3 Create MonthViewStrategy
**New file:** `src/strategies/MonthViewStrategy.ts`
```typescript
export class MonthViewStrategy implements ViewStrategy {
renderGrid(container: HTMLElement, context: ViewContext): void {
// Create 7x6 grid
this.createMonthHeader(container); // Mon-Sun
this.createWeekRows(container, context);
}
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
// Render as small blocks in day cells
// Handle multi-day spanning
}
}
```
### 1.4 Update GridManager
**Modify:** `src/managers/GridManager.ts`
```typescript
export class GridManager {
private strategy: ViewStrategy;
setViewStrategy(strategy: ViewStrategy): void {
this.strategy = strategy;
}
render(): void {
// Delegate to strategy
this.strategy.renderGrid(this.grid, {
currentDate: this.currentWeek,
config: this.config,
events: this.events,
container: this.grid
});
}
}
```
---
## Phase 2: Configuration Split (1 hour)
### 2.1 View-Specific Configs
**New file:** `src/core/ViewConfigs.ts`
```typescript
// Shared by all views
export interface BaseViewConfig {
locale: string;
firstDayOfWeek: number;
dateFormat: string;
eventColors: Record<string, string>;
}
// Week/Day views only
export interface TimeViewConfig extends BaseViewConfig {
hourHeight: number;
dayStartHour: number;
dayEndHour: number;
snapInterval: number;
showCurrentTime: boolean;
}
// Month view only
export interface MonthViewConfig extends BaseViewConfig {
weeksToShow: number; // Usually 6
showWeekNumbers: boolean;
compactMode: boolean;
eventLimit: number; // Max events shown per day
showMoreText: string; // "+2 more"
}
```
### 2.2 Update CalendarConfig
**Modify:** `src/core/CalendarConfig.ts`
```typescript
export class CalendarConfig {
private viewConfigs: Map<string, BaseViewConfig> = new Map();
constructor() {
// Set defaults for each view
this.viewConfigs.set('week', defaultWeekConfig);
this.viewConfigs.set('month', defaultMonthConfig);
}
getViewConfig<T extends BaseViewConfig>(view: string): T {
return this.viewConfigs.get(view) as T;
}
}
```
---
## Phase 3: Event Consolidation (1 hour)
### 3.1 Core Events Only
**New file:** `src/constants/CoreEvents.ts`
```typescript
export const CoreEvents = {
// View lifecycle (5 events)
VIEW_CHANGED: 'view:changed',
VIEW_RENDERED: 'view:rendered',
// Navigation (3 events)
DATE_CHANGED: 'date:changed',
PERIOD_CHANGED: 'period:changed',
// Data (4 events)
EVENTS_LOADING: 'events:loading',
EVENTS_LOADED: 'events:loaded',
EVENT_CLICKED: 'event:clicked',
EVENT_UPDATED: 'event:updated',
// UI State (3 events)
LOADING_START: 'ui:loading:start',
LOADING_END: 'ui:loading:end',
ERROR: 'ui:error',
// Grid (3 events)
GRID_RENDERED: 'grid:rendered',
GRID_CLICKED: 'grid:clicked',
CELL_CLICKED: 'cell:clicked'
};
// Total: ~18 events instead of 102!
```
### 3.2 Migration Map
**Modify:** `src/constants/EventTypes.ts`
```typescript
// Keep old events but map to new ones
export const EventTypes = {
VIEW_CHANGED: CoreEvents.VIEW_CHANGED, // Direct mapping
WEEK_CHANGED: CoreEvents.PERIOD_CHANGED, // Renamed
// ... etc
} as const;
```
---
## Phase 4: Month-Specific Renderers (2 hours)
### 4.1 MonthGridRenderer
**New file:** `src/renderers/MonthGridRenderer.ts`
```typescript
export class MonthGridRenderer {
render(container: HTMLElement, date: Date): void {
const grid = this.createGrid();
// Add day headers
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => {
grid.appendChild(this.createDayHeader(day));
});
// Add 6 weeks of days
const dates = this.getMonthDates(date);
dates.forEach(weekDates => {
weekDates.forEach(date => {
grid.appendChild(this.createDayCell(date));
});
});
container.appendChild(grid);
}
private createGrid(): HTMLElement {
const grid = document.createElement('div');
grid.className = 'month-grid';
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(7, 1fr)';
return grid;
}
}
```
### 4.2 MonthEventRenderer
**New file:** `src/renderers/MonthEventRenderer.ts`
```typescript
export class MonthEventRenderer {
render(events: CalendarEvent[], container: HTMLElement): void {
const dayMap = this.groupEventsByDay(events);
dayMap.forEach((dayEvents, dateStr) => {
const dayCell = container.querySelector(`[data-date="${dateStr}"]`);
if (!dayCell) return;
const limited = dayEvents.slice(0, 3); // Show max 3
limited.forEach(event => {
dayCell.appendChild(this.createEventBlock(event));
});
if (dayEvents.length > 3) {
dayCell.appendChild(this.createMoreIndicator(dayEvents.length - 3));
}
});
}
}
```
---
## Phase 5: Integration (1 hour)
### 5.1 Wire ViewManager
**Modify:** `src/managers/ViewManager.ts`
```typescript
private changeView(newView: CalendarView): void {
// Create appropriate strategy
let strategy: ViewStrategy;
switch(newView) {
case 'week':
case 'day':
strategy = new WeekViewStrategy();
break;
case 'month':
strategy = new MonthViewStrategy();
break;
}
// Update GridManager
this.gridManager.setViewStrategy(strategy);
// Trigger re-render
this.eventBus.emit(CoreEvents.VIEW_CHANGED, { view: newView });
}
```
### 5.2 Enable Month Button
**Modify:** `wwwroot/index.html`
```html
<!-- Remove disabled attribute -->
<swp-view-button data-view="month">Month</swp-view-button>
```
### 5.3 Add Month Styles
**New file:** `wwwroot/css/calendar-month-css.css`
```css
.month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: var(--color-border);
}
.month-day-cell {
background: white;
min-height: 100px;
padding: 4px;
position: relative;
}
.month-day-number {
font-weight: bold;
margin-bottom: 4px;
}
.month-event {
font-size: 0.75rem;
padding: 2px 4px;
margin: 1px 0;
border-radius: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.month-more-indicator {
font-size: 0.7rem;
color: var(--color-text-secondary);
cursor: pointer;
}
```
---
## Implementation Timeline
### Day 1 (Monday)
**Morning (2 hours)**
- [ ] Implement ViewStrategy interface
- [ ] Extract WeekViewStrategy
- [ ] Create MonthViewStrategy skeleton
**Afternoon (1 hour)**
- [ ] Split configuration
- [ ] Update CalendarConfig
### Day 2 (Tuesday)
**Morning (2 hours)**
- [ ] Consolidate events to CoreEvents
- [ ] Create migration mappings
- [ ] Update critical event listeners
**Afternoon (2 hours)**
- [ ] Implement MonthGridRenderer
- [ ] Implement MonthEventRenderer
### Day 3 (Wednesday)
**Morning (2 hours)**
- [ ] Wire everything in ViewManager
- [ ] Update HTML and CSS
- [ ] Test month view
- [ ] Fix edge cases
---
## Success Metrics
### ✅ Definition of Done
- [ ] Month view displays 6 weeks correctly
- [ ] Events show in day cells (max 3 + "more")
- [ ] Navigation works (prev/next month)
- [ ] Switching between week/month works
- [ ] No regression in week view
- [ ] Under 750 lines of new code
### 📊 Expected Impact
- **New code:** ~500-750 lines (vs 2000 without refactoring)
- **Reusability:** 80% of components shared
- **Future views:** Day view = 100 lines, Year view = 200 lines
- **Test coverage:** Easy to test strategies independently
- **Performance:** No impact on existing views
---
## Risk Mitigation
### Potential Issues & Solutions
1. **CSS conflicts between views**
- Solution: Namespace all month CSS with `.month-view`
2. **Event overlap in month cells**
- Solution: Implement "more" indicator after 3 events
3. **Performance with many events**
- Solution: Only render visible month
4. **Browser compatibility**
- Solution: Use CSS Grid with flexbox fallback
---
## Next Steps After Month View
Once this refactoring is complete, adding new views becomes trivial:
- **Day View:** ~100 lines (reuse WeekViewStrategy with 1 column)
- **Year View:** ~200 lines (12 small month grids)
- **Agenda View:** ~150 lines (list layout)
- **Timeline View:** ~300 lines (horizontal time axis)
The strategy pattern makes the calendar truly extensible!

View file

@ -1,460 +0,0 @@
# Complete Calendar Component Specification
## 1. Project Overview
### Purpose
Build a professional calendar component with week, day, and month views, featuring drag-and-drop functionality, event management, and real-time synchronization.
### Technology Stack
- **Frontend**: Vanilla JavaScript (ES Modules), ready for TypeScript conversion
- **Styling**: CSS with nested selectors, CSS Grid/Flexbox
- **Backend** (planned): .NET Core with SignalR
- **Architecture**: Modular manager-based system with event-driven communication
### Design Principles
1. **Modularity**: Each manager handles one specific concern
2. **Loose Coupling**: Communication via custom events on document
3. **No External Dependencies**: Pure JavaScript implementation
4. **Custom HTML Tags**: Semantic markup without Web Components registration
5. **CSS-based Positioning**: Events positioned using CSS calc() and variables
## 2. What Has Been Implemented
### 2.1 Core Infrastructure
#### EventBus.js ✅
- Central event dispatcher for all calendar events
- Publish/subscribe pattern implementation
- Debug logging capabilities
- Event history tracking
- Priority-based listeners
#### CalendarConfig.js ✅
- Centralized configuration management
- Default values for all settings
- DOM data-attribute reading
- Computed value calculations (minuteHeight, totalSlots, etc.)
- Configuration change events
#### EventTypes.js ✅
- All event type constants defined
- Organized by category (view, CRUD, interaction, UI, data, state)
- Consistent naming convention
### 2.2 Managers
#### GridManager.js ✅
- Renders time axis with configurable hours
- Creates week headers with day names and dates
- Generates day columns for events
- Sets up grid interactions (click, dblclick)
- Updates CSS variables for dynamic styling
- Handles grid click position calculations with snap
#### DataManager.js ✅
- Mock data generation for testing
- API request preparation (ready for backend)
- Cache management
- Event CRUD operations
- Loading state management
- Sync status handling
### 2.3 Utilities
#### DateUtils.js ✅
- Week start/end calculations
- Date/time formatting (12/24 hour)
- Duration calculations
- Time-to-minutes conversions
- Week number calculation (ISO standard)
- Snap-to-interval logic
### 2.4 Styles
#### base.css ✅
- CSS reset and variables
- Color scheme definition
- Grid measurements
- Animation keyframes
- Utility classes
#### layout.css ✅
- Main calendar container structure
- CSS Grid layout for calendar
- Time axis styling
- Week headers with sticky positioning
- Scrollable content area
- Work hours background indication
#### navigation.css ✅
- Top navigation bar layout
- Button styling (prev/next/today)
- View selector (day/week/month)
- Search box with icons
- Week info display
#### events.css ✅
- Event card styling by type
- Hover and active states
- Resize handles design
- Multi-day event styling
- Sync status indicators
- CSS-based positioning system
#### popup.css ✅
- Event popup styling
- Chevron arrow positioning
- Action buttons
- Loading overlay
- Snap indicators
### 2.5 HTML Structure ✅
- Semantic custom HTML tags
- Modular component structure
- No inline styles or JavaScript
- Data attributes for configuration
## 3. Implementation Details
### 3.1 Event Positioning System
```css
swp-event {
/* Position via CSS variables */
top: calc(var(--start-minutes) * var(--minute-height));
height: calc(var(--duration-minutes) * var(--minute-height));
}
```
### 3.2 Custom Event Flow
```javascript
// Example event flow for drag operation
1. User mousedown on event
2. DragManager → emit('calendar:dragstart')
3. ResizeManager → disable()
4. GridManager → show snap lines
5. User mousemove
6. DragManager → emit('calendar:dragmove')
7. EventRenderer → update ghost position
8. User mouseup
9. DragManager → emit('calendar:dragend')
10. EventManager → update event data
11. DataManager → sync to backend
```
### 3.3 Configuration Options
```javascript
{
view: 'week', // 'day' | 'week' | 'month'
weekDays: 7, // 4-7 days for week view
dayStartHour: 7, // Calendar start time
dayEndHour: 19, // Calendar end time
workStartHour: 8, // Work hours highlighting
workEndHour: 17,
snapInterval: 15, // Minutes: 5, 10, 15, 30, 60
hourHeight: 60, // Pixels per hour
showCurrentTime: true,
allowDrag: true,
allowResize: true,
allowCreate: true
}
```
## 4. What Needs to Be Implemented
### 4.1 Missing Managers
#### CalendarManager.js 🔲
**Purpose**: Main coordinator for all managers
```javascript
class CalendarManager {
- Initialize all managers in correct order
- Handle app lifecycle (start, destroy)
- Coordinate cross-manager operations
- Global error handling
- State persistence
}
```
#### ViewManager.js 🔲
**Purpose**: Handle view mode changes
```javascript
class ViewManager {
- Switch between day/week/month views
- Calculate visible date range
- Update grid structure for view
- Emit view change events
- Handle view-specific settings
}
```
#### NavigationManager.js 🔲
**Purpose**: Handle navigation controls
```javascript
class NavigationManager {
- Previous/Next period navigation
- Today button functionality
- Update week info display
- Coordinate with animations
- Handle navigation limits
}
```
#### EventManager.js 🔲
**Purpose**: Manage event lifecycle
```javascript
class EventManager {
- Store events in memory
- Handle event CRUD operations
- Manage event selection
- Calculate event overlaps
- Validate event constraints
}
```
#### EventRenderer.js 🔲
**Purpose**: Render events in DOM
```javascript
class EventRenderer {
- Create event DOM elements
- Calculate pixel positions
- Handle collision layouts
- Render multi-day events
- Update event appearance
}
```
#### DragManager.js 🔲
**Purpose**: Handle drag operations
```javascript
class DragManager {
- Track drag state
- Create ghost element
- Calculate snap positions
- Validate drop targets
- Handle multi-select drag
}
```
#### ResizeManager.js 🔲
**Purpose**: Handle resize operations
```javascript
class ResizeManager {
- Add/remove resize handles
- Track resize direction
- Calculate new duration
- Enforce min/max limits
- Snap to intervals
}
```
#### PopupManager.js 🔲
**Purpose**: Show event details popup
```javascript
class PopupManager {
- Show/hide popup
- Smart positioning (left/right)
- Update popup content
- Handle action buttons
- Click-outside detection
}
```
#### SearchManager.js 🔲
**Purpose**: Search functionality
```javascript
class SearchManager {
- Real-time search
- Highlight matching events
- Update transparency
- Clear search
- Search history
}
```
#### TimeManager.js 🔲
**Purpose**: Current time indicator
```javascript
class TimeManager {
- Show red line at current time
- Update position every minute
- Auto-scroll to current time
- Show/hide based on view
}
```
#### LoadingManager.js 🔲
**Purpose**: Loading states
```javascript
class LoadingManager {
- Show/hide spinner
- Block interactions
- Show error states
- Progress indication
}
```
### 4.2 Missing Utilities
#### PositionUtils.js 🔲
```javascript
- pixelsToMinutes(y, config)
- minutesToPixels(minutes, config)
- getEventBounds(element)
- detectCollisions(events)
- calculateOverlapGroups(events)
```
#### SnapUtils.js 🔲
```javascript
- snapToInterval(value, interval)
- getNearestSlot(position, interval)
- calculateSnapPoints(config)
- isValidSnapPosition(position)
```
#### DOMUtils.js 🔲
```javascript
- createElement(tag, attributes, children)
- toggleClass(element, className, force)
- findParent(element, selector)
- batchUpdate(updates)
```
### 4.3 Missing Features
#### Animation System 🔲
- Week-to-week slide transition (as shown in POC)
- Smooth state transitions
- Drag preview animations
- Loading animations
#### Collision Detection System 🔲
```javascript
// Two strategies needed:
1. Side-by-side: Events share column width
2. Overlay: Events stack with z-index
```
#### Multi-day Event Support 🔲
- Events spanning multiple days
- Visual continuation indicators
- Proper positioning in week header area
#### Touch Support 🔲
- Touch drag/drop
- Pinch to zoom
- Swipe navigation
- Long press for context menu
#### Keyboard Navigation 🔲
- Tab through events
- Arrow keys for selection
- Enter to edit
- Delete key support
#### Context Menu 🔲
- Right-click on events
- Right-click on empty slots
- Quick actions menu
#### Event Creation 🔲
- Double-click empty slot
- Drag to create
- Default duration
- Inline editing
#### Advanced Features 🔲
- Undo/redo stack
- Copy/paste events
- Bulk operations
- Print view
- Export (iCal, PDF)
- Recurring events UI
- Event templates
- Color customization
- Resource scheduling
- Timezone support
## 5. Integration Points
### 5.1 Backend API Endpoints
```
GET /api/events?start={date}&end={date}&view={view}
POST /api/events
PATCH /api/events/{id}
DELETE /api/events/{id}
GET /api/events/search?q={query}
```
### 5.2 SignalR Events
```
- EventCreated
- EventUpdated
- EventDeleted
- EventsReloaded
```
### 5.3 Data Models
```typescript
interface CalendarEvent {
id: string;
title: string;
start: string; // ISO 8601
end: string; // ISO 8601
type: 'meeting' | 'meal' | 'work' | 'milestone';
allDay: boolean;
syncStatus: 'synced' | 'pending' | 'error';
recurringId?: string;
resources?: string[];
metadata?: Record<string, any>;
}
```
## 6. Performance Considerations
1. **Virtual Scrolling**: For large date ranges
2. **Event Pooling**: Reuse DOM elements
3. **Throttled Updates**: During drag/resize
4. **Batch Operations**: For multiple changes
5. **Lazy Loading**: Load events as needed
6. **Web Workers**: For heavy calculations
## 7. Testing Strategy
1. **Unit Tests**: Each manager/utility
2. **Integration Tests**: Manager interactions
3. **E2E Tests**: User workflows
4. **Performance Tests**: Large datasets
5. **Accessibility Tests**: Keyboard/screen reader
## 8. Deployment Considerations
1. **Build Process**: Bundle modules
2. **Minification**: Reduce file size
3. **Code Splitting**: Load on demand
4. **CDN**: Static assets
5. **Monitoring**: Error tracking
6. **Analytics**: Usage patterns
## 9. Future Enhancements
1. **AI Integration**: Smart scheduling
2. **Mobile Apps**: Native wrappers
3. **Offline Support**: Service workers
4. **Collaboration**: Real-time cursors
5. **Advanced Analytics**: Usage insights
6. **Third-party Integrations**: Google Calendar, Outlook
## 10. Migration Path
### From POC to Production:
1. Extract animation logic from POC
2. Implement missing managers
3. Add error boundaries
4. Implement loading states
5. Add accessibility
6. Performance optimization
7. Security hardening
8. Documentation
9. Testing suite
10. Deployment pipeline

View file

@ -1,398 +0,0 @@
# Calendar Plantempus - Comprehensive TypeScript Code Review
## Executive Summary
This is a well-architected calendar application built with vanilla TypeScript and DOM APIs, implementing sophisticated event-driven communication patterns and drag-and-drop functionality. The codebase demonstrates advanced TypeScript usage, clean separation of concerns, and performance-optimized DOM manipulation.
## Architecture Overview
### Core Design Patterns
**Event-Driven Architecture**: The application uses a centralized EventBus system with DOM CustomEvents for all inter-component communication. This eliminates tight coupling and provides excellent separation of concerns.
**Manager Pattern**: Each domain responsibility is encapsulated in dedicated managers, creating a modular architecture that's easy to maintain and extend.
**Strategy Pattern**: View rendering uses strategy pattern with `DateEventRenderer` and `ResourceEventRenderer` implementations.
**Factory Pattern**: Used for creating managers and calendar types, promoting loose coupling.
### Key Architectural Strengths
1. **Pure DOM/TypeScript Implementation**: No external frameworks reduces bundle size and complexity
2. **Centralized Configuration**: Singleton pattern for configuration management
3. **Type Safety**: Comprehensive TypeScript types with proper union types and interfaces
4. **Performance Optimizations**: Extensive use of caching, batching, and optimized DOM queries
---
## Core System Analysis
### 1. EventBus System (`src/core/EventBus.ts`) ⭐⭐⭐⭐⭐
**Strengths:**
- Pure DOM CustomEvents implementation - elegant and leverages browser event system
- Comprehensive logging with categorization and filtering
- Proper memory management with listener tracking
- Singleton pattern with clean API
- Built-in debug mode with visual categorization
**Code Quality:**
```typescript
// Excellent event emission with proper validation
emit(eventType: string, detail: any = {}): boolean {
if (!eventType || typeof eventType !== 'string') {
return false;
}
const event = new CustomEvent(eventType, {
detail,
bubbles: true,
cancelable: true
});
return !document.dispatchEvent(event);
}
```
**Minor Improvements:**
- `logEventWithGrouping` method is incomplete (line 105)
- Could benefit from TypeScript generics for type-safe detail objects
### 2. Type System (`src/types/*.ts`) ⭐⭐⭐⭐⭐
**Exceptional Type Safety:**
```typescript
export interface CalendarEvent {
id: string;
title: string;
start: string; // ISO 8601
end: string; // ISO 8601
type: string;
allDay: boolean;
syncStatus: SyncStatus;
resource?: Resource;
recurringId?: string;
metadata?: Record<string, any>;
}
```
**Highlights:**
- Union types for view management (`ViewPeriod`, `CalendarMode`)
- Discriminated unions with `DateModeContext` and `ResourceModeContext`
- Proper interface segregation
- Consistent ISO 8601 date handling
---
## Drag and Drop System Deep Dive ⭐⭐⭐⭐⭐
### DragDropManager (`src/managers/DragDropManager.ts`)
This is the crown jewel of the codebase - a sophisticated, performance-optimized drag-and-drop system.
#### Technical Excellence:
**1. Performance Optimizations:**
```typescript
// Consolidated position calculations to reduce DOM queries
private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } {
const column = this.detectColumn(mousePosition.x, mousePosition.y);
const snappedY = this.calculateSnapPosition(mousePosition.y, column);
return { column, snappedY };
}
```
**2. Intelligent Caching:**
```typescript
private cachedElements: CachedElements = {
scrollContainer: null,
currentColumn: null,
lastColumnDate: null
};
```
**3. Smooth Auto-Scroll:**
```typescript
private startAutoScroll(direction: 'up' | 'down'): void {
const scroll = () => {
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
this.cachedElements.scrollContainer!.scrollTop += scrollAmount;
this.autoScrollAnimationId = requestAnimationFrame(scroll);
};
this.autoScrollAnimationId = requestAnimationFrame(scroll);
}
```
**4. Advanced Features:**
- **Grid Snapping**: Intelligent snapping to 15-minute intervals
- **Column Detection**: Efficient column switching with caching
- **Auto-scroll**: Smooth scrolling when dragging near edges
- **All-day Conversion**: Seamless conversion from timed to all-day events
- **Mouse Offset Preservation**: Maintains grab point during drag
#### Event Flow Architecture:
```
MouseDown → DragStart → DragMove → (Auto-scroll) → DragEnd
↓ ↓ ↓ ↓ ↓
EventBus → EventRenderer → Visual Update → Position → Finalize
```
**Minor Issues:**
- Some hardcoded values (40px for stacking threshold at line 379)
- Mixed Danish and English comments
---
## Event Rendering System ⭐⭐⭐⭐
### EventRenderer (`src/renderers/EventRenderer.ts`)
**Sophisticated Overlap Management:**
```typescript
// Intelligent overlap detection with pixel-perfect precision
private detectPixelOverlap(element1: HTMLElement, element2: HTMLElement): OverlapType {
const top1 = parseFloat(element1.style.top) || 0;
const height1 = parseFloat(element1.style.height) || 0;
const bottom1 = top1 + height1;
const top2 = parseFloat(element2.style.top) || 0;
const height2 = parseFloat(element2.style.height) || 0;
const bottom2 = top2 + height2;
if (bottom1 <= top2 || bottom2 <= top1) {
return OverlapType.NONE;
}
const startDifference = Math.abs(top1 - top2);
return startDifference > 40 ? OverlapType.STACKING : OverlapType.COLUMN_SHARING;
}
```
**Advanced Drag Integration:**
- Real-time timestamp updates during drag
- Seamless event cloning with proper cleanup
- Intelligent overlap re-calculation after drops
**Architectural Strengths:**
- Strategy pattern with `DateEventRenderer` and `ResourceEventRenderer`
- Proper separation of rendering logic from positioning
- Clean drag state management
### SimpleEventOverlapManager (`src/managers/SimpleEventOverlapManager.ts`)
**Clean, Data-Attribute Based Overlap System:**
```typescript
public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
if (!this.eventsOverlapInTime(event1, event2)) {
return OverlapType.NONE;
}
const timeDiffMinutes = Math.abs(
new Date(event1.start).getTime() - new Date(event2.start).getTime()
) / (1000 * 60);
return timeDiffMinutes > 30 ? OverlapType.STACKING : OverlapType.COLUMN_SHARING;
}
```
**Key Improvements Over Legacy System:**
- **Data-Attribute Tracking**: Uses `data-stack-link` instead of in-memory Maps
- **Simplified State Management**: DOM is the single source of truth
- **51% Less Code**: Eliminated complex linked list management
- **Zero State Sync Bugs**: No memory/DOM synchronization issues
**Visual Layout Strategies:**
- **Column Sharing**: Flexbox layout for concurrent events
- **Stacking**: Margin-left offsets with z-index management via data attributes
- **Dynamic Grouping**: Real-time group creation and cleanup
---
## Manager System Analysis
### CalendarManager (`src/managers/CalendarManager.ts`) ⭐⭐⭐⭐
**Excellent Orchestration:**
- Clean initialization sequence with proper error handling
- Intelligent view and date management
- WorkWeek change handling with full grid rebuilds
**Smart Period Calculations:**
```typescript
private calculateCurrentPeriod(): { start: string; end: string } {
switch (this.currentView) {
case 'week':
const weekStart = new Date(current);
const dayOfWeek = weekStart.getDay();
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
weekStart.setDate(weekStart.getDate() - daysToMonday);
// ... proper ISO week calculation
}
}
```
### EventManager (`src/managers/EventManager.ts`) ⭐⭐⭐⭐
**Performance Optimizations:**
```typescript
// Intelligent caching for period queries
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
const cacheKey = `${DateCalculator.formatISODate(startDate)}_${DateCalculator.formatISODate(endDate)}`;
if (this.lastCacheKey === cacheKey && this.eventCache.has(cacheKey)) {
return this.eventCache.get(cacheKey)!;
}
// ... filter and cache logic
}
```
**Strengths:**
- Resource and date calendar support
- Proper cache invalidation
- Event navigation with error handling
- Mock data loading with proper async patterns
### ViewManager (`src/managers/ViewManager.ts`) ⭐⭐⭐⭐
**Clean State Management:**
```typescript
// Generic button group setup eliminates duplicate code
private setupButtonGroup(selector: string, attribute: string, handler: (value: string) => void): void {
const buttons = document.querySelectorAll(selector);
buttons.forEach(button => {
const clickHandler = (event: Event) => {
event.preventDefault();
const value = button.getAttribute(attribute);
if (value) handler(value);
};
button.addEventListener('click', clickHandler);
this.buttonListeners.set(button, clickHandler);
});
}
```
**Performance Features:**
- Button caching with cache invalidation (5-second TTL)
- Consolidated button update logic
- Proper event listener cleanup
---
## Utility System Excellence
### DateCalculator (`src/utils/DateCalculator.ts`) ⭐⭐⭐⭐⭐
**Exceptional Date Handling:**
```typescript
// Proper ISO 8601 week calculation
static getWeekNumber(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1)/7);
}
```
**Features:**
- Static class pattern for performance
- Comprehensive date validation
- ISO week handling (Monday start)
- Internationalization support with `Intl.DateTimeFormat`
- Proper timezone handling
### PositionUtils (`src/utils/PositionUtils.ts`) ⭐⭐⭐⭐
**Pixel-Perfect Calculations:**
```typescript
public static snapToGrid(pixels: number): number {
const gridSettings = calendarConfig.getGridSettings();
const snapInterval = gridSettings.snapInterval;
const snapPixels = PositionUtils.minutesToPixels(snapInterval);
return Math.round(pixels / snapPixels) * snapPixels;
}
```
**Strengths:**
- Delegate date operations to DateCalculator (proper separation)
- Comprehensive position/time conversions
- Grid snapping with configurable intervals
- Work hours validation
---
## Performance Analysis
### Optimizations Implemented:
1. **DOM Query Caching**: Cached elements with TTL-based invalidation
2. **Event Batching**: Consolidated position calculations in drag system
3. **Efficient Event Filtering**: Map-based caching for period queries
4. **Lazy Loading**: Components only query DOM when needed
5. **Memory Management**: Proper cleanup of event listeners and cached references
### Performance Metrics:
- Drag operations: ~60fps through requestAnimationFrame
- Event rendering: O(n log n) complexity with overlap grouping
- View switching: Cached button states prevent unnecessary DOM queries
---
## Code Quality Assessment
### Strengths:
- **Type Safety**: Comprehensive TypeScript with no `any` types
- **Error Handling**: Proper validation and graceful degradation
- **Memory Management**: Cleanup methods in all managers
- **Documentation**: Good inline documentation and method signatures
- **Consistency**: Uniform coding patterns throughout
### Technical Debt:
1. **Mixed Languages**: Danish and English comments/variables
2. **Hardcoded Values**: Some magic numbers (40px threshold, 5s cache TTL)
3. **Configuration**: Some values should be configurable
4. **Testing**: No visible test suite
### Security Considerations:
- No eval() usage
- Proper DOM sanitization in event rendering
- No direct innerHTML with user data
---
## Architecture Recommendations
### Immediate Improvements:
1. **Internationalization**: Standardize to English or implement proper i18n
2. **Configuration**: Move hardcoded values to configuration
3. **Testing**: Add unit tests for critical drag-and-drop logic
4. **Documentation**: Add architectural decision records (ADRs)
### Future Enhancements:
1. **Web Workers**: Move heavy calculations off main thread
2. **Virtual Scrolling**: For large event sets
3. **Touch Support**: Enhanced mobile drag-and-drop
4. **Accessibility**: ARIA labels and keyboard navigation
---
## Conclusion
This is an exceptionally well-crafted calendar application that demonstrates:
- **Advanced TypeScript Usage**: Proper types, interfaces, and modern patterns
- **Performance Excellence**: Sophisticated caching, batching, and optimization
- **Clean Architecture**: Event-driven design with proper separation of concerns
- **Production Ready**: Comprehensive error handling and memory management
**Overall Rating: ⭐⭐⭐⭐⭐ (Exceptional)**
The drag-and-drop system, in particular, is a masterclass in performance optimization and user experience design. The EventBus architecture provides a solid foundation for future enhancements.
**Key Technical Achievements:**
- Zero-framework implementation with modern browser APIs
- Sophisticated event overlap detection and rendering
- Performance-optimized drag operations with smooth auto-scroll
- Comprehensive date/time handling with internationalization support
- Clean, maintainable codebase with excellent type safety
This codebase serves as an excellent example of how to build complex DOM applications with vanilla TypeScript while maintaining high performance and code quality standards.

View file

@ -1,182 +0,0 @@
# EventOverlapManager Complexity Comparison
## Original vs Simplified Implementation
### **Lines of Code Comparison**
| Aspect | Original | Simplified | Reduction |
|--------|----------|------------|-----------|
| Total Lines | ~453 lines | ~220 lines | **51% reduction** |
| Stack Management | ~150 lines | ~20 lines | **87% reduction** |
| State Tracking | Complex Map + linked list | Simple DOM queries | **100% elimination** |
---
## **Key Simplifications**
### 1. **Eliminated Complex State Tracking**
**Before:**
```typescript
// Complex linked list tracking
private stackChains = new Map<string, {
next?: string,
prev?: string,
stackLevel: number
}>();
private removeFromStackChain(eventId: string): string[] {
// 25+ lines of linked list manipulation
const chainInfo = this.stackChains.get(eventId);
// Complex prev/next linking logic...
}
```
**After:**
```typescript
// Simple DOM-based approach
public restackEventsInContainer(container: HTMLElement): void {
const stackedEvents = Array.from(container.querySelectorAll('swp-event'))
.filter(el => this.isStackedEvent(el as HTMLElement));
stackedEvents.forEach((element, index) => {
element.style.marginLeft = `${(index + 1) * 15}px`;
});
}
```
### 2. **Simplified Event Detection**
**Before:**
```typescript
public isStackedEvent(element: HTMLElement): boolean {
const eventId = element.dataset.eventId;
const hasMarginLeft = element.style.marginLeft !== '';
const isInStackChain = eventId ? this.stackChains.has(eventId) : false;
// Two different ways to track the same thing
return hasMarginLeft || isInStackChain;
}
```
**After:**
```typescript
public isStackedEvent(element: HTMLElement): boolean {
const marginLeft = element.style.marginLeft;
return marginLeft !== '' && marginLeft !== '0px';
}
```
### 3. **Cleaner Group Management**
**Before:**
```typescript
public removeFromEventGroup(container: HTMLElement, eventId: string): boolean {
// 50+ lines including:
// - Stack chain checking
// - Complex position calculations
// - Multiple cleanup scenarios
// - Affected event re-stacking
}
```
**After:**
```typescript
public removeFromEventGroup(container: HTMLElement, eventId: string): boolean {
// 20 lines of clean, focused logic:
// - Remove element
// - Handle remaining events
// - Simple container cleanup
}
```
---
## **Benefits of Simplified Approach**
### ✅ **Maintainability**
- **No complex state synchronization**
- **Single source of truth (DOM)**
- **Easier to debug and understand**
### ✅ **Performance**
- **No Map lookups or linked list traversal**
- **Direct DOM queries when needed**
- **Simpler memory management**
### ✅ **Reliability**
- **No state desynchronization bugs**
- **Fewer edge cases**
- **More predictable behavior**
### ✅ **Code Quality**
- **51% fewer lines of code**
- **Simpler mental model**
- **Better separation of concerns**
---
## **What Was Eliminated**
### 🗑️ **Removed Complexity**
1. **Linked List Management**: Complex `next`/`prev` chain tracking
2. **State Synchronization**: Keeping DOM and Map in sync
3. **Chain Reconstruction**: Complex re-linking after removals
4. **Dual Tracking**: Both style-based and Map-based state
5. **Edge Case Handling**: Complex scenarios from state mismatches
### 🎯 **Retained Functionality**
1. **Column Sharing**: Flexbox groups work exactly the same
2. **Event Stacking**: Visual stacking with margin-left offsets
3. **Overlap Detection**: Same time-based algorithm
4. **Drag and Drop**: Full drag support maintained
5. **Visual Appearance**: Identical user experience
---
## **Risk Assessment**
### ⚠️ **Potential Concerns**
1. **DOM Query Performance**: More DOM queries vs Map lookups
- **Mitigation**: Queries are scoped to specific containers
- **Reality**: Minimal impact for typical calendar usage
2. **State Reconstruction**: Re-calculating vs cached state
- **Mitigation**: DOM is the single source of truth
- **Reality**: Eliminates sync bugs completely
### ✅ **Benefits Outweigh Risks**
- **Dramatically simpler codebase**
- **Eliminated entire class of state sync bugs**
- **Much easier to debug and maintain**
- **Better separation of concerns**
---
## **Migration Strategy**
1. ✅ **Created SimpleEventOverlapManager**
2. ✅ **Updated EventRenderer imports**
3. ✅ **Simplified drag handling methods**
4. ✅ **Maintained API compatibility**
5. ✅ **Testing phase completed**
6. ✅ **Removed old EventOverlapManager** (legacy code eliminated)
---
## **Conclusion**
The simplified approach provides **identical functionality** with:
- **51% less code**
- **87% simpler stack management**
- **Zero state synchronization bugs**
- **Much easier maintenance**
**Migration completed successfully** - the old EventOverlapManager has been removed and the system now uses the cleaner SimpleEventOverlapManager implementation.
This is a perfect example of how **complexity often accumulates unnecessarily** and how a **DOM-first approach** can be both simpler and more reliable than complex state management.
## **See Also**
- [Stack Binding System Documentation](docs/stack-binding-system.md) - Detailed explanation of how events are linked together
- [`SimpleEventOverlapManager.ts`](src/managers/SimpleEventOverlapManager.ts) - Current implementation

View file

@ -1,221 +0,0 @@
# Data-Attribute Stack Tracking Solution
## Implementation Summary
Vi har nu implementeret stack tracking via data attributes i stedet for komplekse Map-baserede linked lists.
### 🎯 **How it works:**
#### **Stack Links via Data Attributes**
```html
<!-- Base event -->
<swp-event
data-event-id="event_123"
data-stack-link='{"stackLevel":0,"next":"event_456"}'
style="margin-left: 0px;">
</swp-event>
<!-- Stacked event -->
<swp-event
data-event-id="event_456"
data-stack-link='{"prev":"event_123","stackLevel":1,"next":"event_789"}'
style="margin-left: 15px;">
</swp-event>
<!-- Top stacked event -->
<swp-event
data-event-id="event_789"
data-stack-link='{"prev":"event_456","stackLevel":2}'
style="margin-left: 30px;">
</swp-event>
```
### 🔧 **Key Methods:**
#### **createStackedEvent()**
```typescript
// Links new event to end of chain
let lastElement = underlyingElement;
while (lastLink?.next) {
lastElement = this.findElementById(lastLink.next);
lastLink = this.getStackLink(lastElement);
}
// Create bidirectional link
this.setStackLink(lastElement, { ...lastLink, next: eventId });
this.setStackLink(eventElement, { prev: lastElementId, stackLevel });
```
#### **removeStackedStyling()**
```typescript
// Re-link prev and next
if (link.prev && link.next) {
this.setStackLink(prevElement, { ...prevLink, next: link.next });
this.setStackLink(nextElement, { ...nextLink, prev: link.prev });
}
// Update subsequent stack levels
this.updateSubsequentStackLevels(link.next, -1);
```
#### **restackEventsInContainer()**
```typescript
// Group by stack chains (not all stacked events together!)
for (const element of stackedEvents) {
// Find root of chain
while (rootLink?.prev) {
rootElement = this.findElementById(rootLink.prev);
}
// Collect entire chain
// Re-stack each chain separately
}
```
---
## 🏆 **Advantages vs Map Solution:**
### ✅ **Simplified State Management**
| Aspect | Map + Linked List | Data Attributes |
|--------|------------------|-----------------|
| **State Location** | Separate Map object | In DOM elements |
| **Synchronization** | Manual sync required | Automatic with DOM |
| **Memory Cleanup** | Manual Map cleanup | Automatic with element removal |
| **Debugging** | Console logs only | DevTools inspection |
| **State Consistency** | Possible sync bugs | Always consistent |
### ✅ **Code Complexity Reduction**
```typescript
// OLD: Complex Map management
private stackChains = new Map<string, { next?: string, prev?: string, stackLevel: number }>();
// Find last event in chain - complex iteration
let lastEventId = underlyingId;
while (this.stackChains.has(lastEventId) && this.stackChains.get(lastEventId)?.next) {
lastEventId = this.stackChains.get(lastEventId)!.next!;
}
// Link events - error prone
this.stackChains.get(lastEventId)!.next = eventId;
this.stackChains.set(eventId, { prev: lastEventId, stackLevel });
// NEW: Simple data attribute management
let lastElement = underlyingElement;
while (lastLink?.next) {
lastElement = this.findElementById(lastLink.next);
}
this.setStackLink(lastElement, { ...lastLink, next: eventId });
this.setStackLink(eventElement, { prev: lastElementId, stackLevel });
```
### ✅ **Better Error Handling**
```typescript
// DOM elements can't get out of sync with their own attributes
// When element is removed, its state automatically disappears
// No orphaned Map entries
```
---
## 🧪 **Test Scenarios:**
### **Scenario 1: Multiple Separate Stacks**
```
Column has:
Stack A: Event1 → Event2 → Event3 (times: 09:00-10:00, 09:15-10:15, 09:30-10:30)
Stack B: Event4 → Event5 (times: 14:00-15:00, 14:10-15:10)
Remove Event2 (middle of Stack A):
✅ Expected: Event1 → Event3 (Event3 moves to 15px margin)
✅ Expected: Stack B unchanged (Event4→Event5 still at 0px→15px)
❌ Old naive approach: Would group all events together
```
### **Scenario 2: Remove Base Event**
```
Stack: EventA(base) → EventB → EventC
Remove EventA:
✅ Expected: EventB becomes base (0px), EventC moves to 15px
✅ Data-attribute solution: EventB.stackLevel = 0, EventC.stackLevel = 1
```
### **Scenario 3: Drag and Drop**
```
Drag Event2 from Stack A to new position:
✅ removeStackedStyling() handles re-linking
✅ Other stack events maintain their relationships
✅ No Map synchronization issues
```
---
## 🔍 **Debugging Benefits:**
### **Browser DevTools Inspection:**
```html
<!-- Easy to see stack relationships directly in HTML -->
<swp-event data-stack-link='{"prev":"123","next":"789","stackLevel":1}'>
<!-- Event content -->
</swp-event>
```
### **Console Debugging:**
```javascript
// Easy to inspect stack chains
const element = document.querySelector('[data-event-id="456"]');
const link = JSON.parse(element.dataset.stackLink);
console.log('Stack chain:', link);
```
---
## 📊 **Performance Comparison:**
| Operation | Map Solution | Data-Attribute Solution |
|-----------|--------------|-------------------------|
| **Create Stack** | Map.set() + element.style | JSON.stringify() + element.style |
| **Remove Stack** | Map manipulation + DOM queries | JSON.parse/stringify + DOM queries |
| **Find Chain** | Map iteration | DOM traversal |
| **Memory Usage** | Map + DOM | DOM only |
| **Sync Overhead** | High (keep Map in sync) | None (DOM is source) |
### **Performance Notes:**
- **JSON.parse/stringify**: Very fast for small objects (~10 properties max)
- **DOM traversal**: Limited by chain length (typically 2-5 events)
- **Memory**: Significant reduction (no separate Map)
- **Garbage collection**: Better (automatic cleanup)
---
## ✅ **Solution Status:**
### **Completed:**
- [x] StackLink interface definition
- [x] Helper methods (getStackLink, setStackLink, findElementById)
- [x] createStackedEvent with data-attribute linking
- [x] removeStackedStyling with proper re-linking
- [x] restackEventsInContainer respects separate chains
- [x] isStackedEvent checks both style and data-attributes
- [x] Compilation successful
### **Ready for Testing:**
- [ ] Manual UI testing of stack behavior
- [ ] Drag and drop stacked events
- [ ] Multiple stacks in same column
- [ ] Edge cases (remove first/middle/last)
---
## 🎉 **Conclusion:**
This data-attribute solution provides:
1. **Same functionality** as the Map-based approach
2. **Simpler implementation** (DOM as single source of truth)
3. **Better debugging experience** (DevTools visibility)
4. **Automatic memory management** (no manual cleanup)
5. **No synchronization bugs** (state follows element)
The solution maintains all the precision of the original complex system while dramatically simplifying the implementation and eliminating entire classes of potential bugs.

View file

@ -1,161 +0,0 @@
# Calendar Event System Analysis
## Overview
Analysis of all events used in the Calendar Plantempus system, categorized by type and usage.
## Core Events (25 events)
*Defined in `src/constants/CoreEvents.ts`*
### Lifecycle Events (3)
- `core:initialized` - Calendar initialization complete
- `core:ready` - Calendar ready for use
- `core:destroyed` - Calendar cleanup complete
### View Events (3)
- `view:changed` - Calendar view changed (day/week/month)
- `view:rendered` - View rendering complete
- `workweek:changed` - Work week configuration changed
### Navigation Events (4)
- `nav:date-changed` - Current date changed
- `nav:navigation-completed` - Navigation animation/transition complete
- `nav:period-info-update` - Week/period information updated
- `nav:navigate-to-event` - Request to navigate to specific event
### Data Events (4)
- `data:loading` - Data fetch started
- `data:loaded` - Data fetch completed
- `data:error` - Data fetch error
- `data:events-filtered` - Events filtered
### Grid Events (3)
- `grid:rendered` - Grid rendering complete
- `grid:clicked` - Grid cell clicked
- `grid:cell-selected` - Grid cell selected
### Event Management (4)
- `event:created` - New event created
- `event:updated` - Event updated
- `event:deleted` - Event deleted
- `event:selected` - Event selected
### System Events (2)
- `system:error` - System error occurred
- `system:refresh` - Refresh requested
### Filter Events (1)
- `filter:changed` - Event filter changed
### Rendering Events (1)
- `events:rendered` - Events rendering complete
## Custom Events (22 events)
*Used throughout the system for specific functionality*
### Drag & Drop Events (12)
- `drag:start` - Drag operation started
- `drag:move` - Drag operation in progress
- `drag:end` - Drag operation ended
- `drag:auto-scroll` - Auto-scroll during drag
- `drag:column-change` - Dragged to different column
- `drag:mouseenter-header` - Mouse entered header during drag
- `drag:mouseleave-header` - Mouse left header during drag
- `drag:convert-to-time_event` - Convert all-day to timed event
### Event Interaction (2)
- `event:click` - Event clicked (no drag)
- `event:clicked` - Event clicked (legacy)
### Header Events (3)
- `header:mouseleave` - Mouse left header area
- `header:height-changed` - Header height changed
- `header:rebuilt` - Header DOM rebuilt
### All-Day Events (1)
- `allday:ensure-container` - Ensure all-day container exists
### Column Events (1)
- `column:mouseover` - Mouse over column
### Scroll Events (1)
- `scroll:to-event-time` - Scroll to specific event time
### Workweek Events (1)
- `workweek:header-update` - Update header after workweek change
### Navigation Events (1)
- `navigation:completed` - Navigation completed (different from core event)
## Event Payload Analysis
### Type Safety Issues Found
#### 1. AllDayManager Event Mismatch
**File:** `src/managers/AllDayManager.ts:33-34`
```typescript
// Expected payload:
const { targetDate, originalElement } = (event as CustomEvent).detail;
// Actual payload from DragDropManager:
{
targetDate: string,
mousePosition: { x: number, y: number },
originalElement: HTMLElement,
cloneElement: HTMLElement | null
}
```
#### 2. Inconsistent Event Signatures
Multiple events have different payload structures across different emitters/listeners.
#### 3. No Type Safety
All events use `(event as CustomEvent).detail` without proper TypeScript interfaces.
## Event Usage Statistics
### Most Used Events
1. **Drag Events** - 12 different types, used heavily in drag-drop system
2. **Core Navigation** - 4 types, used across all managers
3. **Grid Events** - 3 types, fundamental to calendar rendering
4. **Header Events** - 3 types, critical for all-day functionality
### Critical Events (High Impact)
- `drag:mouseenter-header` / `drag:mouseleave-header` - Core drag functionality
- `nav:navigation-completed` - Synchronizes multiple managers
- `grid:rendered` - Triggers event rendering
- `events:rendered` - Triggers filtering system
### Simple Events (Low Impact)
- `header:height-changed` - Simple notification
- `allday:ensure-container` - Simple request
- `system:refresh` - Simple trigger
## Recommendations
### Priority 1: Fix Critical Issues
1. Fix AllDayManager event signature mismatch
2. Standardize drag event payloads
3. Document current event contracts
### Priority 2: Type Safety Implementation
1. Create TypeScript interfaces for all event payloads
2. Implement type-safe EventBus
3. Migrate drag events first (highest complexity)
### Priority 3: System Cleanup
1. Consolidate duplicate events (`event:click` vs `event:clicked`)
2. Standardize event naming conventions
3. Remove unused events
## Total Event Count
- **Core Events:** 25
- **Custom Events:** 22
- **Total:** 47 unique event types
## Files Analyzed
- `src/constants/CoreEvents.ts`
- `src/managers/*.ts` (8 files)
- `src/renderers/*.ts` (4 files)
- `src/core/CalendarConfig.ts`
*Analysis completed: 2025-09-20*

View file

@ -1,133 +0,0 @@
# Calendar Initialization Sequence Diagram
Dette diagram viser den aktuelle initialization sekvens baseret på koden.
```mermaid
sequenceDiagram
participant Browser
participant Index as index.ts
participant MF as ManagerFactory
participant CTF as CalendarTypeFactory
participant EB as EventBus
participant CM as CalendarManager
participant EM as EventManager
participant ERS as EventRenderingService
participant GM as GridManager
participant GSM as GridStyleManager
participant GR as GridRenderer
participant SM as ScrollManager
participant NM as NavigationManager
participant NR as NavigationRenderer
participant VM as ViewManager
Browser->>Index: DOMContentLoaded
Index->>Index: initializeCalendar()
Index->>CTF: initialize()
CTF-->>Index: Factory ready
Index->>MF: getInstance()
MF-->>Index: Factory instance
Index->>MF: createManagers(eventBus, config)
MF->>EM: new EventManager(eventBus)
EM->>EB: setupEventListeners()
EM-->>MF: EventManager ready
MF->>ERS: new EventRenderingService(eventBus, eventManager)
ERS->>EB: setupEventListeners()
ERS-->>MF: EventRenderingService ready
MF->>GM: new GridManager()
GM->>GSM: new GridStyleManager(config)
GM->>GR: new GridRenderer(config)
GM->>EB: subscribeToEvents()
GM-->>MF: GridManager ready
MF->>SM: new ScrollManager()
SM->>EB: subscribeToEvents()
SM-->>MF: ScrollManager ready
MF->>NM: new NavigationManager(eventBus)
NM->>NR: new NavigationRenderer(eventBus)
NR->>EB: setupEventListeners()
NM->>EB: setupEventListeners()
NM-->>MF: NavigationManager ready
MF->>VM: new ViewManager(eventBus)
VM->>EB: setupEventListeners()
VM-->>MF: ViewManager ready
MF->>CM: new CalendarManager(eventBus, config, deps...)
CM->>EB: setupEventListeners()
CM-->>MF: CalendarManager ready
MF-->>Index: All managers created
Index->>EB: setDebug(true)
Index->>MF: initializeManagers(managers)
MF->>CM: initialize()
CM->>EM: loadData()
EM->>EM: loadMockData()
EM->>EM: processCalendarData()
EM-->>CM: Data loaded
CM->>GM: setResourceData(resourceData)
GM-->>CM: Resource data set
CM->>GM: render()
GM->>GSM: updateGridStyles(resourceData)
GM->>GR: renderGrid(grid, currentWeek, resourceData, allDayEvents)
GR-->>GM: Grid rendered
GM->>EB: emit(GRID_RENDERED, context)
EB-->>ERS: GRID_RENDERED event
ERS->>EM: getEventsForPeriod(startDate, endDate)
EM-->>ERS: Filtered events
ERS->>ERS: strategy.renderEvents()
CM->>SM: initialize()
SM->>SM: setupScrolling()
CM->>CM: setView(currentView)
CM->>EB: emit(VIEW_CHANGED, viewData)
CM->>CM: setCurrentDate(currentDate)
CM->>EB: emit(DATE_CHANGED, dateData)
CM->>EB: emit(CALENDAR_INITIALIZED, initData)
EB-->>NM: CALENDAR_INITIALIZED
NM->>NM: updateWeekInfo()
NM->>EB: emit(WEEK_INFO_UPDATED, weekInfo)
EB-->>NR: WEEK_INFO_UPDATED
NR->>NR: updateWeekInfoInDOM()
EB-->>VM: CALENDAR_INITIALIZED
VM->>VM: initializeView()
VM->>EB: emit(VIEW_RENDERED, viewData)
CM-->>MF: Initialization complete
MF-->>Index: All managers initialized
Index->>Browser: Calendar ready
```
## Aktuel Arkitektur Status
### Factory Pattern
- ManagerFactory håndterer manager instantiering
- Proper dependency injection via constructor
### Event-Driven Communication
- EventBus koordinerer kommunikation mellem managers
- NavigationRenderer lytter til WEEK_INFO_UPDATED events
- EventRenderingService reagerer på GRID_RENDERED events
### Separation of Concerns
- Managers håndterer business logic
- Renderers håndterer DOM manipulation
- EventBus håndterer kommunikation

View file

@ -1,183 +0,0 @@
# Kodeanalyse og Forbedringsplan - Calendar System
## Overordnet Vurdering
Koden er generelt velstruktureret med god separation of concerns. Der er dog stadig nogle områder med duplikering og potentiale for yderligere optimering.
## Positive Observationer ✅
### 1. God Arkitektur
- **Factory Pattern**: SwpEventElement bruger factory pattern korrekt
- **Event-driven**: Konsistent brug af EventBus for kommunikation
- **Caching**: God brug af caching i DragDropManager og EventManager
- **Separation**: AllDayManager er korrekt separeret fra HeaderManager
### 2. Performance Optimering
- **DOM Caching**: DragDropManager cacher DOM elementer effektivt
- **Event Throttling**: Implementeret i flere managers
- **Lazy Loading**: Smart brug af lazy loading patterns
### 3. TypeScript Best Practices
- Stærk typing med interfaces
- God brug af branded types (EventId)
- Konsistent error handling
## Identificerede Problemer og Forbedringsforslag 🔧
### 1. Duplikeret Time Formatting
**Problem**: `formatTime()` metode findes i:
- EventRenderer.ts (linje 280-297)
- SwpEventElement.ts (linje 44-50)
**Løsning**: Opret en central TimeFormatter utility:
```typescript
// src/utils/TimeFormatter.ts
export class TimeFormatter {
static formatTime(input: number | Date | string): string {
// Centraliseret implementation
}
}
```
### 2. Duplikeret Cache Management
**Problem**: Lignende cache patterns i:
- AllDayManager (linje 11-76)
- HeaderManager
- GridRenderer
**Løsning**: Generisk CacheManager:
```typescript
// src/utils/CacheManager.ts
export class DOMCacheManager<T extends Record<string, HTMLElement | null>> {
private cache: T;
constructor(initialCache: T) {
this.cache = initialCache;
}
get<K extends keyof T>(key: K, selector?: string): T[K] {
if (!this.cache[key] && selector) {
this.cache[key] = document.querySelector(selector) as T[K];
}
return this.cache[key];
}
clear(): void {
Object.keys(this.cache).forEach(key => {
this.cache[key as keyof T] = null;
});
}
}
```
### 3. Overlap Detection Kompleksitet
**Problem**: EventRenderer har stadig "new_" prefixed metoder som indikerer ufærdig refactoring
**Løsning**:
- Fjern "new_" prefix fra metoderne
- Flyt al overlap logik til OverlapDetector
- Simplificer EventRenderer
### 4. Grid Positioning Beregninger
**Problem**: Grid position beregninger gentages flere steder
**Løsning**: Centralisér i GridPositionCalculator:
```typescript
// src/utils/GridPositionCalculator.ts
export class GridPositionCalculator {
static calculateEventPosition(event: CalendarEvent): { top: number; height: number }
static calculateSnapPosition(y: number, snapInterval: number): number
static pixelsToMinutes(pixels: number, hourHeight: number): number
static minutesToPixels(minutes: number, hourHeight: number): number
}
```
### 5. Event Element Creation
**Problem**: SwpEventElement kunne forenkles yderligere
**Forslag**:
- Tilføj flere factory metoder for forskellige event typer
- Implementer builder pattern for komplekse events
### 6. All-Day Event Row Calculation
**Problem**: AllDayManager har kompleks row calculation logik (linje 108-143)
**Løsning**: Udtræk til separat utility:
```typescript
// src/utils/AllDayRowCalculator.ts
export class AllDayRowCalculator {
static calculateRequiredRows(events: HTMLElement[]): number
static expandEventsByDate(events: HTMLElement[]): Record<string, string[]>
}
```
### 7. Manglende Unit Tests
**Problem**: Ingen test filer fundet
**Løsning**: Tilføj tests for kritiske utilities:
- TimeFormatter
- GridPositionCalculator
- OverlapDetector
- AllDayRowCalculator
## Prioriteret Handlingsplan
### Fase 1: Utilities (Høj Prioritet)
1. ✅ SwpEventElement factory (allerede implementeret)
2. ⬜ TimeFormatter utility
3. ⬜ DOMCacheManager
4. ⬜ GridPositionCalculator
### Fase 2: Refactoring (Medium Prioritet)
5. ⬜ Fjern "new_" prefix fra EventRenderer metoder
6. ⬜ Simplificer AllDayManager med AllDayRowCalculator
7. ⬜ Konsolider overlap detection
### Fase 3: Testing & Dokumentation (Lav Prioritet)
8. ⬜ Unit tests for utilities
9. ⬜ JSDoc dokumentation
10. ⬜ Performance benchmarks
## Arkitektur Diagram
```mermaid
graph TD
A[Utilities Layer] --> B[TimeFormatter]
A --> C[DOMCacheManager]
A --> D[GridPositionCalculator]
A --> E[AllDayRowCalculator]
F[Managers] --> A
G[Renderers] --> A
H[Elements] --> A
F --> I[EventManager]
F --> J[DragDropManager]
F --> K[AllDayManager]
G --> L[EventRenderer]
G --> M[AllDayEventRenderer]
H --> N[SwpEventElement]
H --> O[SwpAllDayEventElement]
```
## Performance Forbedringer
### 1. Event Delegation
Overvej at bruge event delegation i stedet for individuelle event listeners på hver event element.
### 2. Virtual Scrolling
For kalendere med mange events, implementer virtual scrolling.
### 3. Web Workers
Overvej at flytte tunge beregninger til Web Workers.
## Konklusion
Koden er generelt i god stand med solid arkitektur. De foreslåede forbedringer vil:
- Reducere code duplication med 30-40%
- Forbedre maintainability
- Gøre koden mere testbar
- Forbedre performance marginalt
Estimeret tid for implementering: 2-3 dage for alle forbedringer.

View file

@ -1,237 +0,0 @@
# Calendar Plantempus - Date Mode Initialization Sequence
## Overview
This document shows the complete initialization sequence and event flow for Date Mode in Calendar Plantempus, including when data is loaded and ready for rendering.
## Sequence Diagram
```mermaid
sequenceDiagram
participant Browser as Browser
participant Index as index.ts
participant Config as CalendarConfig
participant Factory as CalendarTypeFactory
participant CM as CalendarManager
participant EM as EventManager
participant GM as GridManager
participant NM as NavigationManager
participant VM as ViewManager
participant ER as EventRenderer
participant SM as ScrollManager
participant EB as EventBus
participant DOM as DOM
Note over Browser: Page loads calendar application
Browser->>Index: Load application
Note over Index: PHASE 0: Pre-initialization Setup
Index->>Config: new CalendarConfig()
Config->>Config: loadCalendarType() - Read URL ?type=date
Config->>Config: loadFromDOM() - Read data attributes
Config->>Config: Set mode='date', period='week'
Index->>Factory: CalendarTypeFactory.initialize()
Factory->>Factory: Create DateHeaderRenderer
Factory->>Factory: Create DateColumnRenderer
Factory->>Factory: Create DateEventRenderer
Note over Factory: Strategy Pattern renderers ready
Note over Index: PHASE 1: Core Managers Construction
Index->>CM: new CalendarManager(eventBus, config)
CM->>EB: Subscribe to VIEW_CHANGE_REQUESTED
CM->>EB: Subscribe to NAV_PREV, NAV_NEXT
Index->>NM: new NavigationManager(eventBus)
NM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over NM: Will wait to call updateWeekInfo()
Index->>VM: new ViewManager(eventBus)
VM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over Index: PHASE 2: Data & Rendering Managers
Index->>EM: new EventManager(eventBus)
EM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over EM: Will wait to load data
Index->>ER: new EventRenderer(eventBus)
ER->>EB: Subscribe to EVENTS_LOADED
ER->>EB: Subscribe to GRID_RENDERED
Note over ER: Needs BOTH events before rendering
Note over Index: PHASE 3: Layout Managers (Order Critical!)
Index->>SM: new ScrollManager()
SM->>EB: Subscribe to GRID_RENDERED
Note over SM: Must subscribe BEFORE GridManager renders
Index->>GM: new GridManager()
GM->>EB: Subscribe to CALENDAR_INITIALIZED
GM->>EB: Subscribe to CALENDAR_DATA_LOADED
GM->>GM: Set currentWeek = getWeekStart(new Date())
Note over GM: Ready to render, but waiting
Note over Index: PHASE 4: Coordinated Initialization
Index->>CM: initialize()
CM->>EB: emit(CALENDAR_INITIALIZING)
CM->>CM: setView('week'), setCurrentDate()
CM->>EB: emit(CALENDAR_INITIALIZED) ⭐
Note over EB: 🚀 CALENDAR_INITIALIZED triggers all managers
par EventManager Data Loading
EB->>EM: CALENDAR_INITIALIZED
EM->>EM: loadMockData() for date mode
EM->>EM: fetch('/src/data/mock-events.json')
Note over EM: Loading date-specific mock data
EM->>EM: Process events for current week
EM->>EB: emit(CALENDAR_DATA_LOADED, {calendarType: 'date', data})
EM->>EB: emit(EVENTS_LOADED, {events: [...])
and GridManager Initial Rendering
EB->>GM: CALENDAR_INITIALIZED
GM->>GM: render()
GM->>GM: updateGridStyles() - Set --grid-columns: 7
GM->>GM: createHeaderSpacer()
GM->>GM: createTimeAxis(dayStartHour, dayEndHour)
GM->>GM: createGridContainer()
Note over GM: Strategy Pattern - Date Mode Rendering
GM->>Factory: getHeaderRenderer('date') → DateHeaderRenderer
GM->>GM: renderCalendarHeader() - Create day headers
GM->>DOM: Create 7 swp-day-column elements
GM->>Factory: getColumnRenderer('date') → DateColumnRenderer
GM->>GM: renderColumnContainer() - Date columns
GM->>EB: emit(GRID_RENDERED) ⭐
and NavigationManager UI
EB->>NM: CALENDAR_INITIALIZED
NM->>NM: updateWeekInfo()
NM->>DOM: Update week display in navigation
NM->>EB: emit(WEEK_INFO_UPDATED)
and ViewManager Setup
EB->>VM: CALENDAR_INITIALIZED
VM->>VM: initializeView()
VM->>EB: emit(VIEW_RENDERED)
end
Note over GM: GridManager receives its own data event
EB->>GM: CALENDAR_DATA_LOADED
GM->>GM: updateGridStyles() - Recalculate columns if needed
Note over GM: Grid already rendered, just update styles
Note over ER: 🎯 Critical Synchronization Point
EB->>ER: EVENTS_LOADED
ER->>ER: pendingEvents = events (store, don't render yet)
Note over ER: Waiting for grid to be ready...
EB->>ER: GRID_RENDERED
ER->>DOM: querySelectorAll('swp-day-column') - Check if ready
DOM-->>ER: Return 7 day columns (ready!)
Note over ER: Both events loaded AND grid ready → Render!
ER->>Factory: getEventRenderer('date') → DateEventRenderer
ER->>ER: renderEvents(pendingEvents) using DateEventRenderer
ER->>DOM: Position events in day columns
ER->>ER: Clear pendingEvents
ER->>EB: emit(EVENT_RENDERED)
Note over SM: ScrollManager sets up after grid is complete
EB->>SM: GRID_RENDERED
SM->>DOM: querySelector('swp-scrollable-content')
SM->>SM: setupScrolling()
SM->>SM: applyScrollbarStyling()
SM->>SM: setupScrollSynchronization()
Note over Index: 🎊 Date Mode Initialization Complete!
Note over Index: Ready for user interaction
```
## Key Initialization Phases
### Phase 0: Pre-initialization Setup
- **CalendarConfig**: Loads URL parameters (`?type=date`) and DOM attributes
- **CalendarTypeFactory**: Creates strategy pattern renderers for date mode
### Phase 1: Core Managers Construction
- **CalendarManager**: Central coordinator
- **NavigationManager**: Week navigation controls
- **ViewManager**: View state management
### Phase 2: Data & Rendering Managers
- **EventManager**: Handles data loading
- **EventRenderer**: Manages event display with synchronization
### Phase 3: Layout Managers (Order Critical!)
- **ScrollManager**: Must subscribe before GridManager renders
- **GridManager**: Main grid rendering
### Phase 4: Coordinated Initialization
- **CalendarManager.initialize()**: Triggers `CALENDAR_INITIALIZED` event
- All managers respond simultaneously but safely
## Critical Synchronization Points
### 1. Event-Grid Synchronization
```typescript
// EventRenderer waits for BOTH events
if (this.pendingEvents.length > 0) {
const columns = document.querySelectorAll('swp-day-column'); // DATE MODE
if (columns.length > 0) { // Grid must exist first
this.renderEvents(this.pendingEvents);
}
}
```
### 2. Scroll-Grid Dependency
```typescript
// ScrollManager only sets up after grid is rendered
eventBus.on(EventTypes.GRID_RENDERED, () => {
this.setupScrolling(); // Safe to access DOM now
});
```
### 3. Manager Construction Order
```typescript
// Critical order: ScrollManager subscribes BEFORE GridManager renders
const scrollManager = new ScrollManager();
const gridManager = new GridManager();
```
## Date Mode Specifics
### Data Loading
- Uses `/src/data/mock-events.json`
- Processes events for current week
- Emits `CALENDAR_DATA_LOADED` with `calendarType: 'date'`
### Grid Rendering
- Creates 7 `swp-day-column` elements (weekDays: 7)
- Uses `DateHeaderRenderer` strategy
- Uses `DateColumnRenderer` strategy
- Sets `--grid-columns: 7` CSS variable
### Event Rendering
- Uses `DateEventRenderer` strategy
- Positions events in day columns based on start/end time
- Calculates pixel positions using `PositionUtils`
## Race Condition Prevention
1. **Subscription Before Action**: All managers subscribe during construction, act on `CALENDAR_INITIALIZED`
2. **DOM Existence Checks**: Managers verify DOM elements exist before manipulation
3. **Event Ordering**: `GRID_RENDERED` always fires before event rendering attempts
4. **Pending States**: EventRenderer stores pending events until grid is ready
5. **Coordinated Start**: Single `CALENDAR_INITIALIZED` event starts all processes
## Debugging Points
Key events to monitor during initialization:
- `CALENDAR_INITIALIZED` - Start of coordinated setup
- `CALENDAR_DATA_LOADED` - Date data ready
- `GRID_RENDERED` - Grid structure complete
- `EVENTS_LOADED` - Event data ready
- `EVENT_RENDERED` - Events positioned in grid
This sequence ensures deterministic, race-condition-free initialization with comprehensive logging for debugging.

View file

@ -1,114 +0,0 @@
# Korrigeret Analyse: Day Event Drag til Header og Tilbage
## Korrekt Flow Design
### Elementer i Spil
1. **Original Event**: Skal forblive i DOM uændret indtil drag:end
2. **Clone Event**: Det visuelle element der dragges rundt
3. **All-Day Event**: Midlertidig repræsentation i header
## Nuværende Problem
### AllDayManager.handleConvertToAllDay() (linje 274)
```typescript
// PROBLEM: Fjerner original for tidligt!
originalElement.remove(); // ❌ FORKERT
```
Original element fjernes når man hover over header, men det skal først fjernes ved faktisk drop.
### Korrekt Flow
```mermaid
sequenceDiagram
participant User
participant Original as Original Event
participant Clone as Clone Event
participant AllDay as All-Day Event
Note over Original: Start: Original synlig
User->>Clone: Drag start
Note over Clone: Clone oprettes og vises
Note over Original: Original bliver semi-transparent
User->>AllDay: Enter header
Note over Clone: Clone skjules (display:none)
Note over AllDay: All-day event oprettes
Note over Original: Original forbliver (semi-transparent)
User->>Clone: Leave header (tilbage til grid)
Note over AllDay: All-day event fjernes
Note over Clone: Clone vises igen
Note over Original: Original stadig der
User->>Original: Drop
Note over Original: NU fjernes original
Note over Clone: Clone bliver til ny position
```
## Nødvendige Ændringer
### 1. AllDayManager.handleConvertToAllDay() (linje 232-285)
```typescript
// FØR (linje 274):
originalElement.remove(); // ❌
// EFTER:
// originalElement.remove(); // Kommenteret ud - skal IKKE fjernes her
// Original forbliver i DOM med opacity 0.3 fra drag start
```
### 2. AllDayManager.handleConvertFromAllDay() (linje 290-311)
```typescript
// Nuværende kode er faktisk korrekt - den:
// 1. Fjerner all-day event
// 2. Viser clone igen
// Original er stadig der (blev aldrig fjernet)
```
### 3. EventRenderer - handleDragEnd()
```typescript
// Her skal original FAKTISK fjernes
// Efter successful drop:
if (this.originalEvent) {
this.originalEvent.remove(); // ✅ Korrekt tidspunkt
}
```
## Problem Opsummering
**Hovedproblem**: `originalElement.remove()` på linje 274 i AllDayManager sker for tidligt.
**Løsning**:
1. Fjern/kommenter linje 274 i AllDayManager.handleConvertToAllDay()
2. Original element skal kun fjernes i EventRenderer ved faktisk drop (drag:end)
3. Clone håndterer al visuel feedback under drag
## Test Scenarios
1. **Drag → Header → Grid → Drop**
- Original forbliver hele tiden
- Clone skjules/vises korrekt
- Original fjernes kun ved drop
2. **Drag → Header → Drop i Header**
- Original forbliver indtil drop
- All-day event bliver permanent
- Original fjernes ved drop
3. **Drag → Header → ESC**
- Original forbliver
- Drag cancelled, alt tilbage til start
## Affected Code
### AllDayManager.ts
- **Linje 274**: Skal fjernes/kommenteres
- Original element må IKKE fjernes her
### EventRenderer.ts
- **handleDragEnd()**: Skal sikre original fjernes her
- Dette er det ENESTE sted original må fjernes
### DragDropManager.ts
- Koden ser faktisk korrekt ud
- Holder styr på både original og clone

View file

@ -1,104 +0,0 @@
# Bug Analyse: Day Event Drag til Header og Tilbage
## Problem Beskrivelse
Når en day event dragges op til headeren (for at konvertere til all-day) og derefter dragges tilbage til grid UDEN at droppe, opstår der et kritisk problem hvor original event forsvinder.
## Flow Analyse
### Trin 1: Drag Start (Day Event)
- **DragDropManager** (linje 139-182): `handleMouseDown()` registrerer drag start
- Original event element gemmes
- `isDragStarted = false` indtil movement threshold nås
### Trin 2: Drag bevæger sig op mod Header
- **DragDropManager** (linje 187-253): `handleMouseMove()` tracker bevægelse
- Når threshold nås: `drag:start` event emitted
- **EventRenderer** opretter drag clone
### Trin 3: Mouse enters Header ⚠️ PROBLEM STARTER HER
- **DragDropManager** (linje 95-112): Lytter til `header:mouseover`
- Emitter `drag:convert-to-allday_event` event
- **AllDayManager** (linje 232-285): `handleConvertToAllDay()`:
- Opretter all-day event i header
- **FJERNER original timed event permanent** (linje 274: `originalElement.remove()`)
- Skjuler drag clone
### Trin 4: Mouse leaves Header (tilbage til grid) ⚠️ PROBLEM FORTSÆTTER
- **DragDropManager** (linje 128-136): Lytter til `header:mouseleave`
- Emitter `drag:convert-to-time_event` event
- **AllDayManager** (linje 290-311): `handleConvertFromAllDay()`:
- Fjerner all-day event fra container
- Viser drag clone igen
- **MEN: Original event er allerede fjernet og kan ikke genskabes!**
### Trin 5: Drop i Grid ⚠️ DATA TABT
- **DragDropManager** (linje 258-291): `handleMouseUp()`
- Emitter `drag:end` event
- Original element eksisterer ikke længere
- Kun clone eksisterer med ID "clone-{id}"
## Root Cause
**AllDayManager.handleConvertToAllDay()** fjerner permanent det originale element (linje 274) i stedet for at skjule det midlertidigt.
## Konsekvenser
1. Original event data går tabt
2. Event ID bliver "clone-{id}" i stedet for "{id}"
3. Event metadata kan mangle
4. Potentielle styling/positioning problemer
## Løsningsforslag
### Option 1: Behold Original Element (Anbefalet)
```typescript
// AllDayManager.handleConvertToAllDay() - linje 274
// FØR: originalElement.remove();
// EFTER:
originalElement.style.display = 'none';
originalElement.dataset.temporarilyHidden = 'true';
```
### Option 2: Gem Original Data
```typescript
// Gem original element data før fjernelse
const originalData = {
id: originalElement.dataset.eventId,
title: originalElement.dataset.title,
start: originalElement.dataset.start,
end: originalElement.dataset.end,
// ... andre properties
};
// Gem i DragDropManager eller AllDayManager
```
### Option 3: Re-create Original on Leave
```typescript
// AllDayManager.handleConvertFromAllDay()
// Genskab original element fra all-day event data
const originalElement = this.recreateTimedEvent(allDayEvent);
```
## Implementeringsplan
1. **Modificer AllDayManager.handleConvertToAllDay()**
- Skjul original element i stedet for at fjerne
- Marker element som midlertidigt skjult
2. **Modificer AllDayManager.handleConvertFromAllDay()**
- Find original element (ikke kun clone)
- Vis original element igen
- Synkroniser position med clone
3. **Opdater DragDropManager**
- Hold reference til både original og clone
- Ved drop: opdater original, fjern clone
4. **Test Scenarios**
- Drag day event → header → tilbage → drop
- Drag day event → header → drop i header
- Drag day event → header → ESC key
- Multiple hurtige hover over header
## Affected Files
- `src/managers/AllDayManager.ts` (linje 274, 290-311)
- `src/managers/DragDropManager.ts` (linje 95-136)
- `src/renderers/EventRenderer.ts` (potentielt)

View file

@ -1,123 +0,0 @@
# Komplet Bug Analyse: Drag-Drop Header Problemer
## Identificerede Problemer
### Problem 1: Original Element Fjernes For Tidligt
**Lokation**: AllDayManager.handleConvertToAllDay() linje 274
```typescript
originalElement.remove(); // ❌ FORKERT - skal ikke fjernes her
```
### Problem 2: Duplicate All-Day Events ved Mouseover
**Lokation**: AllDayManager.handleConvertToAllDay() linje 232-285
Hver gang `header:mouseover` fyrer (hvilket kan ske mange gange under drag), oprettes et NYT all-day event uden at checke om det allerede eksisterer.
## Event Flow Problem
```mermaid
sequenceDiagram
participant Mouse
participant Header
participant AllDay as AllDayManager
Note over Mouse: Dragger over header
loop Hver mouseover event
Mouse->>Header: mouseover
Header->>AllDay: drag:convert-to-allday_event
AllDay->>AllDay: Opretter NYT all-day event ❌
Note over AllDay: Ingen check for eksisterende!
end
Note over AllDay: Resultat: Multiple all-day events!
```
## Løsning
### Fix 1: Check for Eksisterende All-Day Event
```typescript
// AllDayManager.handleConvertToAllDay()
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
const eventId = originalElement.dataset.eventId;
// CHECK: Eksisterer all-day event allerede?
const existingAllDay = document.querySelector(
`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`
);
if (existingAllDay) {
console.log('All-day event already exists, skipping creation');
return; // Exit early - don't create duplicate
}
// Fortsæt med at oprette all-day event...
}
```
### Fix 2: Fjern IKKE Original Element
```typescript
// Linje 274 - skal fjernes eller kommenteres ud:
// originalElement.remove(); // <-- FJERN DENNE LINJE
```
### Fix 3: Track Conversion State (Optional)
For at undgå gentagne conversions, kunne vi tracke state:
```typescript
// AllDayManager
private convertedEventIds = new Set<string>();
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
const eventId = originalElement.dataset.eventId;
// Check if already converted
if (this.convertedEventIds.has(eventId)) {
return;
}
// Mark as converted
this.convertedEventIds.add(eventId);
// Create all-day event...
}
private handleConvertFromAllDay(draggedEventId: string): void {
// Clear conversion state
this.convertedEventIds.delete(draggedEventId);
// Remove all-day event...
}
```
## Komplet Fix Checklist
1. **AllDayManager.handleConvertToAllDay()**
- [ ] Tilføj check for eksisterende all-day event
- [ ] Fjern/kommenter `originalElement.remove()` linje 274
- [ ] Log når duplicate undgås
2. **AllDayManager.handleConvertFromAllDay()**
- [ ] Verificer at all-day event faktisk fjernes
- [ ] Clear evt. conversion state
3. **Test Scenarios**
- [ ] Hurtig mouseover frem og tilbage over header
- [ ] Langsom drag over header
- [ ] Drag ind i header → ud → ind igen
- [ ] Multiple events dragges samtidig (hvis muligt)
## Root Causes
1. **Manglende idempotency**: handleConvertToAllDay() er ikke idempotent
2. **Forkert element lifecycle**: Original fjernes for tidligt
3. **Manglende state tracking**: Ingen tracking af hvilke events der er konverteret
## Performance Overvejelser
HeaderManager throttler mouseover events (linje 41-49), men det er ikke nok når musen bevæger sig langsomt over header. Vi skal have idempotent logik.
## Implementeringsrækkefølge
1. **Først**: Fix duplicate all-day events (check for existing)
2. **Derefter**: Fjern originalElement.remove()
3. **Test**: Verificer at begge problemer er løst
4. **Optional**: Implementer conversion state tracking for bedre performance

View file

@ -1,143 +0,0 @@
# Implementeringsdetaljer: Check for Eksisterende All-Day Event
## Hvor skal checket implementeres?
**Fil**: `src/managers/AllDayManager.ts`
**Metode**: `handleConvertToAllDay()`
**Linje**: Lige efter linje 232 (start af metoden)
## Præcis Implementation
```typescript
// AllDayManager.ts - handleConvertToAllDay metode (linje 232)
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
// Extract event data from original element
const eventId = originalElement.dataset.eventId;
const title = originalElement.dataset.title || originalElement.textContent || 'Untitled';
const type = originalElement.dataset.type || 'work';
const startStr = originalElement.dataset.start;
const endStr = originalElement.dataset.end;
if (!eventId || !startStr || !endStr) {
console.error('Original element missing required data (eventId, start, end)');
return;
}
// ===== NY KODE STARTER HER (efter linje 243) =====
// CHECK 1: Er dette faktisk clone elementet?
const isClone = eventId.startsWith('clone-');
const actualEventId = isClone ? eventId.replace('clone-', '') : eventId;
// CHECK 2: Eksisterer all-day event allerede?
const container = this.getAllDayContainer();
if (container) {
const existingAllDay = container.querySelector(
`swp-allday-event[data-event-id="${actualEventId}"]`
);
if (existingAllDay) {
console.log(`All-day event for ${actualEventId} already exists, skipping creation`);
return; // Exit early - don't create duplicate
}
}
// ===== NY KODE SLUTTER HER =====
// Fortsæt med normal oprettelse...
// Create CalendarEvent for all-day conversion - preserve original times
const originalStart = new Date(startStr);
// ... resten af koden
}
```
## Hvorfor virker dette?
1. **Check for clone ID**: Vi checker om det draggede element er clone (starter med "clone-")
2. **Normalisér ID**: Vi får det faktiske event ID uden "clone-" prefix
3. **Query DOM**: Vi søger i all-day container efter eksisterende element med samme ID
4. **Early exit**: Hvis det findes, logger vi og returnerer uden at oprette duplicate
## Alternative Implementeringer
### Option A: Track med Set (Hurtigere)
```typescript
// AllDayManager class property
private activeAllDayEvents = new Set<string>();
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
const eventId = originalElement.dataset.eventId;
const actualEventId = eventId?.startsWith('clone-')
? eventId.replace('clone-', '')
: eventId;
// Check i memory først (hurtigere)
if (this.activeAllDayEvents.has(actualEventId)) {
return;
}
// Marker som aktiv
this.activeAllDayEvents.add(actualEventId);
// ... opret all-day event
}
private handleConvertFromAllDay(draggedEventId: string): void {
// Fjern fra tracking
this.activeAllDayEvents.delete(draggedEventId);
// ... fjern all-day event
}
```
### Option B: Data Attribute Flag
```typescript
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
// Check flag på original element
if (originalElement.dataset.hasAllDayVersion === 'true') {
return;
}
// Sæt flag
originalElement.dataset.hasAllDayVersion = 'true';
// ... opret all-day event
}
private handleConvertFromAllDay(draggedEventId: string): void {
// Find original og fjern flag
const original = document.querySelector(`[data-event-id="${draggedEventId}"]`);
if (original) {
delete original.dataset.hasAllDayVersion;
}
// ... fjern all-day event
}
```
## Anbefalet Løsning
**DOM Query Check** (første løsning) er mest robust fordi:
1. Den checker faktisk DOM state
2. Ingen ekstra state at vedligeholde
3. Fungerer selv hvis events kommer ud af sync
## Test Verification
For at teste om det virker:
1. Åbn browser console
2. Drag et event langsomt over header
3. Se efter "already exists" log messages
4. Verificer kun ÉT all-day event i DOM med:
```javascript
document.querySelectorAll('swp-allday-event').length
```
## Linje 274 Fix
Samtidig skal vi fjerne/kommentere linje 274:
```typescript
// originalElement.remove(); // <-- KOMMENTER DENNE UD
```
Dette sikrer original element ikke fjernes for tidligt.

View file

@ -1,270 +0,0 @@
# Improved Calendar Initialization Strategy
## Current Problems
1. **Race Conditions**: Managers try DOM operations before DOM is ready
2. **Sequential Blocking**: All initialization happens sequentially
3. **Poor Error Handling**: No timeouts or retry mechanisms
4. **Late Data Loading**: Data only loads after all managers are created
## Recommended New Architecture
### Phase 1: Early Parallel Startup
```typescript
// index.ts - Improved initialization
export class CalendarInitializer {
async initialize(): Promise<void> {
console.log('📋 Starting Calendar initialization...');
// PHASE 1: Early parallel setup
const setupPromises = [
this.initializeConfig(), // Load URL params, DOM attrs
this.initializeFactory(), // Setup strategy patterns
this.preloadCalendarData(), // Start data loading early
this.waitForDOMReady() // Ensure basic DOM exists
];
await Promise.all(setupPromises);
console.log('✅ Phase 1 complete: Config, Factory, Data preloading started');
// PHASE 2: Manager creation with dependencies
await this.createManagersWithDependencies();
// PHASE 3: Coordinated activation
await this.activateAllManagers();
console.log('🎊 Calendar fully initialized!');
}
}
```
### Phase 2: Dependency-Aware Manager Creation
```typescript
private async createManagersWithDependencies(): Promise<void> {
const managers = new Map<string, any>();
// Core managers (no DOM dependencies)
managers.set('config', calendarConfig);
managers.set('eventBus', eventBus);
managers.set('calendarManager', new CalendarManager(eventBus, calendarConfig));
// DOM-dependent managers (wait for DOM readiness)
await this.waitForRequiredDOM(['swp-calendar', 'swp-calendar-nav']);
managers.set('navigationManager', new NavigationManager(eventBus));
managers.set('viewManager', new ViewManager(eventBus));
// Data managers (can work with preloaded data)
managers.set('eventManager', new EventManager(eventBus));
managers.set('dataManager', new DataManager());
// Layout managers (need DOM structure + other managers)
await this.waitForRequiredDOM(['swp-calendar-container']);
// CRITICAL ORDER: ScrollManager subscribes before GridManager renders
managers.set('scrollManager', new ScrollManager());
managers.set('gridManager', new GridManager());
// Rendering managers (need grid structure)
managers.set('eventRenderer', new EventRenderer(eventBus));
this.managers = managers;
}
```
### Phase 3: Coordinated Activation
```typescript
private async activateAllManagers(): Promise<void> {
// All managers created and subscribed, now activate in coordinated fashion
const calendarManager = this.managers.get('calendarManager');
// This triggers CALENDAR_INITIALIZED, but now all managers are ready
await calendarManager.initialize();
// Wait for critical initialization events
await Promise.all([
this.waitForEvent('CALENDAR_DATA_LOADED', 10000),
this.waitForEvent('GRID_RENDERED', 5000),
this.waitForEvent('EVENTS_LOADED', 10000)
]);
// Ensure event rendering completes
await this.waitForEvent('EVENT_RENDERED', 3000);
}
```
## Specific Timing Improvements
### 1. Early Data Preloading
```typescript
private async preloadCalendarData(): Promise<void> {
const currentDate = new Date();
const mode = calendarConfig.getCalendarMode();
// Start loading data for current period immediately
const dataManager = new DataManager();
const currentPeriod = this.getCurrentPeriod(currentDate, mode);
// Don't await - let this run in background
const dataPromise = dataManager.fetchEventsForPeriod(currentPeriod);
// Also preload adjacent periods
const prevPeriod = this.getPreviousPeriod(currentDate, mode);
const nextPeriod = this.getNextPeriod(currentDate, mode);
// Store promises for later use
this.preloadPromises = {
current: dataPromise,
previous: dataManager.fetchEventsForPeriod(prevPeriod),
next: dataManager.fetchEventsForPeriod(nextPeriod)
};
console.log('📊 Data preloading started for current, previous, and next periods');
}
```
### 2. DOM Readiness Verification
```typescript
private async waitForRequiredDOM(selectors: string[]): Promise<void> {
const maxWait = 5000; // 5 seconds max
const checkInterval = 100; // Check every 100ms
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
const missing = selectors.filter(selector => !document.querySelector(selector));
if (missing.length === 0) {
console.log(`✅ Required DOM elements found: ${selectors.join(', ')}`);
return;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
throw new Error(`❌ Timeout waiting for DOM elements: ${selectors.join(', ')}`);
}
```
### 3. Manager Base Class with Proper Lifecycle
```typescript
export abstract class BaseManager {
protected isInitialized = false;
protected requiredDOMSelectors: string[] = [];
constructor() {
// Don't call init() immediately in constructor!
console.log(`${this.constructor.name}: Created but not initialized`);
}
async initialize(): Promise<void> {
if (this.isInitialized) {
console.log(`${this.constructor.name}: Already initialized, skipping`);
return;
}
// Wait for required DOM elements
if (this.requiredDOMSelectors.length > 0) {
await this.waitForDOM(this.requiredDOMSelectors);
}
// Perform manager-specific initialization
await this.performInitialization();
this.isInitialized = true;
console.log(`${this.constructor.name}: Initialization complete`);
}
protected abstract performInitialization(): Promise<void>;
private async waitForDOM(selectors: string[]): Promise<void> {
// Same DOM waiting logic as above
}
}
```
### 4. Enhanced GridManager
```typescript
export class GridManager extends BaseManager {
protected requiredDOMSelectors = ['swp-calendar-container'];
constructor() {
super(); // Don't call this.init()!
this.currentWeek = this.getWeekStart(new Date());
}
protected async performInitialization(): Promise<void> {
// Now safe to find elements - DOM guaranteed to exist
this.findElements();
this.subscribeToEvents();
// Wait for CALENDAR_INITIALIZED before rendering
await this.waitForEvent('CALENDAR_INITIALIZED');
console.log('GridManager: Starting initial render');
this.render();
}
}
```
### 5. Enhanced EventRenderer with Better Synchronization
```typescript
export class EventRenderer extends BaseManager {
private dataReady = false;
private gridReady = false;
private pendingEvents: CalendarEvent[] = [];
protected async performInitialization(): Promise<void> {
this.subscribeToEvents();
// Wait for both data and grid in parallel
const [eventsData] = await Promise.all([
this.waitForEvent('EVENTS_LOADED'),
this.waitForEvent('GRID_RENDERED')
]);
console.log('EventRenderer: Both events and grid ready, rendering now');
this.renderEvents(eventsData.events);
}
private subscribeToEvents(): void {
this.eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => {
const detail = (e as CustomEvent).detail;
this.pendingEvents = detail.events;
this.dataReady = true;
this.tryRender();
});
this.eventBus.on(EventTypes.GRID_RENDERED, () => {
this.gridReady = true;
this.tryRender();
});
}
private tryRender(): void {
if (this.dataReady && this.gridReady && this.pendingEvents.length > 0) {
this.renderEvents(this.pendingEvents);
this.pendingEvents = [];
}
}
}
```
## Benefits of New Architecture
1. **🚀 Parallel Operations**: Data loading starts immediately while managers are being created
2. **🛡️ Race Condition Prevention**: DOM readiness verified before operations
3. **⚡ Better Performance**: Critical path optimized, non-critical operations parallelized
4. **🔧 Better Error Handling**: Timeouts and retry mechanisms
5. **📊 Predictable Timing**: Clear phases with guaranteed completion order
6. **🐛 Easier Debugging**: Clear lifecycle events and logging
## Implementation Strategy
1. **Phase 1**: Create BaseManager class and update existing managers
2. **Phase 2**: Implement CalendarInitializer with parallel setup
3. **Phase 3**: Add DOM readiness verification throughout
4. **Phase 4**: Implement data preloading strategy
5. **Phase 5**: Add comprehensive error handling and timeouts
This architecture ensures reliable, fast, and maintainable calendar initialization.

View file

@ -1,216 +0,0 @@
# TimeFormatter Specification
## Problem
- Alle events i systemet/mock JSON er i Zulu tid (UTC)
- Nuværende formatTime() metoder håndterer ikke timezone konvertering
- Ingen support for 12/24 timers format baseret på configuration
- Duplikeret formattering logik flere steder
## Løsning: Centraliseret TimeFormatter
### Requirements
1. **Timezone Support**
- Konverter fra UTC/Zulu til brugerens lokale timezone
- Respekter browser timezone settings
- Håndter sommertid korrekt
2. **12/24 Timer Format**
- Læs format præference fra CalendarConfig
- Support både 12-timer (AM/PM) og 24-timer format
- Gør det konfigurerbart per bruger
3. **Centralisering**
- Én enkelt kilde til al tidsformattering
- Konsistent formattering gennem hele applikationen
- Nem at teste og vedligeholde
### Proposed Implementation
```typescript
// src/utils/TimeFormatter.ts
export class TimeFormatter {
private static use24HourFormat: boolean = false;
private static userTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
/**
* Initialize formatter with user preferences
*/
static initialize(config: { use24Hour: boolean; timezone?: string }) {
this.use24HourFormat = config.use24Hour;
if (config.timezone) {
this.userTimezone = config.timezone;
}
}
/**
* Format UTC/Zulu time to local time with correct format
* @param input - UTC Date, ISO string, or minutes from midnight
* @returns Formatted time string in user's preferred format
*/
static formatTime(input: Date | string | number): string {
let date: Date;
if (typeof input === 'number') {
// Minutes from midnight - create date for today
const today = new Date();
today.setHours(0, 0, 0, 0);
today.setMinutes(input);
date = today;
} else if (typeof input === 'string') {
// ISO string - parse as UTC
date = new Date(input);
} else {
date = input;
}
// Convert to local timezone
const localDate = this.convertToLocalTime(date);
// Format based on user preference
if (this.use24HourFormat) {
return this.format24Hour(localDate);
} else {
return this.format12Hour(localDate);
}
}
/**
* Convert UTC date to local timezone
*/
private static convertToLocalTime(utcDate: Date): Date {
// Use Intl.DateTimeFormat for proper timezone conversion
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: this.userTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const parts = formatter.formatToParts(utcDate);
const dateParts: any = {};
parts.forEach(part => {
dateParts[part.type] = part.value;
});
return new Date(
parseInt(dateParts.year),
parseInt(dateParts.month) - 1,
parseInt(dateParts.day),
parseInt(dateParts.hour),
parseInt(dateParts.minute),
parseInt(dateParts.second)
);
}
/**
* Format time in 24-hour format (HH:mm)
*/
private static format24Hour(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Format time in 12-hour format (h:mm AM/PM)
*/
private static format12Hour(date: Date): string {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
}
/**
* Format date and time together
*/
static formatDateTime(date: Date | string): string {
const localDate = typeof date === 'string' ? new Date(date) : date;
const convertedDate = this.convertToLocalTime(localDate);
const dateStr = convertedDate.toLocaleDateString();
const timeStr = this.formatTime(convertedDate);
return `${dateStr} ${timeStr}`;
}
/**
* Get timezone offset in hours
*/
static getTimezoneOffset(): number {
return new Date().getTimezoneOffset() / -60;
}
}
```
### Configuration Integration
```typescript
// src/core/CalendarConfig.ts
export interface TimeFormatSettings {
use24HourFormat: boolean;
timezone?: string; // Optional override, defaults to browser timezone
}
// Add to CalendarConfig
getTimeFormatSettings(): TimeFormatSettings {
return {
use24HourFormat: this.config.use24HourFormat ?? false,
timezone: this.config.timezone // undefined = use browser default
};
}
```
### Usage Examples
```typescript
// Initialize on app start
TimeFormatter.initialize({
use24Hour: calendarConfig.getTimeFormatSettings().use24HourFormat,
timezone: calendarConfig.getTimeFormatSettings().timezone
});
// Format UTC event time to local
const utcEventTime = "2024-01-15T14:30:00Z"; // 2:30 PM UTC
const localTime = TimeFormatter.formatTime(utcEventTime);
// Result (Copenhagen, 24h): "15:30"
// Result (Copenhagen, 12h): "3:30 PM"
// Result (New York, 12h): "9:30 AM"
// Format minutes from midnight
const minutes = 570; // 9:30 AM
const formatted = TimeFormatter.formatTime(minutes);
// Result (24h): "09:30"
// Result (12h): "9:30 AM"
```
### Testing Considerations
1. Test timezone conversions:
- UTC to Copenhagen (UTC+1/+2)
- UTC to New York (UTC-5/-4)
- UTC to Tokyo (UTC+9)
2. Test daylight saving transitions
3. Test 12/24 hour format switching
4. Test edge cases:
- Midnight (00:00 / 12:00 AM)
- Noon (12:00 / 12:00 PM)
- Events spanning multiple days
### Migration Plan
1. Implement TimeFormatter class
2. Add configuration options to CalendarConfig
3. Replace all existing formatTime() calls
4. Update mock data loader to handle UTC properly
5. Test thoroughly with different timezones

View file

@ -1,245 +0,0 @@
# TypeScript Code Review - Calendar Plantempus
**Dato:** September 2025
**Reviewer:** Roo
**Fokus:** Dybdegående analyse efter TimeFormatter implementation
## Executive Summary
Efter implementering af TimeFormatter og gennemgang af codebasen, har jeg identificeret både styrker og forbedringspotentiale. Koden viser god separation of concerns og event-driven arkitektur, men har stadig områder der kan optimeres.
## 🟢 Styrker
### 1. Event-Driven Architecture
- **Konsistent EventBus pattern** gennem hele applikationen
- Ingen direkte dependencies mellem moduler
- God brug af custom events for kommunikation
### 2. Separation of Concerns
- **Managers**: Håndterer business logic (AllDayManager, DragDropManager, etc.)
- **Renderers**: Fokuserer på DOM manipulation
- **Utils**: Isolerede utility funktioner
- **Elements**: Factory pattern for DOM element creation
### 3. Performance Optimering
- **DOM Caching**: Konsistent caching af DOM elementer
- **Throttling**: Event throttling i HeaderManager (16ms delay)
- **Pixel-based calculations**: Fjernet komplekse time-based overlap beregninger
### 4. TypeScript Best Practices
- Stærk typing med interfaces
- Proper null/undefined checks
- Readonly constants hvor relevant
## 🔴 Kritiske Issues
### 1. "new_" Prefix Methods (EventRenderer.ts)
```typescript
// PROBLEM: Midlertidige metode navne
protected new_handleEventOverlaps()
protected new_renderOverlappingEvents()
protected new_applyStackStyling()
protected new_applyColumnSharingStyling()
```
**Impact:** Forvirrende navngivning, indikerer ufærdig refactoring
**Løsning:** Fjern prefix og ryd op i gamle metoder
### 2. Duplikeret Cache Logic
```typescript
// AllDayManager.ts
private cachedAllDayContainer: HTMLElement | null = null;
private cachedCalendarHeader: HTMLElement | null = null;
// HeaderManager.ts
private cachedCalendarHeader: HTMLElement | null = null;
// DragDropManager.ts
private cachedElements: CachedElements = {...}
```
**Impact:** 30+ linjer duplikeret kode
**Løsning:** Opret generisk DOMCacheManager
### 3. Manglende Error Boundaries
```typescript
// SimpleEventOverlapManager.ts
const linkData = element.dataset.stackLink;
try {
return JSON.parse(linkData);
} catch (e) {
console.warn('Failed to parse stack link data:', linkData, e);
return null;
}
```
**Impact:** Silently failing JSON parsing
**Løsning:** Proper error handling med user feedback
## 🟡 Code Smells & Improvements
### 1. Magic Numbers
```typescript
// SimpleEventOverlapManager.ts
const startDifference = Math.abs(top1 - top2);
if (startDifference > 40) { // Magic number!
return OverlapType.STACKING;
}
// DragDropManager.ts
private readonly dragThreshold = 5; // Should be configurable
private readonly scrollSpeed = 10;
private readonly scrollThreshold = 30;
```
**Løsning:** Flyt til configuration constants
### 2. Complex Method Signatures
```typescript
// AllDayManager.ts - 73 linjer!
public checkAndAnimateAllDayHeight(): void {
// Massive method doing too much
}
```
**Løsning:** Split i mindre, fokuserede metoder
### 3. Inconsistent Naming
```typescript
// Mix af naming conventions
getCalendarHeader() // get prefix
findElements() // no prefix
detectColumn() // action verb
cachedElements // noun
```
**Løsning:** Standardiser naming convention
### 4. Memory Leaks Risk
```typescript
// DragDropManager.ts
private boundHandlers = {
mouseMove: this.handleMouseMove.bind(this),
mouseDown: this.handleMouseDown.bind(this),
mouseUp: this.handleMouseUp.bind(this)
};
```
**God praksis!** Men ikke konsistent anvendt alle steder
## 📊 Metrics & Analysis
### Complexity Analysis
| File | Lines | Cyclomatic Complexity | Maintainability |
|------|-------|----------------------|-----------------|
| AllDayManager.ts | 281 | Medium (8) | Good |
| DragDropManager.ts | 521 | High (15) | Needs refactoring |
| SimpleEventOverlapManager.ts | 473 | Very High (20) | Critical |
| HeaderManager.ts | 119 | Low (4) | Excellent |
| GridManager.ts | 348 | Medium (10) | Good |
### Code Duplication
- **Cache management**: ~15% duplication
- **Event handling**: ~10% duplication
- **Position calculations**: ~8% duplication
## 🎯 Prioriterede Forbedringer
### Priority 1: Critical Fixes
1. **Fjern "new_" prefix** fra EventRenderer metoder
2. **Fix TimeFormatter timezone** - Håndter mock data korrekt som UTC
3. **Implementer DOMCacheManager** - Reducer duplication
### Priority 2: Architecture Improvements
1. **GridPositionCalculator** - Centralisér position beregninger
2. **EventThrottler** - Generisk throttling utility
3. **AllDayRowCalculator** - Udtræk kompleks logik fra AllDayManager
### Priority 3: Code Quality
1. **Reduce method complexity** - Split store metoder
2. **Standardize naming** - Konsistent naming convention
3. **Add JSDoc** - Mangler på mange public methods
### Priority 4: Testing
1. **Unit tests** for TimeFormatter
2. **Integration tests** for overlap detection
3. **Performance tests** for large event sets
## 💡 Architectural Recommendations
### 1. Introduce Service Layer
```typescript
// Forslag: EventService
class EventService {
private formatter: TimeFormatter;
private calculator: GridPositionCalculator;
private overlapManager: SimpleEventOverlapManager;
// Centralized event operations
}
```
### 2. Configuration Management
```typescript
interface CalendarConstants {
DRAG_THRESHOLD: number;
SCROLL_SPEED: number;
STACK_OFFSET: number;
OVERLAP_THRESHOLD: number;
}
```
### 3. Error Handling Strategy
```typescript
class CalendarError extends Error {
constructor(
message: string,
public code: string,
public recoverable: boolean
) {
super(message);
}
}
```
## 🚀 Performance Optimizations
### 1. Virtual Scrolling
For måneds-view med mange events, overvej virtual scrolling
### 2. Web Workers
Flyt tunge beregninger (overlap detection) til Web Worker
### 3. RequestIdleCallback
Brug for non-critical updates som analytics
## ✅ Positive Highlights
1. **TimeFormatter Implementation**: Elegant og clean
2. **Event-driven Architecture**: Konsistent og velfungerende
3. **TypeScript Usage**: God type safety
4. **DOM Manipulation**: Effektiv med custom elements
5. **Separation of Concerns**: Klar opdeling af ansvar
## 📋 Recommended Action Plan
### Immediate (1-2 dage)
- [ ] Fjern "new_" prefix fra EventRenderer
- [ ] Implementer DOMCacheManager
- [ ] Fix magic numbers
### Short-term (3-5 dage)
- [ ] Opret GridPositionCalculator
- [ ] Implementer EventThrottler
- [ ] Refactor SimpleEventOverlapManager complexity
### Long-term (1-2 uger)
- [ ] Add comprehensive unit tests
- [ ] Implement service layer
- [ ] Performance optimizations
## Konklusion
Koden er generelt velstruktureret med god separation of concerns og konsistent event-driven arkitektur. TimeFormatter implementationen er elegant og løser timezone problemet godt.
Hovedudfordringerne ligger i:
1. Ufærdig refactoring (new_ prefix)
2. Duplikeret cache logic
3. Høj complexity i overlap detection
4. Manglende tests
Med de foreslåede forbedringer vil kodebasen blive mere maintainable, performant og robust.
**Overall Score: 7.5/10** - God kvalitet med plads til forbedring

View file

@ -1,173 +0,0 @@
# Event Overlap Rendering Implementation Plan - COMPLETED ✅
## Status: IMPLEMENTATION COMPLETED
This implementation plan has been **successfully completed** using `SimpleEventOverlapManager`. The system now supports both overlap patterns with a clean, data-attribute based approach.
## Current Implementation
The system uses `SimpleEventOverlapManager` which provides:
1. **Column Sharing**: Events with similar start times share width using flexbox
2. **Stacking**: Events with >30 min difference stack with margin-left offsets
3. **Data-Attribute Tracking**: Uses `data-stack-link` for chain management
4. **Zero State Sync Issues**: DOM is the single source of truth
## Oversigt (Original Requirements - COMPLETED)
**Column Sharing**: Events med samme start tid deles om bredden med flexbox
**Stacking**: Events med >30 min forskel ligger oven på med reduceret bredde
## Test Scenarier (fra mock-events.json)
### September 2 - Stacking Test
- Event 93: "Team Standup" 09:00-09:30
- Event 94: "Product Planning" 14:00-16:00
- Event 96: "Deep Work" 15:00-15:30 (>30 min efter standup, skal være 15px mindre)
### September 4 - Column Sharing Test
- Event 97: "Team Standup" 09:00-09:30
- Event 98: "Technical Review" 15:00-16:30
- Event 100: "Sprint Review" 15:00-16:00 (samme start tid som Technical Review - skal deles 50/50)
## Teknisk Arkitektur
### 1. SimpleEventOverlapManager Klasse ✅ IMPLEMENTED
```typescript
class SimpleEventOverlapManager {
detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType
groupOverlappingEvents(events: CalendarEvent[]): OverlapGroup[]
createEventGroup(events: CalendarEvent[], position: {top: number, height: number}): HTMLElement
addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void
removeFromEventGroup(container: HTMLElement, eventId: string): boolean
createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement, stackLevel: number): void
// Data-attribute based stack tracking
getStackLink(element: HTMLElement): StackLink | null
isStackedEvent(element: HTMLElement): boolean
}
```
### 2. CSS Struktur
```css
.event-group {
position: absolute;
display: flex;
gap: 1px;
width: 100%;
}
.event-group swp-event {
flex: 1;
position: relative;
}
.stacked-event {
position: absolute;
width: calc(100% - 15px);
right: 0;
z-index: var(--z-stacked-event);
}
```
### 3. DOM Struktur
```html
<!-- Normal event -->
<swp-event>Single Event</swp-event>
<!-- Column sharing group -->
<div class="event-group" style="position: absolute; top: 200px;">
<swp-event>Event 1</swp-event>
<swp-event>Event 2</swp-event>
</div>
<!-- Stacked event -->
<swp-event class="stacked-event" style="top: 210px;">Stacked Event</swp-event>
```
## Implementation Status ✅ ALL PHASES COMPLETED
### Phase 1: Core Infrastructure ✅ COMPLETED
1. ✅ Oprettet SimpleEventOverlapManager klasse
2. ✅ Implementeret overlap detection algoritme med proper time overlap checking
3. ✅ Tilføjet CSS klasser for event-group og stacked-event
### Phase 2: Column Sharing (Flexbox) ✅ COMPLETED
4. ✅ Implementeret createEventGroup metode med flexbox
5. ✅ Implementeret addToEventGroup og removeFromEventGroup
6. ✅ Integreret i BaseEventRenderer.renderEvent
### Phase 3: Stacking Logic ✅ COMPLETED
7. ✅ Implementeret stacking detection (>30 min forskel)
8. ✅ Implementeret createStackedEvent med margin-left offset
9. ✅ Tilføjet z-index management via data-attributes
### Phase 4: Drag & Drop Integration ✅ COMPLETED
10. ✅ Modificeret drag & drop handleDragEnd til overlap detection
11. ✅ Implementeret event repositioning ved drop på eksisterende events
12. ✅ Tilføjet cleanup logik for tomme event-group containers
### Phase 5: Testing & Optimization ✅ COMPLETED
13. ✅ Testet column sharing med events med samme start tid
14. ✅ Testet stacking med events med >30 min forskel
15. ✅ Testet kombinerede scenarier
16. ✅ Performance optimering og cleanup gennemført
## Algoritmer
### Overlap Detection
```typescript
function detectOverlap(events: CalendarEvent[]): OverlapType {
const timeDiff = Math.abs(event1.startTime - event2.startTime);
if (timeDiff === 0) return 'COLUMN_SHARING';
if (timeDiff > 30 * 60 * 1000) return 'STACKING';
return 'NORMAL';
}
```
### Column Sharing Calculation
```typescript
function calculateColumnSharing(events: CalendarEvent[]) {
const eventCount = events.length;
// Flexbox håndterer automatisk: flex: 1 på hver event
return { width: `${100 / eventCount}%`, flex: 1 };
}
```
### Stacking Calculation
```typescript
function calculateStacking(underlyingEvent: HTMLElement) {
const underlyingWidth = underlyingEvent.offsetWidth;
return {
width: underlyingWidth - 15,
right: 0,
zIndex: getNextZIndex()
};
}
```
## Event Bus Integration
- `overlap:detected` - Når overlap detekteres
- `overlap:group-created` - Når event-group oprettes
- `overlap:event-stacked` - Når event stacks oven på andet
- `overlap:group-cleanup` - Når tom group fjernes
## Success Criteria ✅ ALL COMPLETED
- ✅ **Column Sharing**: Events with same start time share width 50/50
- ✅ **Stacking**: Overlapping events stack with 15px margin-left offset
- ✅ **Drag & Drop**: Full drag & drop support with overlap detection
- ✅ **Cleanup**: Automatic cleanup of empty event-group containers
- ✅ **Z-index Management**: Proper layering with data-attribute tracking
- ✅ **Performance**: 51% code reduction with zero state sync bugs
## Current Documentation
- [Stack Binding System](docs/stack-binding-system.md) - Detailed explanation of event linking
- [Complexity Comparison](complexity_comparison.md) - Before/after analysis
- [`SimpleEventOverlapManager.ts`](src/managers/SimpleEventOverlapManager.ts) - Current implementation
- [Code Review](code_review.md) - Technical analysis of the system
## Key Improvements Achieved
- **Simplified Architecture**: Data-attribute based instead of complex in-memory Maps
- **Better Reliability**: Zero state synchronization bugs
- **Easier Maintenance**: 51% less code, much cleaner logic
- **Same Functionality**: Identical user experience with better performance

View file

@ -1,48 +0,0 @@
# Overlap Detection Fix Plan - DEPRECATED
⚠️ **DEPRECATED**: This plan has been completed and superseded by SimpleEventOverlapManager.
## Status: COMPLETED ✅
The overlap detection issues described in this document have been resolved through the implementation of `SimpleEventOverlapManager`, which replaced the complex `EventOverlapManager`.
## What Was Implemented
**Fixed overlap detection logic** - Now properly checks for time overlap before determining overlap type
**Simplified state management** - Uses data-attributes instead of complex in-memory Maps
**Eliminated unnecessary complexity** - 51% reduction in code complexity
**Improved reliability** - Zero state synchronization bugs
## Current Implementation
The system now uses `SimpleEventOverlapManager` with:
- **Data-attribute based tracking** via `data-stack-link`
- **Proper time overlap detection** before classification
- **Clean separation** between column sharing and stacking logic
- **Simplified cleanup** and maintenance
## See Current Documentation
- [Stack Binding System](docs/stack-binding-system.md) - How events are linked together
- [Complexity Comparison](complexity_comparison.md) - Before/after analysis
- [`SimpleEventOverlapManager.ts`](src/managers/SimpleEventOverlapManager.ts) - Current implementation
---
## Original Problem (RESOLVED)
~~Den nuværende overlap detection logik i EventOverlapManager tjekker kun på tidsforskel mellem start tidspunkter, men ikke om events faktisk overlapper i tid. Dette resulterer i forkert stacking behavior.~~
**Resolution**: SimpleEventOverlapManager now properly checks `eventsOverlapInTime()` before determining overlap type.
## Original Implementation Plan (COMPLETED)
All items from the original plan have been implemented in SimpleEventOverlapManager:
✅ Fixed detectOverlap() method with proper time overlap checking
✅ Added eventsOverlapInTime() method
✅ Removed unnecessary data attributes
✅ Simplified event styling and cleanup
✅ Comprehensive testing completed
The new implementation provides identical functionality with much cleaner, more maintainable code.

View file

@ -1,184 +0,0 @@
# Refactored HeaderManager - Fjern Ghost Columns
## 1. HeaderManager Ændringer
```typescript
// src/managers/HeaderManager.ts
/**
* Setup header drag event listeners - REFACTORED VERSION
*/
public setupHeaderDragListeners(): void {
const calendarHeader = this.getCalendarHeader();
if (!calendarHeader) return;
// Use mouseenter instead of mouseover to avoid continuous firing
this.headerEventListener = (event: Event) => {
const target = event.target as HTMLElement;
// Check if we're entering the all-day container
const allDayContainer = target.closest('swp-allday-container');
if (allDayContainer) {
// Calculate target date from mouse X coordinate
const targetDate = this.calculateTargetDateFromMouseX(event as MouseEvent);
if (targetDate) {
const calendarType = calendarConfig.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
eventBus.emit('header:mouseover', {
element: allDayContainer,
targetDate,
headerRenderer
});
}
}
};
// Header mouseleave listener - unchanged
this.headerMouseLeaveListener = (event: Event) => {
eventBus.emit('header:mouseleave', {
element: event.target as HTMLElement
});
};
// Use mouseenter instead of mouseover
calendarHeader.addEventListener('mouseenter', this.headerEventListener, true);
calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener);
}
/**
* Calculate target date from mouse X coordinate
*/
private calculateTargetDateFromMouseX(event: MouseEvent): string | null {
const dayHeaders = document.querySelectorAll('swp-day-header');
const mouseX = event.clientX;
for (const header of dayHeaders) {
const headerElement = header as HTMLElement;
const rect = headerElement.getBoundingClientRect();
// Check if mouse X is within this header's bounds
if (mouseX >= rect.left && mouseX <= rect.right) {
return headerElement.dataset.date || null;
}
}
return null;
}
/**
* Remove event listeners from header - UPDATED
*/
private removeEventListeners(): void {
const calendarHeader = this.getCalendarHeader();
if (!calendarHeader) return;
if (this.headerEventListener) {
// Remove mouseenter listener
calendarHeader.removeEventListener('mouseenter', this.headerEventListener, true);
}
if (this.headerMouseLeaveListener) {
calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener);
}
}
```
## 2. AllDayEventRenderer Ændringer
```typescript
// src/renderers/AllDayEventRenderer.ts
/**
* Get or cache all-day container, create if it doesn't exist - SIMPLIFIED
*/
private getContainer(): HTMLElement | null {
if (!this.container) {
const header = document.querySelector('swp-calendar-header');
if (header) {
// Try to find existing container
this.container = header.querySelector('swp-allday-container');
// If not found, create it
if (!this.container) {
this.container = document.createElement('swp-allday-container');
header.appendChild(this.container);
// NO MORE GHOST COLUMNS! 🎉
// Mouse detection handled by HeaderManager coordinate calculation
}
}
}
return this.container;
}
// REMOVE this method entirely:
// private createGhostColumns(): void { ... }
```
## 3. DragDropManager Ændringer
```typescript
// src/managers/DragDropManager.ts
// In constructor, update the header:mouseover listener
eventBus.on('header:mouseover', (event) => {
const { targetDate, element } = (event as CustomEvent).detail;
if (this.draggedEventId && targetDate) {
// Only proceed if we're actually dragging and have a valid target date
const draggedElement = document.querySelector(`swp-event[data-event-id="${this.draggedEventId}"]`);
if (draggedElement) {
console.log('🎯 Converting to all-day for date:', targetDate);
this.eventBus.emit('drag:convert-to-allday_event', {
targetDate,
originalElement: draggedElement,
headerRenderer: (event as CustomEvent).detail.headerRenderer
});
}
}
});
```
## 4. CSS Ændringer (hvis nødvendigt)
```css
/* Ensure all-day container is properly positioned for mouse events */
swp-allday-container {
position: relative;
width: 100%;
min-height: var(--all-day-row-height, 0px);
display: grid;
grid-template-columns: repeat(7, 1fr); /* Match day columns */
pointer-events: all; /* Ensure mouse events work */
}
/* Remove any ghost column styles */
/* swp-allday-column styles can be removed if they were only for ghosts */
```
## 5. Fordele ved denne løsning:
**Performance**: Ingen kontinuerlige mouseover events
**Simplicity**: Fjerner ghost column kompleksitet
**Accuracy**: Direkte coordinate-baseret detection
**Maintainability**: Mindre kode at vedligeholde
**Debugging**: Lettere at følge event flow
## 6. Potentielle udfordringer:
⚠️ **Event Bubbling**: `mouseenter` med `capture: true` for at fange events tidligt
⚠️ **Coordinate Precision**: Skal teste at coordinate beregning er præcis
⚠️ **Multi-day Events**: Skal stadig håndteres korrekt ved drop
## 7. Test Scenarie:
1. Drag et day-event
2. Træk musen ind i all-day området
3. `mouseenter` fyrer én gang og beregner target date
4. Event konverteres til all-day
5. Træk musen ud af all-day området
6. `mouseleave` fyrer og konverterer tilbage

View file

@ -1,166 +0,0 @@
# Resource Calendar JSON Structure (Opdateret)
Her er den opdaterede JSON struktur med resources som array og detaljerede resource informationer:
```json
{
"date": "2025-08-05",
"resources": [
{
"name": "karina.knudsen",
"displayName": "Karina Knudsen",
"avatarUrl": "/avatars/karina.jpg",
"employeeId": "EMP001",
"events": [
{
"id": "1",
"title": "Balayage langt hår",
"start": "2025-08-05T10:00:00",
"end": "2025-08-05T11:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 60, "color": "#9c27b0" }
},
{
"id": "2",
"title": "Klipning og styling",
"start": "2025-08-05T14:00:00",
"end": "2025-08-05T15:30:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 90, "color": "#e91e63" }
}
]
},
{
"name": "maria.hansen",
"displayName": "Maria Hansen",
"avatarUrl": "/avatars/maria.jpg",
"employeeId": "EMP002",
"events": [
{
"id": "3",
"title": "Permanent",
"start": "2025-08-05T09:00:00",
"end": "2025-08-05T11:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 120, "color": "#3f51b5" }
},
{
"id": "4",
"title": "Farve behandling",
"start": "2025-08-05T13:00:00",
"end": "2025-08-05T15:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 120, "color": "#ff9800" }
}
]
},
{
"name": "lars.nielsen",
"displayName": "Lars Nielsen",
"avatarUrl": "/avatars/lars.jpg",
"employeeId": "EMP003",
"events": [
{
"id": "5",
"title": "Herreklipning",
"start": "2025-08-05T11:00:00",
"end": "2025-08-05T11:30:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 30, "color": "#795548" }
},
{
"id": "6",
"title": "Skæg trimning",
"start": "2025-08-05T16:00:00",
"end": "2025-08-05T16:30:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 30, "color": "#607d8b" }
}
]
},
{
"name": "anna.petersen",
"displayName": "Anna Petersen",
"avatarUrl": "/avatars/anna.jpg",
"employeeId": "EMP004",
"events": [
{
"id": "7",
"title": "Bryllupsfrisure",
"start": "2025-08-05T08:00:00",
"end": "2025-08-05T10:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 120, "color": "#009688" }
}
]
},
{
"name": "thomas.olsen",
"displayName": "Thomas Olsen",
"avatarUrl": "/avatars/thomas.jpg",
"employeeId": "EMP005",
"events": [
{
"id": "8",
"title": "Highlights",
"start": "2025-08-05T12:00:00",
"end": "2025-08-05T14:00:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 120, "color": "#8bc34a" }
},
{
"id": "9",
"title": "Styling konsultation",
"start": "2025-08-05T15:00:00",
"end": "2025-08-05T15:30:00",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": { "duration": 30, "color": "#cddc39" }
}
]
}
]
}
```
## Struktur Forklaring
- **date**: Den specifikke dato for resource calendar visningen
- **resources**: Array af resource objekter
- **Resource objekt**:
- **name**: Unikt navn/ID (kebab-case)
- **displayName**: Navn til visning i UI
- **avatarUrl**: URL til profilbillede
- **employeeId**: Medarbejder ID
- **events**: Array af events for denne resource
## Fordele ved denne struktur:
1. **Fleksibel**: Nemt at tilføje flere resource felter
2. **Skalerbar**: Kan håndtere mange resources
3. **UI-venlig**: DisplayName og avatar til visning
4. **Struktureret**: Klar separation mellem resource info og events
5. **Søgbar**: Name og employeeId til filtrering/søgning
Denne struktur gør det nemt at:
- Vise resource info i headers (displayName, avatar)
- Filtrere events per resource
- Håndtere kun én dato ad gangen i resource mode
- Udvide med flere resource felter senere

View file

@ -94,16 +94,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
} }
/**
* Setup listeners for drag events from DragDropManager
* NOTE: Event listeners moved to EventRendererManager for better separation of concerns
*/
protected setupDragEventListeners(): void {
// All event listeners now handled by EventRendererManager
// This method kept for backward compatibility but does nothing
}
/** /**
* Cleanup method for proper resource management * Cleanup method for proper resource management
*/ */
@ -177,10 +167,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Create SwpEventElement from existing DOM element and clone it // Create SwpEventElement from existing DOM element and clone it
const originalSwpEvent = SwpEventElement.fromExistingElement(originalElement); const originalSwpEvent = SwpEventElement.fromExistingElement(originalElement);
const clonedSwpEvent = originalSwpEvent.createClone(); const clonedSwpEvent = originalSwpEvent.createClone();
// Get the cloned DOM element // Get the cloned DOM element
this.draggedClone = clonedSwpEvent.getElement(); this.draggedClone = clonedSwpEvent.getElement();
// Apply drag styling // Apply drag styling
this.applyDragStyling(this.draggedClone); this.applyDragStyling(this.draggedClone);
@ -551,16 +541,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
renderEvents(events: CalendarEvent[], container: HTMLElement): void { renderEvents(events: CalendarEvent[], container: HTMLElement): void {
// NOTE: Removed clearEvents() to support sliding animation
// With sliding animation, multiple grid containers exist simultaneously
// clearEvents() would remove events from all containers, breaking the animation
// Events are now rendered directly into the new container without clearing
// Only handle regular (non-all-day) events
// Find columns in the specific container for regular events // Find columns in the specific container for regular events
const columns = this.getColumns(container); const columns = this.getColumns(container);
@ -569,7 +550,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const eventsLayer = column.querySelector('swp-events-layer'); const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) { if (eventsLayer) {
// NY TILGANG: Kald vores nye overlap handling
this.handleEventOverlaps(columnEvents, eventsLayer as HTMLElement); this.handleEventOverlaps(columnEvents, eventsLayer as HTMLElement);
} }
}); });