diff --git a/.workbench/image.png b/.workbench/image.png deleted file mode 100644 index 2d7c269..0000000 Binary files a/.workbench/image.png and /dev/null differ diff --git a/calendar-config.js b/.workbench/poc-layouts/calendar-config.js similarity index 100% rename from calendar-config.js rename to .workbench/poc-layouts/calendar-config.js diff --git a/calendar-data-manager.js b/.workbench/poc-layouts/calendar-data-manager.js similarity index 100% rename from calendar-data-manager.js rename to .workbench/poc-layouts/calendar-data-manager.js diff --git a/calendar-date-utils.js b/.workbench/poc-layouts/calendar-date-utils.js similarity index 100% rename from calendar-date-utils.js rename to .workbench/poc-layouts/calendar-date-utils.js diff --git a/calendar-event-types.js b/.workbench/poc-layouts/calendar-event-types.js similarity index 100% rename from calendar-event-types.js rename to .workbench/poc-layouts/calendar-event-types.js diff --git a/calendar-eventbus.js b/.workbench/poc-layouts/calendar-eventbus.js similarity index 100% rename from calendar-eventbus.js rename to .workbench/poc-layouts/calendar-eventbus.js diff --git a/calendar-grid-manager.js b/.workbench/poc-layouts/calendar-grid-manager.js similarity index 100% rename from calendar-grid-manager.js rename to .workbench/poc-layouts/calendar-grid-manager.js diff --git a/calendar-poc-single-file.html b/.workbench/poc-layouts/calendar-poc-single-file.html similarity index 100% rename from calendar-poc-single-file.html rename to .workbench/poc-layouts/calendar-poc-single-file.html diff --git a/month-view-design.html b/.workbench/poc-layouts/month-view-design.html similarity index 100% rename from month-view-design.html rename to .workbench/poc-layouts/month-view-design.html diff --git a/month-view-expanded.html b/.workbench/poc-layouts/month-view-expanded.html similarity index 100% rename from month-view-expanded.html rename to .workbench/poc-layouts/month-view-expanded.html diff --git a/.workbench/review.txt b/.workbench/review.txt deleted file mode 100644 index d1a2b5c..0000000 --- a/.workbench/review.txt +++ /dev/null @@ -1,217 +0,0 @@ -De 6 vigtigste fund (med fixes) - -Gruppering kan “brygge” mellem to grupper uden at merge dem -groupEventsByStartTime finder første eksisterende gruppe med konflikt og lægger eventet deri. Hvis et nyt event konflikter med flere grupper, bliver grupperne ikke merged → inkonsistente “grid”-klumper. Løs: merge alle matchende grupper eller brug union-find/sweep-line konfliktsæt. - -EventStackManager - -ContainerType er “GRID” for alle grupper >1 — også ved dybe overlaps -decideContainerType returnerer altid 'GRID' når events.length > 1. Det kan være tilsigtet, men så skal du være tryg ved, at lange overlappende events, der kun næsten starter samtidigt, stadig pakkes i kolonner fremfor “stacking”. Overvej: GRID kun når samtidighed er vigtigere end varighed, ellers fald tilbage til STACKING. - -EventStackManager - -Stack level-algoritmen kan eskalere niveauer unødigt -createOptimizedStackLinks sætter stackLevel = max(overlappende tidligere) + 1. Det er mere “stak-tårn” end “før-ledig-kolonne” og giver højere niveauer end nødvendigt (ikke minimal farvelægning). Løs: interval partitioning med min-heap (giver laveste ledige level). - -EventStackManager - -Grid-top beregnes fra ét event, men børn positioneres relativt til containerStart -I koordinatoren bruges earliestEvent til top, og renderer bruger earliestEvent.start som containerStart. Det er ok — men sørg for, at earliestEvent garanteret er det tidligste i gruppen og sortér eksplicit inden brug (robusthed mod fremtidige ændringer). - -EventLayoutCoordinator - - - -EventRenderer - -Drag bruger rå new Date(...) i stedet for DateService -Kan give TZ/DST-glitches. Brug samme parse/logik som resten. - -EventRenderer - -Ingen reflow af kolonne efter drop -handleDragEnd normaliserer DOM men recalculerer ikke layout → forkert stacking/margin efter flyt. Kald din kolonne-pipeline igen for den berørte kolonne. - -EventRenderer - -Bonus: getEventsForColumn matcher kun start-dato === kolonnedato; events der krydser midnat forsvinder. Overvej interval-overlap mod døgnets [00:00–23:59:59.999]. - -EventRenderer - -Målrettede patches (små og sikre) -A) Merge grupper når et event rammer flere (EventStackManager) - -Erstat den nuværende “find første gruppe”-logik med merge af alle matchende: - -// inde i groupEventsByStartTime -const matches: number[] = []; -for (let gi = 0; gi < groups.length; gi++) { - const group = groups[gi]; - const conflict = group.events.some(ge => { - const s2s = Math.abs(event.start.getTime() - ge.start.getTime()) / 60000; - if (s2s <= thresholdMinutes) return true; - const e2s = (ge.end.getTime() - event.start.getTime()) / 60000; - if (e2s > 0 && e2s <= thresholdMinutes) return true; - const rev = (event.end.getTime() - ge.start.getTime()) / 60000; - if (rev > 0 && rev <= thresholdMinutes) return true; - return false; - }); - if (conflict) matches.push(gi); -} - -if (matches.length === 0) { - groups.push({ events: [event], containerType: 'NONE', startTime: event.start }); -} else { - // merge alle matchende grupper + dette event - const base = matches[0]; - groups[base].events.push(event); - for (let i = matches.length - 1; i >= 1; i--) { - const idx = matches[i]; - groups[base].events.push(...groups[idx].events); - groups.splice(idx, 1); - } - // opdatér startTime til min start - groups[base].startTime = new Date( - Math.min(...groups[base].events.map(e => e.start.getTime())) - ); -} - - -Nu undgår du “brobygning” der splitter reelt sammenhængende grupper. - -EventStackManager - -B) Minimal stack level med min-heap (EventStackManager) - -Udskift level-tildeling med klassisk interval partitioning: - -public createOptimizedStackLinks(events: CalendarEvent[]): Map { - const res = new Map(); - if (!events.length) return res; - - const sorted = [...events].sort((a,b)=> a.start.getTime() - b.start.getTime()); - type Col = { level: number; end: number }; - const cols: Col[] = []; // min-heap på end - - const push = (c: Col) => { cols.push(c); cols.sort((x,y)=> x.end - y.end); }; - - for (const ev of sorted) { - const t = ev.start.getTime(); - // find første kolonne der er fri - let placed = false; - for (let i = 0; i < cols.length; i++) { - if (cols[i].end <= t) { cols[i].end = ev.end.getTime(); res.set(ev.id, { stackLevel: cols[i].level }); placed = true; break; } - } - if (!placed) { const level = cols.length; push({ level, end: ev.end.getTime() }); res.set(ev.id, { stackLevel: level }); } - } - - // evt. byg prev/next separat hvis nødvendigt - return res; -} - - -Dette giver laveste ledige niveau og undgår “trappetårne”. - -EventStackManager - -C) Konsolidér margin/zIndex + brug DateService i drag (EventRenderer) - -Lad StackManager styre marginLeft konsekvent (og undgå magic numbers): - -// renderGridGroup -groupElement.style.top = `${gridGroup.position.top}px`; -this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel); // i stedet for *15 -this.stackManager.applyStackLinkToElement(groupElement, { stackLevel: gridGroup.stackLevel }); - - -EventRenderer - -Brug DateService i drag: - -public handleDragMove(payload: DragMoveEventPayload): void { - if (!this.draggedClone || !payload.columnBounds) return; - const swp = this.draggedClone as SwpEventElement; - const colDate = this.dateService.parseISODate?.(payload.columnBounds.date) ?? new Date(payload.columnBounds.date); - swp.updatePosition(colDate, payload.snappedY); -} - -public handleColumnChange(e: DragColumnChangeEventPayload): void { - if (!this.draggedClone) return; - const layer = e.newColumn.element.querySelector('swp-events-layer'); - if (layer && this.draggedClone.parentElement !== layer) { - layer.appendChild(this.draggedClone); - const currentTop = parseFloat(this.draggedClone.style.top) || 0; - const swp = this.draggedClone as SwpEventElement; - const colDate = this.dateService.parseISODate?.(e.newColumn.date) ?? new Date(e.newColumn.date); - swp.updatePosition(colDate, currentTop); - } -} - - -EventRenderer - -D) Reflow efter drop (EventRenderer) - -Genberegn layout for den berørte kolonne: - -public handleDragEnd(id: string, original: HTMLElement, clone: HTMLElement, finalColumn: ColumnBounds): void { - if (!clone || !original) { console.warn('Missing clone/original'); return; } - this.fadeOutAndRemove(original); - const cid = clone.dataset.eventId; - if (cid && cid.startsWith('clone-')) clone.dataset.eventId = cid.replace('clone-',''); - clone.classList.remove('dragging'); - - const layer = finalColumn.element.querySelector('swp-events-layer') as HTMLElement | null; - if (layer) { - // 1) Hent kolonnens events fra din model/state (inkl. opdateret event) - const columnEvents: CalendarEvent[] = /* ... */; - // 2) Ryd - layer.querySelectorAll('swp-event, swp-event-group').forEach(el => el.remove()); - // 3) Render igen via layout - this.renderColumnEvents(columnEvents, layer); - } - - this.draggedClone = null; - this.originalEvent = null; -} - - -EventRenderer - -E) Døgn-overlap i kolonnefilter (EventRenderer) - -Hvis ønsket (ellers behold din nuværende): - -protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] { - const d = column.dataset.date; if (!d) return []; - const start = this.dateService.parseISODate(`${d}T00:00:00`); - const end = this.dateService.parseISODate(`${d}T23:59:59.999`); - return events.filter(ev => ev.start < end && ev.end > start); -} - - -EventRenderer - -F) Eksplicit “earliest” i GRID (Coordinator) - -Gør det robust i tilfælde af usorteret input: - -const earliestEvent = [...gridCandidates].sort((a,b)=> a.start.getTime()-b.start.getTime())[0]; -const pos = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end); - - -EventLayoutCoordinator - -Mini-noter - -allocateColumns er O(n²); det er fint for typiske dagvisninger. Hvis I ser >100 events/kolonne, kan I optimere med sweep-line + min-heap. - -EventLayoutCoordinator - -Overvej at lade koordinatoren returnere rene layout-maps (id → {level, z, margin}) og holde DOM-påføring 100% i renderer — det gør DnD-”reflow” enklere at teste. - -EventLayoutCoordinator - - - -EventRenderer \ No newline at end of file diff --git a/scenarios/scenario-1.html b/.workbench/scenarios/scenario-1.html similarity index 100% rename from scenarios/scenario-1.html rename to .workbench/scenarios/scenario-1.html diff --git a/scenarios/scenario-10.html b/.workbench/scenarios/scenario-10.html similarity index 100% rename from scenarios/scenario-10.html rename to .workbench/scenarios/scenario-10.html diff --git a/scenarios/scenario-2.html b/.workbench/scenarios/scenario-2.html similarity index 100% rename from scenarios/scenario-2.html rename to .workbench/scenarios/scenario-2.html diff --git a/scenarios/scenario-3.html b/.workbench/scenarios/scenario-3.html similarity index 100% rename from scenarios/scenario-3.html rename to .workbench/scenarios/scenario-3.html diff --git a/scenarios/scenario-4.html b/.workbench/scenarios/scenario-4.html similarity index 100% rename from scenarios/scenario-4.html rename to .workbench/scenarios/scenario-4.html diff --git a/scenarios/scenario-5.html b/.workbench/scenarios/scenario-5.html similarity index 100% rename from scenarios/scenario-5.html rename to .workbench/scenarios/scenario-5.html diff --git a/scenarios/scenario-6.html b/.workbench/scenarios/scenario-6.html similarity index 100% rename from scenarios/scenario-6.html rename to .workbench/scenarios/scenario-6.html diff --git a/scenarios/scenario-7.html b/.workbench/scenarios/scenario-7.html similarity index 100% rename from scenarios/scenario-7.html rename to .workbench/scenarios/scenario-7.html diff --git a/scenarios/scenario-8.html b/.workbench/scenarios/scenario-8.html similarity index 100% rename from scenarios/scenario-8.html rename to .workbench/scenarios/scenario-8.html diff --git a/scenarios/scenario-9.html b/.workbench/scenarios/scenario-9.html similarity index 100% rename from scenarios/scenario-9.html rename to .workbench/scenarios/scenario-9.html diff --git a/scenarios/scenario-styles.css b/.workbench/scenarios/scenario-styles.css similarity index 100% rename from scenarios/scenario-styles.css rename to .workbench/scenarios/scenario-styles.css diff --git a/scenarios/scenario-test-runner.js b/.workbench/scenarios/scenario-test-runner.js similarity index 100% rename from scenarios/scenario-test-runner.js rename to .workbench/scenarios/scenario-test-runner.js diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f3f6531..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,237 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build and Development Commands - -### TypeScript Build -- **Build:** `npm run build` - Uses esbuild with NovaDI plugin to bundle to `wwwroot/js/calendar.js` -- **Watch:** `npm run watch` - Auto-rebuild on file changes -- **Clean:** `npm run clean` - Remove compiled output - -### Testing -- **Run tests:** `npm test` or `vitest` - Interactive watch mode -- **Run once:** `npm run test:run` or `vitest run` -- **Test UI:** `npm run test:ui` - Visual test interface -- Tests use Vitest with jsdom environment (see `vitest.config.ts`) - -### CSS Build -- **Build CSS:** `npm run css:build` - PostCSS with nesting support -- **Watch CSS:** `npm run css:watch` - Auto-rebuild CSS on changes -- **Production CSS:** `npm run css:build:prod` - Minified with PurgeCSS -- **Analyze CSS:** `npm run css:analyze` - CSS statistics and analysis - -### Server -- **Start:** `dotnet run` - ASP.NET Core Kestrel server on `http://localhost:8000` - -## Architecture Overview - -### Core Architectural Pattern -This is a **manager-based, event-driven calendar application** using pure TypeScript with no UI frameworks. Communication happens exclusively through DOM CustomEvents via a central EventBus. - -**Key Principles:** -- **No global state** - State lives in managers -- **Event-driven** - All inter-component communication via CustomEvents (see `CoreEvents` constants) -- **Dependency Injection** - Uses `@novadi/core` DI container -- **Pure DOM** - No React/Vue/Angular, just vanilla TypeScript + DOM manipulation - -### Dependency Injection Flow - -The application initializes in `src/index.ts` following this sequence: - -1. **CalendarConfig.initialize()** - Static config from DOM attributes (``) -2. **Container setup** - Register all services, managers, renderers, utilities -3. **Manager initialization** - CalendarManager coordinates all other managers -4. **Deep linking** - Handle URL-based event navigation - -All dependencies are auto-wired using NovaDI's `@inject` decorators (configured in `build.js`). - -### Event System - -**EventBus** (`src/core/EventBus.ts`) wraps DOM CustomEvents with debugging/logging: - -```typescript -// Emit -eventBus.emit('view:changed', { view: 'week', date: new Date() }); - -// Listen -eventBus.on('view:changed', (event: CustomEvent) => { - const { view, date } = event.detail; -}); -``` - -**Core events** are defined in `src/constants/CoreEvents.ts` (~20 essential events organized by category: lifecycle, view, navigation, data, grid, event management, system, filter, rendering). - -### Manager Architecture - -Managers are the core organizational units. Each has a specific responsibility: - -**Primary Managers:** -- `CalendarManager` - Main coordinator, initializes all managers -- `ViewManager` - Handles view switching (day/week/month) -- `NavigationManager` - Prev/next/today navigation, date changes -- `EventManager` - Event CRUD operations, selection, lifecycle -- `GridManager` - Calendar grid structure and layout -- `HeaderManager` - Date headers and column rendering -- `AllDayManager` - All-day event section management - -**Interaction Managers:** -- `DragDropManager` - Event drag-and-drop functionality -- `ResizeHandleManager` - Event resize handles -- `DragHoverManager` - Visual feedback during drag operations -- `EdgeScrollManager` - Auto-scroll when dragging near edges -- `ScrollManager` - Grid scroll behavior - -**Support Managers:** -- `ConfigManager` - Event-driven config updates (wraps CalendarConfig) and manages CSS custom properties -- `EventLayoutCoordinator` - Coordinates event positioning -- `EventStackManager` - Handles overlapping events -- `EventFilterManager` - Filter events by criteria -- `WorkHoursManager` - Work hours highlighting - -### Renderer Architecture - -Renderers handle DOM creation and updates (separation of concerns from managers): - -- `EventRenderingService` - Main event rendering coordinator -- `DateEventRenderer` / `AllDayEventRenderer` - Event DOM generation -- `DateHeaderRenderer` - Date header rendering -- `DateColumnRenderer` - Column structure -- `GridRenderer` - Grid structure and time slots -- `NavigationRenderer` - Navigation controls - -### Core Services - -**CalendarConfig** (`src/core/CalendarConfig.ts`): -- Static configuration class -- Loads settings from DOM data attributes on `` element -- Provides computed values (hourHeight, snapInterval, totalSlots, etc.) -- ConfigManager wraps it for event-driven updates and automatically syncs CSS custom properties to the DOM - -**DateService** (`src/utils/DateService.ts`): -- Uses `date-fns` and `date-fns-tz` for date calculations -- Default timezone: `Europe/Copenhagen`, locale: `da-DK` - -**TimeFormatter** (`src/utils/TimeFormatter.ts`): -- Consistent time/date formatting across the app -- Configured via CalendarConfig - -**PositionUtils** (`src/utils/PositionUtils.ts`): -- Convert between pixels and times -- Snap-to-grid calculations - -**URLManager** (`src/utils/URLManager.ts`): -- Deep linking to events -- Parses `eventId` from URL - -### Repository Pattern - -Event data is accessed through `IEventRepository` interface: -- `MockEventRepository` - Current implementation using mock data from `wwwroot/data/mock-events.json` -- Ready for API implementation swap - -## Code Organization - -``` -src/ -├── constants/ # CoreEvents and other constants -├── core/ # EventBus, CalendarConfig (core infrastructure) -├── data/ # Data models and utilities -├── elements/ # Custom HTML elements (if any) -├── managers/ # Manager classes (business logic) -├── renderers/ # DOM rendering logic -├── repositories/ # Data access layer (IEventRepository, MockEventRepository) -├── types/ # TypeScript interfaces and types -├── utils/ # Utility functions (DateService, PositionUtils, etc.) -└── index.ts # Application entry point and DI setup -``` - -## Important Patterns - -### Adding a New Manager - -1. Create in `src/managers/YourManager.ts` -2. Use `@inject` for dependencies -3. Implement optional `initialize()` method if needed -4. Register in `src/index.ts` DI container -5. Listen to events via `eventBus.on()` (injected as `IEventBus`) -6. Emit events via `eventBus.emit()` - -### Event Naming Convention - -Events follow `category:action` pattern: -- `view:changed`, `view:rendered` -- `nav:date-changed`, `nav:navigation-completed` -- `data:loaded`, `data:error` -- `event:created`, `event:updated`, `event:deleted` -- `grid:rendered`, `grid:clicked` - -### Grid Positioning - -Events are positioned using CSS Grid and absolute positioning: -- Time slots are calculated via `CalendarConfig.slotHeight` and `minuteHeight` -- `PositionUtils` handles pixel ↔ time conversions -- Snap-to-grid uses `CalendarConfig.getGridSettings().snapInterval` - -### Work Week Configuration - -CalendarConfig supports work week presets: -- `standard` - Mon-Fri (default) -- `compressed` - Mon-Thu -- `midweek` - Wed-Fri -- `weekend` - Sat-Sun -- `fullweek` - Mon-Sun - -Change via `CalendarConfig.setWorkWeek('preset-id')` - -## Testing - -Tests are written using Vitest with jsdom. Setup file: `test/setup.ts` - -Run individual test file: -```bash -vitest run path/to/test-file.test.ts -``` - -## CSS Architecture - -CSS is modular and built with PostCSS: -- **Source:** `wwwroot/css/src/` (uses PostCSS nesting) -- **Output:** `wwwroot/css/` -- **Main file:** `calendar.css` (currently used) - -Planned modular CSS files: -- `calendar-base-css.css` - Variables and base styles -- `calendar-components-css.css` - UI components -- `calendar-events-css.css` - Event styling -- `calendar-layout-css.css` - Grid layout -- `calendar-popup-css.css` - Modals and popups - -## Debugging - -Enable EventBus debug mode (already enabled in `src/index.ts`): -```typescript -eventBus.setDebug(true); -``` - -Access debug interface in browser console: -```javascript -window.calendarDebug.eventBus.getEventLog(); // All events -window.calendarDebug.eventManager; // Access EventManager -window.calendarDebug.calendarManager; // Access CalendarManager -``` - -## Configuration via HTML - -Set calendar options via data attributes on ``: -```html - - -``` diff --git a/CYCLOMATIC_COMPLEXITY_ANALYSIS.md b/CYCLOMATIC_COMPLEXITY_ANALYSIS.md deleted file mode 100644 index e615a10..0000000 --- a/CYCLOMATIC_COMPLEXITY_ANALYSIS.md +++ /dev/null @@ -1,578 +0,0 @@ -# Cyclomatic Complexity Analysis Report -**Calendar Plantempus Project** -Generated: 2025-10-04 - ---- - -## Executive Summary - -This report analyzes the cyclomatic complexity of the Calendar Plantempus TypeScript codebase, focusing on identifying methods that exceed recommended complexity thresholds and require refactoring. - -### Key Metrics - -| Metric | Value | -|--------|-------| -| **Total Files Analyzed** | 6 | -| **Total Methods Analyzed** | 74 | -| **Methods with Complexity >10** | 4 (5.4%) | -| **Methods with Complexity 6-10** | 5 (6.8%) | -| **Methods with Complexity 1-5** | 65 (87.8%) | - -### Complexity Distribution - -``` -■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ Low (1-5): 87.8% -■■■ Medium (6-10): 6.8% -■ High (>10): 5.4% -``` - -### Overall Assessment - -✅ **Strengths:** -- 87.8% of methods have acceptable complexity -- Web Components (SwpEventElement) demonstrate excellent design -- Rendering services show clean separation of concerns - -🔴 **Critical Issues:** -- 4 methods exceed complexity threshold of 10 -- Stack management logic is overly complex (complexity 18!) -- Drag & drop handlers need refactoring - ---- - -## Detailed File Analysis - -### 1. DragDropManager.ts -**File:** `src/managers/DragDropManager.ts` -**Overall Complexity:** HIGH ⚠️ - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `init()` | 88-133 | 7 | 🟡 Medium | Event listener setup could be extracted | -| `handleMouseDown()` | 135-168 | 5 | ✅ OK | Acceptable complexity | -| `handleMouseMove()` | 173-260 | **15** | 🔴 **Critical** | **NEEDS IMMEDIATE REFACTORING** | -| `handleMouseUp()` | 265-310 | 4 | ✅ OK | Clean implementation | -| `cleanupAllClones()` | 312-320 | 2 | ✅ OK | Simple utility method | -| `cancelDrag()` | 325-350 | 3 | ✅ OK | Straightforward cleanup | -| `calculateDragPosition()` | 355-364 | 2 | ✅ OK | Simple calculation | -| `calculateSnapPosition()` | 369-377 | 1 | ✅ OK | Base complexity | -| `checkAutoScroll()` | 383-403 | 5 | ✅ OK | Could be simplified slightly | -| `startAutoScroll()` | 408-444 | 6 | 🟡 Medium | Autoscroll logic could be extracted | -| `stopAutoScroll()` | 449-454 | 2 | ✅ OK | Simple cleanup | -| `detectDropTarget()` | 468-483 | 4 | ✅ OK | Clear DOM traversal | -| `handleHeaderMouseEnter()` | 488-516 | 4 | ✅ OK | Clean event handling | -| `handleHeaderMouseLeave()` | 521-544 | 4 | ✅ OK | Clean event handling | - -**Decision Points in handleMouseMove():** -1. `if (event.buttons === 1)` - Check if mouse button is pressed -2. `if (!this.isDragStarted && this.draggedElement)` - Check for drag initialization -3. `if (totalMovement >= this.dragThreshold)` - Movement threshold check -4. `if (this.isDragStarted && this.draggedElement && this.draggedClone)` - Drag state validation -5. `if (!this.draggedElement.hasAttribute("data-allday"))` - Event type check -6. `if (deltaY >= this.snapDistancePx)` - Snap interval check -7. Multiple autoscroll conditionals -8. `if (newColumn == null)` - Column validation -9. `if (newColumn?.index !== this.currentColumnBounds?.index)` - Column change detection - -**Recommendation for handleMouseMove():** -```typescript -// Current: 88 lines, complexity 15 -// Suggested refactoring: - -private handleMouseMove(event: MouseEvent): void { - this.updateMousePosition(event); - - if (!this.isMouseButtonPressed(event)) return; - - if (this.shouldStartDrag()) { - this.initializeDrag(); - } - - if (this.isDragActive()) { - this.updateDragPosition(); - this.handleColumnChange(); - } -} - -// Extract methods with complexity 2-4 each: -// - initializeDrag() -// - updateDragPosition() -// - handleColumnChange() -``` - ---- - -### 2. SwpEventElement.ts -**File:** `src/elements/SwpEventElement.ts` -**Overall Complexity:** LOW ✅ - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `connectedCallback()` | 84-89 | 2 | ✅ OK | Simple initialization | -| `attributeChangedCallback()` | 94-98 | 2 | ✅ OK | Clean attribute handling | -| `updatePosition()` | 109-128 | 2 | ✅ OK | Straightforward update logic | -| `createClone()` | 133-152 | 2 | ✅ OK | Simple cloning | -| `render()` | 161-171 | 1 | ✅ OK | Base complexity | -| `updateDisplay()` | 176-194 | 3 | ✅ OK | Clean DOM updates | -| `applyPositioning()` | 199-205 | 1 | ✅ OK | Delegates to PositionUtils | -| `calculateTimesFromPosition()` | 210-230 | 1 | ✅ OK | Simple calculation | -| `fromCalendarEvent()` (static) | 239-252 | 1 | ✅ OK | Factory method | -| `extractCalendarEventFromElement()` (static) | 257-270 | 1 | ✅ OK | Clean extraction | -| `fromAllDayElement()` (static) | 275-311 | 4 | ✅ OK | Acceptable conversion logic | -| `SwpAllDayEventElement.connectedCallback()` | 319-323 | 2 | ✅ OK | Simple setup | -| `SwpAllDayEventElement.createClone()` | 328-335 | 1 | ✅ OK | Base complexity | -| `SwpAllDayEventElement.applyGridPositioning()` | 340-343 | 1 | ✅ OK | Simple positioning | -| `SwpAllDayEventElement.fromCalendarEvent()` (static) | 348-362 | 1 | ✅ OK | Factory method | - -**Best Practices Demonstrated:** -- ✅ Clear separation of concerns -- ✅ Factory methods for object creation -- ✅ Delegation to utility classes (PositionUtils, DateService) -- ✅ BaseSwpEventElement abstraction reduces duplication -- ✅ All methods stay within complexity threshold - -**This file serves as a model for good design in the codebase.** - ---- - -### 3. SimpleEventOverlapManager.ts -**File:** `src/managers/SimpleEventOverlapManager.ts` -**Overall Complexity:** HIGH ⚠️ - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `resolveOverlapType()` | 33-58 | 4 | ✅ OK | Clear overlap detection | -| `groupOverlappingElements()` | 64-84 | 4 | ✅ OK | Acceptable grouping logic | -| `createEventGroup()` | 89-92 | 1 | ✅ OK | Simple factory | -| `addToEventGroup()` | 97-113 | 2 | ✅ OK | Straightforward addition | -| `createStackedEvent()` | 118-165 | 7 | 🟡 Medium | Chain traversal could be extracted | -| `removeStackedStyling()` | 170-284 | **18** | 🔴 **Critical** | **MOST COMPLEX METHOD IN CODEBASE** | -| `updateSubsequentStackLevels()` | 289-313 | 5 | ✅ OK | Could be simplified | -| `isStackedEvent()` | 318-324 | 3 | ✅ OK | Simple boolean check | -| `removeFromEventGroup()` | 329-364 | 6 | 🟡 Medium | Remaining event handling complex | -| `restackEventsInContainer()` | 369-432 | **11** | 🔴 **High** | **NEEDS REFACTORING** | -| `getEventGroup()` | 438-440 | 1 | ✅ OK | Simple utility | -| `isInEventGroup()` | 442-444 | 1 | ✅ OK | Simple utility | -| `getStackLink()` | 449-459 | 3 | ✅ OK | JSON parsing with error handling | -| `setStackLink()` | 461-467 | 2 | ✅ OK | Simple setter | -| `findElementById()` | 469-471 | 1 | ✅ OK | Base complexity | - -**Critical Issue: removeStackedStyling() - Complexity 18** - -**Decision Points Breakdown:** -1. `if (link)` - Check if element has stack link -2. `if (link.prev && link.next)` - Middle element in chain -3. `if (prevElement && nextElement)` - Both neighbors exist -4. `if (!actuallyOverlap)` - Chain breaking decision (CRITICAL BRANCH) -5. `if (nextLink?.next)` - Subsequent elements exist -6. `while (subsequentId)` - Loop through chain -7. `if (!subsequentElement)` - Element validation -8. `else` - Normal stacking (chain maintenance) -9. `else if (link.prev)` - Last element case -10. `if (prevElement)` - Previous element exists -11. `else if (link.next)` - First element case -12. `if (nextElement)` - Next element exists -13. `if (link.prev && link.next)` - Middle element check (duplicate) -14. `if (nextLink && nextLink.next)` - Chain continuation -15. `else` - Chain was broken -16-18. Additional nested conditions - -**Recommendation for removeStackedStyling():** -```typescript -// Current: 115 lines, complexity 18 -// Suggested refactoring: - -public removeStackedStyling(eventElement: HTMLElement): void { - this.clearVisualStyling(eventElement); - - const link = this.getStackLink(eventElement); - if (!link) return; - - // Delegate to specialized methods based on position in chain - if (link.prev && link.next) { - this.removeMiddleElementFromChain(eventElement, link); - } else if (link.prev) { - this.removeLastElementFromChain(eventElement, link); - } else if (link.next) { - this.removeFirstElementFromChain(eventElement, link); - } - - this.setStackLink(eventElement, null); -} - -// Extract to separate methods: -// - clearVisualStyling() - complexity 1 -// - removeMiddleElementFromChain() - complexity 5-6 -// - removeLastElementFromChain() - complexity 3 -// - removeFirstElementFromChain() - complexity 3 -// - breakStackChain() - complexity 4 -// - maintainStackChain() - complexity 4 -``` - -**Critical Issue: restackEventsInContainer() - Complexity 11** - -**Decision Points:** -1. `if (stackedEvents.length === 0)` - Early return -2. `for (const element of stackedEvents)` - Iterate events -3. `if (!eventId || processedEventIds.has(eventId))` - Validation -4. `while (rootLink?.prev)` - Find root of chain -5. `if (!prevElement)` - Break condition -6. `while (currentElement)` - Traverse chain -7. `if (!currentLink?.next)` - End of chain -8. `if (!nextElement)` - Break condition -9. `if (chain.length > 1)` - Only add multi-element chains -10. `forEach` - Restack each chain -11. `if (link)` - Update link data - -**Recommendation for restackEventsInContainer():** -```typescript -// Current: 64 lines, complexity 11 -// Suggested refactoring: - -public restackEventsInContainer(container: HTMLElement): void { - const stackedEvents = this.getStackedEvents(container); - if (stackedEvents.length === 0) return; - - const stackChains = this.collectStackChains(stackedEvents); - stackChains.forEach(chain => this.reapplyStackStyling(chain)); -} - -// Extract to separate methods: -// - getStackedEvents() - complexity 2 -// - collectStackChains() - complexity 6 -// - findStackRoot() - complexity 3 -// - traverseChain() - complexity 3 -// - reapplyStackStyling() - complexity 2 -``` - ---- - -### 4. EventRendererManager.ts -**File:** `src/renderers/EventRendererManager.ts` -**Overall Complexity:** MEDIUM 🟡 - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `renderEvents()` | 35-68 | 3 | ✅ OK | Clean rendering logic | -| `setupEventListeners()` | 70-95 | 1 | ✅ OK | Simple delegation | -| `handleGridRendered()` | 101-127 | 5 | ✅ OK | Could reduce conditionals | -| `handleViewChanged()` | 133-138 | 1 | ✅ OK | Simple cleanup | -| `setupDragEventListeners()` | 144-238 | **10** | 🔴 **High** | **NEEDS REFACTORING** | -| `handleConvertToTimeEvent()` | 243-292 | 4 | ✅ OK | Acceptable conversion logic | -| `clearEvents()` | 294-296 | 1 | ✅ OK | Delegates to strategy | -| `refresh()` | 298-300 | 1 | ✅ OK | Simple refresh | - -**Issue: setupDragEventListeners() - Complexity 10** - -**Decision Points:** -1. `if (hasAttribute('data-allday'))` - Filter all-day events -2. `if (draggedElement && strategy.handleDragStart && columnBounds)` - Validation -3. `if (hasAttribute('data-allday'))` - Filter check -4. `if (strategy.handleDragMove)` - Strategy check -5. `if (strategy.handleDragAutoScroll)` - Strategy check -6. `if (target === 'swp-day-column' && finalColumn)` - Drop target validation -7. `if (draggedElement && draggedClone && strategy.handleDragEnd)` - Validation -8. `if (dayEventClone)` - Cleanup check -9. `if (hasAttribute('data-allday'))` - Filter check -10. `if (strategy.handleColumnChange)` - Strategy check - -**Recommendation:** -```typescript -// Current: 95 lines, complexity 10 -// Suggested refactoring: - -private setupDragEventListeners(): void { - this.setupDragStartListener(); - this.setupDragMoveListener(); - this.setupDragEndListener(); - this.setupDragAutoScrollListener(); - this.setupColumnChangeListener(); - this.setupConversionListener(); - this.setupNavigationListener(); -} - -// Each listener method: complexity 2-3 -``` - ---- - -### 5. EventRenderer.ts -**File:** `src/renderers/EventRenderer.ts` -**Overall Complexity:** LOW ✅ - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `handleDragStart()` | 50-72 | 2 | ✅ OK | Clean drag initialization | -| `handleDragMove()` | 77-84 | 2 | ✅ OK | Simple position update | -| `handleDragAutoScroll()` | 89-97 | 2 | ✅ OK | Simple scroll handling | -| `handleColumnChange()` | 102-115 | 3 | ✅ OK | Clean column switching | -| `handleDragEnd()` | 120-141 | 3 | ✅ OK | Proper cleanup | -| `handleNavigationCompleted()` | 146-148 | 1 | ✅ OK | Placeholder method | -| `fadeOutAndRemove()` | 153-160 | 1 | ✅ OK | Simple animation | -| `renderEvents()` | 163-182 | 2 | ✅ OK | Straightforward rendering | -| `renderEvent()` | 184-186 | 1 | ✅ OK | Factory delegation | -| `calculateEventPosition()` | 188-191 | 1 | ✅ OK | Delegates to utility | -| `clearEvents()` | 193-200 | 2 | ✅ OK | Simple cleanup | -| `getColumns()` | 202-205 | 1 | ✅ OK | DOM query | -| `getEventsForColumn()` | 207-221 | 2 | ✅ OK | Filter logic | - -**Best Practices:** -- ✅ All methods under complexity 4 -- ✅ Clear method names -- ✅ Delegation to utilities -- ✅ Single responsibility per method - ---- - -### 6. AllDayEventRenderer.ts -**File:** `src/renderers/AllDayEventRenderer.ts` -**Overall Complexity:** LOW ✅ - -| Method | Lines | Complexity | Status | Notes | -|--------|-------|------------|--------|-------| -| `getContainer()` | 20-32 | 3 | ✅ OK | Container initialization | -| `getAllDayContainer()` | 35-37 | 1 | ✅ OK | Simple query | -| `handleDragStart()` | 41-65 | 3 | ✅ OK | Clean drag setup | -| `renderAllDayEventWithLayout()` | 72-83 | 2 | ✅ OK | Simple rendering | -| `removeAllDayEvent()` | 89-97 | 3 | ✅ OK | Clean removal | -| `clearCache()` | 102-104 | 1 | ✅ OK | Simple reset | -| `renderAllDayEventsForPeriod()` | 109-116 | 1 | ✅ OK | Delegates to helper | -| `clearAllDayEvents()` | 118-123 | 2 | ✅ OK | Simple cleanup | -| `handleViewChanged()` | 125-127 | 1 | ✅ OK | Simple handler | - -**Best Practices:** -- ✅ Consistent low complexity across all methods -- ✅ Clear separation of concerns -- ✅ Focused functionality - ---- - -## Recommendations - -### Immediate Action Required (Complexity >10) - -#### 1. SimpleEventOverlapManager.removeStackedStyling() - Priority: CRITICAL -**Current Complexity:** 18 -**Target Complexity:** 4-6 per method - -**Refactoring Steps:** -1. Extract `clearVisualStyling()` - Remove inline styles -2. Extract `removeMiddleElementFromChain()` - Handle middle element removal -3. Extract `removeLastElementFromChain()` - Handle last element removal -4. Extract `removeFirstElementFromChain()` - Handle first element removal -5. Extract `breakStackChain()` - Handle non-overlapping chain breaking -6. Extract `maintainStackChain()` - Handle overlapping chain maintenance - -**Expected Impact:** -- Main method: complexity 4 -- Helper methods: complexity 3-6 each -- Improved testability -- Easier maintenance - ---- - -#### 2. DragDropManager.handleMouseMove() - Priority: HIGH -**Current Complexity:** 15 -**Target Complexity:** 4-5 per method - -**Refactoring Steps:** -1. Extract `updateMousePosition()` - Update tracking variables -2. Extract `shouldStartDrag()` - Check movement threshold -3. Extract `initializeDrag()` - Create clone and emit start event -4. Extract `updateDragPosition()` - Handle position and autoscroll -5. Extract `handleColumnChange()` - Detect and handle column transitions - -**Expected Impact:** -- Main method: complexity 4 -- Helper methods: complexity 3-4 each -- Better separation of drag lifecycle stages - ---- - -#### 3. SimpleEventOverlapManager.restackEventsInContainer() - Priority: HIGH -**Current Complexity:** 11 -**Target Complexity:** 3-4 per method - -**Refactoring Steps:** -1. Extract `getStackedEvents()` - Filter stacked events -2. Extract `collectStackChains()` - Build stack chains -3. Extract `findStackRoot()` - Find root of chain -4. Extract `traverseChain()` - Collect chain elements -5. Extract `reapplyStackStyling()` - Apply visual styling - -**Expected Impact:** -- Main method: complexity 3 -- Helper methods: complexity 2-4 each - ---- - -#### 4. EventRendererManager.setupDragEventListeners() - Priority: MEDIUM -**Current Complexity:** 10 -**Target Complexity:** 2-3 per method - -**Refactoring Steps:** -1. Extract `setupDragStartListener()` -2. Extract `setupDragMoveListener()` -3. Extract `setupDragEndListener()` -4. Extract `setupDragAutoScrollListener()` -5. Extract `setupColumnChangeListener()` -6. Extract `setupConversionListener()` -7. Extract `setupNavigationListener()` - -**Expected Impact:** -- Main method: complexity 1 (just calls helpers) -- Helper methods: complexity 2-3 each -- Improved readability - ---- - -### Medium Priority (Complexity 6-10) - -#### 5. SimpleEventOverlapManager.createStackedEvent() - Complexity 7 -Consider extracting chain traversal logic into `findEndOfChain()` - -#### 6. DragDropManager.startAutoScroll() - Complexity 6 -Extract scroll calculation into `calculateScrollAmount()` - -#### 7. SimpleEventOverlapManager.removeFromEventGroup() - Complexity 6 -Extract remaining event handling into `handleRemainingEvents()` - ---- - -## Code Quality Metrics - -### Complexity by File - -``` -DragDropManager.ts: ████████░░ 8/10 (1 critical, 2 medium) -SwpEventElement.ts: ██░░░░░░░░ 2/10 (excellent!) -SimpleEventOverlapManager.ts: ██████████ 10/10 (2 critical, 2 medium) -EventRendererManager.ts: ██████░░░░ 6/10 (1 critical) -EventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!) -AllDayEventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!) -``` - -### Methods Requiring Attention - -| Priority | File | Method | Complexity | Effort | -|----------|------|--------|------------|--------| -| 🔴 Critical | SimpleEventOverlapManager | removeStackedStyling | 18 | High | -| 🔴 Critical | DragDropManager | handleMouseMove | 15 | High | -| 🔴 High | SimpleEventOverlapManager | restackEventsInContainer | 11 | Medium | -| 🔴 High | EventRendererManager | setupDragEventListeners | 10 | Low | -| 🟡 Medium | SimpleEventOverlapManager | createStackedEvent | 7 | Low | -| 🟡 Medium | DragDropManager | startAutoScroll | 6 | Low | -| 🟡 Medium | SimpleEventOverlapManager | removeFromEventGroup | 6 | Low | - ---- - -## Positive Examples - -### SwpEventElement.ts - Excellent Design Pattern - -This file demonstrates best practices: - -```typescript -// ✅ Clear, focused methods with single responsibility -public updatePosition(columnDate: Date, snappedY: number): void { - this.style.top = `${snappedY + 1}px`; - const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY); - const startDate = this.dateService.createDateAtTime(columnDate, startMinutes); - let endDate = this.dateService.createDateAtTime(columnDate, endMinutes); - - if (endMinutes >= 1440) { - const extraDays = Math.floor(endMinutes / 1440); - endDate = this.dateService.addDays(endDate, extraDays); - } - - this.start = startDate; - this.end = endDate; -} -// Complexity: 2 (one if statement) -``` - -**Why this works:** -- Single responsibility (update position) -- Delegates complex calculations to helper methods -- Clear variable names -- Minimal branching - ---- - -## Action Plan - -### Phase 1: Critical Refactoring (Week 1-2) -1. ✅ Refactor `SimpleEventOverlapManager.removeStackedStyling()` (18 → 4-6) -2. ✅ Refactor `DragDropManager.handleMouseMove()` (15 → 4-5) - -**Expected Impact:** -- Reduce highest complexity from 18 to 4-6 -- Improve maintainability significantly -- Enable easier testing - -### Phase 2: High Priority (Week 3) -3. ✅ Refactor `SimpleEventOverlapManager.restackEventsInContainer()` (11 → 3-4) -4. ✅ Refactor `EventRendererManager.setupDragEventListeners()` (10 → 2-3) - -**Expected Impact:** -- Eliminate all methods with complexity >10 -- Improve overall code quality score - -### Phase 3: Medium Priority (Week 4) -5. ✅ Review and simplify medium complexity methods (complexity 6-7) -6. ✅ Add unit tests for extracted methods - -**Expected Impact:** -- All methods under complexity threshold of 10 -- Comprehensive test coverage - -### Phase 4: Continuous Improvement -7. ✅ Establish cyclomatic complexity checks in CI/CD -8. ✅ Set max complexity threshold to 10 -9. ✅ Regular code reviews focusing on complexity - ---- - -## Tools & Resources - -### Recommended Tools for Ongoing Monitoring: -- **TypeScript ESLint** with `complexity` rule -- **SonarQube** for continuous code quality monitoring -- **CodeClimate** for maintainability scoring - -### Suggested ESLint Configuration: -```json -{ - "rules": { - "complexity": ["error", 10], - "max-lines-per-function": ["warn", 50], - "max-depth": ["error", 4] - } -} -``` - ---- - -## Conclusion - -The Calendar Plantempus codebase shows **mixed code quality**: - -**Strengths:** -- 87.8% of methods have acceptable complexity -- Web Components demonstrate excellent design patterns -- Clear separation of concerns in rendering services - -**Areas for Improvement:** -- Stack management logic is overly complex -- Some drag & drop handlers need refactoring -- File naming could better reflect complexity (e.g., "Simple"EventOverlapManager has complexity 18!) - -**Overall Grade: B-** - -With the recommended refactoring, the codebase can easily achieve an **A grade** by reducing the 4 critical methods to acceptable complexity levels. - ---- - -**Generated by:** Claude Code Cyclomatic Complexity Analyzer -**Date:** 2025-10-04 -**Analyzer Version:** 1.0 diff --git a/README.md b/README.md deleted file mode 100644 index 8ae48fe..0000000 --- a/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Calendar Plantempus - -En moderne, event-drevet kalenderapplikation bygget med TypeScript og ASP.NET Core. - -## Projekt Information - -- **Projekt ID:** 8ecf2aa3-a2e4-4cc3-aa18-1c4352f00ff1 -- **Repository:** Calendar (afb8a8ec-cdbc-4c55-8631-fd0285974485) -- **Status:** Under aktiv udvikling - -## Teknisk Arkitektur - -- **Frontend:** TypeScript med esbuild som bundler -- **Arkitektur:** Event-drevet med CustomEvents (`document.dispatchEvent`/`addEventListener`) -- **Backend:** ASP.NET Core Kestrel server -- **Styling:** Modulær CSS struktur uden eksterne frameworks -- **Bundling:** esbuild for TypeScript transpilering og bundling - -## Arkitekturelle Principper - -- **Ingen global state** - Alt state håndteres i de relevante managers -- **Event-drevet kommunikation** - Alle komponenter kommunikerer via DOM CustomEvents -- **Modulær opbygning** - Hver manager har et specifikt ansvarsområde -- **Ren DOM manipulation** - Ingen eksterne JavaScript frameworks (React, Vue, etc.) -- **Custom HTML tags** - Semantisk markup med custom elements - -## Implementerede Komponenter - -Projektet følger en manager-baseret arkitektur, hvor hver manager er ansvarlig for et specifikt aspekt af kalenderen: - -### 1. CalendarManager -Hovedkoordinator for alle managers -- Initialiserer og koordinerer alle andre managers -- Håndterer global konfiguration -- Administrerer kalender lifecycle - -### 2. ViewManager -Håndterer kalendervisninger -- Skifter mellem dag/uge/måned visninger -- Opdaterer UI baseret på den valgte visning -- Renderer kalender grid struktur - -### 3. NavigationManager -Håndterer navigation -- Implementerer prev/next/today funktionalitet -- Håndterer dato navigation -- Opdaterer week info (uge nummer, dato range) - -### 4. EventManager -Administrerer events -- Håndterer event lifecycle og CRUD operationer -- Loader og synkroniserer event data -- Administrerer event selection og state - -### 5. EventRenderer -Renderer events i DOM -- Positionerer events korrekt i kalender grid -- Håndterer event styling baseret på type -- Implementerer visual feedback for event interactions - -### 6. DataManager -Håndterer data operationer -- Mock data loading for udvikling -- Event data transformation -- Data persistence interface - -### 7. GridManager -Administrerer kalender grid -- Opretter og vedligeholder grid struktur -- Håndterer time slots og positioning -- Responsive grid layout - -## CSS Struktur - -Projektet har en modulær CSS struktur for bedre organisering: - -- **`calendar-base-css.css`** - Grundlæggende styling og CSS custom properties -- **`calendar-components-css.css`** - UI komponenter og controls -- **`calendar-events-css.css`** - Event styling og farver -- **`calendar-layout-css.css`** - Layout struktur og grid -- **`calendar-popup-css.css`** - Popup og modal styling -- **`calendar.css`** - Samlet styling fra POC (bruges i øjeblikket) - -## Kommende Funktionalitet - -Baseret på projektstrukturen planlægges følgende komponenter: - -### Utilities -- **PositionUtils** - Konvertering mellem pixels og tidspunkter -- **SnapUtils** - Snap-to-interval funktionalitet -- **DOMUtils** - DOM manipulation utilities - -### Interaction Managers -- **DragManager** - Drag & drop funktionalitet for events -- **ResizeManager** - Resize funktionalitet for events -- **PopupManager** - Håndtering af event detaljer og popups - -### Feature Managers -- **SearchManager** - Søgefunktionalitet i events -- **TimeManager** - Current time indicator -- **LoadingManager** - Loading states og error handling - -### Avancerede Features -- Collision detection system for overlappende events -- Animation system for smooth transitions -- Event creation funktionalitet (double-click, drag-to-create) -- Multi-day event support -- Touch support for mobile enheder -- Keyboard navigation - -## Projekt Struktur - -``` -Calendar Plantempus/ -├── src/ # TypeScript source files -│ ├── constants/ # Konstanter og enums -│ ├── core/ # Core funktionalitet -│ ├── managers/ # Manager klasser -│ ├── types/ # TypeScript type definitioner -│ └── utils/ # Utility funktioner -├── wwwroot/ # Static web assets -│ ├── css/ # Stylesheets -│ ├── js/ # Compiled JavaScript -│ └── index.html # Main HTML file -├── build.js # esbuild configuration -├── tsconfig.json # TypeScript configuration -├── package.json # Node.js dependencies -└── Program.cs # ASP.NET Core server -``` - -## Kom i Gang - -### Forudsætninger -- .NET 8.0 SDK -- Node.js (for esbuild) - -### Installation -1. Klon repository -2. Installer dependencies: `npm install` -3. Build TypeScript: `npm run build` -4. Start server: `dotnet run` -5. Åbn browser på `http://localhost:8000` - -### Development -- **Build TypeScript:** `npm run build` -- **Watch mode:** `npm run watch` (hvis konfigureret) -- **Start server:** `dotnet run` - -## Event System - -Projektet bruger et event-drevet system hvor alle komponenter kommunikerer via DOM CustomEvents: - -```typescript -// Dispatch event -document.dispatchEvent(new CustomEvent('calendar:view-changed', { - detail: { view: 'week', date: new Date() } -})); - -// Listen for event -document.addEventListener('calendar:view-changed', (event) => { - // Handle view change -}); -``` - -## Bidrag - -Dette projekt følger clean code principper og modulær arkitektur. Når du bidrager: - -1. Følg den eksisterende manager-baserede struktur -2. Brug event-drevet kommunikation mellem komponenter -3. Undgå global state - hold state i relevante managers -4. Skriv semantisk HTML med custom tags -5. Brug modulær CSS struktur - -## Licens - -[Specificer licens her] \ No newline at end of file diff --git a/STACKING_CONCEPT.md b/STACKING_CONCEPT.md deleted file mode 100644 index dd1a928..0000000 --- a/STACKING_CONCEPT.md +++ /dev/null @@ -1,772 +0,0 @@ -# Event Stacking Concept -**Calendar Plantempus - Visual Event Overlap Management** - ---- - -## Overview - -**Event Stacking** is a visual technique for displaying overlapping calendar events by offsetting them horizontally with a cascading effect. This creates a clear visual hierarchy showing which events overlap in time. - ---- - -## Visual Concept - -### Basic Stacking - -When multiple events overlap in time, they are "stacked" with increasing left margin: - -``` -Timeline: -08:00 ───────────────────────────────── - │ -09:00 │ Event A starts - │ ┌─────────────────────┐ - │ │ Meeting A │ -10:00 │ │ │ - │ │ Event B starts │ - │ │ ┌─────────────────────┐ -11:00 │ │ │ Meeting B │ - │ └──│─────────────────────┘ - │ │ │ -12:00 │ │ Event C starts │ - │ │ ┌─────────────────────┐ - │ └──│─────────────────────┘ -13:00 │ │ Meeting C │ - │ └─────────────────────┘ -14:00 ───────────────────────────────── - -Visual Result (stacked view): -┌─────────────────────┐ -│ Meeting A │ -│ ┌─────────────────────┐ -│ │ Meeting B │ -└─│─────────────────────┘ - │ ┌─────────────────────┐ - │ │ Meeting C │ - └─│─────────────────────┘ - └─────────────────────┘ -``` - -Each subsequent event is offset by **15px** to the right. - ---- - -## Stack Link Data Structure - -Stack links create a **doubly-linked list** stored directly in DOM elements as data attributes. - -### Interface Definition - -```typescript -interface StackLink { - prev?: string; // Event ID of previous event in stack - next?: string; // Event ID of next event in stack - stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.) -} -``` - -### Storage in DOM - -Stack links are stored as JSON in the `data-stack-link` attribute: - -```html - - - - - - - - -``` - -### Benefits of DOM Storage - -✅ **State follows the element** - No external state management needed -✅ **Survives drag & drop** - Links persist through DOM manipulations -✅ **Easy to query** - Can traverse chain using DOM queries -✅ **Self-contained** - Each element knows its position in the stack - ---- - -## Overlap Detection - -Events overlap when their time ranges intersect. - -### Time-Based Overlap Algorithm - -```typescript -function doEventsOverlap(eventA: CalendarEvent, eventB: CalendarEvent): boolean { - // Two events overlap if: - // - Event A starts before Event B ends AND - // - Event A ends after Event B starts - return eventA.start < eventB.end && eventA.end > eventB.start; -} -``` - -### Example Cases - -**Case 1: Events Overlap** -``` -Event A: 09:00 ──────── 11:00 -Event B: 10:00 ──────── 12:00 -Result: OVERLAP (10:00 to 11:00) -``` - -**Case 2: No Overlap** -``` -Event A: 09:00 ──── 10:00 -Event B: 11:00 ──── 12:00 -Result: NO OVERLAP -``` - -**Case 3: Complete Containment** -``` -Event A: 09:00 ──────────────── 13:00 -Event B: 10:00 ─── 11:00 -Result: OVERLAP (Event B fully inside Event A) -``` - ---- - -## Visual Styling - -### CSS Calculations - -```typescript -const STACK_OFFSET_PX = 15; - -// For each event in stack: -marginLeft = stackLevel * STACK_OFFSET_PX; -zIndex = 100 + stackLevel; -``` - -### Example with 3 Stacked Events - -```typescript -Event A (stackLevel: 0): - marginLeft = 0 * 15 = 0px - zIndex = 100 + 0 = 100 - -Event B (stackLevel: 1): - marginLeft = 1 * 15 = 15px - zIndex = 100 + 1 = 101 - -Event C (stackLevel: 2): - marginLeft = 2 * 15 = 30px - zIndex = 100 + 2 = 102 -``` - -Result: Event C appears on top, Event A at the base. - ---- - -## Optimized Stacking (Smart Stacking) - -### The Problem: Naive Stacking vs Optimized Stacking - -**Naive Approach:** Simply stack all overlapping events sequentially. - -``` -Event A: 09:00 ════════════════════════════ 14:00 -Event B: 10:00 ═════ 12:00 -Event C: 12:30 ═══ 13:00 - -Naive Result: -Event A: stackLevel 0 -Event B: stackLevel 1 -Event C: stackLevel 2 ← INEFFICIENT! C doesn't overlap B -``` - -**Optimized Approach:** Events that don't overlap each other can share the same stack level. - -``` -Event A: 09:00 ════════════════════════════ 14:00 -Event B: 10:00 ═════ 12:00 -Event C: 12:30 ═══ 13:00 - -Optimized Result: -Event A: stackLevel 0 -Event B: stackLevel 1 ← Both at level 1 -Event C: stackLevel 1 ← because they don't overlap! -``` - -### Visual Comparison: The Key Insight - -**Example Timeline:** -``` -Timeline: -09:00 ───────────────────────────────── - │ Event A starts - │ ┌─────────────────────────────┐ -10:00 │ │ Event A │ - │ │ │ - │ │ Event B starts │ - │ │ ╔═══════════════╗ │ -11:00 │ │ ║ Event B ║ │ - │ │ ║ ║ │ -12:00 │ │ ╚═══════════════╝ │ - │ │ │ - │ │ Event C starts │ - │ │ ╔═══════════╗ │ -13:00 │ │ ║ Event C ║ │ - │ └───────╚═══════════╝─────────┘ -14:00 ───────────────────────────────── - -Key Observation: -• Event B (10:00-12:00) and Event C (12:30-13:00) do NOT overlap! -• They are separated by 30 minutes (12:00 to 12:30) -• Both overlap with Event A, but not with each other -``` - -**Naive Stacking (Wasteful):** -``` -Visual Result (Naive - Inefficient): - -┌─────────────────────────────────────────────────┐ -│ Event A │ -│ ┌─────────────────────┐ │ -│ │ Event B │ │ -│ │ ┌─────────────────────┐ │ -│ └─│─────────────────────┘ │ -│ │ Event C │ │ -│ └─────────────────────┘ │ -└─────────────────────────────────────────────────┘ - 0px 15px 30px - └──┴────┘ - Wasted space! - -Stack Levels: -• Event A: stackLevel 0 (marginLeft: 0px) -• Event B: stackLevel 1 (marginLeft: 15px) -• Event C: stackLevel 2 (marginLeft: 30px) ← UNNECESSARY! - -Problem: Event C is pushed 30px to the right even though - it doesn't conflict with Event B! -``` - -**Optimized Stacking (Efficient):** -``` -Visual Result (Optimized - Efficient): - -┌─────────────────────────────────────────────────┐ -│ Event A │ -│ ┌─────────────────────┐ ┌─────────────────────┐│ -│ │ Event B │ │ Event C ││ -│ └─────────────────────┘ └─────────────────────┘│ -└─────────────────────────────────────────────────┘ - 0px 15px 15px - └────────────────────┘ - Same offset for both! - -Stack Levels: -• Event A: stackLevel 0 (marginLeft: 0px) -• Event B: stackLevel 1 (marginLeft: 15px) -• Event C: stackLevel 1 (marginLeft: 15px) ← OPTIMIZED! - -Benefit: Event C reuses stackLevel 1 because Event B - has already ended when Event C starts. - No visual conflict, saves 15px of horizontal space! -``` - -**Side-by-Side Comparison:** -``` -Naive (3 levels): Optimized (2 levels): - - A A - ├─ B ├─ B - │ └─ C └─ C - - Uses 45px width Uses 30px width - (0 + 15 + 30) (0 + 15 + 15) - - 33% space savings! → -``` - -### Algorithm: Greedy Stack Level Assignment - -The optimized stacking algorithm assigns the lowest available stack level to each event: - -```typescript -function createOptimizedStackLinks(events: CalendarEvent[]): Map { - // Step 1: Sort events by start time - const sorted = events.sort((a, b) => a.start - b.start) - - // Step 2: Track which stack levels are occupied at each time point - const stackLinks = new Map() - - for (const event of sorted) { - // Find the lowest available stack level for this event - let stackLevel = 0 - - // Check which levels are occupied by overlapping events - const overlapping = sorted.filter(other => - other !== event && doEventsOverlap(event, other) - ) - - // Try each level starting from 0 - while (true) { - const levelOccupied = overlapping.some(other => - stackLinks.get(other.id)?.stackLevel === stackLevel - ) - - if (!levelOccupied) { - break // Found available level - } - - stackLevel++ // Try next level - } - - // Assign the lowest available level - stackLinks.set(event.id, { stackLevel }) - } - - return stackLinks -} -``` - -### Example Scenarios - -#### Scenario 1: Three Events, Two Parallel Tracks - -``` -Input: - Event A: 09:00-14:00 (long event) - Event B: 10:00-12:00 - Event C: 12:30-13:00 - -Analysis: - A overlaps with: B, C - B overlaps with: A (not C) - C overlaps with: A (not B) - -Result: - Event A: stackLevel 0 (base) - Event B: stackLevel 1 (first available) - Event C: stackLevel 1 (level 1 is free, B doesn't conflict) -``` - -#### Scenario 2: Four Events, Three at Same Level - -``` -Input: - Event A: 09:00-15:00 (very long event) - Event B: 10:00-11:00 - Event C: 11:30-12:30 - Event D: 13:00-14:00 - -Analysis: - A overlaps with: B, C, D - B, C, D don't overlap with each other - -Result: - Event A: stackLevel 0 - Event B: stackLevel 1 - Event C: stackLevel 1 (B is done, level 1 free) - Event D: stackLevel 1 (B and C are done, level 1 free) -``` - -#### Scenario 3: Nested Events with Optimization - -``` -Input: - Event A: 09:00-15:00 - Event B: 10:00-13:00 - Event C: 11:00-12:00 - Event D: 12:30-13:30 - -Analysis: - A overlaps with: B, C, D - B overlaps with: A, C (not D) - C overlaps with: A, B (not D) - D overlaps with: A (not B, not C) - -Result: - Event A: stackLevel 0 (base) - Event B: stackLevel 1 (overlaps with A) - Event C: stackLevel 2 (overlaps with A and B) - Event D: stackLevel 2 (overlaps with A only, level 2 is free) -``` - -### Stack Links with Optimization - -**Important:** With optimized stacking, events at the same stack level are NOT linked via prev/next! - -```typescript -// Traditional chain (naive): -Event A: { stackLevel: 0, next: "event-b" } -Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } -Event C: { stackLevel: 2, prev: "event-b" } - -// Optimized (B and C at same level, no link between them): -Event A: { stackLevel: 0 } -Event B: { stackLevel: 1 } // No prev/next -Event C: { stackLevel: 1 } // No prev/next -``` - -### Benefits of Optimized Stacking - -✅ **Space Efficiency:** Reduces horizontal space usage by up to 50% -✅ **Better Readability:** Events are visually closer, easier to see relationships -✅ **Scalability:** Works well with many events in a day -✅ **Performance:** Same O(n²) complexity as naive approach - -### Trade-offs - -⚠️ **No Single Chain:** Events at the same level aren't linked, making traversal more complex -⚠️ **More Complex Logic:** Requires checking all overlaps, not just sequential ordering -⚠️ **Visual Ambiguity:** Users might wonder why some events are at the same level - -## Stack Chain Operations - -### Building a Stack Chain (Naive Approach) - -When events overlap, they form a chain sorted by start time: - -```typescript -// Input: Events with overlapping times -Event A: 09:00-11:00 -Event B: 10:00-12:00 -Event C: 11:30-13:00 - -// Step 1: Sort by start time (earliest first) -Sorted: [Event A, Event B, Event C] - -// Step 2: Create links -Event A: { stackLevel: 0, next: "event-b" } -Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } -Event C: { stackLevel: 2, prev: "event-b" } -``` - -### Traversing Forward - -```typescript -// Start at any event -currentEvent = Event B; - -// Get stack link -stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" } - -// Move to next event -nextEventId = stackLink.next; // "event-c" -nextEvent = document.querySelector(`[data-event-id="${nextEventId}"]`); -``` - -### Traversing Backward - -```typescript -// Start at any event -currentEvent = Event B; - -// Get stack link -stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" } - -// Move to previous event -prevEventId = stackLink.prev; // "event-a" -prevEvent = document.querySelector(`[data-event-id="${prevEventId}"]`); -``` - -### Finding Stack Root - -```typescript -function findStackRoot(event: HTMLElement): HTMLElement { - let current = event; - let stackLink = getStackLink(current); - - // Traverse backward until we find an event with no prev link - while (stackLink?.prev) { - const prevEvent = document.querySelector( - `[data-event-id="${stackLink.prev}"]` - ); - if (!prevEvent) break; - - current = prevEvent; - stackLink = getStackLink(current); - } - - return current; // This is the root (stackLevel 0) -} -``` - ---- - -## Use Cases - -### 1. Adding a New Event to Existing Stack - -``` -Existing Stack: - Event A (09:00-11:00) - stackLevel 0 - Event B (10:00-12:00) - stackLevel 1 - -New Event: - Event C (10:30-11:30) - -Steps: -1. Detect overlap with Event A and Event B -2. Sort all three by start time: [A, B, C] -3. Rebuild stack links: - - Event A: { stackLevel: 0, next: "event-b" } - - Event B: { stackLevel: 1, prev: "event-a", next: "event-c" } - - Event C: { stackLevel: 2, prev: "event-b" } -4. Apply visual styling -``` - -### 2. Removing Event from Middle of Stack - -``` -Before: - Event A (stackLevel 0) ─→ Event B (stackLevel 1) ─→ Event C (stackLevel 2) - -Remove Event B: - -After: - Event A (stackLevel 0) ─→ Event C (stackLevel 1) - -Steps: -1. Get Event B's stack link: { prev: "event-a", next: "event-c" } -2. Update Event A's next: "event-c" -3. Update Event C's prev: "event-a" -4. Update Event C's stackLevel: 1 (was 2) -5. Recalculate Event C's marginLeft: 15px (was 30px) -6. Remove Event B's stack link -``` - -### 3. Moving Event to Different Time - -``` -Before (events overlap): - Event A (09:00-11:00) - stackLevel 0 - Event B (10:00-12:00) - stackLevel 1 - -Move Event B to 14:00-16:00 (no longer overlaps): - -After: - Event A (09:00-11:00) - NO STACK LINK (standalone) - Event B (14:00-16:00) - NO STACK LINK (standalone) - -Steps: -1. Detect that Event B no longer overlaps Event A -2. Remove Event B from stack chain -3. Clear Event A's next link -4. Clear Event B's stack link entirely -5. Reset both events' marginLeft to 0px -``` - ---- - -## Edge Cases - -### Case 1: Single Event (No Overlap) - -``` -Event A: 09:00-10:00 (alone in time slot) - -Stack Link: NONE (no data-stack-link attribute) -Visual: marginLeft = 0px, zIndex = default -``` - -### Case 2: Two Events, Same Start Time - -``` -Event A: 10:00-11:00 -Event B: 10:00-12:00 (same start, different end) - -Sort by: start time first, then by end time (shortest first) -Result: Event A (stackLevel 0), Event B (stackLevel 1) -``` - -### Case 3: Multiple Separate Chains in Same Column - -``` -Chain 1: - Event A (09:00-10:00) - stackLevel 0 - Event B (09:30-10:30) - stackLevel 1 - -Chain 2: - Event C (14:00-15:00) - stackLevel 0 - Event D (14:30-15:30) - stackLevel 1 - -Note: Two independent chains, each with their own root at stackLevel 0 -``` - -### Case 4: Complete Containment - -``` -Event A: 09:00-13:00 (large event) -Event B: 10:00-11:00 (inside A) -Event C: 11:30-12:30 (inside A) - -All three overlap, so they form one chain: -Event A - stackLevel 0 -Event B - stackLevel 1 -Event C - stackLevel 2 -``` - ---- - -## Algorithm Pseudocode - -### Creating Stack for New Event - -``` -function createStackForNewEvent(newEvent, columnEvents): - // Step 1: Find overlapping events - overlapping = columnEvents.filter(event => - doEventsOverlap(newEvent, event) - ) - - if overlapping is empty: - // No stack needed - return null - - // Step 2: Combine and sort by start time - allEvents = [...overlapping, newEvent] - allEvents.sort((a, b) => a.start - b.start) - - // Step 3: Create stack links - stackLinks = new Map() - - for (i = 0; i < allEvents.length; i++): - link = { - stackLevel: i, - prev: i > 0 ? allEvents[i-1].id : undefined, - next: i < allEvents.length-1 ? allEvents[i+1].id : undefined - } - stackLinks.set(allEvents[i].id, link) - - // Step 4: Apply to DOM - for each event in allEvents: - element = findElementById(event.id) - element.dataset.stackLink = JSON.stringify(stackLinks.get(event.id)) - element.style.marginLeft = stackLinks.get(event.id).stackLevel * 15 + 'px' - element.style.zIndex = 100 + stackLinks.get(event.id).stackLevel - - return stackLinks -``` - -### Removing Event from Stack - -``` -function removeEventFromStack(eventId): - element = findElementById(eventId) - stackLink = JSON.parse(element.dataset.stackLink) - - if not stackLink: - return // Not in a stack - - // Update previous element - if stackLink.prev: - prevElement = findElementById(stackLink.prev) - prevLink = JSON.parse(prevElement.dataset.stackLink) - prevLink.next = stackLink.next - prevElement.dataset.stackLink = JSON.stringify(prevLink) - - // Update next element - if stackLink.next: - nextElement = findElementById(stackLink.next) - nextLink = JSON.parse(nextElement.dataset.stackLink) - nextLink.prev = stackLink.prev - - // Shift down stack level - nextLink.stackLevel = nextLink.stackLevel - 1 - nextElement.dataset.stackLink = JSON.stringify(nextLink) - - // Update visual styling - nextElement.style.marginLeft = nextLink.stackLevel * 15 + 'px' - nextElement.style.zIndex = 100 + nextLink.stackLevel - - // Cascade update to all subsequent events - updateSubsequentStackLevels(nextElement, -1) - - // Clear removed element's stack link - delete element.dataset.stackLink - element.style.marginLeft = '0px' -``` - ---- - -## Performance Considerations - -### Time Complexity - -- **Overlap Detection:** O(n) where n = number of events in column -- **Stack Creation:** O(n log n) due to sorting -- **Chain Traversal:** O(n) worst case (entire chain) -- **Stack Removal:** O(n) worst case (update all subsequent) - -### Space Complexity - -- **Stack Links:** O(1) per event (stored in DOM attribute) -- **No Global State:** All state is in DOM - -### Optimization Tips - -1. **Batch Updates:** When adding multiple events, batch DOM updates -2. **Lazy Evaluation:** Only recalculate stacks when events change -3. **Event Delegation:** Use event delegation instead of per-element listeners -4. **Virtual Scrolling:** For large calendars, only render visible events - ---- - -## Implementation Guidelines - -### Separation of Concerns - -**Pure Logic (No DOM):** -- Overlap detection algorithms -- Stack link calculation -- Sorting logic - -**DOM Manipulation:** -- Applying stack links to elements -- Updating visual styles -- Chain traversal - -**Event Handling:** -- Detecting event changes -- Triggering stack recalculation -- Cleanup on event removal - -### Testing Strategy - -1. **Unit Tests:** Test overlap detection in isolation -2. **Integration Tests:** Test stack creation with DOM -3. **Visual Tests:** Test CSS styling calculations -4. **Edge Cases:** Test boundary conditions - ---- - -## Future Enhancements - -### Potential Improvements - -1. **Smart Stacking:** Detect non-overlapping sub-groups and stack independently -2. **Column Sharing:** For events with similar start times, use flexbox columns -3. **Compact Mode:** Reduce stack offset for dense calendars -4. **Color Coding:** Visual indication of stack depth -5. **Stack Preview:** Hover to highlight entire stack chain - ---- - -## Glossary - -- **Stack:** Group of overlapping events displayed with horizontal offset -- **Stack Link:** Data structure connecting events in a stack (doubly-linked list) -- **Stack Level:** Position in stack (0 = base, 1+ = offset) -- **Stack Root:** First event in stack (stackLevel 0, no prev link) -- **Stack Chain:** Complete sequence of linked events -- **Overlap:** Two events with intersecting time ranges -- **Offset:** Horizontal margin applied to stacked events (15px per level) - ---- - -**Document Version:** 1.0 -**Last Updated:** 2025-10-04 -**Status:** Conceptual Documentation - Ready for TDD Implementation diff --git a/complexity-output.json b/complexity-output.json deleted file mode 100644 index e69de29..0000000