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

@ -3,7 +3,8 @@
"permissions": {
"allow": [
"Bash(npm run build:*)",
"Bash(powershell:*)"
"Bash(powershell:*)",
"Bash(rg:*)"
],
"deny": []
}

View file

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

View file

@ -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<void> {
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<void> {
const managers = new Map<string, any>();
// 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<void> {
// 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<void> {
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<void> {
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<void> {
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<void>;
private async waitForDOM(selectors: string[]): Promise<void> {
// 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<void> {
// 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<void> {
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.

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,12 +66,8 @@ 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_ERROR: 'calendar:datafetcherror',
@ -53,49 +75,41 @@ export const EventTypes = {
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_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;
updateGridSettings(updates: Partial<GridSettings>): void {
this.gridSettings = { ...this.gridSettings, ...updates };
// 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
});
}
/**
* Set calendar type
* Get date view settings
*/
setCalendarType(type: CalendarType): void {
const oldType = this.calendarType;
this.calendarType = type;
getDateViewSettings(): DateViewSettings {
return { ...this.dateViewSettings };
}
// Emit calendar type change event
/**
* 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...');
// Declare managers outside try block for global access
let calendarManager: CalendarManager;
let navigationManager: NavigationManager;
let viewManager: ViewManager;
let eventManager: EventManager;
let eventRenderer: EventRenderer;
let gridManager: GridManager;
let scrollManager: ScrollManager;
try {
// Use the singleton calendar configuration
const config = calendarConfig;
// 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();
// 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
calendarManager.initialize();
// 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 with all core managers');
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
});
try {
// Delegate to StateManager for coordinated initialization
await this.stateManager.initialize();
// Set initial view and date
// Set initial view and date after successful initialization
this.setView(this.currentView);
this.setCurrentDate(this.currentDate);
this.isInitialized = true;
console.log('✅ CalendarManager: Initialization complete');
// Emit initialization complete event
this.eventBus.emit(EventTypes.CALENDAR_INITIALIZED, {
view: this.currentView,
date: this.currentDate
});
console.log('CalendarManager initialized successfully');
} 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,9 +81,22 @@ export class EventManager {
throw new Error(`Failed to load mock events: ${response.status}`);
}
const data = await response.json();
console.log(`EventManager: Loaded data for ${calendarType} calendar`);
// 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 processCalendarData(calendarType: string, data: any): void {
if (calendarType === 'resource') {
const resourceData: ResourceCalendarData = await response.json();
// Flatten events from all resources and add resource metadata
const resourceData = data as ResourceCalendarData;
this.events = resourceData.resources.flatMap(resource =>
resource.events.map(event => ({
...event,
@ -70,32 +105,17 @@ export class EventManager {
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
});
console.log(`EventManager: Processed ${this.events.length} events from ${resourceData.resources.length} resources`);
} else {
this.events = await response.json();
console.log(`EventManager: Loaded ${this.events.length} date calendar events`);
}
console.log('EventManager: First event:', this.events[0]);
console.log('EventManager: Last event:', this.events[this.events.length - 1]);
} catch (error) {
console.error('EventManager: Failed to load mock events:', error);
this.events = []; // Fallback to empty array
this.events = data as CalendarEvent[];
console.log(`EventManager: Processed ${this.events.length} date events`);
}
}
private syncEvents(): void {
// Emit events for rendering
this.eventBus.emit(EventTypes.EVENTS_LOADED, {
events: this.events
});
console.log(`EventManager: Synced ${this.events.length} events`);
// 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
if (detail.data && detail.data.calendarMode === 'resource') {
// Resource data will be passed in the state event
// For now just update grid styles
this.updateGridStyles();
// Re-render if grid is already rendered
if (this.grid && this.grid.children.length > 0) {
this.render();
}
});
@ -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,19 +26,29 @@ 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', () => {
this.updateScrollableHeight();
});
// 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;
}
/**