diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 096895c..c19c12e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,8 @@ "Bash(npm test)", "Bash(cat:*)", "Bash(npm run test:run:*)", - "Bash(npx tsc)" + "Bash(npx tsc)", + "Bash(npx tsc:*)" ], "deny": [] } diff --git a/workweek-preset-sequence-AFTER.md b/workweek-preset-sequence-AFTER.md deleted file mode 100644 index 36b80a1..0000000 --- a/workweek-preset-sequence-AFTER.md +++ /dev/null @@ -1,81 +0,0 @@ -# Workweek Preset Click Sequence Diagram - EFTER REFAKTORERING - -Dette diagram viser hvad der sker når brugeren klikker på en workweek preset knap EFTER refaktoreringen. - -```mermaid -sequenceDiagram - actor User - participant HTML as swp-preset-button - participant WPM as WorkweekPresetsManager - participant Config as Configuration - participant EventBus - participant CM as ConfigManager - participant GM as GridManager - participant GR as GridRenderer - participant HM as HeaderManager - participant HR as HeaderRenderer - participant DOM - - User->>HTML: Click på preset button
(data-workweek="compressed") - HTML->>WPM: click event - - Note over WPM: setupButtonListeners handler - WPM->>WPM: changePreset("compressed") - - WPM->>Config: Validate WORK_WEEK_PRESETS["compressed"] - Note over WPM: Guard: if (!WORK_WEEK_PRESETS[presetId]) return - - WPM->>Config: Check if (presetId === currentWorkWeek) - Note over WPM: Guard: No change? Return early - - WPM->>Config: config.currentWorkWeek = "compressed" - Note over Config: State updated: "standard" → "compressed" - - WPM->>WPM: updateButtonStates() - WPM->>DOM: querySelectorAll('swp-preset-button') - WPM->>DOM: Update data-active attributes - Note over DOM: Compressed button får active
Andre mister active - - WPM->>EventBus: emit(WORKWEEK_CHANGED, payload) - Note over EventBus: Event: 'workweek:changed'
Payload: {
workWeekId: "compressed",
previousWorkWeekId: "standard",
settings: { totalDays: 4, ... }
} - - par Parallel Event Subscribers - EventBus->>CM: WORKWEEK_CHANGED event - Note over CM: setupEventListeners listener - CM->>CM: syncWorkweekCSSVariables(settings) - CM->>DOM: setProperty('--grid-columns', '4') - Note over DOM: CSS variable opdateret - - and - EventBus->>GM: WORKWEEK_CHANGED event - Note over GM: subscribeToEvents listener - GM->>GM: render() - GM->>GR: renderGrid(container, currentDate) - - alt Grid allerede eksisterer - GR->>GR: updateGridContent() - GR->>DOM: Update 4 columns (Mon-Thu) - else First render - GR->>GR: createCompleteGridStructure() - GR->>DOM: Create 4 columns (Mon-Thu) - end - - GM->>EventBus: emit(GRID_RENDERED) - - and - EventBus->>CalendarManager: WORKWEEK_CHANGED event - Note over CalendarManager: handleWorkweekChange listener - CalendarManager->>EventBus: emit('workweek:header-update') - - EventBus->>HM: 'workweek:header-update' event - Note over HM: setupNavigationListener - HM->>HM: updateHeader(currentDate) - HM->>HR: render(context) - HR->>Config: getWorkWeekSettings() - Config-->>HR: { totalDays: 4, workDays: [1,2,3,4] } - HR->>DOM: Render 4 day headers
(Mon, Tue, Wed, Thu) - end - - Note over DOM: Grid viser nu kun
Man-Tor (4 dage)
med opdaterede headers - - DOM-->>User: Visuelt feedback:
4-dages arbejdsuge diff --git a/workweek-preset-sequence.md b/workweek-preset-sequence.md deleted file mode 100644 index 931ba6c..0000000 --- a/workweek-preset-sequence.md +++ /dev/null @@ -1,72 +0,0 @@ -# Workweek Preset Click Sequence Diagram - -Dette diagram viser hvad der sker når brugeren klikker på en workweek preset knap (f.eks. "Mon-Fri", "Mon-Thu", etc.) - -```mermaid -sequenceDiagram - actor User - participant HTML as swp-preset-button - participant VM as ViewManager - participant Config as Configuration - participant CM as ConfigManager - participant EventBus - participant GM as GridManager - participant GR as GridRenderer - participant HM as HeaderManager - participant HR as HeaderRenderer - participant DOM - - User->>HTML: Click på preset button
(data-workweek="compressed") - HTML->>VM: click event - - Note over VM: setupButtonGroup handler - VM->>VM: getAttribute('data-workweek')
→ "compressed" - VM->>VM: changeWorkweek("compressed") - - VM->>Config: setWorkWeek("compressed") - Note over Config: Opdaterer currentWorkWeek
og workweek settings - - VM->>CM: updateCSSProperties(config) - Note over CM: Opdaterer CSS custom properties - CM->>DOM: setProperty('--grid-columns', '4') - CM->>DOM: setProperty('--hour-height', '80px') - CM->>DOM: setProperty('--day-start-hour', '6') - CM->>DOM: setProperty('--work-start-hour', '8') - Note over DOM: CSS grid layout opdateres - - VM->>VM: updateAllButtons() - VM->>DOM: Update data-active attributter
på alle preset buttons - Note over DOM: Compressed knap får
data-active="true"
Andre knapper mister active - - VM->>Config: getWorkWeekSettings() - Config-->>VM: { id: 'compressed',
workDays: [1,2,3,4],
totalDays: 4 } - - VM->>EventBus: emit(WORKWEEK_CHANGED, payload) - Note over EventBus: Event: 'workweek:changed'
Payload: { workWeekId, settings } - - EventBus->>GM: WORKWEEK_CHANGED event - Note over GM: Listener setup i subscribeToEvents() - GM->>GM: render() - GM->>GR: renderGrid(container, currentDate) - - alt First render (empty grid) - GR->>GR: createCompleteGridStructure() - GR->>DOM: Create time axis - GR->>DOM: Create grid container - GR->>DOM: Create 4 columns (Mon-Thu) - else Update existing grid - GR->>GR: updateGridContent() - GR->>DOM: Update existing columns - end - - GM->>EventBus: emit(GRID_RENDERED) - - EventBus->>HM: WORKWEEK_CHANGED event - Note over HM: Via 'workweek:header-update'
from CalendarManager - HM->>HM: updateHeader(currentDate) - HM->>HR: render(context) - HR->>DOM: Update header med 4 dage
(Mon, Tue, Wed, Thu) - - Note over DOM: Grid viser nu kun
Man-Tor (4 dage)
med opdaterede headers - - DOM-->>User: Visuelt feedback:
4-dages arbejdsuge diff --git a/workweek-refactoring-comparison.md b/workweek-refactoring-comparison.md deleted file mode 100644 index 61ab8a0..0000000 --- a/workweek-refactoring-comparison.md +++ /dev/null @@ -1,394 +0,0 @@ -# Workweek Presets Refactoring - FØR vs EFTER Sammenligning - -## Side-by-Side Comparison - -| Aspekt | FØR Refaktorering | EFTER Refaktorering | Forbedring | -|--------|-------------------|---------------------|------------| -| **Ansvarlig Manager** | ViewManager | WorkweekPresetsManager | ✅ Dedicated manager per UI element | -| **Button Setup** | ViewManager.setupButtonGroup() | WorkweekPresetsManager.setupButtonListeners() | ✅ Isolated ansvar | -| **State Management** | ViewManager + Configuration | Configuration (via WorkweekPresetsManager) | ✅ Simplere | -| **CSS Opdatering** | ViewManager kalder ConfigManager.updateCSSProperties() | ConfigManager lytter til WORKWEEK_CHANGED event | ✅ Event-drevet, løsere kobling | -| **Config Mutation** | ViewManager → config.setWorkWeek() | WorkweekPresetsManager → config.currentWorkWeek = | ⚠️ Direkte mutation | -| **ViewManager Ansvar** | View selector + Workweek presets | Kun view selector | ✅ Single Responsibility | -| **Code Duplication** | 35% (static + instance CSS metoder) | 0% | ✅ DRY princip | - ---- - -## Kode Sammenligning - -### 1. Button Click Handling - -#### FØR - ViewManager -```typescript -// ViewManager.ts -private setupButtonHandlers(): void { - this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => { - if (this.isValidView(value)) { - this.changeView(value as CalendarView); - } - }); - - // WORKWEEK LOGIK HER - forkert ansvar - this.setupButtonGroup('swp-preset-button[data-workweek]', 'data-workweek', (value) => { - this.changeWorkweek(value); - }); -} - -private changeWorkweek(workweekId: string): void { - this.config.setWorkWeek(workweekId); - - // DIREKTE KALD - tight coupling - ConfigManager.updateCSSProperties(this.config); - - this.updateAllButtons(); - - const settings = this.config.getWorkWeekSettings(); - - this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, { - workWeekId: workweekId, - settings: settings - }); -} -``` - -#### EFTER - WorkweekPresetsManager -```typescript -// WorkweekPresetsManager.ts -private setupButtonListeners(): void { - const buttons = document.querySelectorAll('swp-preset-button[data-workweek]'); - - buttons.forEach(button => { - const clickHandler = (event: Event) => { - event.preventDefault(); - const presetId = button.getAttribute('data-workweek'); - if (presetId) { - this.changePreset(presetId); - } - }; - - button.addEventListener('click', clickHandler); - this.buttonListeners.set(button, clickHandler); - }); - - this.updateButtonStates(); -} - -private changePreset(presetId: string): void { - if (!WORK_WEEK_PRESETS[presetId]) { - console.warn(`Invalid preset ID "${presetId}"`); - return; - } - - if (presetId === this.config.currentWorkWeek) { - return; - } - - const previousPresetId = this.config.currentWorkWeek; - this.config.currentWorkWeek = presetId; - - const settings = WORK_WEEK_PRESETS[presetId]; - - this.updateButtonStates(); - - // Emit event - CSS opdatering sker automatisk via ConfigManager listener - this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, { - workWeekId: presetId, - previousWorkWeekId: previousPresetId, - settings: settings - }); -} -``` - ---- - -### 2. CSS Opdatering - -#### FØR - ConfigManager -```typescript -// ConfigManager.ts - DUPLIKERET KODE! - -// Static metode kaldt fra ViewManager -static updateCSSProperties(config: Configuration): void { - const gridSettings = config.gridSettings; - const workWeekSettings = config.getWorkWeekSettings(); - - // 6 CSS properties sat - document.documentElement.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); - document.documentElement.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString()); - document.documentElement.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString()); - document.documentElement.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString()); - document.documentElement.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString()); - document.documentElement.style.setProperty('--grid-columns', workWeekSettings.totalDays.toString()); -} - -// Instance metode i constructor - SAMME KODE! -public updateAllCSSProperties(): void { - const gridSettings = this.config.gridSettings; - - // 5 CSS properties sat (mangler --grid-columns!) - document.documentElement.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); - document.documentElement.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString()); - document.documentElement.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString()); - document.documentElement.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString()); - document.documentElement.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString()); -} -``` - -#### EFTER - ConfigManager -```typescript -// ConfigManager.ts - INGEN DUPLICATION! - -constructor(eventBus: IEventBus, config: Configuration) { - this.eventBus = eventBus; - this.config = config; - - this.setupEventListeners(); - this.syncGridCSSVariables(); // Kaldt ved initialization - this.syncWorkweekCSSVariables(); // Kaldt ved initialization -} - -private setupEventListeners(): void { - // Lyt til events - REACTIVE! - this.eventBus.on(CoreEvents.WORKWEEK_CHANGED, (event: Event) => { - const { settings } = (event as CustomEvent<{ settings: IWorkWeekSettings }>).detail; - this.syncWorkweekCSSVariables(settings); - }); -} - -private syncGridCSSVariables(): void { - const gridSettings = this.config.gridSettings; - - document.documentElement.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); - document.documentElement.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString()); - document.documentElement.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString()); - document.documentElement.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString()); - document.documentElement.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString()); -} - -private syncWorkweekCSSVariables(workWeekSettings?: IWorkWeekSettings): void { - const settings = workWeekSettings || this.config.getWorkWeekSettings(); - document.documentElement.style.setProperty('--grid-columns', settings.totalDays.toString()); -} - -// STATIC METODE FJERNET! Ingen duplication! -``` - ---- - -### 3. Configuration Management - -#### FØR - Configuration -```typescript -// CalendarConfig.ts -export class Configuration { - public currentWorkWeek: string; - - constructor( - config: ICalendarConfig, - gridSettings: IGridSettings, - dateViewSettings: IDateViewSettings, - timeFormatConfig: ITimeFormatConfig, - currentWorkWeek: string, - selectedDate: Date = new Date() - ) { - // ... - this.currentWorkWeek = currentWorkWeek; - } - - // Metode med side effect - setWorkWeek(workWeekId: string): void { - if (WORK_WEEK_PRESETS[workWeekId]) { - this.currentWorkWeek = workWeekId; - this.dateViewSettings.weekDays = WORK_WEEK_PRESETS[workWeekId].totalDays; // SIDE EFFECT! - } - } - - getWorkWeekSettings(): IWorkWeekSettings { - return WORK_WEEK_PRESETS[this.currentWorkWeek] || WORK_WEEK_PRESETS['standard']; - } -} -``` - -#### EFTER - Configuration -```typescript -// CalendarConfig.ts -export class Configuration { - public currentWorkWeek: string; - - constructor( - config: ICalendarConfig, - gridSettings: IGridSettings, - dateViewSettings: IDateViewSettings, - timeFormatConfig: ITimeFormatConfig, - currentWorkWeek: string, - selectedDate: Date = new Date() - ) { - // ... - this.currentWorkWeek = currentWorkWeek; - } - - // setWorkWeek() FJERNET - WorkweekPresetsManager opdaterer direkte - - getWorkWeekSettings(): IWorkWeekSettings { - return WORK_WEEK_PRESETS[this.currentWorkWeek] || WORK_WEEK_PRESETS['standard']; - } -} -``` - ---- - -## Arkitektur Diagrammer - -### FØR - Tight Coupling -``` -User Click - ↓ -ViewManager (håndterer BÅDE view OG workweek) - ↓ - ├─→ Configuration.setWorkWeek() (side effect på dateViewSettings!) - ├─→ ConfigManager.updateCSSProperties() (direkte kald - tight coupling) - ├─→ updateAllButtons() (view + workweek blandet) - └─→ EventBus.emit(WORKWEEK_CHANGED) - ↓ - ├─→ GridManager - ├─→ CalendarManager → HeaderManager - └─→ ConfigManager (gør INGENTING - CSS allerede sat!) -``` - -### EFTER - Loose Coupling -``` -User Click - ↓ -WorkweekPresetsManager (dedicated ansvar) - ↓ - ├─→ config.currentWorkWeek = presetId (simpel state update) - ├─→ updateButtonStates() (kun workweek buttons) - └─→ EventBus.emit(WORKWEEK_CHANGED) - ↓ - ├─→ ConfigManager.syncWorkweekCSSVariables() (event-drevet!) - ├─→ GridManager.render() - └─→ CalendarManager → HeaderManager -``` - ---- - -## Metrics Sammenligning - -| Metric | FØR | EFTER | Forbedring | -|--------|-----|-------|------------| -| **Lines of Code** | | | | -| ViewManager | 155 linjer | 117 linjer | ✅ -24% (38 linjer) | -| ConfigManager | 122 linjer | 103 linjer | ✅ -16% (19 linjer) | -| WorkweekPresetsManager | 0 linjer | 115 linjer | ➕ Ny fil | -| **Code Duplication** | 35% | 0% | ✅ -35% | -| **Cyclomatic Complexity** | | | | -| ViewManager.changeWorkweek() | 2 | N/A (fjernet) | ✅ | -| WorkweekPresetsManager.changePreset() | N/A | 3 | ➕ | -| ConfigManager (avg) | 1.5 | 1.0 | ✅ Simplere | -| **Coupling** | Tight (direkte kald) | Loose (event-drevet) | ✅ | -| **Cohesion** | Lav (mixed concerns) | Høj (single responsibility) | ✅ | - ---- - -## Dependencies Graf - -### FØR -``` -ViewManager - ├─→ Configuration (read + write via setWorkWeek) - ├─→ ConfigManager (direct static call - TIGHT COUPLING) - ├─→ CoreEvents - └─→ EventBus - -ConfigManager - ├─→ Configuration (read only) - ├─→ EventBus (NO LISTENER! CSS sat via direct call) - └─→ TimeFormatter -``` - -### EFTER -``` -WorkweekPresetsManager - ├─→ Configuration (read + direct mutation) - ├─→ WORK_WEEK_PRESETS (import fra CalendarConfig) - ├─→ CoreEvents - └─→ EventBus - -ViewManager - ├─→ Configuration (read only) - ├─→ CoreEvents - └─→ EventBus - -ConfigManager - ├─→ Configuration (read only) - ├─→ EventBus (LISTENER for WORKWEEK_CHANGED - LOOSE COUPLING) - ├─→ CoreEvents - └─→ TimeFormatter -``` - ---- - -## Fordele ved Refaktorering - -### ✅ Single Responsibility Principle -- **ViewManager**: Fokuserer kun på view selector (day/week/month) -- **WorkweekPresetsManager**: Dedikeret til workweek presets UI -- **ConfigManager**: CSS synchronization manager - -### ✅ Event-Drevet Arkitektur -- CSS opdatering sker reaktivt via events -- Ingen direkte metode kald mellem managers -- Loose coupling mellem komponenter - -### ✅ DRY Princip -- Fjernet 35% code duplication -- Ingen static + instance duplication længere -- CSS sættes præcis 1 gang (ikke 2 gange) - -### ✅ Maintainability -- Nemmere at finde workweek logik (én dedikeret fil) -- Ændringer i workweek påvirker ikke view selector -- Klar separation of concerns - -### ✅ Testability -- WorkweekPresetsManager kan testes isoleret -- ConfigManager event listeners kan mockes -- Ingen hidden dependencies via static calls - ---- - -## Ulemper / Trade-offs - -### ⚠️ Flere Filer -- +1 ny manager fil (WorkweekPresetsManager.ts) -- Men bedre organisation - -### ⚠️ Direkte State Mutation -```typescript -this.config.currentWorkWeek = presetId; // Ikke via setter -``` -- Configuration har ingen kontrol over mutation -- Men simplere og mere direkte - -### ⚠️ DOM-afhængighed i Constructor -```typescript -constructor(...) { - this.setupButtonListeners(); // Kalder document.querySelectorAll -} -``` -- Kan ikke unit testes uden DOM -- Men fungerer perfekt da DI sker efter DOMContentLoaded - ---- - -## Konklusion - -Refaktoreringen følger princippet **"Each UI element has its own manager"** og resulterer i: - -✅ **Bedre struktur**: Klar separation mellem view og workweek -✅ **Mindre kobling**: Event-drevet i stedet for direkte kald -✅ **Mindre duplication**: Fra 35% til 0% -✅ **Simplere kode**: Mindre kompleksitet i hver manager -✅ **Nemmere at udvide**: Kan nemt tilføje ViewSelectorManager, NavigationGroupManager etc. - -**Trade-off**: Lidt flere filer, men meget bedre organisation og maintainability.