import { EventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes'; import { EventManager } from './EventManager'; import { GridManager } from './GridManager'; import { HeaderManager } from './HeaderManager'; import { EventRenderingService } from '../renderers/EventRendererManager'; import { ScrollManager } from './ScrollManager'; import { DateService } from '../utils/DateService'; import { EventFilterManager } from './EventFilterManager'; import { InitializationReport } from '../types/ManagerTypes'; /** * CalendarManager - Main coordinator for all calendar managers * Uses singleton calendarConfig for consistent configuration access */ export class CalendarManager { private eventBus: IEventBus; private eventManager: EventManager; private gridManager: GridManager; private headerManager: HeaderManager; private eventRenderer: EventRenderingService; private scrollManager: ScrollManager; private eventFilterManager: EventFilterManager; private dateService: DateService; private currentView: CalendarView = 'week'; private currentDate: Date = new Date(); private isInitialized: boolean = false; constructor( eventBus: IEventBus, eventManager: EventManager, gridManager: GridManager, eventRenderer: EventRenderingService, scrollManager: ScrollManager ) { this.eventBus = eventBus; this.eventManager = eventManager; this.gridManager = gridManager; this.headerManager = new HeaderManager(); this.eventRenderer = eventRenderer; this.scrollManager = scrollManager; this.eventFilterManager = new EventFilterManager(); const timezone = calendarConfig.getTimezone?.(); this.dateService = new DateService(timezone); this.setupEventListeners(); } /** * Initialize calendar system using simple direct calls */ public async initialize(): Promise { if (this.isInitialized) { return; } try { // Debug: Check calendar type const calendarType = calendarConfig.getCalendarMode(); // Step 1: Load data await this.eventManager.loadData(); // Step 2: Pass data to GridManager and render grid structure if (calendarType === 'resource') { const resourceData = this.eventManager.getResourceData(); this.gridManager.setResourceData(this.eventManager.getRawData() as import('../types/CalendarTypes').ResourceCalendarData); } await this.gridManager.render(); // Step 2a: Setup header drag listeners after grid render (when DOM is available) this.headerManager.setupHeaderDragListeners(); // Step 2b: Trigger event rendering now that data is loaded // Re-emit GRID_RENDERED to trigger EventRendererManager 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 this.scrollManager.initialize(); // Step 4: Set initial view and date BEFORE event rendering this.setView(this.currentView); this.setCurrentDate(this.currentDate); // Step 5: Event rendering will be triggered by GRID_RENDERED event this.isInitialized = true; // Emit initialization complete event this.eventBus.emit(CoreEvents.INITIALIZED, { currentDate: this.currentDate, currentView: this.currentView }); } catch (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; // 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())) { 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(); // 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() { return calendarConfig; } /** * Check om calendar er initialiseret */ public isCalendarInitialized(): boolean { return this.isInitialized; } /** * Get initialization report for debugging */ public getInitializationReport(): InitializationReport { return { initialized: this.isInitialized, timestamp: Date.now(), managers: { calendar: { initialized: this.isInitialized }, event: { initialized: true }, grid: { initialized: true }, header: { initialized: true }, scroll: { initialized: true } } }; } /** * Genindlæs calendar data */ public refresh(): void { this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { view: this.currentView, date: this.currentDate }); } /** * Ryd calendar og nulstil til standard tilstand */ public reset(): void { 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; 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 = calendarConfig.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 = calendarConfig.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 { // 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(); // Notify HeaderManager with correct current date after grid rebuild this.eventBus.emit('workweek:header-update', { currentDate: this.currentDate, currentView: this.currentView, workweek: calendarConfig.getCurrentWorkWeek() }); } /** * Re-render events after grid structure changes */ private rerenderEvents(): void { // 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) { 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 } /** * Update week info based on actual rendered columns */ private updateWeekInfoFromRenderedColumns(): void { // Get actual dates from rendered columns const columns = document.querySelectorAll('swp-day-column'); if (columns.length === 0) { 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) { return; } // Parse dates const firstDate = new Date(firstDateStr); const lastDate = new Date(lastDateStr); // Calculate week number from first date const weekNumber = this.dateService.getWeekNumber(firstDate); // Format date range const dateRange = this.dateService.formatDateRange(firstDate, lastDate); // Emit week info update this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, { weekNumber, dateRange, weekStart: firstDate, weekEnd: lastDate }); } }