From 59b3c64c55e28d198c0dd3dd3e5f925fff51527e Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Sat, 9 Aug 2025 00:31:44 +0200 Subject: [PATCH] Major refactorering to get a hold on all these events --- .claude/settings.local.json | 3 +- docs/date-mode-initialization-sequence.md | 237 +++++++++++ docs/improved-initialization-strategy.md | 270 +++++++++++++ src/constants/EventTypes.ts | 118 +++--- src/core/CalendarConfig.ts | 310 ++++++++++---- src/factories/CalendarTypeFactory.ts | 9 +- src/index.ts | 83 ++-- src/managers/CalendarManager.ts | 73 ++-- src/managers/CalendarStateManager.ts | 471 ++++++++++++++++++++++ src/managers/DataManager.ts | 23 +- src/managers/EventManager.ts | 112 ++--- src/managers/EventRenderer.ts | 67 ++- src/managers/GridManager.ts | 156 ++++--- src/managers/NavigationManager.ts | 16 +- src/managers/ScrollManager.ts | 23 +- src/types/CalendarState.ts | 170 ++++++++ src/types/CalendarTypes.ts | 73 ++-- src/utils/PositionUtils.ts | 44 +- 18 files changed, 1901 insertions(+), 357 deletions(-) create mode 100644 docs/date-mode-initialization-sequence.md create mode 100644 docs/improved-initialization-strategy.md create mode 100644 src/managers/CalendarStateManager.ts create mode 100644 src/types/CalendarState.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0c962a0..4df7b43 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "permissions": { "allow": [ "Bash(npm run build:*)", - "Bash(powershell:*)" + "Bash(powershell:*)", + "Bash(rg:*)" ], "deny": [] } diff --git a/docs/date-mode-initialization-sequence.md b/docs/date-mode-initialization-sequence.md new file mode 100644 index 0000000..64dce9b --- /dev/null +++ b/docs/date-mode-initialization-sequence.md @@ -0,0 +1,237 @@ +# Calendar Plantempus - Date Mode Initialization Sequence + +## Overview +This document shows the complete initialization sequence and event flow for Date Mode in Calendar Plantempus, including when data is loaded and ready for rendering. + +## Sequence Diagram + +```mermaid +sequenceDiagram + participant Browser as Browser + participant Index as index.ts + participant Config as CalendarConfig + participant Factory as CalendarTypeFactory + participant CM as CalendarManager + participant EM as EventManager + participant GM as GridManager + participant NM as NavigationManager + participant VM as ViewManager + participant ER as EventRenderer + participant SM as ScrollManager + participant EB as EventBus + participant DOM as DOM + + Note over Browser: Page loads calendar application + Browser->>Index: Load application + + Note over Index: PHASE 0: Pre-initialization Setup + Index->>Config: new CalendarConfig() + Config->>Config: loadCalendarType() - Read URL ?type=date + Config->>Config: loadFromDOM() - Read data attributes + Config->>Config: Set mode='date', period='week' + + Index->>Factory: CalendarTypeFactory.initialize() + Factory->>Factory: Create DateHeaderRenderer + Factory->>Factory: Create DateColumnRenderer + Factory->>Factory: Create DateEventRenderer + Note over Factory: Strategy Pattern renderers ready + + Note over Index: PHASE 1: Core Managers Construction + Index->>CM: new CalendarManager(eventBus, config) + CM->>EB: Subscribe to VIEW_CHANGE_REQUESTED + CM->>EB: Subscribe to NAV_PREV, NAV_NEXT + + Index->>NM: new NavigationManager(eventBus) + NM->>EB: Subscribe to CALENDAR_INITIALIZED + Note over NM: Will wait to call updateWeekInfo() + + Index->>VM: new ViewManager(eventBus) + VM->>EB: Subscribe to CALENDAR_INITIALIZED + + Note over Index: PHASE 2: Data & Rendering Managers + Index->>EM: new EventManager(eventBus) + EM->>EB: Subscribe to CALENDAR_INITIALIZED + Note over EM: Will wait to load data + + Index->>ER: new EventRenderer(eventBus) + ER->>EB: Subscribe to EVENTS_LOADED + ER->>EB: Subscribe to GRID_RENDERED + Note over ER: Needs BOTH events before rendering + + Note over Index: PHASE 3: Layout Managers (Order Critical!) + Index->>SM: new ScrollManager() + SM->>EB: Subscribe to GRID_RENDERED + Note over SM: Must subscribe BEFORE GridManager renders + + Index->>GM: new GridManager() + GM->>EB: Subscribe to CALENDAR_INITIALIZED + GM->>EB: Subscribe to CALENDAR_DATA_LOADED + GM->>GM: Set currentWeek = getWeekStart(new Date()) + Note over GM: Ready to render, but waiting + + Note over Index: PHASE 4: Coordinated Initialization + Index->>CM: initialize() + + CM->>EB: emit(CALENDAR_INITIALIZING) + CM->>CM: setView('week'), setCurrentDate() + CM->>EB: emit(CALENDAR_INITIALIZED) ⭐ + + Note over EB: πŸš€ CALENDAR_INITIALIZED triggers all managers + + par EventManager Data Loading + EB->>EM: CALENDAR_INITIALIZED + EM->>EM: loadMockData() for date mode + EM->>EM: fetch('/src/data/mock-events.json') + Note over EM: Loading date-specific mock data + EM->>EM: Process events for current week + EM->>EB: emit(CALENDAR_DATA_LOADED, {calendarType: 'date', data}) + EM->>EB: emit(EVENTS_LOADED, {events: [...]) + + and GridManager Initial Rendering + EB->>GM: CALENDAR_INITIALIZED + GM->>GM: render() + GM->>GM: updateGridStyles() - Set --grid-columns: 7 + GM->>GM: createHeaderSpacer() + GM->>GM: createTimeAxis(dayStartHour, dayEndHour) + GM->>GM: createGridContainer() + + Note over GM: Strategy Pattern - Date Mode Rendering + GM->>Factory: getHeaderRenderer('date') β†’ DateHeaderRenderer + GM->>GM: renderCalendarHeader() - Create day headers + GM->>DOM: Create 7 swp-day-column elements + + GM->>Factory: getColumnRenderer('date') β†’ DateColumnRenderer + GM->>GM: renderColumnContainer() - Date columns + GM->>EB: emit(GRID_RENDERED) ⭐ + + and NavigationManager UI + EB->>NM: CALENDAR_INITIALIZED + NM->>NM: updateWeekInfo() + NM->>DOM: Update week display in navigation + NM->>EB: emit(WEEK_INFO_UPDATED) + + and ViewManager Setup + EB->>VM: CALENDAR_INITIALIZED + VM->>VM: initializeView() + VM->>EB: emit(VIEW_RENDERED) + end + + Note over GM: GridManager receives its own data event + EB->>GM: CALENDAR_DATA_LOADED + GM->>GM: updateGridStyles() - Recalculate columns if needed + Note over GM: Grid already rendered, just update styles + + Note over ER: 🎯 Critical Synchronization Point + EB->>ER: EVENTS_LOADED + ER->>ER: pendingEvents = events (store, don't render yet) + Note over ER: Waiting for grid to be ready... + + EB->>ER: GRID_RENDERED + ER->>DOM: querySelectorAll('swp-day-column') - Check if ready + DOM-->>ER: Return 7 day columns (ready!) + + Note over ER: Both events loaded AND grid ready β†’ Render! + ER->>Factory: getEventRenderer('date') β†’ DateEventRenderer + ER->>ER: renderEvents(pendingEvents) using DateEventRenderer + ER->>DOM: Position events in day columns + ER->>ER: Clear pendingEvents + ER->>EB: emit(EVENT_RENDERED) + + Note over SM: ScrollManager sets up after grid is complete + EB->>SM: GRID_RENDERED + SM->>DOM: querySelector('swp-scrollable-content') + SM->>SM: setupScrolling() + SM->>SM: applyScrollbarStyling() + SM->>SM: setupScrollSynchronization() + + Note over Index: 🎊 Date Mode Initialization Complete! + Note over Index: Ready for user interaction +``` + +## Key Initialization Phases + +### Phase 0: Pre-initialization Setup +- **CalendarConfig**: Loads URL parameters (`?type=date`) and DOM attributes +- **CalendarTypeFactory**: Creates strategy pattern renderers for date mode + +### Phase 1: Core Managers Construction +- **CalendarManager**: Central coordinator +- **NavigationManager**: Week navigation controls +- **ViewManager**: View state management + +### Phase 2: Data & Rendering Managers +- **EventManager**: Handles data loading +- **EventRenderer**: Manages event display with synchronization + +### Phase 3: Layout Managers (Order Critical!) +- **ScrollManager**: Must subscribe before GridManager renders +- **GridManager**: Main grid rendering + +### Phase 4: Coordinated Initialization +- **CalendarManager.initialize()**: Triggers `CALENDAR_INITIALIZED` event +- All managers respond simultaneously but safely + +## Critical Synchronization Points + +### 1. Event-Grid Synchronization +```typescript +// EventRenderer waits for BOTH events +if (this.pendingEvents.length > 0) { + const columns = document.querySelectorAll('swp-day-column'); // DATE MODE + if (columns.length > 0) { // Grid must exist first + this.renderEvents(this.pendingEvents); + } +} +``` + +### 2. Scroll-Grid Dependency +```typescript +// ScrollManager only sets up after grid is rendered +eventBus.on(EventTypes.GRID_RENDERED, () => { + this.setupScrolling(); // Safe to access DOM now +}); +``` + +### 3. Manager Construction Order +```typescript +// Critical order: ScrollManager subscribes BEFORE GridManager renders +const scrollManager = new ScrollManager(); +const gridManager = new GridManager(); +``` + +## Date Mode Specifics + +### Data Loading +- Uses `/src/data/mock-events.json` +- Processes events for current week +- Emits `CALENDAR_DATA_LOADED` with `calendarType: 'date'` + +### Grid Rendering +- Creates 7 `swp-day-column` elements (weekDays: 7) +- Uses `DateHeaderRenderer` strategy +- Uses `DateColumnRenderer` strategy +- Sets `--grid-columns: 7` CSS variable + +### Event Rendering +- Uses `DateEventRenderer` strategy +- Positions events in day columns based on start/end time +- Calculates pixel positions using `PositionUtils` + +## Race Condition Prevention + +1. **Subscription Before Action**: All managers subscribe during construction, act on `CALENDAR_INITIALIZED` +2. **DOM Existence Checks**: Managers verify DOM elements exist before manipulation +3. **Event Ordering**: `GRID_RENDERED` always fires before event rendering attempts +4. **Pending States**: EventRenderer stores pending events until grid is ready +5. **Coordinated Start**: Single `CALENDAR_INITIALIZED` event starts all processes + +## Debugging Points + +Key events to monitor during initialization: +- `CALENDAR_INITIALIZED` - Start of coordinated setup +- `CALENDAR_DATA_LOADED` - Date data ready +- `GRID_RENDERED` - Grid structure complete +- `EVENTS_LOADED` - Event data ready +- `EVENT_RENDERED` - Events positioned in grid + +This sequence ensures deterministic, race-condition-free initialization with comprehensive logging for debugging. \ No newline at end of file diff --git a/docs/improved-initialization-strategy.md b/docs/improved-initialization-strategy.md new file mode 100644 index 0000000..dec3b03 --- /dev/null +++ b/docs/improved-initialization-strategy.md @@ -0,0 +1,270 @@ +# Improved Calendar Initialization Strategy + +## Current Problems + +1. **Race Conditions**: Managers try DOM operations before DOM is ready +2. **Sequential Blocking**: All initialization happens sequentially +3. **Poor Error Handling**: No timeouts or retry mechanisms +4. **Late Data Loading**: Data only loads after all managers are created + +## Recommended New Architecture + +### Phase 1: Early Parallel Startup +```typescript +// index.ts - Improved initialization +export class CalendarInitializer { + async initialize(): Promise { + console.log('πŸ“‹ Starting Calendar initialization...'); + + // PHASE 1: Early parallel setup + const setupPromises = [ + this.initializeConfig(), // Load URL params, DOM attrs + this.initializeFactory(), // Setup strategy patterns + this.preloadCalendarData(), // Start data loading early + this.waitForDOMReady() // Ensure basic DOM exists + ]; + + await Promise.all(setupPromises); + console.log('βœ… Phase 1 complete: Config, Factory, Data preloading started'); + + // PHASE 2: Manager creation with dependencies + await this.createManagersWithDependencies(); + + // PHASE 3: Coordinated activation + await this.activateAllManagers(); + + console.log('🎊 Calendar fully initialized!'); + } +} +``` + +### Phase 2: Dependency-Aware Manager Creation +```typescript +private async createManagersWithDependencies(): Promise { + const managers = new Map(); + + // Core managers (no DOM dependencies) + managers.set('config', calendarConfig); + managers.set('eventBus', eventBus); + managers.set('calendarManager', new CalendarManager(eventBus, calendarConfig)); + + // DOM-dependent managers (wait for DOM readiness) + await this.waitForRequiredDOM(['swp-calendar', 'swp-calendar-nav']); + + managers.set('navigationManager', new NavigationManager(eventBus)); + managers.set('viewManager', new ViewManager(eventBus)); + + // Data managers (can work with preloaded data) + managers.set('eventManager', new EventManager(eventBus)); + managers.set('dataManager', new DataManager()); + + // Layout managers (need DOM structure + other managers) + await this.waitForRequiredDOM(['swp-calendar-container']); + + // CRITICAL ORDER: ScrollManager subscribes before GridManager renders + managers.set('scrollManager', new ScrollManager()); + managers.set('gridManager', new GridManager()); + + // Rendering managers (need grid structure) + managers.set('eventRenderer', new EventRenderer(eventBus)); + + this.managers = managers; +} +``` + +### Phase 3: Coordinated Activation +```typescript +private async activateAllManagers(): Promise { + // All managers created and subscribed, now activate in coordinated fashion + const calendarManager = this.managers.get('calendarManager'); + + // This triggers CALENDAR_INITIALIZED, but now all managers are ready + await calendarManager.initialize(); + + // Wait for critical initialization events + await Promise.all([ + this.waitForEvent('CALENDAR_DATA_LOADED', 10000), + this.waitForEvent('GRID_RENDERED', 5000), + this.waitForEvent('EVENTS_LOADED', 10000) + ]); + + // Ensure event rendering completes + await this.waitForEvent('EVENT_RENDERED', 3000); +} +``` + +## Specific Timing Improvements + +### 1. Early Data Preloading +```typescript +private async preloadCalendarData(): Promise { + const currentDate = new Date(); + const mode = calendarConfig.getCalendarMode(); + + // Start loading data for current period immediately + const dataManager = new DataManager(); + const currentPeriod = this.getCurrentPeriod(currentDate, mode); + + // Don't await - let this run in background + const dataPromise = dataManager.fetchEventsForPeriod(currentPeriod); + + // Also preload adjacent periods + const prevPeriod = this.getPreviousPeriod(currentDate, mode); + const nextPeriod = this.getNextPeriod(currentDate, mode); + + // Store promises for later use + this.preloadPromises = { + current: dataPromise, + previous: dataManager.fetchEventsForPeriod(prevPeriod), + next: dataManager.fetchEventsForPeriod(nextPeriod) + }; + + console.log('πŸ“Š Data preloading started for current, previous, and next periods'); +} +``` + +### 2. DOM Readiness Verification +```typescript +private async waitForRequiredDOM(selectors: string[]): Promise { + const maxWait = 5000; // 5 seconds max + const checkInterval = 100; // Check every 100ms + + const startTime = Date.now(); + + while (Date.now() - startTime < maxWait) { + const missing = selectors.filter(selector => !document.querySelector(selector)); + + if (missing.length === 0) { + console.log(`βœ… Required DOM elements found: ${selectors.join(', ')}`); + return; + } + + await new Promise(resolve => setTimeout(resolve, checkInterval)); + } + + throw new Error(`❌ Timeout waiting for DOM elements: ${selectors.join(', ')}`); +} +``` + +### 3. Manager Base Class with Proper Lifecycle +```typescript +export abstract class BaseManager { + protected isInitialized = false; + protected requiredDOMSelectors: string[] = []; + + constructor() { + // Don't call init() immediately in constructor! + console.log(`${this.constructor.name}: Created but not initialized`); + } + + async initialize(): Promise { + if (this.isInitialized) { + console.log(`${this.constructor.name}: Already initialized, skipping`); + return; + } + + // Wait for required DOM elements + if (this.requiredDOMSelectors.length > 0) { + await this.waitForDOM(this.requiredDOMSelectors); + } + + // Perform manager-specific initialization + await this.performInitialization(); + + this.isInitialized = true; + console.log(`${this.constructor.name}: Initialization complete`); + } + + protected abstract performInitialization(): Promise; + + private async waitForDOM(selectors: string[]): Promise { + // Same DOM waiting logic as above + } +} +``` + +### 4. Enhanced GridManager +```typescript +export class GridManager extends BaseManager { + protected requiredDOMSelectors = ['swp-calendar-container']; + + constructor() { + super(); // Don't call this.init()! + this.currentWeek = this.getWeekStart(new Date()); + } + + protected async performInitialization(): Promise { + // Now safe to find elements - DOM guaranteed to exist + this.findElements(); + this.subscribeToEvents(); + + // Wait for CALENDAR_INITIALIZED before rendering + await this.waitForEvent('CALENDAR_INITIALIZED'); + + console.log('GridManager: Starting initial render'); + this.render(); + } +} +``` + +### 5. Enhanced EventRenderer with Better Synchronization +```typescript +export class EventRenderer extends BaseManager { + private dataReady = false; + private gridReady = false; + private pendingEvents: CalendarEvent[] = []; + + protected async performInitialization(): Promise { + this.subscribeToEvents(); + + // Wait for both data and grid in parallel + const [eventsData] = await Promise.all([ + this.waitForEvent('EVENTS_LOADED'), + this.waitForEvent('GRID_RENDERED') + ]); + + console.log('EventRenderer: Both events and grid ready, rendering now'); + this.renderEvents(eventsData.events); + } + + private subscribeToEvents(): void { + this.eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => { + const detail = (e as CustomEvent).detail; + this.pendingEvents = detail.events; + this.dataReady = true; + this.tryRender(); + }); + + this.eventBus.on(EventTypes.GRID_RENDERED, () => { + this.gridReady = true; + this.tryRender(); + }); + } + + private tryRender(): void { + if (this.dataReady && this.gridReady && this.pendingEvents.length > 0) { + this.renderEvents(this.pendingEvents); + this.pendingEvents = []; + } + } +} +``` + +## Benefits of New Architecture + +1. **πŸš€ Parallel Operations**: Data loading starts immediately while managers are being created +2. **πŸ›‘οΈ Race Condition Prevention**: DOM readiness verified before operations +3. **⚑ Better Performance**: Critical path optimized, non-critical operations parallelized +4. **πŸ”§ Better Error Handling**: Timeouts and retry mechanisms +5. **πŸ“Š Predictable Timing**: Clear phases with guaranteed completion order +6. **πŸ› Easier Debugging**: Clear lifecycle events and logging + +## Implementation Strategy + +1. **Phase 1**: Create BaseManager class and update existing managers +2. **Phase 2**: Implement CalendarInitializer with parallel setup +3. **Phase 3**: Add DOM readiness verification throughout +4. **Phase 4**: Implement data preloading strategy +5. **Phase 5**: Add comprehensive error handling and timeouts + +This architecture ensures reliable, fast, and maintainable calendar initialization. \ No newline at end of file diff --git a/src/constants/EventTypes.ts b/src/constants/EventTypes.ts index 1bb162f..50ec025 100644 --- a/src/constants/EventTypes.ts +++ b/src/constants/EventTypes.ts @@ -1,15 +1,38 @@ -// Calendar event type constants +// Legacy Calendar event type constants /** - * Calendar event type constants for DOM CustomEvents + * Legacy event type constants for DOM CustomEvents + * + * IMPORTANT: This file contains events for specific UI interactions and config updates. + * For initialization and coordination events, use StateEvents from ../types/CalendarState.ts + * + * This file has been cleaned up to remove redundant/unused events. */ export const EventTypes = { - // View events + // Configuration events + CONFIG_UPDATE: 'calendar:configupdate', + CALENDAR_TYPE_CHANGED: 'calendar:calendartypechanged', + SELECTED_DATE_CHANGED: 'calendar:selecteddatechanged', + + // View change events VIEW_CHANGE: 'calendar:viewchange', + VIEW_CHANGED: 'calendar:viewchanged', + VIEW_CHANGE_REQUESTED: 'calendar:viewchangerequested', VIEW_RENDERED: 'calendar:viewrendered', PERIOD_CHANGE: 'calendar:periodchange', - // Event CRUD + // Navigation events + WEEK_CHANGED: 'calendar:weekchanged', + WEEK_INFO_UPDATED: 'calendar:weekinfoupdated', + NAV_PREV: 'calendar:navprev', + NAV_NEXT: 'calendar:navnext', + NAV_TODAY: 'calendar:navtoday', + NAVIGATE_TO_DATE: 'calendar:navigatetodate', + NAVIGATE_TO_TODAY: 'calendar:navigatetotoday', + NAVIGATE_NEXT: 'calendar:navigatenext', + NAVIGATE_PREVIOUS: 'calendar:navigateprevious', + + // Event CRUD (still used for UI layer) EVENT_CREATE: 'calendar:eventcreate', EVENT_CREATED: 'calendar:eventcreated', EVENT_UPDATE: 'calendar:eventupdate', @@ -19,9 +42,12 @@ export const EventTypes = { EVENT_RENDERED: 'calendar:eventrendered', EVENT_SELECTED: 'calendar:eventselected', EVENTS_LOADED: 'calendar:eventsloaded', - RESOURCE_DATA_LOADED: 'calendar:resourcedataloaded', - // Interaction events + // User interaction events + GRID_CLICK: 'calendar:gridclick', + GRID_DBLCLICK: 'calendar:griddblclick', + + // Drag and drop events DRAG_START: 'calendar:dragstart', DRAG_MOVE: 'calendar:dragmove', DRAG_END: 'calendar:dragend', @@ -40,62 +66,50 @@ export const EventTypes = { SEARCH_UPDATE: 'calendar:searchupdate', SEARCH_CLEAR: 'calendar:searchclear', - // Grid events - GRID_CLICK: 'calendar:gridclick', - GRID_DBLCLICK: 'calendar:griddblclick', - GRID_RENDERED: 'calendar:gridrendered', - - // Data events + // Data events (legacy - prefer StateEvents) + DATE_CHANGED: 'calendar:datechanged', DATA_FETCH_START: 'calendar:datafetchstart', - DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess', + DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess', DATA_FETCH_ERROR: 'calendar:datafetcherror', DATA_SYNC_START: 'calendar:datasyncstart', DATA_SYNC_SUCCESS: 'calendar:datasyncsuccess', DATA_SYNC_ERROR: 'calendar:datasyncerror', - // State events - STATE_UPDATE: 'calendar:stateupdate', - CONFIG_UPDATE: 'calendar:configupdate', - CALENDAR_TYPE_CHANGED: 'calendar:calendartypechanged', - SELECTED_DATE_CHANGED: 'calendar:selecteddatechanged', + // Initialization events (legacy - prefer StateEvents) + CALENDAR_INITIALIZED: 'calendar:initialized', + CALENDAR_DATA_LOADED: 'calendar:calendardataloaded', + GRID_RENDERED: 'calendar:gridrendered', + + // Management events (legacy - prefer StateEvents) + REFRESH_REQUESTED: 'calendar:refreshrequested', + RESET_REQUESTED: 'calendar:resetrequested', + CALENDAR_REFRESH_REQUESTED: 'calendar:refreshrequested', + CALENDAR_RESET: 'calendar:reset', + + // System events + ERROR: 'calendar:error', // Time events TIME_UPDATE: 'calendar:timeupdate', - // Navigation events - NAV_PREV: 'calendar:navprev', - NAV_NEXT: 'calendar:navnext', - NAV_TODAY: 'calendar:navtoday', - NAVIGATE_TO_DATE: 'calendar:navigatetodate', - WEEK_CHANGED: 'calendar:weekchanged', - WEEK_INFO_UPDATED: 'calendar:weekinfoupdated', - WEEK_CONTAINER_CREATED: 'calendar:weekcontainercreated', - - // Loading events + // Loading events LOADING_START: 'calendar:loadingstart', - LOADING_END: 'calendar:loadingend', - - // Error events - ERROR: 'calendar:error', - - // Init events - READY: 'calendar:ready', - DESTROY: 'calendar:destroy', - - // Calendar Manager Events - CALENDAR_INITIALIZING: 'calendar:initializing', - CALENDAR_INITIALIZED: 'calendar:initialized', - VIEW_CHANGED: 'calendar:viewchanged', - DATE_CHANGED: 'calendar:datechanged', - CALENDAR_REFRESH_REQUESTED: 'calendar:refreshrequested', - CALENDAR_RESET: 'calendar:reset', - VIEW_CHANGE_REQUESTED: 'calendar:viewchangerequested', - NAVIGATE_TO_TODAY: 'calendar:navigatetotoday', - NAVIGATE_NEXT: 'calendar:navigatenext', - NAVIGATE_PREVIOUS: 'calendar:navigateprevious', - REFRESH_REQUESTED: 'calendar:refreshrequested', - RESET_REQUESTED: 'calendar:resetrequested' + LOADING_END: 'calendar:loadingend' } as const; -// Type for event type values -export type EventType = typeof EventTypes[keyof typeof EventTypes]; \ No newline at end of file +// Type for event bus event type values +export type EventBusType = typeof EventTypes[keyof typeof EventTypes]; + +/** + * REMOVED EVENTS (now handled by StateEvents): + * - CALENDAR_INITIALIZING: Use StateEvents.CALENDAR_STATE_CHANGED + * - CALENDAR_INITIALIZED: Use StateEvents.CALENDAR_STATE_CHANGED + * - CALENDAR_DATA_LOADED: Use StateEvents.DATA_LOADED + * - GRID_RENDERED: Use StateEvents.GRID_RENDERED + * - VIEW_CHANGE_REQUESTED: Use StateEvents.VIEW_CHANGE_REQUESTED + * - VIEW_CHANGED: Use StateEvents.VIEW_CHANGED + * - DATA_FETCH_*: Use StateEvents.DATA_LOADING_STARTED/DATA_LOADED/DATA_FAILED + * - DATA_SYNC_*: Use StateEvents for better coordination + * - CALENDAR_READY: Use StateEvents.CALENDAR_READY + * - RENDERING_*: Use StateEvents.RENDERING_STARTED/RENDERING_COMPLETE + */ \ No newline at end of file diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index 8e9b8c0..7aeef1a 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -2,15 +2,49 @@ import { eventBus } from './EventBus'; import { EventTypes } from '../constants/EventTypes'; -import { CalendarConfig as ICalendarConfig, ViewType, CalendarType } from '../types/CalendarTypes'; +import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode, DateViewType, CalendarType } from '../types/CalendarTypes'; /** - * View-specific settings interface + * Layout and timing settings for the calendar grid */ -interface ViewSettings { - columns: number; - showAllDay: boolean; +interface GridSettings { + // Time boundaries + dayStartHour: number; + dayEndHour: number; + workStartHour: number; + workEndHour: number; + + // Layout settings + hourHeight: number; + snapInterval: number; + fitToWidth: boolean; scrollToHour: number | null; + + // Display options + showCurrentTime: boolean; + showWorkHours: boolean; +} + +/** + * View settings for date-based calendar mode + */ +interface DateViewSettings { + period: ViewPeriod; // day/week/month + weekDays: number; // Number of days to show in week view + firstDayOfWeek: number; // 0=Sunday, 1=Monday + showAllDay: boolean; // Show all-day event row +} + +/** + * View settings for resource-based calendar mode + */ +interface ResourceViewSettings { + maxResources: number; // Maximum resources to display + showAvatars: boolean; // Display user avatars + avatarSize: number; // Avatar size in pixels + resourceNameFormat: 'full' | 'short'; // How to display names + showResourceDetails: boolean; // Show additional resource info + showAllDay: boolean; // Show all-day event row } /** @@ -18,29 +52,14 @@ interface ViewSettings { */ export class CalendarConfig { private config: ICalendarConfig; - private calendarType: CalendarType = 'date'; + private calendarMode: CalendarMode = 'date'; private selectedDate: Date | null = null; + private gridSettings: GridSettings; + private dateViewSettings: DateViewSettings; + private resourceViewSettings: ResourceViewSettings; constructor() { this.config = { - // View settings - view: 'week', // 'day' | 'week' | 'month' - weekDays: 7, // 4-7 days for week view - firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday - - // Time settings - dayStartHour: 0, // Calendar starts at midnight (default) - dayEndHour: 24, // Calendar ends at midnight (default) - workStartHour: 8, // Work hours start - workEndHour: 17, // Work hours end - snapInterval: 15, // Minutes: 5, 10, 15, 30, 60 - - // Display settings - hourHeight: 60, // Pixels per hour - showCurrentTime: true, - showWorkHours: true, - fitToWidth: false, // Fit columns to calendar width (no horizontal scroll) - // Scrollbar styling scrollbarWidth: 16, // Width of scrollbar in pixels scrollbarColor: '#666', // Scrollbar thumb color @@ -68,8 +87,40 @@ export class CalendarConfig { maxEventDuration: 480 // 8 hours }; + // Grid display settings + this.gridSettings = { + hourHeight: 60, + dayStartHour: 0, + dayEndHour: 24, + workStartHour: 8, + workEndHour: 17, + snapInterval: 15, + showCurrentTime: true, + showWorkHours: true, + fitToWidth: false, + scrollToHour: 8 + }; + + // Date view settings + this.dateViewSettings = { + period: 'week', + weekDays: 7, + firstDayOfWeek: 1, + showAllDay: true + }; + + // Resource view settings + this.resourceViewSettings = { + maxResources: 10, + showAvatars: true, + avatarSize: 32, + resourceNameFormat: 'full', + showResourceDetails: true, + showAllDay: true + }; + // Set computed values - this.config.minEventDuration = this.config.snapInterval; + this.config.minEventDuration = this.gridSettings.snapInterval; // Load calendar type from URL parameter this.loadCalendarType(); @@ -86,13 +137,13 @@ export class CalendarConfig { const typeParam = urlParams.get('type'); const dateParam = urlParams.get('date'); - // Set calendar type + // Set calendar mode if (typeParam === 'resource' || typeParam === 'date') { - this.calendarType = typeParam; - console.log(`CalendarConfig: Calendar type set to '${this.calendarType}' from URL parameter`); + this.calendarMode = typeParam; + console.log(`CalendarConfig: Calendar mode set to '${this.calendarMode}' from URL parameter`); } else { - this.calendarType = 'date'; // Default - console.log(`CalendarConfig: Calendar type defaulted to '${this.calendarType}'`); + this.calendarMode = 'date'; // Default + console.log(`CalendarConfig: Calendar mode defaulted to '${this.calendarMode}'`); } // Set selected date @@ -121,13 +172,19 @@ export class CalendarConfig { // Read data attributes const attrs = calendar.dataset; - if (attrs.view) this.config.view = attrs.view as ViewType; - if (attrs.weekDays) this.config.weekDays = parseInt(attrs.weekDays); - if (attrs.snapInterval) this.config.snapInterval = parseInt(attrs.snapInterval); - if (attrs.dayStartHour) this.config.dayStartHour = parseInt(attrs.dayStartHour); - if (attrs.dayEndHour) this.config.dayEndHour = parseInt(attrs.dayEndHour); - if (attrs.hourHeight) this.config.hourHeight = parseInt(attrs.hourHeight); - if (attrs.fitToWidth !== undefined) this.config.fitToWidth = attrs.fitToWidth === 'true'; + // Update date view settings + if (attrs.view) this.dateViewSettings.period = attrs.view as ViewPeriod; + if (attrs.weekDays) this.dateViewSettings.weekDays = parseInt(attrs.weekDays); + + // Update grid settings + if (attrs.snapInterval) this.gridSettings.snapInterval = parseInt(attrs.snapInterval); + if (attrs.dayStartHour) this.gridSettings.dayStartHour = parseInt(attrs.dayStartHour); + if (attrs.dayEndHour) this.gridSettings.dayEndHour = parseInt(attrs.dayEndHour); + if (attrs.hourHeight) this.gridSettings.hourHeight = parseInt(attrs.hourHeight); + if (attrs.fitToWidth !== undefined) this.gridSettings.fitToWidth = attrs.fitToWidth === 'true'; + + // Update computed values + this.config.minEventDuration = this.gridSettings.snapInterval; } /** @@ -144,10 +201,7 @@ export class CalendarConfig { const oldValue = this.config[key]; this.config[key] = value; - // Update computed values - if (key === 'snapInterval') { - this.config.minEventDuration = value as number; - } + // Update computed values handled in specific update methods // Emit config update event eventBus.emit(EventTypes.CONFIG_UPDATE, { @@ -178,11 +232,11 @@ export class CalendarConfig { */ get minuteHeight(): number { - return this.config.hourHeight / 60; + return this.gridSettings.hourHeight / 60; } get totalHours(): number { - return this.config.dayEndHour - this.config.dayStartHour; + return this.gridSettings.dayEndHour - this.gridSettings.dayStartHour; } get totalMinutes(): number { @@ -190,7 +244,7 @@ export class CalendarConfig { } get slotsPerHour(): number { - return 60 / this.config.snapInterval; + return 60 / this.gridSettings.snapInterval; } get totalSlots(): number { @@ -198,7 +252,7 @@ export class CalendarConfig { } get slotHeight(): number { - return this.config.hourHeight / this.slotsPerHour; + return this.gridSettings.hourHeight / this.slotsPerHour; } /** @@ -209,48 +263,144 @@ export class CalendarConfig { } /** - * Get view-specific settings + * Get grid display settings */ - getViewSettings(view: ViewType = this.config.view): ViewSettings { - const settings: Record = { - day: { - columns: 1, - showAllDay: true, - scrollToHour: 8 - }, - week: { - columns: this.config.weekDays, - showAllDay: true, - scrollToHour: 8 - }, - month: { - columns: 7, - showAllDay: false, - scrollToHour: null - } - }; - - return settings[view] || settings.week; + getGridSettings(): GridSettings { + return { ...this.gridSettings }; } /** - * Get calendar type + * Update grid display settings */ - getCalendarType(): CalendarType { - return this.calendarType; - } - - /** - * Set calendar type - */ - setCalendarType(type: CalendarType): void { - const oldType = this.calendarType; - this.calendarType = type; + updateGridSettings(updates: Partial): void { + this.gridSettings = { ...this.gridSettings, ...updates }; - // Emit calendar type change event + // Update computed values + if (updates.snapInterval) { + this.config.minEventDuration = updates.snapInterval; + } + + // Emit grid settings update event + eventBus.emit(EventTypes.CONFIG_UPDATE, { + key: 'gridSettings', + value: this.gridSettings, + oldValue: this.gridSettings + }); + } + + /** + * Get date view settings + */ + getDateViewSettings(): DateViewSettings { + return { ...this.dateViewSettings }; + } + + /** + * Legacy method - for backwards compatibility + */ + getDateHeaderSettings(): DateViewSettings { + return this.getDateViewSettings(); + } + + /** + * Update date view settings + */ + updateDateViewSettings(updates: Partial): void { + this.dateViewSettings = { ...this.dateViewSettings, ...updates }; + + // Emit date view settings update event + eventBus.emit(EventTypes.CONFIG_UPDATE, { + key: 'dateViewSettings', + value: this.dateViewSettings, + oldValue: this.dateViewSettings + }); + } + + /** + * Legacy method - for backwards compatibility + */ + updateDateHeaderSettings(updates: Partial): void { + this.updateDateViewSettings(updates); + } + + /** + * Get resource view settings + */ + getResourceViewSettings(): ResourceViewSettings { + return { ...this.resourceViewSettings }; + } + + /** + * Legacy method - for backwards compatibility + */ + getResourceHeaderSettings(): ResourceViewSettings { + return this.getResourceViewSettings(); + } + + /** + * Update resource view settings + */ + updateResourceViewSettings(updates: Partial): void { + this.resourceViewSettings = { ...this.resourceViewSettings, ...updates }; + + // Emit resource view settings update event + eventBus.emit(EventTypes.CONFIG_UPDATE, { + key: 'resourceViewSettings', + value: this.resourceViewSettings, + oldValue: this.resourceViewSettings + }); + } + + /** + * Legacy method - for backwards compatibility + */ + updateResourceHeaderSettings(updates: Partial): void { + this.updateResourceViewSettings(updates); + } + + /** + * Check if current mode is resource-based + */ + isResourceMode(): boolean { + return this.calendarMode === 'resource'; + } + + /** + * Check if current mode is date-based + */ + isDateMode(): boolean { + return this.calendarMode === 'date'; + } + + /** + * Legacy methods - for backwards compatibility + */ + isResourceView(): boolean { + return this.isResourceMode(); + } + + isDateView(): boolean { + return this.isDateMode(); + } + + /** + * Get calendar mode + */ + getCalendarMode(): CalendarMode { + return this.calendarMode; + } + + /** + * Set calendar mode + */ + setCalendarMode(mode: CalendarMode): void { + const oldMode = this.calendarMode; + this.calendarMode = mode; + + // Emit calendar mode change event eventBus.emit(EventTypes.CALENDAR_TYPE_CHANGED, { - oldType, - newType: type + oldType: oldMode, + newType: mode }); } diff --git a/src/factories/CalendarTypeFactory.ts b/src/factories/CalendarTypeFactory.ts index 735ab16..844b63a 100644 --- a/src/factories/CalendarTypeFactory.ts +++ b/src/factories/CalendarTypeFactory.ts @@ -19,11 +19,17 @@ export interface RendererConfig { */ export class CalendarTypeFactory { private static renderers: Map = new Map(); + private static isInitialized: boolean = false; /** - * Initialize the factory with default renderers + * Initialize the factory with default renderers (only runs once) */ static initialize(): void { + if (this.isInitialized) { + console.warn('CalendarTypeFactory: Already initialized, skipping'); + return; + } + // Register default renderers this.registerRenderers('date', { headerRenderer: new DateHeaderRenderer(), @@ -37,6 +43,7 @@ export class CalendarTypeFactory { eventRenderer: new ResourceEventRenderer() }); + this.isInitialized = true; console.log('CalendarTypeFactory: Initialized with default renderers', Array.from(this.renderers.keys())); } diff --git a/src/index.ts b/src/index.ts index 63760c7..d7ef47c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,32 +8,61 @@ import { EventRenderer } from './managers/EventRenderer.js'; import { GridManager } from './managers/GridManager.js'; import { ScrollManager } from './managers/ScrollManager.js'; import { calendarConfig } from './core/CalendarConfig.js'; +import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js'; /** - * Initialize the calendar application + * Initialize the calendar application with new state-driven approach */ -function initializeCalendar(): void { - console.log('πŸ—“οΈ Initializing Calendar Plantempus...'); +async function initializeCalendar(): Promise { + console.log('πŸ—“οΈ Initializing Calendar Plantempus with state management...'); - // Use the singleton calendar configuration - const config = calendarConfig; + // 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; - // Initialize managers - const calendarManager = new CalendarManager(eventBus, config); - const navigationManager = new NavigationManager(eventBus); - const viewManager = new ViewManager(eventBus); - const eventManager = new EventManager(eventBus); - const eventRenderer = new EventRenderer(eventBus); - const scrollManager = new ScrollManager(); // Initialize BEFORE GridManager - const gridManager = new GridManager(); - - // Enable debug mode for development - eventBus.setDebug(true); - - // Initialize all managers - calendarManager.initialize(); - - console.log('βœ… Calendar Plantempus initialized successfully with all core managers'); + try { + // Use the singleton calendar configuration + const config = calendarConfig; + + // Initialize the CalendarTypeFactory before creating managers + console.log('🏭 Phase 0: Initializing CalendarTypeFactory...'); + CalendarTypeFactory.initialize(); + + // Initialize managers in proper order + console.log('πŸ“‹ Phase 1: Creating core managers...'); + calendarManager = new CalendarManager(eventBus, config); + navigationManager = new NavigationManager(eventBus); + viewManager = new ViewManager(eventBus); + + console.log('🎯 Phase 2: Creating data and rendering managers...'); + // These managers will now respond to state-driven events + eventManager = new EventManager(eventBus); + eventRenderer = new EventRenderer(eventBus); + + console.log('πŸ—οΈ Phase 3: Creating layout managers...'); + scrollManager = new ScrollManager(); // Will respond to GRID_RENDERED + gridManager = new GridManager(); // Will respond to RENDERING_STARTED + + // Enable debug mode for development + eventBus.setDebug(true); + + // Initialize all managers using state-driven coordination + console.log('πŸš€ Phase 4: Starting state-driven initialization...'); + await calendarManager.initialize(); // Now async and fully coordinated + + console.log('🎊 Calendar Plantempus initialized successfully!'); + console.log('πŸ“Š Initialization Report:', calendarManager.getInitializationReport()); + + } catch (error) { + console.error('πŸ’₯ Calendar initialization failed:', error); + // Could implement fallback or retry logic here + throw error; + } // Expose to window for debugging (window as any).calendarDebug = { @@ -48,9 +77,15 @@ function initializeCalendar(): void { }; } -// Initialize when DOM is ready +// Initialize when DOM is ready - now handles async properly if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeCalendar); + document.addEventListener('DOMContentLoaded', () => { + initializeCalendar().catch(error => { + console.error('Failed to initialize calendar:', error); + }); + }); } else { - initializeCalendar(); + initializeCalendar().catch(error => { + console.error('Failed to initialize calendar:', error); + }); } \ No newline at end of file diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index b316950..67a0855 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -2,14 +2,17 @@ import { EventBus } from '../core/EventBus.js'; import { EventTypes } from '../constants/EventTypes.js'; import { CalendarConfig } from '../core/CalendarConfig.js'; import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js'; +import { CalendarStateManager } from './CalendarStateManager.js'; +import { StateEvents } from '../types/CalendarState.js'; /** - * CalendarManager - Hovedkoordinator for alle calendar managers - * HΓ₯ndterer initialisering, koordinering og kommunikation mellem alle managers + * CalendarManager - Main coordinator for all calendar managers + * Now delegates initialization to CalendarStateManager for better coordination */ export class CalendarManager { private eventBus: IEventBus; private config: CalendarConfig; + private stateManager: CalendarStateManager; private currentView: CalendarView = 'week'; private currentDate: Date = new Date(); private isInitialized: boolean = false; @@ -17,40 +20,37 @@ export class CalendarManager { constructor(eventBus: IEventBus, config: CalendarConfig) { this.eventBus = eventBus; this.config = config; + this.stateManager = new CalendarStateManager(); this.setupEventListeners(); + console.log('πŸ“‹ CalendarManager: Created with state management'); } /** - * Initialiser calendar systemet + * Initialize calendar system using state-driven approach */ - public initialize(): void { + public async initialize(): Promise { if (this.isInitialized) { console.warn('CalendarManager is already initialized'); return; } - console.log('Initializing CalendarManager...'); + console.log('πŸš€ CalendarManager: Starting state-driven initialization'); - // Emit initialization event - this.eventBus.emit(EventTypes.CALENDAR_INITIALIZING, { - view: this.currentView, - date: this.currentDate, - config: this.config - }); - - // Set initial view and date - this.setView(this.currentView); - this.setCurrentDate(this.currentDate); - - this.isInitialized = true; - - // Emit initialization complete event - this.eventBus.emit(EventTypes.CALENDAR_INITIALIZED, { - view: this.currentView, - date: this.currentDate - }); - - console.log('CalendarManager initialized successfully'); + try { + // Delegate to StateManager for coordinated initialization + await this.stateManager.initialize(); + + // Set initial view and date after successful initialization + this.setView(this.currentView); + this.setCurrentDate(this.currentDate); + + this.isInitialized = true; + console.log('βœ… CalendarManager: Initialization complete'); + + } catch (error) { + console.error('❌ CalendarManager initialization failed:', error); + throw error; // Let the caller handle the error + } } /** @@ -139,7 +139,28 @@ export class CalendarManager { * Check om calendar er initialiseret */ public isCalendarInitialized(): boolean { - return this.isInitialized; + return this.isInitialized && this.stateManager.isReady(); + } + + /** + * Get current calendar state + */ + public getCurrentState(): string { + return this.stateManager.getCurrentState(); + } + + /** + * Get state manager for advanced operations + */ + public getStateManager(): CalendarStateManager { + return this.stateManager; + } + + /** + * Get initialization report for debugging + */ + public getInitializationReport(): any { + return this.stateManager.getInitializationReport(); } /** diff --git a/src/managers/CalendarStateManager.ts b/src/managers/CalendarStateManager.ts new file mode 100644 index 0000000..1393abb --- /dev/null +++ b/src/managers/CalendarStateManager.ts @@ -0,0 +1,471 @@ +// Calendar state management and coordination + +import { eventBus } from '../core/EventBus'; +import { calendarConfig } from '../core/CalendarConfig'; +import { + CalendarState, + StateEvents, + CalendarEvent, + StateChangeEvent, + ErrorEvent, + VALID_STATE_TRANSITIONS, + InitializationPhase, + STATE_TO_PHASE +} from '../types/CalendarState'; + +/** + * Central coordinator for calendar initialization and state management + * Ensures proper sequencing and eliminates race conditions + */ +export class CalendarStateManager { + private currentState: CalendarState = CalendarState.UNINITIALIZED; + private stateHistory: Array<{ state: CalendarState; timestamp: number }> = []; + private initializationStartTime: number = 0; + private phaseTimings: Map = new Map(); + + constructor() { + console.log('πŸ“‹ CalendarStateManager: Created'); + this.recordStateChange(CalendarState.UNINITIALIZED); + } + + /** + * Get current calendar state + */ + getCurrentState(): CalendarState { + return this.currentState; + } + + /** + * Check if calendar is in ready state + */ + isReady(): boolean { + return this.currentState === CalendarState.READY; + } + + /** + * Get current initialization phase + */ + getCurrentPhase(): InitializationPhase { + return STATE_TO_PHASE[this.currentState]; + } + + /** + * Main initialization method - coordinates all calendar setup + */ + async initialize(): Promise { + console.log('πŸš€ CalendarStateManager: Starting calendar initialization'); + this.initializationStartTime = Date.now(); + + try { + // Phase 1: Configuration loading (blocks everything else) + await this.executeConfigurationPhase(); + + // Phase 2: Parallel data loading and DOM structure setup + await this.executeDataAndDOMPhase(); + + // Phase 3: Event rendering (requires both data and DOM) + await this.executeEventRenderingPhase(); + + // Phase 4: Finalization + await this.executeFinalizationPhase(); + + const totalTime = Date.now() - this.initializationStartTime; + console.log(`🎊 Calendar initialization complete in ${totalTime}ms`); + + } catch (error) { + console.error('❌ Calendar initialization failed:', error); + await this.handleInitializationError(error as Error); + } + } + + /** + * Phase 1: Configuration Loading + * Must complete before any other operations + */ + private async executeConfigurationPhase(): Promise { + console.log('πŸ“– Phase 1: Configuration Loading'); + await this.transitionTo(CalendarState.INITIALIZING); + + this.startPhase(InitializationPhase.CONFIGURATION); + + // Emit config loading started + this.emitEvent(StateEvents.CONFIG_LOADING_STARTED, 'CalendarStateManager', { + configSource: 'URL and DOM attributes' + }); + + // Configuration is already loaded in CalendarConfig constructor + // but we validate and emit the completion event + const configValid = this.validateConfiguration(); + + if (!configValid) { + throw new Error('Invalid calendar configuration'); + } + + this.emitEvent(StateEvents.CONFIG_LOADED, 'CalendarStateManager', { + calendarMode: calendarConfig.getCalendarMode(), + dateViewSettings: calendarConfig.getDateViewSettings(), + gridSettings: calendarConfig.getGridSettings() + }); + + await this.transitionTo(CalendarState.CONFIG_LOADED); + this.endPhase(InitializationPhase.CONFIGURATION); + } + + /** + * Phase 2: Parallel Data Loading and DOM Setup + * These can run concurrently to improve performance + */ + private async executeDataAndDOMPhase(): Promise { + console.log('πŸ“Š Phase 2: Data Loading and DOM Setup (Parallel)'); + this.startPhase(InitializationPhase.DATA_AND_DOM); + + // Start both data loading and rendering setup in parallel + const dataPromise = this.coordinateDataLoading(); + const domPromise = this.coordinateDOMSetup(); + + // Wait for both to complete + await Promise.all([dataPromise, domPromise]); + + this.endPhase(InitializationPhase.DATA_AND_DOM); + } + + /** + * Coordinate data loading process + */ + private async coordinateDataLoading(): Promise { + await this.transitionTo(CalendarState.DATA_LOADING); + + this.emitEvent(StateEvents.DATA_LOADING_STARTED, 'CalendarStateManager', { + mode: calendarConfig.getCalendarMode(), + period: this.getCurrentPeriod() + }); + + // EventManager will respond to DATA_LOADING_STARTED and load data + // We wait for its DATA_LOADED response + await this.waitForEvent(StateEvents.DATA_LOADED, 10000); + + await this.transitionTo(CalendarState.DATA_LOADED); + console.log('βœ… Data loading phase complete'); + } + + /** + * Coordinate DOM structure setup + */ + private async coordinateDOMSetup(): Promise { + await this.transitionTo(CalendarState.RENDERING); + + this.emitEvent(StateEvents.RENDERING_STARTED, 'CalendarStateManager', { + phase: 'DOM structure setup' + }); + + // GridManager will respond to RENDERING_STARTED and create DOM structure + // We wait for its GRID_RENDERED response + await this.waitForEvent(StateEvents.GRID_RENDERED, 5000); + + await this.transitionTo(CalendarState.RENDERED); + console.log('βœ… DOM setup phase complete'); + } + + /** + * Phase 3: Event Rendering + * Requires both data and DOM to be ready + */ + private async executeEventRenderingPhase(): Promise { + console.log('🎨 Phase 3: Event Rendering'); + this.startPhase(InitializationPhase.EVENT_RENDERING); + + // Both data and DOM are ready, trigger event rendering + // EventRenderer will wait for both GRID_RENDERED and DATA_LOADED + + // Wait for events to be rendered + await this.waitForEvent(StateEvents.EVENTS_RENDERED, 3000); + + this.emitEvent(StateEvents.RENDERING_COMPLETE, 'CalendarStateManager', { + phase: 'Event rendering complete' + }); + + this.endPhase(InitializationPhase.EVENT_RENDERING); + console.log('βœ… Event rendering phase complete'); + } + + /** + * Phase 4: Finalization + * System is ready for user interaction + */ + private async executeFinalizationPhase(): Promise { + console.log('🏁 Phase 4: Finalization'); + this.startPhase(InitializationPhase.FINALIZATION); + + await this.transitionTo(CalendarState.READY); + + const totalTime = Date.now() - this.initializationStartTime; + + this.emitEvent(StateEvents.CALENDAR_READY, 'CalendarStateManager', { + initializationTime: totalTime, + finalState: this.currentState, + phaseTimings: this.getPhaseTimings() + }); + + this.endPhase(InitializationPhase.FINALIZATION); + console.log(`πŸŽ‰ Calendar is ready! Total initialization time: ${totalTime}ms`); + } + + /** + * Transition to a new state with validation + */ + private async transitionTo(newState: CalendarState): Promise { + if (!this.isValidTransition(this.currentState, newState)) { + const error = new Error(`Invalid state transition: ${this.currentState} β†’ ${newState}`); + await this.handleInitializationError(error); + return; + } + + const oldState = this.currentState; + this.currentState = newState; + this.recordStateChange(newState); + + // Emit state change event + const stateChangeEvent: StateChangeEvent = { + type: StateEvents.CALENDAR_STATE_CHANGED, + component: 'CalendarStateManager', + timestamp: Date.now(), + data: { + from: oldState, + to: newState, + transitionValid: true + }, + metadata: { + phase: STATE_TO_PHASE[newState] + } + }; + + eventBus.emit(StateEvents.CALENDAR_STATE_CHANGED, stateChangeEvent); + console.log(`πŸ“ State: ${oldState} β†’ ${newState} [${STATE_TO_PHASE[newState]}]`); + } + + /** + * Validate state transition + */ + private isValidTransition(from: CalendarState, to: CalendarState): boolean { + const allowedTransitions = VALID_STATE_TRANSITIONS[from] || []; + return allowedTransitions.includes(to); + } + + /** + * Handle initialization errors with recovery attempts + */ + private async handleInitializationError(error: Error): Promise { + console.error('πŸ’₯ Initialization error:', error); + + const errorEvent: ErrorEvent = { + type: StateEvents.CALENDAR_ERROR, + component: 'CalendarStateManager', + error, + timestamp: Date.now(), + data: { + failedComponent: 'CalendarStateManager', + currentState: this.currentState, + canRecover: this.canRecoverFromError(error) + } + }; + + eventBus.emit(StateEvents.CALENDAR_ERROR, errorEvent); + + // Attempt recovery if possible + if (this.canRecoverFromError(error)) { + await this.attemptRecovery(error); + } else { + await this.transitionTo(CalendarState.ERROR); + } + } + + /** + * Attempt to recover from errors + */ + private async attemptRecovery(error: Error): Promise { + console.log('πŸ”§ Attempting error recovery...'); + + this.emitEvent(StateEvents.RECOVERY_ATTEMPTED, 'CalendarStateManager', { + error: error.message, + currentState: this.currentState + }); + + try { + // Simple recovery strategy: try to continue from a stable state + if (this.currentState === CalendarState.DATA_LOADING) { + // Retry data loading + await this.coordinateDataLoading(); + } else if (this.currentState === CalendarState.RENDERING) { + // Retry DOM setup + await this.coordinateDOMSetup(); + } + + this.emitEvent(StateEvents.RECOVERY_SUCCESS, 'CalendarStateManager', { + recoveredFrom: error.message + }); + + } catch (recoveryError) { + console.error('❌ Recovery failed:', recoveryError); + + this.emitEvent(StateEvents.RECOVERY_FAILED, 'CalendarStateManager', { + originalError: error.message, + recoveryError: (recoveryError as Error).message + }); + + await this.transitionTo(CalendarState.ERROR); + } + } + + /** + * Determine if error is recoverable + */ + private canRecoverFromError(error: Error): boolean { + // Simple recovery logic - can be extended + const recoverableErrors = [ + 'timeout', + 'network', + 'dom not ready', + 'data loading failed' + ]; + + return recoverableErrors.some(pattern => + error.message.toLowerCase().includes(pattern) + ); + } + + /** + * Validate calendar configuration + */ + private validateConfiguration(): boolean { + try { + const mode = calendarConfig.getCalendarMode(); + const gridSettings = calendarConfig.getGridSettings(); + + // Basic validation + if (!mode || !['date', 'resource'].includes(mode)) { + console.error('Invalid calendar mode:', mode); + return false; + } + + if (!gridSettings.hourHeight || gridSettings.hourHeight < 20) { + console.error('Invalid hour height:', gridSettings.hourHeight); + return false; + } + + return true; + } catch (error) { + console.error('Configuration validation failed:', error); + return false; + } + } + + /** + * Get current period for data loading + */ + private getCurrentPeriod(): { start: string; end: string } { + const currentDate = calendarConfig.getSelectedDate() || new Date(); + const mode = calendarConfig.getCalendarMode(); + + if (mode === 'date') { + const dateSettings = calendarConfig.getDateViewSettings(); + + if (dateSettings.period === 'week') { + const weekStart = new Date(currentDate); + weekStart.setDate(currentDate.getDate() - currentDate.getDay()); + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekStart.getDate() + 6); + + return { + start: weekStart.toISOString().split('T')[0], + end: weekEnd.toISOString().split('T')[0] + }; + } + } + + // Default to current day + return { + start: currentDate.toISOString().split('T')[0], + end: currentDate.toISOString().split('T')[0] + }; + } + + /** + * Utility methods + */ + private recordStateChange(state: CalendarState): void { + this.stateHistory.push({ + state, + timestamp: Date.now() + }); + } + + private startPhase(phase: InitializationPhase): void { + this.phaseTimings.set(phase, { start: Date.now() }); + } + + private endPhase(phase: InitializationPhase): void { + const timing = this.phaseTimings.get(phase); + if (timing) { + timing.end = Date.now(); + console.log(`⏱️ ${phase} completed in ${timing.end - timing.start}ms`); + } + } + + private getPhaseTimings(): Record { + const timings: Record = {}; + + this.phaseTimings.forEach((timing, phase) => { + if (timing.start && timing.end) { + timings[phase] = timing.end - timing.start; + } + }); + + return timings; + } + + private emitEvent(type: string, component: string, data?: any): void { + const event: CalendarEvent = { + type, + component, + timestamp: Date.now(), + data, + metadata: { + phase: this.getCurrentPhase() + } + }; + + eventBus.emit(type, event); + } + + private async waitForEvent(eventType: string, timeout: number = 5000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Timeout waiting for event: ${eventType}`)); + }, timeout); + + const handler = (event: Event) => { + clearTimeout(timer); + resolve((event as CustomEvent).detail); + eventBus.off(eventType, handler); + }; + + eventBus.on(eventType, handler); + }); + } + + /** + * Debug methods + */ + getStateHistory(): Array<{ state: CalendarState; timestamp: number }> { + return [...this.stateHistory]; + } + + getInitializationReport(): any { + return { + currentState: this.currentState, + totalTime: Date.now() - this.initializationStartTime, + phaseTimings: this.getPhaseTimings(), + stateHistory: this.stateHistory + }; + } +} \ No newline at end of file diff --git a/src/managers/DataManager.ts b/src/managers/DataManager.ts index 50e4e38..e457fc6 100644 --- a/src/managers/DataManager.ts +++ b/src/managers/DataManager.ts @@ -2,14 +2,14 @@ import { eventBus } from '../core/EventBus'; import { EventTypes } from '../constants/EventTypes'; -import { CalendarEvent, EventData, Period, EventType } from '../types/CalendarTypes'; +import { CalendarEvent, EventData, Period } from '../types/CalendarTypes'; /** * Event creation data interface */ interface EventCreateData { title: string; - type: EventType; + type: string; start: string; end: string; allDay: boolean; @@ -67,7 +67,7 @@ export class DataManager { * Fetch events for a specific period */ async fetchEventsForPeriod(period: Period): Promise { - const cacheKey = `${period.start}-${period.end}-${period.view}`; + const cacheKey = `${period.start}-${period.end}`; // Check cache first if (this.cache.has(cacheKey)) { @@ -90,8 +90,7 @@ export class DataManager { // Real API call const params = new URLSearchParams({ start: period.start, - end: period.end, - view: period.view + end: period.end }); const response = await fetch(`${this.baseUrl}?${params}`); @@ -275,8 +274,8 @@ export class DataManager { */ private getMockData(period: Period): EventData { const events: CalendarEvent[] = []; - const types: EventType[] = ['meeting', 'meal', 'work', 'milestone']; - const titles: Record = { + const types: string[] = ['meeting', 'meal', 'work', 'milestone']; + const titles: Record = { meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'], meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'], work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'], @@ -296,7 +295,7 @@ export class DataManager { if (isWeekend) { // Maybe one or two events on weekends if (Math.random() > 0.7) { - const type: EventType = 'meal'; + const type: string = 'meal'; const title = titles[type][Math.floor(Math.random() * titles[type].length)]; const hour = 12 + Math.floor(Math.random() * 4); @@ -358,10 +357,11 @@ export class DataManager { } } - // Add a multi-day event - if (period.view === 'week') { + // Add a multi-day event if period spans multiple days + const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); + if (daysDiff > 1) { const midWeek = new Date(startDate); - midWeek.setDate(midWeek.getDate() + 2); + midWeek.setDate(midWeek.getDate() + Math.min(2, daysDiff - 1)); events.push({ id: `evt-${events.length + 1}`, @@ -379,7 +379,6 @@ export class DataManager { meta: { start: period.start, end: period.end, - view: period.view, total: events.length } }; diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index eb7c1c8..8d75494 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -1,6 +1,7 @@ import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; +import { StateEvents } from '../types/CalendarState'; import { calendarConfig } from '../core/CalendarConfig'; /** @@ -15,33 +16,54 @@ export class EventManager { console.log('EventManager: Constructor called'); this.eventBus = eventBus; this.setupEventListeners(); - console.log('EventManager: About to call loadMockData()'); - this.loadMockData().then(() => { - console.log('EventManager: loadMockData() completed, syncing events'); - // Data loaded, sync events after loading - this.syncEvents(); - }).catch(error => { - console.error('EventManager: loadMockData() failed:', error); - }); + console.log('EventManager: Waiting for CALENDAR_INITIALIZED before loading data'); } private setupEventListeners(): void { - this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => { - this.syncEvents(); + // Listen for state-driven data loading request + this.eventBus.on(StateEvents.DATA_LOADING_STARTED, (e: Event) => { + const detail = (e as CustomEvent).detail; + console.log('EventManager: Received DATA_LOADING_STARTED, starting data load'); + + this.loadMockData().then(() => { + console.log('EventManager: loadMockData() completed, emitting DATA_LOADED'); + // Emit state-driven data loaded event + this.eventBus.emit(StateEvents.DATA_LOADED, { + type: StateEvents.DATA_LOADED, + component: 'EventManager', + timestamp: Date.now(), + data: { + eventCount: this.events.length, + calendarMode: calendarConfig.getCalendarMode(), + period: detail.data?.period || { start: '', end: '' }, + events: this.events // Include actual events for EventRenderer + }, + metadata: { + phase: 'data-loading' + } + }); + }).catch(error => { + console.error('EventManager: loadMockData() failed:', error); + this.eventBus.emit(StateEvents.DATA_FAILED, { + type: StateEvents.DATA_FAILED, + component: 'EventManager', + timestamp: Date.now(), + error, + metadata: { + phase: 'data-loading' + } + }); + }); }); - this.eventBus.on(EventTypes.DATE_CHANGED, () => { - this.syncEvents(); - }); - this.eventBus.on(EventTypes.VIEW_RENDERED, () => { - this.syncEvents(); - }); + // Legacy event listeners removed - data is now managed via state-driven events only + } private async loadMockData(): Promise { try { - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); let jsonFile: string; console.log(`EventManager: Calendar type detected: '${calendarType}'`); @@ -59,43 +81,41 @@ export class EventManager { throw new Error(`Failed to load mock events: ${response.status}`); } - if (calendarType === 'resource') { - const resourceData: ResourceCalendarData = await response.json(); - // Flatten events from all resources and add resource metadata - this.events = resourceData.resources.flatMap(resource => - resource.events.map(event => ({ - ...event, - resourceName: resource.name, - resourceDisplayName: resource.displayName, - resourceEmployeeId: resource.employeeId - })) - ); - console.log(`EventManager: Loaded ${this.events.length} events from ${resourceData.resources.length} resources`); - - // Emit resource data for GridManager - this.eventBus.emit(EventTypes.RESOURCE_DATA_LOADED, { - resourceData: resourceData - }); - } else { - this.events = await response.json(); - console.log(`EventManager: Loaded ${this.events.length} date calendar events`); - } + const data = await response.json(); + console.log(`EventManager: Loaded data for ${calendarType} calendar`); - console.log('EventManager: First event:', this.events[0]); - console.log('EventManager: Last event:', this.events[this.events.length - 1]); + // Remove legacy double emission - data is sent via StateEvents.DATA_LOADED only + + // Process data for internal use + this.processCalendarData(calendarType, data); } catch (error) { console.error('EventManager: Failed to load mock events:', error); this.events = []; // Fallback to empty array } } - private syncEvents(): void { - // Emit events for rendering - this.eventBus.emit(EventTypes.EVENTS_LOADED, { - events: this.events - }); + private processCalendarData(calendarType: string, data: any): void { + if (calendarType === 'resource') { + const resourceData = data as ResourceCalendarData; + this.events = resourceData.resources.flatMap(resource => + resource.events.map(event => ({ + ...event, + resourceName: resource.name, + resourceDisplayName: resource.displayName, + resourceEmployeeId: resource.employeeId + })) + ); + console.log(`EventManager: Processed ${this.events.length} events from ${resourceData.resources.length} resources`); + } else { + this.events = data as CalendarEvent[]; + console.log(`EventManager: Processed ${this.events.length} date events`); + } + } - console.log(`EventManager: Synced ${this.events.length} events`); + private syncEvents(): void { + // Events are now synced via StateEvents.DATA_LOADED during initialization + // This method maintained for internal state management only + console.log(`EventManager: Internal sync - ${this.events.length} events in memory`); } public getEvents(): CalendarEvent[] { diff --git a/src/managers/EventRenderer.ts b/src/managers/EventRenderer.ts index ddcb03e..b739a5b 100644 --- a/src/managers/EventRenderer.ts +++ b/src/managers/EventRenderer.ts @@ -1,6 +1,7 @@ import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; +import { StateEvents } from '../types/CalendarState'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; @@ -11,30 +12,36 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; export class EventRenderer { private eventBus: IEventBus; private pendingEvents: CalendarEvent[] = []; + private dataReady: boolean = false; + private gridReady: boolean = false; constructor(eventBus: IEventBus) { this.eventBus = eventBus; this.setupEventListeners(); - - // Initialize the factory (if not already done) - CalendarTypeFactory.initialize(); } private setupEventListeners(): void { - this.eventBus.on(EventTypes.EVENTS_LOADED, (event: Event) => { + // Listen for state-driven data loaded event + this.eventBus.on(StateEvents.DATA_LOADED, (event: Event) => { const customEvent = event as CustomEvent; - const { events } = customEvent.detail; - console.log('EventRenderer: Received EVENTS_LOADED with', events.length, 'events'); - // Store events but don't render yet - wait for grid to be ready - this.pendingEvents = events; + // Events are in customEvent.detail (direct from StateEvent payload) + const eventCount = customEvent.detail.data?.eventCount || 0; + const events = customEvent.detail.data?.events || []; + console.log('EventRenderer: Received DATA_LOADED with', eventCount, 'events'); + this.pendingEvents = events; // Store the actual events + this.dataReady = true; this.tryRenderEvents(); }); - this.eventBus.on(EventTypes.GRID_RENDERED, () => { - // Grid is ready, now we can render events + // Listen for state-driven grid rendered event + this.eventBus.on(StateEvents.GRID_RENDERED, (event: Event) => { + const customEvent = event as CustomEvent; + console.log('EventRenderer: Received GRID_RENDERED'); + this.gridReady = true; this.tryRenderEvents(); }); + this.eventBus.on(EventTypes.VIEW_RENDERED, () => { // Clear existing events when view changes this.clearEvents(); @@ -48,20 +55,50 @@ export class EventRenderer { } private tryRenderEvents(): void { - // Only render if we have both events and appropriate columns are ready - console.log('EventRenderer: tryRenderEvents called, pending events:', this.pendingEvents.length); + // Only render if we have both data and grid ready + console.log('EventRenderer: tryRenderEvents called', { + dataReady: this.dataReady, + gridReady: this.gridReady, + pendingEvents: this.pendingEvents.length + }); + + if (!this.dataReady || !this.gridReady) { + console.log('EventRenderer: Waiting - data ready:', this.dataReady, 'grid ready:', this.gridReady); + return; + } if (this.pendingEvents.length > 0) { - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); let columnsSelector = calendarType === 'resource' ? 'swp-resource-column' : 'swp-day-column'; const columns = document.querySelectorAll(columnsSelector); console.log(`EventRenderer: Found ${columns.length} ${columnsSelector} elements for ${calendarType} calendar`); if (columns.length > 0) { + console.log('🎨 EventRenderer: Both data and grid ready, rendering events!'); + const eventCount = this.pendingEvents.length; this.renderEvents(this.pendingEvents); this.pendingEvents = []; // Clear pending events after rendering + + // Emit events rendered event + this.eventBus.emit(StateEvents.EVENTS_RENDERED, { + type: StateEvents.EVENTS_RENDERED, + component: 'EventRenderer', + timestamp: Date.now(), + data: { + eventCount, + calendarMode: calendarType, + renderMethod: 'state-driven' + }, + metadata: { + phase: 'event-rendering' + } + }); + } else { + console.log('EventRenderer: Grid not ready yet, columns not found'); } + } else { + console.log('EventRenderer: No pending events to render'); } } @@ -69,7 +106,7 @@ export class EventRenderer { console.log('EventRenderer: renderEvents called with', events.length, 'events'); // Get the appropriate event renderer strategy - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType); console.log(`EventRenderer: Using ${calendarType} event renderer strategy`); @@ -84,7 +121,7 @@ export class EventRenderer { } private clearEvents(): void { - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType); eventRenderer.clearEvents(); } diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 6420d3c..8b5f0af 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -3,6 +3,7 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { EventTypes } from '../constants/EventTypes'; +import { StateEvents } from '../types/CalendarState'; import { DateUtils } from '../utils/DateUtils'; import { ResourceCalendarData } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; @@ -29,13 +30,11 @@ export class GridManager { private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar constructor() { + console.log('πŸ—οΈ GridManager: Constructor called'); this.init(); } private init(): void { - // Initialize the factory - CalendarTypeFactory.initialize(); - this.findElements(); this.subscribeToEvents(); @@ -43,8 +42,8 @@ export class GridManager { if (!this.currentWeek) { this.currentWeek = this.getWeekStart(new Date()); console.log('GridManager: Set initial currentWeek to', this.currentWeek); - // Render initial grid - this.render(); + // Don't render immediately - wait for proper initialization event + console.log('GridManager: Waiting for initialization complete before rendering'); } } @@ -58,6 +57,13 @@ export class GridManager { } private subscribeToEvents(): void { + // Listen for state-driven rendering start event + eventBus.on(StateEvents.RENDERING_STARTED, (e: Event) => { + const detail = (e as CustomEvent).detail; + console.log('GridManager: Received RENDERING_STARTED, starting DOM structure setup'); + this.render(); + }); + // Re-render grid on config changes eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => { const detail = (e as CustomEvent).detail; @@ -96,18 +102,15 @@ export class GridManager { this.updateAllDayEvents(detail.events); }); - // Handle resource data loaded - eventBus.on(EventTypes.RESOURCE_DATA_LOADED, (e: Event) => { + // Handle data loaded for resource mode + eventBus.on(StateEvents.DATA_LOADED, (e: Event) => { const detail = (e as CustomEvent).detail; - this.resourceData = detail.resourceData; - console.log(`GridManager: Received resource data for ${this.resourceData!.resources.length} resources`); + console.log(`GridManager: Received DATA_LOADED`); - // Update grid styles with new column count immediately - this.updateGridStyles(); - - // Re-render if grid is already rendered - if (this.grid && this.grid.children.length > 0) { - this.render(); + if (detail.data && detail.data.calendarMode === 'resource') { + // Resource data will be passed in the state event + // For now just update grid styles + this.updateGridStyles(); } }); @@ -124,12 +127,54 @@ export class GridManager { this.updateGridStyles(); this.renderGrid(); - // Emit grid rendered event + // Emit state-driven grid rendered event + const columnCount = this.getColumnCount(); console.log('GridManager: Emitting GRID_RENDERED event'); - eventBus.emit(EventTypes.GRID_RENDERED); + + eventBus.emit(StateEvents.GRID_RENDERED, { + type: StateEvents.GRID_RENDERED, + component: 'GridManager', + timestamp: Date.now(), + data: { + columnCount, + gridMode: calendarConfig.getCalendarMode(), + domElementsCreated: [ + 'swp-header-spacer', + 'swp-time-axis', + 'swp-grid-container', + 'swp-calendar-header', + 'swp-scrollable-content' + ] + }, + metadata: { + phase: 'rendering' + } + }); + console.log('GridManager: GRID_RENDERED event emitted'); } + /** + * Get current column count based on calendar mode + */ + private getColumnCount(): number { + const calendarType = calendarConfig.getCalendarMode(); + + if (calendarType === 'resource' && this.resourceData) { + return this.resourceData.resources.length; + } else if (calendarType === 'date') { + const dateSettings = calendarConfig.getDateViewSettings(); + switch (dateSettings.period) { + case 'day': return 1; + case 'week': return dateSettings.weekDays; + case 'month': return 7; + default: return dateSettings.weekDays; + } + } + + return 7; // Default + } + /** * Render the complete grid using POC structure */ @@ -148,10 +193,10 @@ export class GridManager { // Only clear and rebuild if grid is empty (first render) if (this.grid.children.length === 0) { console.log('GridManager: First render - creating grid structure'); - // Create POC structure: header-spacer + time-axis + week-container + right-column + bottom spacers + // Create POC structure: header-spacer + time-axis + grid-container this.createHeaderSpacer(); this.createTimeAxis(); - this.createWeekContainer(); + this.createGridContainer(); } else { console.log('GridManager: Re-render - updating existing structure'); // Just update the calendar header for all-day events @@ -172,15 +217,16 @@ export class GridManager { } /** - * Create time axis (positioned beside week container) like in POC + * Create time axis (positioned beside grid container) like in POC */ private createTimeAxis(): void { if (!this.grid) return; const timeAxis = document.createElement('swp-time-axis'); const timeAxisContent = document.createElement('swp-time-axis-content'); - const startHour = calendarConfig.get('dayStartHour'); - const endHour = calendarConfig.get('dayEndHour'); + const gridSettings = calendarConfig.getGridSettings(); + const startHour = gridSettings.dayStartHour; + const endHour = gridSettings.dayEndHour; console.log('GridManager: Creating time axis - startHour:', startHour, 'endHour:', endHour); for (let hour = startHour; hour < endHour; hour++) { @@ -196,17 +242,17 @@ export class GridManager { } /** - * Create week container with header and scrollable content using Strategy Pattern + * Create grid container with header and scrollable content using Strategy Pattern */ - private createWeekContainer(): void { + private createGridContainer(): void { if (!this.grid || !this.currentWeek) return; - const weekContainer = document.createElement('swp-grid-container'); + const gridContainer = document.createElement('swp-grid-container'); // Create calendar header using Strategy Pattern const calendarHeader = document.createElement('swp-calendar-header'); this.renderCalendarHeader(calendarHeader); - weekContainer.appendChild(calendarHeader); + gridContainer.appendChild(calendarHeader); // Create scrollable content const scrollableContent = document.createElement('swp-scrollable-content'); @@ -222,9 +268,9 @@ export class GridManager { timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); - weekContainer.appendChild(scrollableContent); + gridContainer.appendChild(scrollableContent); - this.grid.appendChild(weekContainer); + this.grid.appendChild(gridContainer); } /** @@ -233,7 +279,7 @@ export class GridManager { private renderCalendarHeader(calendarHeader: HTMLElement): void { if (!this.currentWeek) return; - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); const context: HeaderRenderContext = { @@ -256,7 +302,7 @@ export class GridManager { if (!this.currentWeek) return; console.log('GridManager: renderColumnContainer called'); - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType); const context: ColumnRenderContext = { @@ -330,30 +376,44 @@ export class GridManager { */ private updateGridStyles(): void { const root = document.documentElement; - const config = calendarConfig.getAll(); + const gridSettings = calendarConfig.getGridSettings(); const calendar = document.querySelector('swp-calendar') as HTMLElement; - const calendarType = calendarConfig.getCalendarType(); + const calendarType = calendarConfig.getCalendarMode(); // Set CSS variables - root.style.setProperty('--hour-height', `${config.hourHeight}px`); - root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`); - root.style.setProperty('--snap-interval', config.snapInterval.toString()); - root.style.setProperty('--day-start-hour', config.dayStartHour.toString()); - root.style.setProperty('--day-end-hour', config.dayEndHour.toString()); - root.style.setProperty('--work-start-hour', config.workStartHour.toString()); - root.style.setProperty('--work-end-hour', config.workEndHour.toString()); + root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); + root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`); + root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString()); + root.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString()); + root.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString()); + root.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString()); + root.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString()); // Set number of columns based on calendar type let columnCount = 7; // Default for date mode if (calendarType === 'resource' && this.resourceData) { columnCount = this.resourceData.resources.length; } else if (calendarType === 'date') { - columnCount = config.weekDays; + const dateSettings = calendarConfig.getDateViewSettings(); + // Calculate columns based on view type - business logic moved from config + switch (dateSettings.period) { + case 'day': + columnCount = 1; + break; + case 'week': + columnCount = dateSettings.weekDays; + break; + case 'month': + columnCount = 7; + break; + default: + columnCount = dateSettings.weekDays; + } } root.style.setProperty('--grid-columns', columnCount.toString()); // Set day column min width based on fitToWidth setting - if (config.fitToWidth) { + if (gridSettings.fitToWidth) { root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space } else { root.style.setProperty('--day-column-min-width', '250px'); // Default min-width for horizontal scroll mode @@ -361,7 +421,7 @@ export class GridManager { // Set fitToWidth data attribute for CSS targeting if (calendar) { - calendar.setAttribute('data-fit-to-width', config.fitToWidth.toString()); + calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString()); } console.log('GridManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar'); @@ -419,10 +479,11 @@ export class GridManager { const rect = dayColumn.getBoundingClientRect(); const y = event.clientY - rect.top; - const hourHeight = calendarConfig.get('hourHeight'); + const gridSettings = calendarConfig.getGridSettings(); + const hourHeight = gridSettings.hourHeight; const minuteHeight = hourHeight / 60; - const snapInterval = calendarConfig.get('snapInterval'); - const dayStartHour = calendarConfig.get('dayStartHour'); + const snapInterval = gridSettings.snapInterval; + const dayStartHour = gridSettings.dayStartHour; // Calculate total minutes from day start let totalMinutes = Math.floor(y / minuteHeight); @@ -446,8 +507,9 @@ export class GridManager { scrollToHour(hour: number): void { if (!this.grid) return; - const hourHeight = calendarConfig.get('hourHeight'); - const dayStartHour = calendarConfig.get('dayStartHour'); + const gridSettings = calendarConfig.getGridSettings(); + const hourHeight = gridSettings.hourHeight; + const dayStartHour = gridSettings.dayStartHour; const headerHeight = 80; // Header row height const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight); diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 44d66cb..b7b6b3f 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -13,6 +13,7 @@ export class NavigationManager { private animationQueue: number = 0; constructor(eventBus: IEventBus) { + console.log('🧭 NavigationManager: Constructor called'); this.eventBus = eventBus; this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC this.targetWeek = new Date(this.currentWeek); @@ -21,10 +22,17 @@ export class NavigationManager { private init(): void { this.setupEventListeners(); - this.updateWeekInfo(); + // Don't update week info immediately - wait for DOM to be ready + console.log('NavigationManager: Waiting for CALENDAR_INITIALIZED before updating DOM'); } private setupEventListeners(): void { + // Initial DOM update when calendar is initialized + this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => { + console.log('NavigationManager: Received CALENDAR_INITIALIZED, updating week info'); + this.updateWeekInfo(); + }); + // Listen for navigation button clicks document.addEventListener('click', (e) => { const target = e.target as HTMLElement; @@ -157,10 +165,16 @@ export class NavigationManager { if (weekNumberElement) { weekNumberElement.textContent = `Week ${weekNumber}`; + console.log('NavigationManager: Updated week number:', `Week ${weekNumber}`); + } else { + console.warn('NavigationManager: swp-week-number element not found in DOM'); } if (dateRangeElement) { dateRangeElement.textContent = dateRange; + console.log('NavigationManager: Updated date range:', dateRange); + } else { + console.warn('NavigationManager: swp-date-range element not found in DOM'); } // Notify other managers about week info update diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts index 3865ae3..d77256a 100644 --- a/src/managers/ScrollManager.ts +++ b/src/managers/ScrollManager.ts @@ -3,6 +3,7 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { EventTypes } from '../constants/EventTypes'; +import { StateEvents } from '../types/CalendarState'; /** * Manages scrolling functionality for the calendar using native scrollbars @@ -15,6 +16,7 @@ export class ScrollManager { private resizeObserver: ResizeObserver | null = null; constructor() { + console.log('πŸ“œ ScrollManager: Constructor called'); this.init(); } @@ -24,10 +26,20 @@ export class ScrollManager { private subscribeToEvents(): void { // Initialize scroll when grid is rendered - eventBus.on(EventTypes.GRID_RENDERED, () => { + eventBus.on(StateEvents.GRID_RENDERED, () => { console.log('ScrollManager: Received GRID_RENDERED event'); this.setupScrolling(); }); + + // Add safety check - if grid is already rendered when ScrollManager initializes + // This prevents race condition where GridManager renders before ScrollManager subscribes + //setTimeout(() => { + // const existingGrid = document.querySelector('swp-calendar-container'); + // if (existingGrid && existingGrid.children.length > 0) { + // console.log('ScrollManager: Grid already exists, setting up scrolling'); + // this.setupScrolling(); + // } + //}, 0); // Handle window resize window.addEventListener('resize', () => { @@ -35,8 +47,8 @@ export class ScrollManager { }); // Handle config updates for scrollbar styling - eventBus.on(EventTypes.CONFIG_UPDATE, (event: CustomEvent) => { - const { key } = event.detail; + eventBus.on(EventTypes.CONFIG_UPDATE, (event: Event) => { + const { key } = (event as CustomEvent).detail; if (key.startsWith('scrollbar')) { this.applyScrollbarStyling(); } @@ -131,8 +143,9 @@ export class ScrollManager { * Scroll to specific hour */ scrollToHour(hour: number): void { - const hourHeight = calendarConfig.get('hourHeight'); - const dayStartHour = calendarConfig.get('dayStartHour'); + const gridSettings = calendarConfig.getGridSettings(); + const hourHeight = gridSettings.hourHeight; + const dayStartHour = gridSettings.dayStartHour; const scrollTop = (hour - dayStartHour) * hourHeight; this.scrollTo(scrollTop); diff --git a/src/types/CalendarState.ts b/src/types/CalendarState.ts new file mode 100644 index 0000000..c8d89bc --- /dev/null +++ b/src/types/CalendarState.ts @@ -0,0 +1,170 @@ +// Calendar state management types + +/** + * Calendar initialization and runtime states + * Represents the progression from startup to ready state + */ +export enum CalendarState { + UNINITIALIZED = 'uninitialized', + INITIALIZING = 'initializing', + CONFIG_LOADED = 'config_loaded', + DATA_LOADING = 'data_loading', + DATA_LOADED = 'data_loaded', + RENDERING = 'rendering', + RENDERED = 'rendered', + READY = 'ready', + ERROR = 'error' +} + +/** + * State-driven events with clear progression and timing + */ +export const StateEvents = { + // Core lifecycle events + CALENDAR_STATE_CHANGED: 'calendar:state:changed', + + // Configuration phase + CONFIG_LOADING_STARTED: 'calendar:config:loading:started', + CONFIG_LOADED: 'calendar:config:loaded', + CONFIG_FAILED: 'calendar:config:failed', + + // Data loading phase (can run parallel with rendering setup) + DATA_LOADING_STARTED: 'calendar:data:loading:started', + DATA_LOADED: 'calendar:data:loaded', + DATA_FAILED: 'calendar:data:failed', + + // Rendering phase + RENDERING_STARTED: 'calendar:rendering:started', + DOM_STRUCTURE_READY: 'calendar:dom:structure:ready', + GRID_RENDERED: 'calendar:grid:rendered', + EVENTS_RENDERED: 'calendar:events:rendered', + RENDERING_COMPLETE: 'calendar:rendering:complete', + + // System ready + CALENDAR_READY: 'calendar:ready', + + // Error handling + CALENDAR_ERROR: 'calendar:error', + RECOVERY_ATTEMPTED: 'calendar:recovery:attempted', + RECOVERY_SUCCESS: 'calendar:recovery:success', + RECOVERY_FAILED: 'calendar:recovery:failed', + + // User interaction events (unchanged) + VIEW_CHANGE_REQUESTED: 'calendar:view:change:requested', + VIEW_CHANGED: 'calendar:view:changed', + NAVIGATION_REQUESTED: 'calendar:navigation:requested', + +} as const; + +/** + * Standardized event payload structure + */ +export interface CalendarEvent { + type: string; + component: string; + timestamp: number; + data?: any; + error?: Error; + metadata?: { + duration?: number; + dependencies?: string[]; + phase?: string; + retryCount?: number; + }; +} + +/** + * State change event payload + */ +export interface StateChangeEvent extends CalendarEvent { + type: typeof StateEvents.CALENDAR_STATE_CHANGED; + data: { + from: CalendarState; + to: CalendarState; + transitionValid: boolean; + }; +} + +/** + * Error event payload + */ +export interface ErrorEvent extends CalendarEvent { + type: typeof StateEvents.CALENDAR_ERROR; + error: Error; + data: { + failedComponent: string; + currentState: CalendarState; + canRecover: boolean; + }; +} + +/** + * Data loaded event payload + */ +export interface DataLoadedEvent extends CalendarEvent { + type: typeof StateEvents.DATA_LOADED; + data: { + eventCount: number; + calendarMode: 'date' | 'resource'; + period: { + start: string; + end: string; + }; + }; +} + +/** + * Grid rendered event payload + */ +export interface GridRenderedEvent extends CalendarEvent { + type: typeof StateEvents.GRID_RENDERED; + data: { + columnCount: number; + rowCount?: number; + gridMode: 'date' | 'resource'; + domElementsCreated: string[]; + }; +} + +/** + * Valid state transitions map + * Defines which state transitions are allowed + */ +export const VALID_STATE_TRANSITIONS: Record = { + [CalendarState.UNINITIALIZED]: [CalendarState.INITIALIZING, CalendarState.ERROR], + [CalendarState.INITIALIZING]: [CalendarState.CONFIG_LOADED, CalendarState.ERROR], + [CalendarState.CONFIG_LOADED]: [CalendarState.DATA_LOADING, CalendarState.RENDERING, CalendarState.ERROR], + [CalendarState.DATA_LOADING]: [CalendarState.DATA_LOADED, CalendarState.ERROR], + [CalendarState.DATA_LOADED]: [CalendarState.RENDERING, CalendarState.RENDERED, CalendarState.ERROR], + [CalendarState.RENDERING]: [CalendarState.RENDERED, CalendarState.ERROR], + [CalendarState.RENDERED]: [CalendarState.READY, CalendarState.ERROR], + [CalendarState.READY]: [CalendarState.DATA_LOADING, CalendarState.ERROR], // Allow refresh + [CalendarState.ERROR]: [CalendarState.INITIALIZING, CalendarState.CONFIG_LOADED] // Recovery paths +}; + +/** + * State phases for logical grouping + */ +export enum InitializationPhase { + STARTUP = 'startup', + CONFIGURATION = 'configuration', + DATA_AND_DOM = 'data-and-dom', + EVENT_RENDERING = 'event-rendering', + FINALIZATION = 'finalization', + ERROR_RECOVERY = 'error-recovery' +} + +/** + * Map states to their initialization phases + */ +export const STATE_TO_PHASE: Record = { + [CalendarState.UNINITIALIZED]: InitializationPhase.STARTUP, + [CalendarState.INITIALIZING]: InitializationPhase.STARTUP, + [CalendarState.CONFIG_LOADED]: InitializationPhase.CONFIGURATION, + [CalendarState.DATA_LOADING]: InitializationPhase.DATA_AND_DOM, + [CalendarState.DATA_LOADED]: InitializationPhase.DATA_AND_DOM, + [CalendarState.RENDERING]: InitializationPhase.DATA_AND_DOM, + [CalendarState.RENDERED]: InitializationPhase.EVENT_RENDERING, + [CalendarState.READY]: InitializationPhase.FINALIZATION, + [CalendarState.ERROR]: InitializationPhase.ERROR_RECOVERY +}; \ No newline at end of file diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts index 62599b8..76805a9 100644 --- a/src/types/CalendarTypes.ts +++ b/src/types/CalendarTypes.ts @@ -1,11 +1,16 @@ // Calendar type definitions -export type ViewType = 'day' | 'week' | 'month'; -export type CalendarView = ViewType; // Alias for compatibility +// Time period view types (how much time to display) +export type ViewPeriod = 'day' | 'week' | 'month'; -export type CalendarType = 'date' | 'resource'; +// Calendar mode types (how to organize the data) +export type CalendarMode = 'date' | 'resource'; -export type EventType = 'meeting' | 'meal' | 'work' | 'milestone'; +// Legacy aliases for backwards compatibility +export type DateViewType = ViewPeriod; +export type ViewType = DateViewType; +export type CalendarView = ViewType; +export type CalendarType = CalendarMode; export type SyncStatus = 'synced' | 'pending' | 'error'; @@ -27,37 +32,22 @@ export interface CalendarEvent { title: string; start: string; // ISO 8601 end: string; // ISO 8601 - type: EventType; + type: string; // Flexible event type - can be any string value allDay: boolean; syncStatus: SyncStatus; + // Resource information (only present in resource calendar mode) - resourceName?: string; - resourceDisplayName?: string; - resourceEmployeeId?: string; + resource?: { + name: string; + displayName: string; + employeeId: string; + }; + recurringId?: string; - resources?: string[]; metadata?: Record; } export interface CalendarConfig { - // View settings - view: ViewType; - weekDays: number; // 4-7 days for week view - firstDayOfWeek: number; // 0 = Sunday, 1 = Monday - - // Time settings - dayStartHour: number; // Calendar starts at hour - dayEndHour: number; // Calendar ends at hour - workStartHour: number; // Work hours start - workEndHour: number; // Work hours end - snapInterval: number; // Minutes: 5, 10, 15, 30, 60 - - // Display settings - hourHeight: number; // Pixels per hour - showCurrentTime: boolean; - showWorkHours: boolean; - fitToWidth: boolean; // Fit columns to calendar width vs horizontal scroll - // Scrollbar styling scrollbarWidth: number; // Width of scrollbar in pixels scrollbarColor: string; // Scrollbar thumb color @@ -116,7 +106,7 @@ export interface GridPosition { export interface Period { start: string; end: string; - view: ViewType; + mode?: CalendarMode; // Optional: which calendar mode this period is for } export interface EventData { @@ -124,7 +114,30 @@ export interface EventData { meta: { start: string; end: string; - view: ViewType; total: number; + mode?: CalendarMode; // Which calendar mode this data is for }; -} \ No newline at end of file +} + +/** + * Context interfaces for different calendar modes + */ +export interface DateModeContext { + mode: 'date'; + currentWeek: Date; + period: ViewPeriod; + weekDays: number; + firstDayOfWeek: number; +} + +export interface ResourceModeContext { + mode: 'resource'; + selectedDate: Date; + resources: Resource[]; + maxResources: number; +} + +/** + * Union type for type-safe mode contexts + */ +export type CalendarModeContext = DateModeContext | ResourceModeContext; \ No newline at end of file diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index f8ec61e..49930af 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,8 +1,8 @@ import { CalendarConfig } from '../core/CalendarConfig.js'; /** - * PositionUtils - Utility funktioner til pixel/minut konvertering - * HΓ₯ndterer positionering og stΓΈrrelse beregninger for calendar events + * PositionUtils - Utility functions for pixel/minute conversion + * Handles positioning and size calculations for calendar events */ export class PositionUtils { private config: CalendarConfig; @@ -12,41 +12,45 @@ export class PositionUtils { } /** - * Konverter minutter til pixels + * Convert minutes to pixels */ public minutesToPixels(minutes: number): number { - const pixelsPerHour = this.config.get('hourHeight'); + const gridSettings = this.config.getGridSettings(); + const pixelsPerHour = gridSettings.hourHeight; return (minutes / 60) * pixelsPerHour; } /** - * Konverter pixels til minutter + * Convert pixels to minutes */ public pixelsToMinutes(pixels: number): number { - const pixelsPerHour = this.config.get('hourHeight'); + const gridSettings = this.config.getGridSettings(); + const pixelsPerHour = gridSettings.hourHeight; return (pixels / pixelsPerHour) * 60; } /** - * Konverter tid (HH:MM) til pixels fra dag start + * Convert time (HH:MM) to pixels from day start */ public timeToPixels(timeString: string): number { const [hours, minutes] = timeString.split(':').map(Number); const totalMinutes = (hours * 60) + minutes; - const dayStartMinutes = this.config.get('dayStartHour') * 60; + const gridSettings = this.config.getGridSettings(); + const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return this.minutesToPixels(minutesFromDayStart); } /** - * Konverter Date object til pixels fra dag start + * Convert Date object to pixels from day start */ public dateToPixels(date: Date): number { const hours = date.getHours(); const minutes = date.getMinutes(); const totalMinutes = (hours * 60) + minutes; - const dayStartMinutes = this.config.get('dayStartHour') * 60; + const gridSettings = this.config.getGridSettings(); + const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; return this.minutesToPixels(minutesFromDayStart); @@ -57,7 +61,8 @@ export class PositionUtils { */ public pixelsToTime(pixels: number): string { const minutes = this.pixelsToMinutes(pixels); - const dayStartMinutes = this.config.get('dayStartHour') * 60; + const gridSettings = this.config.getGridSettings(); + const dayStartMinutes = gridSettings.dayStartHour * 60; const totalMinutes = dayStartMinutes + minutes; const hours = Math.floor(totalMinutes / 60); @@ -103,7 +108,8 @@ export class PositionUtils { * Snap position til grid interval */ public snapToGrid(pixels: number): number { - const snapInterval = this.config.get('snapInterval'); + const gridSettings = this.config.getGridSettings(); + const snapInterval = gridSettings.snapInterval; const snapPixels = this.minutesToPixels(snapInterval); return Math.round(pixels / snapPixels) * snapPixels; @@ -115,7 +121,8 @@ export class PositionUtils { public snapTimeToInterval(timeString: string): string { const [hours, minutes] = timeString.split(':').map(Number); const totalMinutes = (hours * 60) + minutes; - const snapInterval = this.config.get('snapInterval'); + const gridSettings = this.config.getGridSettings(); + const snapInterval = gridSettings.snapInterval; const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; const snappedHours = Math.floor(snappedMinutes / 60); @@ -186,7 +193,8 @@ export class PositionUtils { */ public isWithinWorkHours(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); - return hours >= this.config.get('workStartHour') && hours < this.config.get('workEndHour'); + const gridSettings = this.config.getGridSettings(); + return hours >= gridSettings.workStartHour && hours < gridSettings.workEndHour; } /** @@ -194,7 +202,8 @@ export class PositionUtils { */ public isWithinDayBounds(timeString: string): boolean { const [hours] = timeString.split(':').map(Number); - return hours >= this.config.get('dayStartHour') && hours < this.config.get('dayEndHour'); + const gridSettings = this.config.getGridSettings(); + return hours >= gridSettings.dayStartHour && hours < gridSettings.dayEndHour; } /** @@ -209,8 +218,9 @@ export class PositionUtils { * Hent maksimum event hΓΈjde i pixels (hele dagen) */ public getMaximumEventHeight(): number { - const dayDurationHours = this.config.get('dayEndHour') - this.config.get('dayStartHour'); - return dayDurationHours * this.config.get('hourHeight'); + const gridSettings = this.config.getGridSettings(); + const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour; + return dayDurationHours * gridSettings.hourHeight; } /**