Major refactorering to get a hold on all these events

This commit is contained in:
Janus Knudsen 2025-08-09 00:31:44 +02:00
parent 2a766cf685
commit 59b3c64c55
18 changed files with 1901 additions and 357 deletions

View file

@ -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];
// 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
*/

View file

@ -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<ViewType, ViewSettings> = {
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<GridSettings>): 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<DateViewSettings>): 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<DateViewSettings>): 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<ResourceViewSettings>): 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<ResourceViewSettings>): 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
});
}

View file

@ -19,11 +19,17 @@ export interface RendererConfig {
*/
export class CalendarTypeFactory {
private static renderers: Map<CalendarType, RendererConfig> = 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()));
}

View file

@ -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<void> {
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);
});
}

View file

@ -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<void> {
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();
}
/**

View file

@ -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<InitializationPhase, { start: number; end?: number }> = 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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string, number> {
const timings: Record<string, number> = {};
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<any> {
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
};
}
}

View file

@ -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<EventData> {
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<EventType, string[]> = {
const types: string[] = ['meeting', 'meal', 'work', 'milestone'];
const titles: Record<string, string[]> = {
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
}
};

View file

@ -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<void> {
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[] {

View file

@ -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();
}

View file

@ -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);

View file

@ -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

View file

@ -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);

170
src/types/CalendarState.ts Normal file
View file

@ -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, CalendarState[]> = {
[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, InitializationPhase> = {
[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
};

View file

@ -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<string, any>;
}
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
};
}
}
/**
* 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;

View file

@ -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;
}
/**