From f86ae09ec3f015d8684407d15910b15a18012edf Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Fri, 14 Nov 2025 16:25:03 +0100 Subject: [PATCH] Refactor calendar datasource architecture Centralizes date calculation logic into DateColumnDataSource Improves separation of concerns across rendering components Key changes: - Introduces IColumnInfo interface for flexible column data - Moves date calculation from multiple managers to dedicated datasource - Removes duplicate date rendering logic - Prepares architecture for future resource-based views --- .claude/settings.local.json | 9 + ...-datasource-architecture-implementation.md | 769 ++++++++++++++++++ src/datasources/DateColumnDataSource.ts | 23 +- src/managers/DragDropManager.ts | 2 +- src/managers/GridManager.ts | 13 +- src/managers/HeaderManager.ts | 19 +- src/managers/NavigationManager.ts | 6 +- src/renderers/ColumnRenderer.ts | 11 +- src/renderers/DateHeaderRenderer.ts | 20 +- src/renderers/EventRenderer.ts | 9 +- src/renderers/EventRendererManager.ts | 7 +- src/renderers/GridRenderer.ts | 27 +- src/types/ColumnDataSource.ts | 15 +- src/types/EventTypes.ts | 2 +- src/utils/ColumnDetectionUtils.ts | 29 +- 15 files changed, 885 insertions(+), 76 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 coding-sessions/2025-11-13-datasource-architecture-implementation.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2206350 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/coding-sessions/2025-11-13-datasource-architecture-implementation.md b/coding-sessions/2025-11-13-datasource-architecture-implementation.md new file mode 100644 index 0000000..697d89f --- /dev/null +++ b/coding-sessions/2025-11-13-datasource-architecture-implementation.md @@ -0,0 +1,769 @@ +# DataSource Architecture Implementation + +**Date:** November 13, 2025 +**Type:** Architecture refactoring, Data flow optimization +**Status:** ✅ Complete (with course corrections) +**Main Goal:** Implement DataSource pattern to centralize date calculation and prepare for dual-mode calendar (date-based vs resource-based) + +--- + +## Executive Summary + +Successfully implemented DataSource architecture to centralize date calculations and clean up rendering responsibilities. Initial implementation had architectural violations that were corrected through user guidance. + +**Key Outcomes:** +- ✅ DateColumnDataSource created as single source of truth for dates +- ✅ GridRenderer responsibility corrected (structure only, not event rendering) +- ✅ ColumnRenderer receives dates instead of calculating them +- ✅ Navigation flow fixed (no duplicate rendering) +- ✅ Clean separation of concerns restored +- ⚠️ Initial conversation compaction caused architectural mistakes + +**Code Volume:** +- Created: 2 new files (DateColumnDataSource.ts, ColumnDataSource.ts interface) +- Modified: 5 files (GridManager.ts, NavigationManager.ts, GridRenderer.ts, ColumnRenderer.ts, EventTypes.ts) + +--- + +## User Corrections & Course Changes + +### Correction #1: GridRenderer Event Rendering Violation + +**What Happened:** +After conversation compaction, I mistakenly added event rendering logic directly into GridRenderer. + +**Code I Added (WRONG):** +```typescript +// GridRenderer.ts - renderColumnContainer() +private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void { + dates.forEach(date => { + const eventsForDate = events.filter(event => {...}); + + // Created columns... + + // ❌ WRONG: Rendering events directly in GridRenderer + this.renderColumnEvents(eventsForDate, eventsLayer); + }); +} + +// Added entire event layout logic to GridRenderer +private renderColumnEvents() { ... } +private renderGridGroup() { ... } +private renderEvent() { ... } +``` + +**User Intervention:** +``` +"hvorfor har du flyttet eventrendering iind i griidrendere? +det er jo columnsrenderes opgave" +``` + +followed by: + +``` +"ja naturligivs da" +``` + +**Problem:** +- GridRenderer should ONLY create grid structure (skeleton) +- Event rendering is EventRenderingService's responsibility +- ColumnRenderer creates columns, not GridRenderer's loop +- Violated separation of concerns +- Duplicated logic that already existed in DateEventRenderer + +**Correct Solution:** +```typescript +// GridRenderer.ts - renderColumnContainer() +private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void { + // Delegate to ColumnRenderer + this.columnRenderer.render(columnContainer, { + dates: dates, + config: this.config + }); + // Events rendered by EventRenderingService via GRID_RENDERED event +} +``` + +**Lesson:** After conversation compaction, I lost context about architectural boundaries. GridRenderer = structure, EventRenderingService = events, ColumnRenderer = columns. Never mix responsibilities. + +--- + +### Correction #2: NavigationManager Configuration Injection + +**What Happened:** +I attempted to inject Configuration into NavigationManager so it could calculate dates. + +**User Intervention:** +``` +"hvad er det her du har lavet... NavigationManager skal bruge Configuration +for at få adgang til workweek settings, så den kan beregne dates. +Lad mig opdatere det: + +ja det vi nok. Men hva dbruger GridManager det event tiil?" +``` + +**Problem:** +- NavigationManager doesn't need to know about workweek settings +- Date calculation should be DataSource's responsibility +- NavigationManager should focus on animation, not data + +**Initial Wrong Approach:** +```typescript +// NavigationManager.ts +constructor(..., config: Configuration) { + this.config = config; +} + +private animateTransition() { + const workWeekSettings = this.config.getWorkWeekSettings(); + const dates = this.dateService.getWorkWeekDates(targetWeek, workWeekSettings.workDays); + // ❌ NavigationManager calculating dates manually +} +``` + +**Correct Solution:** +```typescript +// NavigationManager.ts +constructor(..., config: Configuration) { + this.config = config; + this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week'); +} + +private animateTransition() { + this.dataSource.setCurrentDate(targetWeek); + const dates = this.dataSource.getColumns(); + // ✅ DataSource calculates dates +} +``` + +**Lesson:** NavigationManager needed Configuration, but not to calculate dates itself. It needed it to create a DataSource instance, which handles date calculation. + +--- + +### Correction #3: GridManager Navigation Rendering Conflict + +**What Happened:** +GridManager was calling `render()` when receiving NAVIGATION_COMPLETED event, creating conflict with NavigationManager's animation. + +**User Analysis:** +I explained the problem flow: +``` +1. NavigationManager creates NEW grid with animation +2. After animation: emits NAVIGATION_COMPLETED +3. GridManager listens and calls render() → updates OLD grid +4. Result: Two grids, broken state +``` + +User response: +``` +"ja" (approved the fix) +``` + +**Problem:** +```typescript +// GridManager.ts - BEFORE (WRONG) +eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => { + this.currentDate = detail.newDate; + this.dataSource.setCurrentDate(this.currentDate); + this.render(); // ❌ Re-renders, conflicts with NavigationManager's new grid +}); +``` + +**Correct Solution:** +```typescript +// GridManager.ts - AFTER (CORRECT) +// Listen for navigation events from NavigationManager +// NavigationManager has already created new grid with animation +// GridManager only needs to update state, NOT re-render +eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => { + this.currentDate = detail.newDate; + this.dataSource.setCurrentDate(this.currentDate); + // Do NOT call render() - NavigationManager already created new grid +}); +``` + +**Lesson:** Two managers can't both render during navigation. NavigationManager owns animation rendering, GridManager only syncs state. + +--- + +## Implementation Details + +### Phase 1: DateColumnDataSource Creation + +**File Created:** `src/datasources/DateColumnDataSource.ts` + +**Purpose:** Single source of truth for date calculation based on workweek settings. + +```typescript +export class DateColumnDataSource implements IColumnDataSource { + private dateService: DateService; + private config: Configuration; + private currentDate: Date; + private currentView: CalendarView; + + public getColumns(): Date[] { + switch (this.currentView) { + case 'week': + return this.getWeekDates(); + case 'month': + return this.getMonthDates(); + case 'day': + return [this.currentDate]; + } + } + + private getWeekDates(): Date[] { + const weekStart = this.getISOWeekStart(this.currentDate); + const workWeekSettings = this.config.getWorkWeekSettings(); + return this.dateService.getWorkWeekDates(weekStart, workWeekSettings.workDays); + } +} +``` + +**Key Methods:** +- `getColumns()` - Returns Date[] based on current view +- `setCurrentDate(date)` - Updates current date +- `setCurrentView(view)` - Switches between day/week/month +- `getType()` - Returns 'date' (vs 'resource' for future mode) + +--- + +### Phase 2: GridManager Integration + +**File Modified:** `src/managers/GridManager.ts` + +**Before:** +```typescript +// GridManager calculated dates itself +const weekStart = this.getISOWeekStart(this.currentDate); +const workWeekSettings = this.config.getWorkWeekSettings(); +const dates = this.dateService.getWorkWeekDates(weekStart, workWeekSettings.workDays); +``` + +**After:** +```typescript +// GridManager uses DataSource +constructor(...) { + this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView); +} + +public async render(): Promise { + const dates = this.dataSource.getColumns(); // Single source of truth + const events = await this.eventManager.getEventsForPeriod(dates[0], dates[dates.length - 1]); + + this.gridRenderer.renderGrid(this.container, this.currentDate, this.currentView, dates, events); + + eventBus.emit(CoreEvents.GRID_RENDERED, { + container: this.container, + currentDate: this.currentDate, + dates: dates + }); +} +``` + +**Navigation Handling Fixed:** +```typescript +eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => { + this.currentDate = detail.newDate; + this.dataSource.setCurrentDate(this.currentDate); + // Removed render() call - NavigationManager already created new grid +}); +``` + +--- + +### Phase 3: NavigationManager DataSource Integration + +**File Modified:** `src/managers/NavigationManager.ts` + +**Added:** +```typescript +import { DateColumnDataSource } from '../datasources/DateColumnDataSource'; +import { Configuration } from '../configurations/CalendarConfig'; + +constructor(..., config: Configuration) { + this.config = config; + this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week'); +} +``` + +**Animation Flow Updated:** +```typescript +private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { + // Update DataSource with target week and get dates + this.dataSource.setCurrentDate(targetWeek); + const dates = this.dataSource.getColumns(); + + // Create new grid with correct dates + newGrid = this.gridRenderer.createNavigationGrid(container, dates); + + // Animate transition... + + // After animation: emit NAVIGATION_COMPLETED + this.eventBus.emit(CoreEvents.NAVIGATION_COMPLETED, { + direction, + newDate: this.currentWeek + }); +} +``` + +--- + +### Phase 4: GridRenderer Responsibility Cleanup + +**File Modified:** `src/renderers/GridRenderer.ts` + +**Removed:** +- WorkHoursManager injection (ColumnRenderer handles it) +- Event rendering logic (EventRenderingService handles it) +- Column creation loop (ColumnRenderer handles it) + +**Kept:** +- Grid skeleton creation (swp-grid-container, swp-time-grid) +- Time axis rendering +- Delegation to ColumnRenderer + +**Before (WRONG - after conversation compaction):** +```typescript +private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void { + dates.forEach(date => { + const column = document.createElement('swp-day-column'); + // Apply work hours styling + this.applyWorkHoursStyling(column, date); + // Render events + this.renderColumnEvents(eventsForDate, eventsLayer); + }); +} +``` + +**After (CORRECT):** +```typescript +private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void { + // Delegate to ColumnRenderer + this.columnRenderer.render(columnContainer, { + dates: dates, + config: this.config + }); + // Events rendered by EventRenderingService via GRID_RENDERED event +} +``` + +--- + +### Phase 5: ColumnRenderer Context Update + +**File Modified:** `src/renderers/ColumnRenderer.ts` + +**Context Interface Changed:** +```typescript +// Before +export interface IColumnRenderContext { + currentWeek: Date; + config: Configuration; +} + +// After +export interface IColumnRenderContext { + dates: Date[]; // ← Receives dates instead of calculating them + config: Configuration; +} +``` + +**Render Method Updated:** +```typescript +// Before +render(columnContainer: HTMLElement, context: IColumnRenderContext): void { + const { currentWeek, config } = context; + const workWeekSettings = config.getWorkWeekSettings(); + const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); + // ❌ ColumnRenderer calculating dates +} + +// After +render(columnContainer: HTMLElement, context: IColumnRenderContext): void { + const { dates } = context; + // ✅ ColumnRenderer receives dates from DataSource + + dates.forEach((date) => { + const column = document.createElement('swp-day-column'); + this.applyWorkHoursToColumn(column, date); + // ... + }); +} +``` + +--- + +## Architecture Flow + +### Before DataSource: + +``` +┌──────────────────┐ +│ GridManager │ ← Calculates dates manually +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ GridRenderer │ ← No dates parameter +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ ColumnRenderer │ ← Calculates dates again (duplicate logic) +└──────────────────┘ + +NavigationManager: Also calculates dates separately +``` + +### After DataSource: + +``` +┌─────────────────────┐ +│ DateColumnDataSource│ ← Single source of truth for dates +└──────────┬──────────┘ + │ + ┌────┴────────────────────┐ + │ │ +┌─────▼────────┐ ┌────────▼──────────┐ +│ GridManager │ │ NavigationManager │ +└─────┬────────┘ └────────┬──────────┘ + │ │ + │ dates[] │ dates[] + │ │ + ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ +│ GridRenderer │ │ GridRenderer │ +└────────┬─────────┘ │ (createNavGrid) │ + │ └──────────────────┘ + ▼ +┌──────────────────┐ +│ ColumnRenderer │ ← Receives dates, creates columns +└────────┬─────────┘ + │ + ▼ +┌──────────────────┐ +│ swp-day-column │ ← Column structure with work hours +└──────────────────┘ + +EventRenderingService ← Listens to GRID_RENDERED, renders events +``` + +--- + +## Responsibilities After Refactoring + +### DateColumnDataSource +- ✅ Calculate dates based on view (day/week/month) +- ✅ Apply workweek settings +- ✅ Single source of truth for dates + +### GridManager +- ✅ Coordinate rendering flow +- ✅ Get dates from DataSource +- ✅ Fetch events from EventManager +- ✅ Call GridRenderer with dates + events +- ✅ Emit GRID_RENDERED event +- ❌ NOT calculate dates itself +- ❌ NOT render during NAVIGATION_COMPLETED + +### NavigationManager +- ✅ Handle animation +- ✅ Use DataSource to get dates for new grid +- ✅ Create new grid via GridRenderer.createNavigationGrid() +- ✅ Emit NAVIGATION_COMPLETED after animation +- ❌ NOT calculate dates manually + +### GridRenderer +- ✅ Create grid skeleton structure +- ✅ Render time axis +- ✅ Delegate column creation to ColumnRenderer +- ❌ NOT create columns directly +- ❌ NOT render events +- ❌ NOT handle work hours styling + +### ColumnRenderer +- ✅ Receive dates from context +- ✅ Create swp-day-column elements +- ✅ Apply work hours styling +- ✅ Create eventsLayer structure +- ❌ NOT calculate dates itself + +### EventRenderingService +- ✅ Listen to GRID_RENDERED event +- ✅ Render events into columns +- ✅ Handle event layout (stacked vs grid) +- ❌ NOT be called directly by GridRenderer + +--- + +## Challenges & Solutions + +### Challenge 1: Conversation Compaction Memory Loss + +**Issue:** After conversation compaction, I lost context about architectural boundaries and started violating separation of concerns. + +**Impact:** +- Added event rendering to GridRenderer +- Added column creation loop to GridRenderer +- Duplicated logic that already existed + +**Solution:** +User corrections guided me back to correct architecture: +1. GridRenderer delegates to ColumnRenderer +2. EventRenderingService handles events via GRID_RENDERED +3. DataSource calculates dates, not managers + +**Lesson:** After conversation compaction, explicitly review architectural boundaries before implementing. Ask about responsibilities if unsure. + +--- + +### Challenge 2: Navigation Rendering Conflict + +**Issue:** Both NavigationManager and GridManager tried to render during navigation, creating duplicate grids. + +**Root Cause:** +``` +NavigationManager: Creates NEW grid with animation +GridManager: Receives NAVIGATION_COMPLETED → renders AGAIN +Result: Two grids, broken state +``` + +**Solution:** +GridManager only updates state on NAVIGATION_COMPLETED, doesn't re-render: +```typescript +eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => { + this.currentDate = detail.newDate; + this.dataSource.setCurrentDate(this.currentDate); + // Removed render() call +}); +``` + +--- + +### Challenge 3: DataSource Scope Confusion + +**Issue:** Initially unsure whether NavigationManager should calculate dates or use DataSource. + +**User Guidance:** +NavigationManager should use DateColumnDataSource just like GridManager, ensuring consistent date calculation. + +**Solution:** +Both managers create their own DataSource instances: +```typescript +// GridManager +this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView); + +// NavigationManager +this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week'); +``` + +--- + +## Navigation Flow (Final) + +### User Clicks "Next Week": + +``` +1. NavigationButtons + ├─ Calculates newDate (next week) + └─ emit NAV_BUTTON_CLICKED { direction: 'next', newDate } + +2. NavigationManager (listens to NAV_BUTTON_CLICKED) + ├─ Updates DataSource with newDate + ├─ Gets dates from DataSource.getColumns() + ├─ Creates NEW grid: createNavigationGrid(dates) + ├─ Animates slide transition (old grid out, new grid in) + └─ After animation: emit NAVIGATION_COMPLETED { direction: 'next', newDate } + +3. GridManager (listens to NAVIGATION_COMPLETED) + ├─ Updates currentDate + ├─ Updates dataSource.setCurrentDate() + └─ Does NOT call render() ← Key fix! + +4. EventRenderingService (listens to GRID_RENDERED - emitted during step 2) + ├─ Reads GRID_RENDERED payload { container, dates } + ├─ Fetches events for date range + └─ Renders events into new grid's columns +``` + +**Result:** Single grid created by NavigationManager with animation, events rendered by EventRenderingService, GridManager stays in sync. + +--- + +## Files Modified + +### Created: +1. `src/types/ColumnDataSource.ts` - IColumnDataSource interface +2. `src/datasources/DateColumnDataSource.ts` - Date-based DataSource implementation + +### Modified: +1. `src/managers/GridManager.ts` + - Added DateColumnDataSource + - Removed date calculation logic + - Fixed NAVIGATION_COMPLETED handler (removed render() call) + +2. `src/managers/NavigationManager.ts` + - Added DateColumnDataSource + - Uses DataSource.getColumns() instead of manual calculation + - Added Configuration injection + +3. `src/renderers/GridRenderer.ts` + - Removed WorkHoursManager dependency + - Removed event rendering logic (corrected after mistake) + - Removed column creation loop (delegates to ColumnRenderer) + - Updated createNavigationGrid() to accept dates array + +4. `src/renderers/ColumnRenderer.ts` + - Changed IColumnRenderContext to receive dates array + - Removed date calculation logic + - Simplified render() method + +5. `src/types/EventTypes.ts` + - No changes needed (considered adding skipRender flag but not required) + +--- + +## Test Results + +### Build Status: +```bash +npm run build +# ✅ Success +# Total transform time: 1105.12ms +# No TypeScript errors +``` + +### Manual Testing Required: +- ❓ Navigation animation works correctly +- ❓ Events render after navigation +- ❓ Work hours styling applied +- ❓ Workweek preset changes reflected + +**Note:** No automated tests exist for navigation flow or DataSource. + +--- + +## Lessons Learned + +### 1. Conversation Compaction Awareness +- **Problem:** Lost architectural context after compaction +- **Impact:** Violated separation of concerns, added code to wrong places +- **Solution:** After compaction, explicitly review architectural boundaries before coding +- **Prevention:** Ask user to confirm responsibilities if unsure + +### 2. Separation of Concerns Is Critical +- **GridRenderer:** Structure only +- **ColumnRenderer:** Columns only +- **EventRenderingService:** Events only +- **DataSource:** Dates only +- **Mixing these leads to maintenance hell** + +### 3. Single Source of Truth Pattern +- **Before:** 3 places calculated dates (GridManager, ColumnRenderer, NavigationManager) +- **After:** 1 place calculates dates (DateColumnDataSource) +- **Benefit:** Change workweek logic once, applies everywhere + +### 4. Event-Driven Rendering Conflicts +- Two managers can't both render on same event +- NavigationManager owns animation rendering +- GridManager only syncs state during navigation +- Clear ownership prevents bugs + +### 5. Dependency Injection Must Match Responsibility +- GridRenderer doesn't need WorkHoursManager (ColumnRenderer does) +- NavigationManager needs Configuration (to create DataSource) +- Only inject what's actually used + +--- + +## Future Considerations + +### Potential Improvements + +1. **Resource-Based View Support** + - Create `ResourceColumnDataSource` implementing `IColumnDataSource` + - Returns resource columns instead of date columns + - Switch between DateColumnDataSource and ResourceColumnDataSource based on view mode + +2. **DataSource Caching** + - Cache calculated dates to avoid recalculation + - Invalidate cache on workweek settings change + - Performance optimization for frequent navigation + +3. **Navigation Testing** + - Add integration tests for navigation flow + - Test animation + event rendering coordination + - Verify no duplicate grids created + +4. **DataSource Configuration** + - Allow DataSource to be configured via DI container + - Support custom DataSource implementations + - Enable A/B testing of different date calculation strategies + +--- + +## Code Statistics + +### Lines Changed: +- Created: ~150 lines (DateColumnDataSource + interface) +- Modified: ~80 lines (GridManager, NavigationManager, GridRenderer, ColumnRenderer) +- Removed: ~60 lines (duplicate date calculations, event rendering in GridRenderer) +- Net: +170 lines + +### Complexity: +- Cyclomatic complexity reduced (centralized date logic) +- Coupling reduced (fewer dependencies in GridRenderer) +- Cohesion improved (each class has single responsibility) + +--- + +## Production Readiness + +### ✅ Ready for Testing + +**Confidence Level:** Medium-High + +**Reasons:** +1. Build succeeds without errors +2. Architecture follows separation of concerns +3. All architectural violations corrected +4. User-guided course corrections applied + +**Concerns:** +1. No automated tests for navigation flow +2. Manual testing required +3. Event rendering after navigation not verified +4. DataSource not tested with different workweek configs + +**Testing Checklist:** +- [ ] Navigate next/previous week +- [ ] Verify events render after animation +- [ ] Check work hours styling applied +- [ ] Change workweek preset and navigate +- [ ] Verify no duplicate grids created +- [ ] Test navigation while events are being dragged (edge case) + +**Rollback Plan:** +If navigation breaks: +1. Git revert to before DataSource implementation +2. Restore direct date calculations in managers +3. Restore GridManager render() call on NAVIGATION_COMPLETED + +--- + +## Conclusion + +Successfully implemented DataSource architecture despite initial architectural violations caused by conversation compaction. User corrections guided the implementation back to correct separation of concerns. + +The migration process revealed: +1. Importance of maintaining architectural awareness after conversation compaction +2. Value of centralized data calculation (DataSource pattern) +3. Critical need for clear responsibility boundaries +4. How event-driven architecture requires careful coordination + +**Key Success:** Single source of truth for dates, clean separation of structure/columns/events rendering, navigation rendering conflict resolved. + +**Key Learning:** After conversation compaction, always verify architectural boundaries before implementing. + +**Status:** ✅ Implementation complete, ready for manual testing. diff --git a/src/datasources/DateColumnDataSource.ts b/src/datasources/DateColumnDataSource.ts index 5edf94e..9bf5fc6 100644 --- a/src/datasources/DateColumnDataSource.ts +++ b/src/datasources/DateColumnDataSource.ts @@ -1,4 +1,4 @@ -import { IColumnDataSource } from '../types/ColumnDataSource'; +import { IColumnDataSource, IColumnInfo } from '../types/ColumnDataSource'; import { DateService } from '../utils/DateService'; import { Configuration } from '../configurations/CalendarConfig'; import { CalendarView } from '../types/CalendarTypes'; @@ -32,17 +32,28 @@ export class DateColumnDataSource implements IColumnDataSource { /** * Get columns (dates) to display */ - public getColumns(): Date[] { + public getColumns(): IColumnInfo[] { + let dates: Date[]; + switch (this.currentView) { case 'week': - return this.getWeekDates(); + dates = this.getWeekDates(); + break; case 'month': - return this.getMonthDates(); + dates = this.getMonthDates(); + break; case 'day': - return [this.currentDate]; + dates = [this.currentDate]; + break; default: - return this.getWeekDates(); + dates = this.getWeekDates(); } + + // Convert Date[] to IColumnInfo[] + return dates.map(date => ({ + identifier: this.dateService.formatISODate(date), + data: date + })); } /** diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index c16d48b..209de3d 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -738,7 +738,7 @@ export class DragDropManager { } const dragMouseLeavePayload: IDragMouseLeaveHeaderEventPayload = { - targetDate: targetColumn.date, + targetColumn: targetColumn, mousePosition: position, originalElement: this.originalElement, draggedClone: this.draggedClone diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 6ad0708..5faa9f9 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -87,20 +87,21 @@ export class GridManager { return; } - // Get dates from datasource - single source of truth - const dates = this.dataSource.getColumns(); + // Get columns from datasource - single source of truth + const columns = this.dataSource.getColumns(); - // Get events for the period from EventManager + // Extract dates for EventManager query + const dates = columns.map(col => col.data as Date); const startDate = dates[0]; const endDate = dates[dates.length - 1]; const events = await this.eventManager.getEventsForPeriod(startDate, endDate); - // Delegate to GridRenderer with dates and events + // Delegate to GridRenderer with columns and events this.gridRenderer.renderGrid( this.container, this.currentDate, this.currentView, - dates, + columns, events ); @@ -108,7 +109,7 @@ export class GridManager { eventBus.emit(CoreEvents.GRID_RENDERED, { container: this.container, currentDate: this.currentDate, - dates: dates + columns: columns }); } } \ No newline at end of file diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index 9462a5c..4ae245a 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -4,6 +4,7 @@ import { CoreEvents } from '../constants/CoreEvents'; import { IHeaderRenderer, IHeaderRenderContext } from '../renderers/DateHeaderRenderer'; import { IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IHeaderReadyEventPayload } from '../types/EventTypes'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; +import { DateColumnDataSource } from '../datasources/DateColumnDataSource'; /** * HeaderManager - Handles all header-related event logic @@ -13,10 +14,12 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; export class HeaderManager { private headerRenderer: IHeaderRenderer; private config: Configuration; + private dataSource: DateColumnDataSource; - constructor(headerRenderer: IHeaderRenderer, config: Configuration) { + constructor(headerRenderer: IHeaderRenderer, config: Configuration, dataSource: DateColumnDataSource) { this.headerRenderer = headerRenderer; this.config = config; + this.dataSource = dataSource; // Bind handler methods for event listeners this.handleDragMouseEnterHeader = this.handleDragMouseEnterHeader.bind(this); @@ -43,11 +46,11 @@ export class HeaderManager { * Handle drag mouse enter header event */ private handleDragMouseEnterHeader(event: Event): void { - const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } = + const { targetColumn, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; console.log('🎯 HeaderManager: Received drag:mouseenter-header', { - targetDate, + targetColumn: targetColumn.identifier, originalElement: !!originalElement, cloneElement: !!cloneElement }); @@ -57,11 +60,11 @@ export class HeaderManager { * Handle drag mouse leave header event */ private handleDragMouseLeaveHeader(event: Event): void { - const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = + const { targetColumn, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; console.log('🚪 HeaderManager: Received drag:mouseleave-header', { - targetDate, + targetColumn: targetColumn?.identifier, originalElement: !!originalElement, cloneElement: !!cloneElement }); @@ -111,9 +114,13 @@ export class HeaderManager { // Clear existing content calendarHeader.innerHTML = ''; + // Update DataSource with current date and get columns + this.dataSource.setCurrentDate(currentDate); + const columns = this.dataSource.getColumns(); + // Render new header content using injected renderer const context: IHeaderRenderContext = { - currentWeek: currentDate, + columns: columns, config: this.config }; diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index a3d0405..4fa93b0 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -191,12 +191,12 @@ export class NavigationManager { console.log('Calling GridRenderer instead of NavigationRenderer'); console.log('Target week:', targetWeek); - // Update DataSource with target week and get dates + // Update DataSource with target week and get columns this.dataSource.setCurrentDate(targetWeek); - const dates = this.dataSource.getColumns(); + const columns = this.dataSource.getColumns(); // Always create a fresh container for consistent behavior - newGrid = this.gridRenderer.createNavigationGrid(container, dates); + newGrid = this.gridRenderer.createNavigationGrid(container, columns); console.groupEnd(); diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index e64506b..638cd88 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -3,6 +3,7 @@ import { Configuration } from '../configurations/CalendarConfig'; import { DateService } from '../utils/DateService'; import { WorkHoursManager } from '../managers/WorkHoursManager'; +import { IColumnInfo } from '../types/ColumnDataSource'; /** * Interface for column rendering strategies @@ -15,7 +16,7 @@ export interface IColumnRenderer { * Context for column rendering */ export interface IColumnRenderContext { - dates: Date[]; + columns: IColumnInfo[]; config: Configuration; } @@ -35,11 +36,13 @@ export class DateColumnRenderer implements IColumnRenderer { } render(columnContainer: HTMLElement, context: IColumnRenderContext): void { - const { dates } = context; + const { columns } = context; - dates.forEach((date) => { + columns.forEach((columnInfo) => { + const date = columnInfo.data as Date; const column = document.createElement('swp-day-column'); - (column as any).dataset.date = this.dateService.formatISODate(date); + + column.dataset.columnId = columnInfo.identifier; // Apply work hours styling this.applyWorkHoursToColumn(column, date); diff --git a/src/renderers/DateHeaderRenderer.ts b/src/renderers/DateHeaderRenderer.ts index 67d6e80..bc5eff8 100644 --- a/src/renderers/DateHeaderRenderer.ts +++ b/src/renderers/DateHeaderRenderer.ts @@ -2,6 +2,7 @@ import { Configuration } from '../configurations/CalendarConfig'; import { DateService } from '../utils/DateService'; +import { IColumnInfo } from '../types/ColumnDataSource'; /** * Interface for header rendering strategies @@ -15,7 +16,7 @@ export interface IHeaderRenderer { * Context for header rendering */ export interface IHeaderRenderContext { - currentWeek: Date; + columns: IColumnInfo[]; config: Configuration; } @@ -26,26 +27,22 @@ export class DateHeaderRenderer implements IHeaderRenderer { private dateService!: DateService; render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void { - const { currentWeek, config } = context; + const { columns, config } = context; // FIRST: Always create all-day container as part of standard header structure const allDayContainer = document.createElement('swp-allday-container'); calendarHeader.appendChild(allDayContainer); // Initialize date service with timezone and locale from config - const timezone = config.timeFormatConfig.timezone; const locale = config.timeFormatConfig.locale; this.dateService = new DateService(config); - const workWeekSettings = config.getWorkWeekSettings(); - const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays); - const weekDays = config.dateViewSettings.weekDays; - const daysToShow = dates.slice(0, weekDays); - - daysToShow.forEach((date, index) => { + columns.forEach((columnInfo) => { + const date = columnInfo.data as Date; const header = document.createElement('swp-day-header'); + if (this.dateService.isSameDay(date, new Date())) { - (header as any).dataset.today = 'true'; + header.dataset.today = 'true'; } const dayName = this.dateService.getDayName(date, 'long', locale).toUpperCase(); @@ -54,7 +51,8 @@ export class DateHeaderRenderer implements IHeaderRenderer { ${dayName} ${date.getDate()} `; - (header as any).dataset.date = this.dateService.formatISODate(date); + + header.dataset.columnId = columnInfo.identifier; calendarHeader.appendChild(header); }); diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 694d10b..2e72007 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -390,14 +390,15 @@ export class DateEventRenderer implements IEventRenderer { } protected getEventsForColumn(column: HTMLElement, events: ICalendarEvent[]): ICalendarEvent[] { - const columnDate = column.dataset.date; - if (!columnDate) { + const columnId = column.dataset.columnId; + if (!columnId) { return []; } // Create start and end of day for interval overlap check - const columnStart = this.dateService.parseISO(`${columnDate}T00:00:00`); - const columnEnd = this.dateService.parseISO(`${columnDate}T23:59:59.999`); + // In date-mode, columnId is ISO date string like "2024-11-13" + const columnStart = this.dateService.parseISO(`${columnId}T00:00:00`); + const columnEnd = this.dateService.parseISO(`${columnId}T23:59:59.999`); const columnEvents = events.filter(event => { // Interval overlap: event overlaps with column day if event.start < columnEnd AND event.end > columnStart diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 092b6e6..3855210 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -91,12 +91,15 @@ export class EventRenderingService { * Handle GRID_RENDERED event - render events in the current grid */ private handleGridRendered(event: CustomEvent): void { - const { container, dates } = event.detail; + const { container, columns } = event.detail; - if (!container || !dates || dates.length === 0) { + if (!container || !columns || columns.length === 0) { return; } + // Extract dates from columns + const dates = columns.map((col: any) => col.data as Date); + // Calculate startDate and endDate from dates array const startDate = dates[0]; const endDate = dates[dates.length - 1]; diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 0574a31..3929c49 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -5,6 +5,7 @@ import { eventBus } from '../core/EventBus'; import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; import { TimeFormatter } from '../utils/TimeFormatter'; +import { IColumnInfo } from '../types/ColumnDataSource'; /** * GridRenderer - Centralized DOM rendering for calendar grid structure @@ -111,7 +112,7 @@ export class GridRenderer { grid: HTMLElement, currentDate: Date, view: CalendarView = 'week', - dates: Date[] = [], + columns: IColumnInfo[] = [], events: ICalendarEvent[] = [] ): void { @@ -124,10 +125,10 @@ export class GridRenderer { // Only clear and rebuild if grid is empty (first render) if (grid.children.length === 0) { - this.createCompleteGridStructure(grid, currentDate, view, dates, events); + this.createCompleteGridStructure(grid, currentDate, view, columns, events); } else { // Optimized update - only refresh dynamic content - this.updateGridContent(grid, currentDate, view, dates, events); + this.updateGridContent(grid, currentDate, view, columns, events); } } @@ -151,7 +152,7 @@ export class GridRenderer { grid: HTMLElement, currentDate: Date, view: CalendarView, - dates: Date[], + columns: IColumnInfo[], events: ICalendarEvent[] ): void { // Create all elements in memory first for better performance @@ -167,7 +168,7 @@ export class GridRenderer { fragment.appendChild(timeAxis); // Create grid container with caching - const gridContainer = this.createOptimizedGridContainer(dates, events); + const gridContainer = this.createOptimizedGridContainer(columns, events); this.cachedGridContainer = gridContainer; fragment.appendChild(gridContainer); @@ -218,7 +219,7 @@ export class GridRenderer { * @returns Complete grid container element */ private createOptimizedGridContainer( - dates: Date[], + columns: IColumnInfo[], events: ICalendarEvent[] ): HTMLElement { const gridContainer = document.createElement('swp-grid-container'); @@ -237,7 +238,7 @@ export class GridRenderer { // Create column container const columnContainer = document.createElement('swp-day-columns'); - this.renderColumnContainer(columnContainer, dates, events); + this.renderColumnContainer(columnContainer, columns, events); timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); @@ -259,12 +260,12 @@ export class GridRenderer { */ private renderColumnContainer( columnContainer: HTMLElement, - dates: Date[], + columns: IColumnInfo[], events: ICalendarEvent[] ): void { // Delegate to ColumnRenderer this.columnRenderer.render(columnContainer, { - dates: dates, + columns: columns, config: this.config }); } @@ -285,14 +286,14 @@ export class GridRenderer { grid: HTMLElement, currentDate: Date, view: CalendarView, - dates: Date[], + columns: IColumnInfo[], events: ICalendarEvent[] ): void { // Update column container if needed const columnContainer = grid.querySelector('swp-day-columns'); if (columnContainer) { columnContainer.innerHTML = ''; - this.renderColumnContainer(columnContainer as HTMLElement, dates, events); + this.renderColumnContainer(columnContainer as HTMLElement, columns, events); } } /** @@ -308,9 +309,9 @@ export class GridRenderer { * @param dates - Array of dates to render * @returns New grid element ready for animation */ - public createNavigationGrid(parentContainer: HTMLElement, dates: Date[]): HTMLElement { + public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[]): HTMLElement { // Create grid structure without events (events rendered by EventRenderingService) - const newGrid = this.createOptimizedGridContainer(dates, []); + const newGrid = this.createOptimizedGridContainer(columns, []); // Position new grid for animation - NO transform here, let Animation API handle it newGrid.style.position = 'absolute'; diff --git a/src/types/ColumnDataSource.ts b/src/types/ColumnDataSource.ts index 442cc59..16c12c9 100644 --- a/src/types/ColumnDataSource.ts +++ b/src/types/ColumnDataSource.ts @@ -1,3 +1,12 @@ +/** + * Column information container + * Contains both identifier and actual data for a column + */ +export interface IColumnInfo { + identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode) + data: Date | any; // Date for date-mode, IResource for resource-mode +} + /** * IColumnDataSource - Defines the contract for providing column data * @@ -6,10 +15,10 @@ */ export interface IColumnDataSource { /** - * Get the list of column identifiers to render - * @returns Array of identifiers (dates or resource IDs) + * Get the list of columns to render + * @returns Array of column information */ - getColumns(): Date[]; + getColumns(): IColumnInfo[]; /** * Get the type of columns this datasource provides diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index 3344ea7..919c1b6 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -60,7 +60,7 @@ export interface IDragMouseEnterHeaderEventPayload { // Drag mouse leave header event payload export interface IDragMouseLeaveHeaderEventPayload { - targetDate: string | null; + targetColumn: IColumnBounds | null; mousePosition: IMousePosition; originalElement: HTMLElement| null; draggedClone: HTMLElement| null; diff --git a/src/utils/ColumnDetectionUtils.ts b/src/utils/ColumnDetectionUtils.ts index 148015a..d650477 100644 --- a/src/utils/ColumnDetectionUtils.ts +++ b/src/utils/ColumnDetectionUtils.ts @@ -7,7 +7,7 @@ import { IMousePosition } from "../types/DragDropTypes"; export interface IColumnBounds { - date: string; + identifier: string; left: number; right: number; boundingClientRect: DOMRect, @@ -31,15 +31,15 @@ export class ColumnDetectionUtils { // Cache hver kolonnes x-grænser columns.forEach(column => { const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; + const identifier = (column as HTMLElement).dataset.columnId; - if (date) { + if (identifier) { this.columnBoundsCache.push({ boundingClientRect : rect, element: column as HTMLElement, - date, + identifier, left: rect.left, - right: rect.right, + right: rect.right, index: index++ }); } @@ -68,18 +68,15 @@ export class ColumnDetectionUtils { } /** - * Get column bounds by Date + * Get column bounds by identifier */ - public static getColumnBoundsByDate(date: Date): IColumnBounds | null { + public static getColumnBoundsByIdentifier(identifier: string): IColumnBounds | null { if (this.columnBoundsCache.length === 0) { this.updateColumnBoundsCache(); } - // Convert Date to YYYY-MM-DD format - let dateString = date.toISOString().split('T')[0]; - - // Find column that matches the date - let column = this.columnBoundsCache.find(col => col.date === dateString); + // Find column that matches the identifier + let column = this.columnBoundsCache.find(col => col.identifier === identifier); return column || null; } @@ -96,15 +93,15 @@ export class ColumnDetectionUtils { // Cache hver kolonnes x-grænser dayColumns.forEach(column => { const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; + const identifier = (column as HTMLElement).dataset.columnId; - if (date) { + if (identifier) { dayHeaders.push({ boundingClientRect : rect, element: column as HTMLElement, - date, + identifier, left: rect.left, - right: rect.right, + right: rect.right, index: index++ }); }