Refactors calendar initialization with factory

Implements a factory pattern for manager creation and
initialization, improving dependency management and
extensibility.

This change replaces direct manager instantiation with a
`ManagerFactory` that handles dependency injection. This
enhances code organization and testability. It also includes
an initialization sequence diagram for better understanding
of the calendar's architecture and data flow.
This commit is contained in:
Janus Knudsen 2025-08-17 23:44:30 +02:00
parent 32ee35eb02
commit 26f0cb8aaa
11 changed files with 333 additions and 150 deletions

View file

@ -5,7 +5,8 @@
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(powershell:*)", "Bash(powershell:*)",
"Bash(rg:*)", "Bash(rg:*)",
"Bash(find:*)" "Bash(find:*)",
"Bash(mv:*)"
], ],
"deny": [] "deny": []
} }

View file

@ -0,0 +1,133 @@
# Calendar Initialization Sequence Diagram
Dette diagram viser den aktuelle initialization sekvens baseret på koden.
```mermaid
sequenceDiagram
participant Browser
participant Index as index.ts
participant MF as ManagerFactory
participant CTF as CalendarTypeFactory
participant EB as EventBus
participant CM as CalendarManager
participant EM as EventManager
participant ERS as EventRenderingService
participant GM as GridManager
participant GSM as GridStyleManager
participant GR as GridRenderer
participant SM as ScrollManager
participant NM as NavigationManager
participant NR as NavigationRenderer
participant VM as ViewManager
Browser->>Index: DOMContentLoaded
Index->>Index: initializeCalendar()
Index->>CTF: initialize()
CTF-->>Index: Factory ready
Index->>MF: getInstance()
MF-->>Index: Factory instance
Index->>MF: createManagers(eventBus, config)
MF->>EM: new EventManager(eventBus)
EM->>EB: setupEventListeners()
EM-->>MF: EventManager ready
MF->>ERS: new EventRenderingService(eventBus, eventManager)
ERS->>EB: setupEventListeners()
ERS-->>MF: EventRenderingService ready
MF->>GM: new GridManager()
GM->>GSM: new GridStyleManager(config)
GM->>GR: new GridRenderer(config)
GM->>EB: subscribeToEvents()
GM-->>MF: GridManager ready
MF->>SM: new ScrollManager()
SM->>EB: subscribeToEvents()
SM-->>MF: ScrollManager ready
MF->>NM: new NavigationManager(eventBus)
NM->>NR: new NavigationRenderer(eventBus)
NR->>EB: setupEventListeners()
NM->>EB: setupEventListeners()
NM-->>MF: NavigationManager ready
MF->>VM: new ViewManager(eventBus)
VM->>EB: setupEventListeners()
VM-->>MF: ViewManager ready
MF->>CM: new CalendarManager(eventBus, config, deps...)
CM->>EB: setupEventListeners()
CM-->>MF: CalendarManager ready
MF-->>Index: All managers created
Index->>EB: setDebug(true)
Index->>MF: initializeManagers(managers)
MF->>CM: initialize()
CM->>EM: loadData()
EM->>EM: loadMockData()
EM->>EM: processCalendarData()
EM-->>CM: Data loaded
CM->>GM: setResourceData(resourceData)
GM-->>CM: Resource data set
CM->>GM: render()
GM->>GSM: updateGridStyles(resourceData)
GM->>GR: renderGrid(grid, currentWeek, resourceData, allDayEvents)
GR-->>GM: Grid rendered
GM->>EB: emit(GRID_RENDERED, context)
EB-->>ERS: GRID_RENDERED event
ERS->>EM: getEventsForPeriod(startDate, endDate)
EM-->>ERS: Filtered events
ERS->>ERS: strategy.renderEvents()
CM->>SM: initialize()
SM->>SM: setupScrolling()
CM->>CM: setView(currentView)
CM->>EB: emit(VIEW_CHANGED, viewData)
CM->>CM: setCurrentDate(currentDate)
CM->>EB: emit(DATE_CHANGED, dateData)
CM->>EB: emit(CALENDAR_INITIALIZED, initData)
EB-->>NM: CALENDAR_INITIALIZED
NM->>NM: updateWeekInfo()
NM->>EB: emit(WEEK_INFO_UPDATED, weekInfo)
EB-->>NR: WEEK_INFO_UPDATED
NR->>NR: updateWeekInfoInDOM()
EB-->>VM: CALENDAR_INITIALIZED
VM->>VM: initializeView()
VM->>EB: emit(VIEW_RENDERED, viewData)
CM-->>MF: Initialization complete
MF-->>Index: All managers initialized
Index->>Browser: Calendar ready
```
## Aktuel Arkitektur Status
### Factory Pattern
- ManagerFactory håndterer manager instantiering
- Proper dependency injection via constructor
### Event-Driven Communication
- EventBus koordinerer kommunikation mellem managers
- NavigationRenderer lytter til WEEK_INFO_UPDATED events
- EventRenderingService reagerer på GRID_RENDERED events
### Separation of Concerns
- Managers håndterer business logic
- Renderers håndterer DOM manipulation
- EventBus håndterer kommunikation

View file

@ -0,0 +1,88 @@
import { IEventBus } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig';
import { EventManager } from '../managers/EventManager';
import { EventRenderingService } from '../renderers/EventRendererManager';
import { GridManager } from '../managers/GridManager';
import { ScrollManager } from '../managers/ScrollManager';
import { NavigationManager } from '../managers/NavigationManager';
import { ViewManager } from '../managers/ViewManager';
import { CalendarManager } from '../managers/CalendarManager';
/**
* Factory for creating and managing calendar managers with proper dependency injection
*/
export class ManagerFactory {
private static instance: ManagerFactory;
private constructor() {}
public static getInstance(): ManagerFactory {
if (!ManagerFactory.instance) {
ManagerFactory.instance = new ManagerFactory();
}
return ManagerFactory.instance;
}
/**
* Create all managers with proper dependency injection
*/
public createManagers(eventBus: IEventBus, config: CalendarConfig): {
eventManager: EventManager;
eventRenderer: EventRenderingService;
gridManager: GridManager;
scrollManager: ScrollManager;
navigationManager: NavigationManager;
viewManager: ViewManager;
calendarManager: CalendarManager;
} {
console.log('🏭 ManagerFactory: Creating managers with proper DI...');
// Create managers in dependency order
const eventManager = new EventManager(eventBus);
const eventRenderer = new EventRenderingService(eventBus, eventManager);
const gridManager = new GridManager();
const scrollManager = new ScrollManager();
const navigationManager = new NavigationManager(eventBus);
const viewManager = new ViewManager(eventBus);
// CalendarManager depends on all other managers
const calendarManager = new CalendarManager(
eventBus,
config,
eventManager,
gridManager,
eventRenderer,
scrollManager
);
console.log('✅ ManagerFactory: All managers created successfully');
return {
eventManager,
eventRenderer,
gridManager,
scrollManager,
navigationManager,
viewManager,
calendarManager
};
}
/**
* Initialize all managers in the correct order
*/
public async initializeManagers(managers: {
calendarManager: CalendarManager;
[key: string]: any;
}): Promise<void> {
console.log('🚀 ManagerFactory: Initializing managers...');
try {
await managers.calendarManager.initialize();
console.log('✅ ManagerFactory: All managers initialized successfully');
} catch (error) {
console.error('❌ ManagerFactory: Manager initialization failed:', error);
throw error;
}
}
}

View file

@ -1,29 +1,14 @@
// Main entry point for Calendar Plantempus // Main entry point for Calendar Plantempus
import { eventBus } from './core/EventBus.js'; import { eventBus } from './core/EventBus.js';
import { CalendarManager } from './managers/CalendarManager.js';
import { NavigationManager } from './managers/NavigationManager.js';
import { ViewManager } from './managers/ViewManager.js';
import { EventManager } from './managers/EventManager.js';
import { EventRenderer } from './renderers/EventRendererManager.js';
import { GridManager } from './managers/GridManager.js';
import { ScrollManager } from './managers/ScrollManager.js';
import { calendarConfig } from './core/CalendarConfig.js'; import { calendarConfig } from './core/CalendarConfig.js';
import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js'; import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js';
import { ManagerFactory } from './factories/ManagerFactory.js';
/** /**
* Initialize the calendar application with simple direct calls * Initialize the calendar application with simple direct calls
*/ */
async function initializeCalendar(): Promise<void> { async function initializeCalendar(): Promise<void> {
console.log('🗓️ Initializing Calendar Plantempus with simple coordination...'); console.log('🗓️ Initializing Calendar Plantempus with factory pattern...');
// Declare managers outside try block for global access
let calendarManager: CalendarManager;
let navigationManager: NavigationManager;
let viewManager: ViewManager;
let eventManager: EventManager;
let eventRenderer: EventRenderer;
let gridManager: GridManager;
let scrollManager: ScrollManager;
try { try {
// Use the singleton calendar configuration // Use the singleton calendar configuration
@ -33,46 +18,29 @@ async function initializeCalendar(): Promise<void> {
console.log('🏭 Initializing CalendarTypeFactory...'); console.log('🏭 Initializing CalendarTypeFactory...');
CalendarTypeFactory.initialize(); CalendarTypeFactory.initialize();
// Create all managers // Create managers using factory pattern
console.log('📋 Creating managers...'); const managerFactory = ManagerFactory.getInstance();
calendarManager = new CalendarManager(eventBus, config); const managers = managerFactory.createManagers(eventBus, config);
navigationManager = new NavigationManager(eventBus); // No EventRenderer dependency
viewManager = new ViewManager(eventBus);
eventManager = new EventManager(eventBus);
eventRenderer = new EventRenderer(eventBus, eventManager); // Pass EventManager
gridManager = new GridManager();
scrollManager = new ScrollManager();
// Set manager references in CalendarManager
calendarManager.setManagers(eventManager, gridManager, eventRenderer, scrollManager);
// Enable debug mode for development // Enable debug mode for development
eventBus.setDebug(true); eventBus.setDebug(true);
// Initialize using simple direct calls // Initialize all managers
console.log('🚀 Starting simple initialization...'); await managerFactory.initializeManagers(managers);
await calendarManager.initialize();
console.log('🎊 Calendar Plantempus initialized successfully!'); console.log('🎊 Calendar Plantempus initialized successfully!');
console.log('📊 Initialization Report:', calendarManager.getInitializationReport()); console.log('📊 Initialization Report:', managers.calendarManager.getInitializationReport());
// Expose to window for debugging
(window as any).calendarDebug = {
eventBus,
...managers
};
} catch (error) { } catch (error) {
console.error('💥 Calendar initialization failed:', error); console.error('💥 Calendar initialization failed:', error);
// Could implement fallback or retry logic here
throw error; throw error;
} }
// Expose to window for debugging
(window as any).calendarDebug = {
eventBus,
calendarManager,
navigationManager,
viewManager,
eventManager,
eventRenderer,
gridManager,
scrollManager
};
} }
// Initialize when DOM is ready - now handles async properly // Initialize when DOM is ready - now handles async properly

View file

@ -0,0 +1,53 @@
/**
* Base interface for all managers
*/
export interface IManager {
/**
* Initialize the manager
*/
initialize?(): Promise<void> | void;
/**
* Refresh the manager's state
*/
refresh?(): void;
/**
* Destroy the manager and clean up resources
*/
destroy?(): void;
}
/**
* Interface for managers that handle events
*/
export interface IEventManager extends IManager {
loadData(): Promise<void>;
getEvents(): any[];
getEventsForPeriod(startDate: Date, endDate: Date): any[];
}
/**
* Interface for managers that handle rendering
*/
export interface IRenderingManager extends IManager {
render(): Promise<void> | void;
}
/**
* Interface for managers that handle navigation
*/
export interface INavigationManager extends IManager {
getCurrentWeek(): Date;
navigateToToday(): void;
navigateToNextWeek(): void;
navigateToPreviousWeek(): void;
}
/**
* Interface for managers that handle scrolling
*/
export interface IScrollManager extends IManager {
scrollTo(scrollTop: number): void;
scrollToHour(hour: number): void;
}

View file

@ -4,7 +4,7 @@ import { CalendarConfig } from '../core/CalendarConfig.js';
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js'; import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
import { EventManager } from './EventManager.js'; import { EventManager } from './EventManager.js';
import { GridManager } from './GridManager.js'; import { GridManager } from './GridManager.js';
import { EventRenderer } from '../renderers/EventRendererManager.js'; import { EventRenderingService } from '../renderers/EventRendererManager.js';
import { ScrollManager } from './ScrollManager.js'; import { ScrollManager } from './ScrollManager.js';
/** /**
@ -16,27 +16,28 @@ export class CalendarManager {
private config: CalendarConfig; private config: CalendarConfig;
private eventManager: EventManager; private eventManager: EventManager;
private gridManager: GridManager; private gridManager: GridManager;
private eventRenderer: EventRenderer; private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager; private scrollManager: ScrollManager;
private currentView: CalendarView = 'week'; private currentView: CalendarView = 'week';
private currentDate: Date = new Date(); private currentDate: Date = new Date();
private isInitialized: boolean = false; private isInitialized: boolean = false;
constructor(eventBus: IEventBus, config: CalendarConfig) { constructor(
eventBus: IEventBus,
config: CalendarConfig,
eventManager: EventManager,
gridManager: GridManager,
eventRenderer: EventRenderingService,
scrollManager: ScrollManager
) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.config = config; this.config = config;
this.setupEventListeners();
console.log('📋 CalendarManager: Created with direct coordination');
}
/**
* Set manager references (called from index.ts)
*/
public setManagers(eventManager: EventManager, gridManager: GridManager, eventRenderer: EventRenderer, scrollManager: ScrollManager): void {
this.eventManager = eventManager; this.eventManager = eventManager;
this.gridManager = gridManager; this.gridManager = gridManager;
this.eventRenderer = eventRenderer; this.eventRenderer = eventRenderer;
this.scrollManager = scrollManager; this.scrollManager = scrollManager;
this.setupEventListeners();
console.log('📋 CalendarManager: Created with proper dependency injection');
} }
/** /**

View file

@ -184,81 +184,6 @@ export class EventManager {
this.syncEvents(); this.syncEvents();
} }
/**
* Load events for a specific week into a container (POC-style)
*/
private loadEventsForWeek(weekStart: Date, weekEnd: Date, container: HTMLElement): void {
console.log(`EventManager: Loading events for week ${weekStart.toDateString()} - ${weekEnd.toDateString()}`);
// Filter events for this week
const weekEvents = this.events.filter(event => {
const eventDate = new Date(event.start);
return eventDate >= weekStart && eventDate <= weekEnd;
});
console.log(`EventManager: Found ${weekEvents.length} events for this week`);
// Render events in the container (POC approach)
this.renderEventsInContainer(weekEvents, container);
}
/**
* Render events in a specific container (POC-style)
*/
private renderEventsInContainer(events: CalendarEvent[], container: HTMLElement): void {
const dayColumns = container.querySelectorAll('swp-day-column');
events.forEach(event => {
const eventDate = new Date(event.start);
const dayOfWeek = eventDate.getDay(); // 0 = Sunday
const column = dayColumns[dayOfWeek];
if (column) {
const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) {
this.renderEventInColumn(event, eventsLayer as HTMLElement);
}
}
});
}
/**
* Render a single event in a column (POC-style)
*/
private renderEventInColumn(event: CalendarEvent, eventsLayer: HTMLElement): void {
const eventElement = document.createElement('swp-event');
eventElement.dataset.type = event.type || 'meeting';
// Calculate position (simplified - assumes 7 AM start like POC)
const startTime = new Date(event.start);
const hours = startTime.getHours();
const minutes = startTime.getMinutes();
const startMinutes = (hours - 7) * 60 + minutes; // 7 is start hour like POC
// Calculate duration
const endTime = new Date(event.end);
const durationMs = endTime.getTime() - startTime.getTime();
const durationMinutes = Math.floor(durationMs / (1000 * 60));
eventElement.style.top = `${startMinutes}px`;
eventElement.style.height = `${durationMinutes}px`;
eventElement.innerHTML = `
<swp-event-time>${this.formatTime(hours, minutes)}</swp-event-time>
<swp-event-title>${event.title}</swp-event-title>
`;
eventsLayer.appendChild(eventElement);
}
/**
* Format time for display (POC-style)
*/
private formatTime(hours: number, minutes: number): string {
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
public destroy(): void { public destroy(): void {
this.events = []; this.events = [];

View file

@ -197,25 +197,12 @@ export class NavigationManager {
}); });
} }
// Utility functions (from POC) - moved formatting to NavigationRenderer
private updateWeekInfo(): void { private updateWeekInfo(): void {
const weekNumber = DateUtils.getWeekNumber(this.currentWeek); const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
const weekEnd = DateUtils.addDays(this.currentWeek, 6); const weekEnd = DateUtils.addDays(this.currentWeek, 6);
const dateRange = DateUtils.formatDateRange(this.currentWeek, weekEnd); const dateRange = DateUtils.formatDateRange(this.currentWeek, weekEnd);
// Update week info in DOM // Notify other managers about week info update - DOM manipulation should happen via events
const weekNumberElement = document.querySelector('swp-week-number');
const dateRangeElement = document.querySelector('swp-date-range');
if (weekNumberElement)
weekNumberElement.textContent = `Week ${weekNumber}`;
if (dateRangeElement)
dateRangeElement.textContent = dateRange;
// Notify other managers about week info update
this.eventBus.emit(EventTypes.WEEK_INFO_UPDATED, { this.eventBus.emit(EventTypes.WEEK_INFO_UPDATED, {
weekNumber, weekNumber,
dateRange, dateRange,

View file

@ -8,10 +8,10 @@ import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer'; import { EventRendererStrategy } from './EventRenderer';
/** /**
* EventRenderer - Render events i DOM med positionering using Strategy Pattern * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection * Håndterer event positioning og overlap detection
*/ */
export class EventRenderer { export class EventRenderingService {
private eventBus: IEventBus; private eventBus: IEventBus;
private eventManager: EventManager; private eventManager: EventManager;
private strategy: EventRendererStrategy; private strategy: EventRendererStrategy;

View file

@ -3,7 +3,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from './HeaderRenderer'; import { HeaderRenderContext } from './HeaderRenderer';
import { ColumnRenderContext } from './ColumnRenderer'; import { ColumnRenderContext } from './ColumnRenderer';
/** /**
* GridRenderer - Handles DOM rendering for the calendar grid * GridRenderer - Handles DOM rendering for the calendar grid
* Separated from GridManager to follow Single Responsibility Principle * Separated from GridManager to follow Single Responsibility Principle

View file

@ -11,6 +11,34 @@ export class NavigationRenderer {
constructor(eventBus: IEventBus) { constructor(eventBus: IEventBus) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.setupEventListeners();
}
/**
* Setup event listeners for DOM updates
*/
private setupEventListeners(): void {
this.eventBus.on(EventTypes.WEEK_INFO_UPDATED, (event: Event) => {
const customEvent = event as CustomEvent;
const { weekNumber, dateRange } = customEvent.detail;
this.updateWeekInfoInDOM(weekNumber, dateRange);
});
}
/**
* Update week info in DOM elements
*/
private updateWeekInfoInDOM(weekNumber: number, dateRange: string): void {
const weekNumberElement = document.querySelector('swp-week-number');
const dateRangeElement = document.querySelector('swp-date-range');
if (weekNumberElement) {
weekNumberElement.textContent = `Week ${weekNumber}`;
}
if (dateRangeElement) {
dateRangeElement.textContent = dateRange;
}
} }
/** /**