Cleanup test files and move to another folder
This commit is contained in:
parent
8456d8aa28
commit
9c765b35ab
28 changed files with 0 additions and 1981 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -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<string, StackLink> {
|
||||
const res = new Map<string, StackLink>();
|
||||
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
|
||||
237
CLAUDE.md
237
CLAUDE.md
|
|
@ -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 (`<swp-calendar>`)
|
||||
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 `<swp-calendar>` 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 `<swp-calendar>`:
|
||||
```html
|
||||
<swp-calendar
|
||||
data-view="week"
|
||||
data-week-days="7"
|
||||
data-snap-interval="15"
|
||||
data-day-start-hour="6"
|
||||
data-day-end-hour="22"
|
||||
data-hour-height="60"
|
||||
data-fit-to-width="false">
|
||||
</swp-calendar>
|
||||
```
|
||||
|
|
@ -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
|
||||
177
README.md
177
README.md
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
<swp-event
|
||||
data-event-id="event-1"
|
||||
data-stack-link='{"stackLevel":0,"next":"event-2"}'>
|
||||
</swp-event>
|
||||
|
||||
<swp-event
|
||||
data-event-id="event-2"
|
||||
data-stack-link='{"stackLevel":1,"prev":"event-1","next":"event-3"}'>
|
||||
</swp-event>
|
||||
|
||||
<swp-event
|
||||
data-event-id="event-3"
|
||||
data-stack-link='{"stackLevel":2,"prev":"event-2"}'>
|
||||
</swp-event>
|
||||
```
|
||||
|
||||
### 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<string, StackLink> {
|
||||
// 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<string, StackLink>()
|
||||
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue