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:
parent
92463ef173
commit
996459f226
25 changed files with 4 additions and 4947 deletions
|
|
@ -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*
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue