Calendar/src/managers/CalendarManager.ts
Janus Knudsen 9c65143df2 Refactors event rendering and display
Improves event rendering by introducing dedicated event
renderers and streamlining event display logic.

- Adds a base event renderer and specialized date and
  resource-based renderers to handle event display logic.
- Renders all-day events within a dedicated container in the
  calendar header.
- Removes the direct filtering of all-day events from the
  `GridManager`.
- Fixes an issue where the 'Summer Festival' event started on the
  wrong date.

The changes enhance the flexibility and maintainability of the
calendar, provide dedicated containers and styling for allday events and fix date issues related to certain events
2025-08-24 00:13:07 +02:00

484 lines
No EOL
16 KiB
TypeScript

import { EventBus } from '../core/EventBus.js';
import { CoreEvents } from '../constants/CoreEvents.js';
import { CalendarConfig } from '../core/CalendarConfig.js';
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
import { EventManager } from './EventManager.js';
import { GridManager } from './GridManager.js';
import { EventRenderingService } from '../renderers/EventRendererManager.js';
import { ScrollManager } from './ScrollManager.js';
import { DateCalculator } from '../utils/DateCalculator.js';
import { EventFilterManager } from './EventFilterManager.js';
/**
* CalendarManager - Main coordinator for all calendar managers
* Uses simple direct method calls instead of complex state management
*/
export class CalendarManager {
private eventBus: IEventBus;
private config: CalendarConfig;
private eventManager: EventManager;
private gridManager: GridManager;
private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager;
private eventFilterManager: EventFilterManager;
private dateCalculator: DateCalculator;
private currentView: CalendarView = 'week';
private currentDate: Date = new Date();
private isInitialized: boolean = false;
constructor(
eventBus: IEventBus,
config: CalendarConfig,
eventManager: EventManager,
gridManager: GridManager,
eventRenderer: EventRenderingService,
scrollManager: ScrollManager
) {
this.eventBus = eventBus;
this.config = config;
this.eventManager = eventManager;
this.gridManager = gridManager;
this.eventRenderer = eventRenderer;
this.scrollManager = scrollManager;
this.eventFilterManager = new EventFilterManager();
this.dateCalculator = new DateCalculator(config);
this.setupEventListeners();
console.log('📋 CalendarManager: Created with proper dependency injection');
}
/**
* Initialize calendar system using simple direct calls
*/
public async initialize(): Promise<void> {
if (this.isInitialized) {
console.warn('CalendarManager is already initialized');
return;
}
console.log('🚀 CalendarManager: Starting simple initialization');
try {
// Debug: Check calendar type
const calendarType = this.config.getCalendarMode();
console.log(`🔍 CalendarManager: Initializing ${calendarType} calendar`);
// Step 1: Load data
console.log('📊 Loading event data...');
await this.eventManager.loadData();
// Step 2: Pass data to GridManager and render grid structure
console.log('🏗️ Rendering grid...');
if (calendarType === 'resource') {
const resourceData = this.eventManager.getResourceData();
this.gridManager.setResourceData(resourceData);
}
await this.gridManager.render();
// Step 2b: Trigger event rendering now that data is loaded
// Re-emit GRID_RENDERED to trigger EventRendererManager
console.log('🎨 Triggering event rendering...');
const gridContainer = document.querySelector('swp-calendar-container');
if (gridContainer) {
const periodRange = this.gridManager.getDisplayDates();
this.eventBus.emit(CoreEvents.GRID_RENDERED, {
container: gridContainer,
currentDate: new Date(),
startDate: periodRange[0],
endDate: periodRange[periodRange.length - 1],
columnCount: periodRange.length
});
}
// Step 3: Initialize scroll synchronization
console.log('📜 Setting up scroll synchronization...');
this.scrollManager.initialize();
// Step 4: Set initial view and date BEFORE event rendering
console.log('⚙️ Setting initial view and date...');
this.setView(this.currentView);
this.setCurrentDate(this.currentDate);
// Step 5: Event rendering will be triggered by GRID_RENDERED event
console.log('🎨 Event rendering will be triggered automatically by grid events...');
this.isInitialized = true;
console.log('✅ CalendarManager: Simple initialization complete');
// Emit initialization complete event
this.eventBus.emit(CoreEvents.INITIALIZED, {
currentDate: this.currentDate,
currentView: this.currentView
});
} catch (error) {
console.error('❌ CalendarManager initialization failed:', error);
throw error;
}
}
/**
* Skift calendar view (dag/uge/måned)
*/
public setView(view: CalendarView): void {
if (this.currentView === view) {
return;
}
const previousView = this.currentView;
this.currentView = view;
console.log(`Changing view from ${previousView} to ${view}`);
// Emit view change event
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
previousView,
currentView: view,
date: this.currentDate
});
// Grid re-rendering will trigger event rendering automatically via GRID_RENDERED event
}
/**
* Sæt aktuel dato
*/
public setCurrentDate(date: Date): void {
// Validate input date
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
console.error('CalendarManager.setCurrentDate: Invalid date provided', date);
return;
}
const previousDate = this.currentDate;
this.currentDate = new Date(date);
// Validate that both dates are valid before logging
const prevDateStr = previousDate && !isNaN(previousDate.getTime()) ? previousDate.toISOString() : 'Invalid Date';
const newDateStr = this.currentDate.toISOString();
console.log(`Changing date from ${prevDateStr} to ${newDateStr}`);
// Emit date change event
this.eventBus.emit(CoreEvents.DATE_CHANGED, {
previousDate,
currentDate: this.currentDate,
view: this.currentView
});
// Grid update for new date will trigger event rendering automatically via GRID_RENDERED event
}
/**
* Naviger til i dag
*/
public goToToday(): void {
this.setCurrentDate(new Date());
}
/**
* Naviger til næste periode (dag/uge/måned afhængig af view)
*/
public goToNext(): void {
const nextDate = this.calculateNextDate();
this.setCurrentDate(nextDate);
}
/**
* Naviger til forrige periode (dag/uge/måned afhængig af view)
*/
public goToPrevious(): void {
const previousDate = this.calculatePreviousDate();
this.setCurrentDate(previousDate);
}
/**
* Hent aktuel view
*/
public getCurrentView(): CalendarView {
return this.currentView;
}
/**
* Hent aktuel dato
*/
public getCurrentDate(): Date {
return new Date(this.currentDate);
}
/**
* Hent calendar konfiguration
*/
public getConfig(): CalendarConfig {
return this.config;
}
/**
* Check om calendar er initialiseret
*/
public isCalendarInitialized(): boolean {
return this.isInitialized;
}
/**
* Get initialization report for debugging
*/
public getInitializationReport(): any {
return {
isInitialized: this.isInitialized,
currentView: this.currentView,
currentDate: this.currentDate,
initializationTime: 'N/A - simple initialization'
};
}
/**
* Genindlæs calendar data
*/
public refresh(): void {
console.log('Refreshing calendar...');
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
view: this.currentView,
date: this.currentDate
});
}
/**
* Ryd calendar og nulstil til standard tilstand
*/
public reset(): void {
console.log('Resetting calendar...');
this.currentView = 'week';
this.currentDate = new Date();
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
view: this.currentView,
date: this.currentDate
});
}
/**
* Setup event listeners for at håndtere events fra andre managers
*/
private setupEventListeners(): void {
// Listen for workweek changes only
this.eventBus.on(CoreEvents.WORKWEEK_CHANGED, (event: Event) => {
const customEvent = event as CustomEvent;
console.log('CalendarManager: Workweek changed to', customEvent.detail.workWeekId);
this.handleWorkweekChange();
// Also update week info display since workweek affects date range display
this.updateWeekInfoForWorkweekChange();
});
}
/**
* Beregn næste dato baseret på aktuel view
*/
private calculateNextDate(): Date {
const nextDate = new Date(this.currentDate);
switch (this.currentView) {
case 'day':
nextDate.setDate(nextDate.getDate() + 1);
break;
case 'week':
const workWeekSettings = this.config.getWorkWeekSettings();
nextDate.setDate(nextDate.getDate() + 7); // Move to next calendar week regardless of work days
break;
case 'month':
nextDate.setMonth(nextDate.getMonth() + 1);
break;
}
return nextDate;
}
/**
* Beregn forrige dato baseret på aktuel view
*/
private calculatePreviousDate(): Date {
const previousDate = new Date(this.currentDate);
switch (this.currentView) {
case 'day':
previousDate.setDate(previousDate.getDate() - 1);
break;
case 'week':
const workWeekSettings = this.config.getWorkWeekSettings();
previousDate.setDate(previousDate.getDate() - 7); // Move to previous calendar week regardless of work days
break;
case 'month':
previousDate.setMonth(previousDate.getMonth() - 1);
break;
}
return previousDate;
}
/**
* Calculate the current period based on view and date
*/
private calculateCurrentPeriod(): { start: string; end: string } {
const current = new Date(this.currentDate);
switch (this.currentView) {
case 'day':
const dayStart = new Date(current);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(current);
dayEnd.setHours(23, 59, 59, 999);
return {
start: dayStart.toISOString(),
end: dayEnd.toISOString()
};
case 'week':
// Find start of week (Monday)
const weekStart = new Date(current);
const dayOfWeek = weekStart.getDay();
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Sunday = 0, so 6 days back to Monday
weekStart.setDate(weekStart.getDate() - daysToMonday);
weekStart.setHours(0, 0, 0, 0);
// Find end of week (Sunday)
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
return {
start: weekStart.toISOString(),
end: weekEnd.toISOString()
};
case 'month':
const monthStart = new Date(current.getFullYear(), current.getMonth(), 1);
const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0, 23, 59, 59, 999);
return {
start: monthStart.toISOString(),
end: monthEnd.toISOString()
};
default:
// Fallback to week view
const fallbackStart = new Date(current);
fallbackStart.setDate(fallbackStart.getDate() - 3);
fallbackStart.setHours(0, 0, 0, 0);
const fallbackEnd = new Date(current);
fallbackEnd.setDate(fallbackEnd.getDate() + 3);
fallbackEnd.setHours(23, 59, 59, 999);
return {
start: fallbackStart.toISOString(),
end: fallbackEnd.toISOString()
};
}
}
/**
* Handle workweek configuration changes
*/
private handleWorkweekChange(): void {
console.log('CalendarManager: Handling workweek change - forcing full grid rebuild');
// Force a complete grid rebuild by clearing existing structure
const container = document.querySelector('swp-calendar-container');
if (container) {
container.innerHTML = ''; // Clear everything to force full rebuild
}
// Re-render the grid with new workweek settings (will now rebuild everything)
this.gridManager.render();
// Re-initialize scroll manager after grid rebuild
this.scrollManager.initialize();
// Re-render events in the new grid structure
this.rerenderEvents();
}
/**
* Re-render events after grid structure changes
*/
private rerenderEvents(): void {
console.log('CalendarManager: Re-rendering events for new workweek');
// Get current period data to determine date range
const periodData = this.calculateCurrentPeriod();
// Find the grid container to render events in
const container = document.querySelector('swp-calendar-container');
if (!container) {
console.warn('CalendarManager: No container found for event re-rendering');
return;
}
// Trigger event rendering for the current date range using correct method
this.eventRenderer.renderEvents({
container: container as HTMLElement,
startDate: new Date(periodData.start),
endDate: new Date(periodData.end)
});
}
/**
* Update week info display after workweek changes
*/
private updateWeekInfoForWorkweekChange(): void {
// Don't do anything here - let GRID_RENDERED event handle it
console.log('CalendarManager: Workweek changed - week info will update after grid renders');
}
/**
* Update week info based on actual rendered columns
*/
private updateWeekInfoFromRenderedColumns(): void {
console.log('CalendarManager: Updating week info from rendered columns');
// Get actual dates from rendered columns
const columns = document.querySelectorAll('swp-day-column');
if (columns.length === 0) {
console.warn('CalendarManager: No columns found for week info update');
return;
}
// Get first and last column dates
const firstColumn = columns[0] as HTMLElement;
const lastColumn = columns[columns.length - 1] as HTMLElement;
const firstDateStr = firstColumn.dataset.date;
const lastDateStr = lastColumn.dataset.date;
if (!firstDateStr || !lastDateStr) {
console.warn('CalendarManager: Column dates not found');
return;
}
// Parse dates
const firstDate = new Date(firstDateStr);
const lastDate = new Date(lastDateStr);
// Calculate week number from first date
const weekNumber = this.dateCalculator.getWeekNumber(firstDate);
// Format date range
const dateRange = this.dateCalculator.formatDateRange(firstDate, lastDate);
console.log('CalendarManager: Week info from columns:', {
firstDate: firstDateStr,
lastDate: lastDateStr,
weekNumber,
dateRange
});
// Emit week info update
this.eventBus.emit(CoreEvents.WEEK_CHANGED, {
weekNumber,
dateRange,
weekStart: firstDate,
weekEnd: lastDate
});
}
}