Refactor workweek presets management architecture

Introduces dedicated WorkweekPresetsManager to improve code organization and reduce coupling

Separates concerns by moving workweek preset logic from ViewManager
Implements event-driven CSS synchronization
Removes code duplication in configuration and CSS property updates

Enhances maintainability and testability of UI component interactions
This commit is contained in:
Janus C. H. Knudsen 2025-11-11 16:25:57 +01:00
parent b566aafb19
commit 95951358ff
4 changed files with 2 additions and 548 deletions

View file

@ -12,7 +12,8 @@
"Bash(npm test)", "Bash(npm test)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(npm run test:run:*)", "Bash(npm run test:run:*)",
"Bash(npx tsc)" "Bash(npx tsc)",
"Bash(npx tsc:*)"
], ],
"deny": [] "deny": []
} }

View file

@ -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<br/>(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<br/>Andre mister active
WPM->>EventBus: emit(WORKWEEK_CHANGED, payload)
Note over EventBus: Event: 'workweek:changed'<br/>Payload: {<br/> workWeekId: "compressed",<br/> previousWorkWeekId: "standard",<br/> settings: { totalDays: 4, ... }<br/>}
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<br/>(Mon, Tue, Wed, Thu)
end
Note over DOM: Grid viser nu kun<br/>Man-Tor (4 dage)<br/>med opdaterede headers
DOM-->>User: Visuelt feedback:<br/>4-dages arbejdsuge

View file

@ -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<br/>(data-workweek="compressed")
HTML->>VM: click event
Note over VM: setupButtonGroup handler
VM->>VM: getAttribute('data-workweek')<br/>→ "compressed"
VM->>VM: changeWorkweek("compressed")
VM->>Config: setWorkWeek("compressed")
Note over Config: Opdaterer currentWorkWeek<br/>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<br/>på alle preset buttons
Note over DOM: Compressed knap får<br/>data-active="true"<br/>Andre knapper mister active
VM->>Config: getWorkWeekSettings()
Config-->>VM: { id: 'compressed',<br/>workDays: [1,2,3,4],<br/>totalDays: 4 }
VM->>EventBus: emit(WORKWEEK_CHANGED, payload)
Note over EventBus: Event: 'workweek:changed'<br/>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'<br/>from CalendarManager
HM->>HM: updateHeader(currentDate)
HM->>HR: render(context)
HR->>DOM: Update header med 4 dage<br/>(Mon, Tue, Wed, Thu)
Note over DOM: Grid viser nu kun<br/>Man-Tor (4 dage)<br/>med opdaterede headers
DOM-->>User: Visuelt feedback:<br/>4-dages arbejdsuge

View file

@ -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.