Major refactorering to get a hold on all these events
This commit is contained in:
parent
2a766cf685
commit
59b3c64c55
18 changed files with 1901 additions and 357 deletions
|
|
@ -3,7 +3,8 @@
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(powershell:*)"
|
"Bash(powershell:*)",
|
||||||
|
"Bash(rg:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
237
docs/date-mode-initialization-sequence.md
Normal file
237
docs/date-mode-initialization-sequence.md
Normal 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.
|
||||||
270
docs/improved-initialization-strategy.md
Normal file
270
docs/improved-initialization-strategy.md
Normal 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.
|
||||||
|
|
@ -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 = {
|
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_CHANGE: 'calendar:viewchange',
|
||||||
|
VIEW_CHANGED: 'calendar:viewchanged',
|
||||||
|
VIEW_CHANGE_REQUESTED: 'calendar:viewchangerequested',
|
||||||
VIEW_RENDERED: 'calendar:viewrendered',
|
VIEW_RENDERED: 'calendar:viewrendered',
|
||||||
PERIOD_CHANGE: 'calendar:periodchange',
|
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_CREATE: 'calendar:eventcreate',
|
||||||
EVENT_CREATED: 'calendar:eventcreated',
|
EVENT_CREATED: 'calendar:eventcreated',
|
||||||
EVENT_UPDATE: 'calendar:eventupdate',
|
EVENT_UPDATE: 'calendar:eventupdate',
|
||||||
|
|
@ -19,9 +42,12 @@ export const EventTypes = {
|
||||||
EVENT_RENDERED: 'calendar:eventrendered',
|
EVENT_RENDERED: 'calendar:eventrendered',
|
||||||
EVENT_SELECTED: 'calendar:eventselected',
|
EVENT_SELECTED: 'calendar:eventselected',
|
||||||
EVENTS_LOADED: 'calendar:eventsloaded',
|
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_START: 'calendar:dragstart',
|
||||||
DRAG_MOVE: 'calendar:dragmove',
|
DRAG_MOVE: 'calendar:dragmove',
|
||||||
DRAG_END: 'calendar:dragend',
|
DRAG_END: 'calendar:dragend',
|
||||||
|
|
@ -40,12 +66,8 @@ export const EventTypes = {
|
||||||
SEARCH_UPDATE: 'calendar:searchupdate',
|
SEARCH_UPDATE: 'calendar:searchupdate',
|
||||||
SEARCH_CLEAR: 'calendar:searchclear',
|
SEARCH_CLEAR: 'calendar:searchclear',
|
||||||
|
|
||||||
// Grid events
|
// Data events (legacy - prefer StateEvents)
|
||||||
GRID_CLICK: 'calendar:gridclick',
|
DATE_CHANGED: 'calendar:datechanged',
|
||||||
GRID_DBLCLICK: 'calendar:griddblclick',
|
|
||||||
GRID_RENDERED: 'calendar:gridrendered',
|
|
||||||
|
|
||||||
// Data events
|
|
||||||
DATA_FETCH_START: 'calendar:datafetchstart',
|
DATA_FETCH_START: 'calendar:datafetchstart',
|
||||||
DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess',
|
DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess',
|
||||||
DATA_FETCH_ERROR: 'calendar:datafetcherror',
|
DATA_FETCH_ERROR: 'calendar:datafetcherror',
|
||||||
|
|
@ -53,49 +75,41 @@ export const EventTypes = {
|
||||||
DATA_SYNC_SUCCESS: 'calendar:datasyncsuccess',
|
DATA_SYNC_SUCCESS: 'calendar:datasyncsuccess',
|
||||||
DATA_SYNC_ERROR: 'calendar:datasyncerror',
|
DATA_SYNC_ERROR: 'calendar:datasyncerror',
|
||||||
|
|
||||||
// State events
|
// Initialization events (legacy - prefer StateEvents)
|
||||||
STATE_UPDATE: 'calendar:stateupdate',
|
CALENDAR_INITIALIZED: 'calendar:initialized',
|
||||||
CONFIG_UPDATE: 'calendar:configupdate',
|
CALENDAR_DATA_LOADED: 'calendar:calendardataloaded',
|
||||||
CALENDAR_TYPE_CHANGED: 'calendar:calendartypechanged',
|
GRID_RENDERED: 'calendar:gridrendered',
|
||||||
SELECTED_DATE_CHANGED: 'calendar:selecteddatechanged',
|
|
||||||
|
// 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 events
|
||||||
TIME_UPDATE: 'calendar:timeupdate',
|
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_START: 'calendar:loadingstart',
|
||||||
LOADING_END: 'calendar:loadingend',
|
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'
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Type for event type values
|
// Type for event bus event type values
|
||||||
export type EventType = typeof EventTypes[keyof typeof EventTypes];
|
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
|
||||||
|
*/
|
||||||
|
|
@ -2,15 +2,49 @@
|
||||||
|
|
||||||
import { eventBus } from './EventBus';
|
import { eventBus } from './EventBus';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
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 {
|
interface GridSettings {
|
||||||
columns: number;
|
// Time boundaries
|
||||||
showAllDay: boolean;
|
dayStartHour: number;
|
||||||
|
dayEndHour: number;
|
||||||
|
workStartHour: number;
|
||||||
|
workEndHour: number;
|
||||||
|
|
||||||
|
// Layout settings
|
||||||
|
hourHeight: number;
|
||||||
|
snapInterval: number;
|
||||||
|
fitToWidth: boolean;
|
||||||
scrollToHour: number | null;
|
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 {
|
export class CalendarConfig {
|
||||||
private config: ICalendarConfig;
|
private config: ICalendarConfig;
|
||||||
private calendarType: CalendarType = 'date';
|
private calendarMode: CalendarMode = 'date';
|
||||||
private selectedDate: Date | null = null;
|
private selectedDate: Date | null = null;
|
||||||
|
private gridSettings: GridSettings;
|
||||||
|
private dateViewSettings: DateViewSettings;
|
||||||
|
private resourceViewSettings: ResourceViewSettings;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = {
|
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
|
// Scrollbar styling
|
||||||
scrollbarWidth: 16, // Width of scrollbar in pixels
|
scrollbarWidth: 16, // Width of scrollbar in pixels
|
||||||
scrollbarColor: '#666', // Scrollbar thumb color
|
scrollbarColor: '#666', // Scrollbar thumb color
|
||||||
|
|
@ -68,8 +87,40 @@ export class CalendarConfig {
|
||||||
maxEventDuration: 480 // 8 hours
|
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
|
// Set computed values
|
||||||
this.config.minEventDuration = this.config.snapInterval;
|
this.config.minEventDuration = this.gridSettings.snapInterval;
|
||||||
|
|
||||||
// Load calendar type from URL parameter
|
// Load calendar type from URL parameter
|
||||||
this.loadCalendarType();
|
this.loadCalendarType();
|
||||||
|
|
@ -86,13 +137,13 @@ export class CalendarConfig {
|
||||||
const typeParam = urlParams.get('type');
|
const typeParam = urlParams.get('type');
|
||||||
const dateParam = urlParams.get('date');
|
const dateParam = urlParams.get('date');
|
||||||
|
|
||||||
// Set calendar type
|
// Set calendar mode
|
||||||
if (typeParam === 'resource' || typeParam === 'date') {
|
if (typeParam === 'resource' || typeParam === 'date') {
|
||||||
this.calendarType = typeParam;
|
this.calendarMode = typeParam;
|
||||||
console.log(`CalendarConfig: Calendar type set to '${this.calendarType}' from URL parameter`);
|
console.log(`CalendarConfig: Calendar mode set to '${this.calendarMode}' from URL parameter`);
|
||||||
} else {
|
} else {
|
||||||
this.calendarType = 'date'; // Default
|
this.calendarMode = 'date'; // Default
|
||||||
console.log(`CalendarConfig: Calendar type defaulted to '${this.calendarType}'`);
|
console.log(`CalendarConfig: Calendar mode defaulted to '${this.calendarMode}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set selected date
|
// Set selected date
|
||||||
|
|
@ -121,13 +172,19 @@ export class CalendarConfig {
|
||||||
// Read data attributes
|
// Read data attributes
|
||||||
const attrs = calendar.dataset;
|
const attrs = calendar.dataset;
|
||||||
|
|
||||||
if (attrs.view) this.config.view = attrs.view as ViewType;
|
// Update date view settings
|
||||||
if (attrs.weekDays) this.config.weekDays = parseInt(attrs.weekDays);
|
if (attrs.view) this.dateViewSettings.period = attrs.view as ViewPeriod;
|
||||||
if (attrs.snapInterval) this.config.snapInterval = parseInt(attrs.snapInterval);
|
if (attrs.weekDays) this.dateViewSettings.weekDays = parseInt(attrs.weekDays);
|
||||||
if (attrs.dayStartHour) this.config.dayStartHour = parseInt(attrs.dayStartHour);
|
|
||||||
if (attrs.dayEndHour) this.config.dayEndHour = parseInt(attrs.dayEndHour);
|
// Update grid settings
|
||||||
if (attrs.hourHeight) this.config.hourHeight = parseInt(attrs.hourHeight);
|
if (attrs.snapInterval) this.gridSettings.snapInterval = parseInt(attrs.snapInterval);
|
||||||
if (attrs.fitToWidth !== undefined) this.config.fitToWidth = attrs.fitToWidth === 'true';
|
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];
|
const oldValue = this.config[key];
|
||||||
this.config[key] = value;
|
this.config[key] = value;
|
||||||
|
|
||||||
// Update computed values
|
// Update computed values handled in specific update methods
|
||||||
if (key === 'snapInterval') {
|
|
||||||
this.config.minEventDuration = value as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit config update event
|
// Emit config update event
|
||||||
eventBus.emit(EventTypes.CONFIG_UPDATE, {
|
eventBus.emit(EventTypes.CONFIG_UPDATE, {
|
||||||
|
|
@ -178,11 +232,11 @@ export class CalendarConfig {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get minuteHeight(): number {
|
get minuteHeight(): number {
|
||||||
return this.config.hourHeight / 60;
|
return this.gridSettings.hourHeight / 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalHours(): number {
|
get totalHours(): number {
|
||||||
return this.config.dayEndHour - this.config.dayStartHour;
|
return this.gridSettings.dayEndHour - this.gridSettings.dayStartHour;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalMinutes(): number {
|
get totalMinutes(): number {
|
||||||
|
|
@ -190,7 +244,7 @@ export class CalendarConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
get slotsPerHour(): number {
|
get slotsPerHour(): number {
|
||||||
return 60 / this.config.snapInterval;
|
return 60 / this.gridSettings.snapInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalSlots(): number {
|
get totalSlots(): number {
|
||||||
|
|
@ -198,7 +252,7 @@ export class CalendarConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
get slotHeight(): number {
|
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 {
|
getGridSettings(): GridSettings {
|
||||||
const settings: Record<ViewType, ViewSettings> = {
|
return { ...this.gridSettings };
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get calendar type
|
* Update grid display settings
|
||||||
*/
|
*/
|
||||||
getCalendarType(): CalendarType {
|
updateGridSettings(updates: Partial<GridSettings>): void {
|
||||||
return this.calendarType;
|
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 {
|
getDateViewSettings(): DateViewSettings {
|
||||||
const oldType = this.calendarType;
|
return { ...this.dateViewSettings };
|
||||||
this.calendarType = type;
|
}
|
||||||
|
|
||||||
// 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, {
|
eventBus.emit(EventTypes.CALENDAR_TYPE_CHANGED, {
|
||||||
oldType,
|
oldType: oldMode,
|
||||||
newType: type
|
newType: mode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,17 @@ export interface RendererConfig {
|
||||||
*/
|
*/
|
||||||
export class CalendarTypeFactory {
|
export class CalendarTypeFactory {
|
||||||
private static renderers: Map<CalendarType, RendererConfig> = new Map();
|
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 {
|
static initialize(): void {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
console.warn('CalendarTypeFactory: Already initialized, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Register default renderers
|
// Register default renderers
|
||||||
this.registerRenderers('date', {
|
this.registerRenderers('date', {
|
||||||
headerRenderer: new DateHeaderRenderer(),
|
headerRenderer: new DateHeaderRenderer(),
|
||||||
|
|
@ -37,6 +43,7 @@ export class CalendarTypeFactory {
|
||||||
eventRenderer: new ResourceEventRenderer()
|
eventRenderer: new ResourceEventRenderer()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
console.log('CalendarTypeFactory: Initialized with default renderers', Array.from(this.renderers.keys()));
|
console.log('CalendarTypeFactory: Initialized with default renderers', Array.from(this.renderers.keys()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
69
src/index.ts
69
src/index.ts
|
|
@ -8,32 +8,61 @@ import { EventRenderer } from './managers/EventRenderer.js';
|
||||||
import { GridManager } from './managers/GridManager.js';
|
import { GridManager } from './managers/GridManager.js';
|
||||||
import { ScrollManager } from './managers/ScrollManager.js';
|
import { ScrollManager } from './managers/ScrollManager.js';
|
||||||
import { calendarConfig } from './core/CalendarConfig.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 {
|
async function initializeCalendar(): Promise<void> {
|
||||||
console.log('🗓️ Initializing Calendar Plantempus...');
|
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
|
// Use the singleton calendar configuration
|
||||||
const config = calendarConfig;
|
const config = calendarConfig;
|
||||||
|
|
||||||
// Initialize managers
|
// Initialize the CalendarTypeFactory before creating managers
|
||||||
const calendarManager = new CalendarManager(eventBus, config);
|
console.log('🏭 Phase 0: Initializing CalendarTypeFactory...');
|
||||||
const navigationManager = new NavigationManager(eventBus);
|
CalendarTypeFactory.initialize();
|
||||||
const viewManager = new ViewManager(eventBus);
|
|
||||||
const eventManager = new EventManager(eventBus);
|
// Initialize managers in proper order
|
||||||
const eventRenderer = new EventRenderer(eventBus);
|
console.log('📋 Phase 1: Creating core managers...');
|
||||||
const scrollManager = new ScrollManager(); // Initialize BEFORE GridManager
|
calendarManager = new CalendarManager(eventBus, config);
|
||||||
const gridManager = new GridManager();
|
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
|
// Enable debug mode for development
|
||||||
eventBus.setDebug(true);
|
eventBus.setDebug(true);
|
||||||
|
|
||||||
// Initialize all managers
|
// Initialize all managers using state-driven coordination
|
||||||
calendarManager.initialize();
|
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
|
// Expose to window for debugging
|
||||||
(window as any).calendarDebug = {
|
(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') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializeCalendar);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initializeCalendar().catch(error => {
|
||||||
|
console.error('Failed to initialize calendar:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
initializeCalendar();
|
initializeCalendar().catch(error => {
|
||||||
|
console.error('Failed to initialize calendar:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2,14 +2,17 @@ import { EventBus } from '../core/EventBus.js';
|
||||||
import { EventTypes } from '../constants/EventTypes.js';
|
import { EventTypes } from '../constants/EventTypes.js';
|
||||||
import { CalendarConfig } from '../core/CalendarConfig.js';
|
import { CalendarConfig } from '../core/CalendarConfig.js';
|
||||||
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.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
|
* CalendarManager - Main coordinator for all calendar managers
|
||||||
* Håndterer initialisering, koordinering og kommunikation mellem alle managers
|
* Now delegates initialization to CalendarStateManager for better coordination
|
||||||
*/
|
*/
|
||||||
export class CalendarManager {
|
export class CalendarManager {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
private config: CalendarConfig;
|
private config: CalendarConfig;
|
||||||
|
private stateManager: CalendarStateManager;
|
||||||
private currentView: CalendarView = 'week';
|
private currentView: CalendarView = 'week';
|
||||||
private currentDate: Date = new Date();
|
private currentDate: Date = new Date();
|
||||||
private isInitialized: boolean = false;
|
private isInitialized: boolean = false;
|
||||||
|
|
@ -17,40 +20,37 @@ export class CalendarManager {
|
||||||
constructor(eventBus: IEventBus, config: CalendarConfig) {
|
constructor(eventBus: IEventBus, config: CalendarConfig) {
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.stateManager = new CalendarStateManager();
|
||||||
this.setupEventListeners();
|
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) {
|
if (this.isInitialized) {
|
||||||
console.warn('CalendarManager is already initialized');
|
console.warn('CalendarManager is already initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Initializing CalendarManager...');
|
console.log('🚀 CalendarManager: Starting state-driven initialization');
|
||||||
|
|
||||||
// Emit initialization event
|
try {
|
||||||
this.eventBus.emit(EventTypes.CALENDAR_INITIALIZING, {
|
// Delegate to StateManager for coordinated initialization
|
||||||
view: this.currentView,
|
await this.stateManager.initialize();
|
||||||
date: this.currentDate,
|
|
||||||
config: this.config
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set initial view and date
|
// Set initial view and date after successful initialization
|
||||||
this.setView(this.currentView);
|
this.setView(this.currentView);
|
||||||
this.setCurrentDate(this.currentDate);
|
this.setCurrentDate(this.currentDate);
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
|
console.log('✅ CalendarManager: Initialization complete');
|
||||||
|
|
||||||
// Emit initialization complete event
|
} catch (error) {
|
||||||
this.eventBus.emit(EventTypes.CALENDAR_INITIALIZED, {
|
console.error('❌ CalendarManager initialization failed:', error);
|
||||||
view: this.currentView,
|
throw error; // Let the caller handle the error
|
||||||
date: this.currentDate
|
}
|
||||||
});
|
|
||||||
|
|
||||||
console.log('CalendarManager initialized successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,7 +139,28 @@ export class CalendarManager {
|
||||||
* Check om calendar er initialiseret
|
* Check om calendar er initialiseret
|
||||||
*/
|
*/
|
||||||
public isCalendarInitialized(): boolean {
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
471
src/managers/CalendarStateManager.ts
Normal file
471
src/managers/CalendarStateManager.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
import { CalendarEvent, EventData, Period, EventType } from '../types/CalendarTypes';
|
import { CalendarEvent, EventData, Period } from '../types/CalendarTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event creation data interface
|
* Event creation data interface
|
||||||
*/
|
*/
|
||||||
interface EventCreateData {
|
interface EventCreateData {
|
||||||
title: string;
|
title: string;
|
||||||
type: EventType;
|
type: string;
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
allDay: boolean;
|
allDay: boolean;
|
||||||
|
|
@ -67,7 +67,7 @@ export class DataManager {
|
||||||
* Fetch events for a specific period
|
* Fetch events for a specific period
|
||||||
*/
|
*/
|
||||||
async fetchEventsForPeriod(period: Period): Promise<EventData> {
|
async fetchEventsForPeriod(period: Period): Promise<EventData> {
|
||||||
const cacheKey = `${period.start}-${period.end}-${period.view}`;
|
const cacheKey = `${period.start}-${period.end}`;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (this.cache.has(cacheKey)) {
|
if (this.cache.has(cacheKey)) {
|
||||||
|
|
@ -90,8 +90,7 @@ export class DataManager {
|
||||||
// Real API call
|
// Real API call
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
start: period.start,
|
start: period.start,
|
||||||
end: period.end,
|
end: period.end
|
||||||
view: period.view
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}?${params}`);
|
const response = await fetch(`${this.baseUrl}?${params}`);
|
||||||
|
|
@ -275,8 +274,8 @@ export class DataManager {
|
||||||
*/
|
*/
|
||||||
private getMockData(period: Period): EventData {
|
private getMockData(period: Period): EventData {
|
||||||
const events: CalendarEvent[] = [];
|
const events: CalendarEvent[] = [];
|
||||||
const types: EventType[] = ['meeting', 'meal', 'work', 'milestone'];
|
const types: string[] = ['meeting', 'meal', 'work', 'milestone'];
|
||||||
const titles: Record<EventType, string[]> = {
|
const titles: Record<string, string[]> = {
|
||||||
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
|
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
|
||||||
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
|
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
|
||||||
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
|
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
|
||||||
|
|
@ -296,7 +295,7 @@ export class DataManager {
|
||||||
if (isWeekend) {
|
if (isWeekend) {
|
||||||
// Maybe one or two events on weekends
|
// Maybe one or two events on weekends
|
||||||
if (Math.random() > 0.7) {
|
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 title = titles[type][Math.floor(Math.random() * titles[type].length)];
|
||||||
const hour = 12 + Math.floor(Math.random() * 4);
|
const hour = 12 + Math.floor(Math.random() * 4);
|
||||||
|
|
||||||
|
|
@ -358,10 +357,11 @@ export class DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a multi-day event
|
// Add a multi-day event if period spans multiple days
|
||||||
if (period.view === 'week') {
|
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
if (daysDiff > 1) {
|
||||||
const midWeek = new Date(startDate);
|
const midWeek = new Date(startDate);
|
||||||
midWeek.setDate(midWeek.getDate() + 2);
|
midWeek.setDate(midWeek.getDate() + Math.min(2, daysDiff - 1));
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
id: `evt-${events.length + 1}`,
|
id: `evt-${events.length + 1}`,
|
||||||
|
|
@ -379,7 +379,6 @@ export class DataManager {
|
||||||
meta: {
|
meta: {
|
||||||
start: period.start,
|
start: period.start,
|
||||||
end: period.end,
|
end: period.end,
|
||||||
view: period.view,
|
|
||||||
total: events.length
|
total: events.length
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { EventBus } from '../core/EventBus';
|
import { EventBus } from '../core/EventBus';
|
||||||
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
|
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { StateEvents } from '../types/CalendarState';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,33 +16,54 @@ export class EventManager {
|
||||||
console.log('EventManager: Constructor called');
|
console.log('EventManager: Constructor called');
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
console.log('EventManager: About to call loadMockData()');
|
console.log('EventManager: Waiting for CALENDAR_INITIALIZED before loading data');
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => {
|
// Listen for state-driven data loading request
|
||||||
this.syncEvents();
|
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, () => {
|
// Legacy event listeners removed - data is now managed via state-driven events only
|
||||||
this.syncEvents();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMockData(): Promise<void> {
|
private async loadMockData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
let jsonFile: string;
|
let jsonFile: string;
|
||||||
|
|
||||||
console.log(`EventManager: Calendar type detected: '${calendarType}'`);
|
console.log(`EventManager: Calendar type detected: '${calendarType}'`);
|
||||||
|
|
@ -59,9 +81,22 @@ export class EventManager {
|
||||||
throw new Error(`Failed to load mock events: ${response.status}`);
|
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') {
|
if (calendarType === 'resource') {
|
||||||
const resourceData: ResourceCalendarData = await response.json();
|
const resourceData = data as ResourceCalendarData;
|
||||||
// Flatten events from all resources and add resource metadata
|
|
||||||
this.events = resourceData.resources.flatMap(resource =>
|
this.events = resourceData.resources.flatMap(resource =>
|
||||||
resource.events.map(event => ({
|
resource.events.map(event => ({
|
||||||
...event,
|
...event,
|
||||||
|
|
@ -70,32 +105,17 @@ export class EventManager {
|
||||||
resourceEmployeeId: resource.employeeId
|
resourceEmployeeId: resource.employeeId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
console.log(`EventManager: Loaded ${this.events.length} events from ${resourceData.resources.length} resources`);
|
console.log(`EventManager: Processed ${this.events.length} events from ${resourceData.resources.length} resources`);
|
||||||
|
|
||||||
// Emit resource data for GridManager
|
|
||||||
this.eventBus.emit(EventTypes.RESOURCE_DATA_LOADED, {
|
|
||||||
resourceData: resourceData
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.events = await response.json();
|
this.events = data as CalendarEvent[];
|
||||||
console.log(`EventManager: Loaded ${this.events.length} date calendar events`);
|
console.log(`EventManager: Processed ${this.events.length} date 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncEvents(): void {
|
private syncEvents(): void {
|
||||||
// Emit events for rendering
|
// Events are now synced via StateEvents.DATA_LOADED during initialization
|
||||||
this.eventBus.emit(EventTypes.EVENTS_LOADED, {
|
// This method maintained for internal state management only
|
||||||
events: this.events
|
console.log(`EventManager: Internal sync - ${this.events.length} events in memory`);
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`EventManager: Synced ${this.events.length} events`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEvents(): CalendarEvent[] {
|
public getEvents(): CalendarEvent[] {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { EventBus } from '../core/EventBus';
|
import { EventBus } from '../core/EventBus';
|
||||||
import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
|
import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { StateEvents } from '../types/CalendarState';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
|
|
||||||
|
|
@ -11,30 +12,36 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
export class EventRenderer {
|
export class EventRenderer {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
private pendingEvents: CalendarEvent[] = [];
|
private pendingEvents: CalendarEvent[] = [];
|
||||||
|
private dataReady: boolean = false;
|
||||||
|
private gridReady: boolean = false;
|
||||||
|
|
||||||
constructor(eventBus: IEventBus) {
|
constructor(eventBus: IEventBus) {
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
// Initialize the factory (if not already done)
|
|
||||||
CalendarTypeFactory.initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
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 customEvent = event as CustomEvent;
|
||||||
const { events } = customEvent.detail;
|
// Events are in customEvent.detail (direct from StateEvent payload)
|
||||||
console.log('EventRenderer: Received EVENTS_LOADED with', events.length, 'events');
|
const eventCount = customEvent.detail.data?.eventCount || 0;
|
||||||
// Store events but don't render yet - wait for grid to be ready
|
const events = customEvent.detail.data?.events || [];
|
||||||
this.pendingEvents = events;
|
console.log('EventRenderer: Received DATA_LOADED with', eventCount, 'events');
|
||||||
|
this.pendingEvents = events; // Store the actual events
|
||||||
|
this.dataReady = true;
|
||||||
this.tryRenderEvents();
|
this.tryRenderEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventBus.on(EventTypes.GRID_RENDERED, () => {
|
// Listen for state-driven grid rendered event
|
||||||
// Grid is ready, now we can render events
|
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.tryRenderEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.eventBus.on(EventTypes.VIEW_RENDERED, () => {
|
this.eventBus.on(EventTypes.VIEW_RENDERED, () => {
|
||||||
// Clear existing events when view changes
|
// Clear existing events when view changes
|
||||||
this.clearEvents();
|
this.clearEvents();
|
||||||
|
|
@ -48,20 +55,50 @@ export class EventRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryRenderEvents(): void {
|
private tryRenderEvents(): void {
|
||||||
// Only render if we have both events and appropriate columns are ready
|
// Only render if we have both data and grid ready
|
||||||
console.log('EventRenderer: tryRenderEvents called, pending events:', this.pendingEvents.length);
|
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) {
|
if (this.pendingEvents.length > 0) {
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
let columnsSelector = calendarType === 'resource' ? 'swp-resource-column' : 'swp-day-column';
|
let columnsSelector = calendarType === 'resource' ? 'swp-resource-column' : 'swp-day-column';
|
||||||
const columns = document.querySelectorAll(columnsSelector);
|
const columns = document.querySelectorAll(columnsSelector);
|
||||||
|
|
||||||
console.log(`EventRenderer: Found ${columns.length} ${columnsSelector} elements for ${calendarType} calendar`);
|
console.log(`EventRenderer: Found ${columns.length} ${columnsSelector} elements for ${calendarType} calendar`);
|
||||||
|
|
||||||
if (columns.length > 0) {
|
if (columns.length > 0) {
|
||||||
|
console.log('🎨 EventRenderer: Both data and grid ready, rendering events!');
|
||||||
|
const eventCount = this.pendingEvents.length;
|
||||||
this.renderEvents(this.pendingEvents);
|
this.renderEvents(this.pendingEvents);
|
||||||
this.pendingEvents = []; // Clear pending events after rendering
|
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');
|
console.log('EventRenderer: renderEvents called with', events.length, 'events');
|
||||||
|
|
||||||
// Get the appropriate event renderer strategy
|
// Get the appropriate event renderer strategy
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
|
const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
|
||||||
|
|
||||||
console.log(`EventRenderer: Using ${calendarType} event renderer strategy`);
|
console.log(`EventRenderer: Using ${calendarType} event renderer strategy`);
|
||||||
|
|
@ -84,7 +121,7 @@ export class EventRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearEvents(): void {
|
private clearEvents(): void {
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
|
const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType);
|
||||||
eventRenderer.clearEvents();
|
eventRenderer.clearEvents();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { StateEvents } from '../types/CalendarState';
|
||||||
import { DateUtils } from '../utils/DateUtils';
|
import { DateUtils } from '../utils/DateUtils';
|
||||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
|
|
@ -29,13 +30,11 @@ export class GridManager {
|
||||||
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
console.log('🏗️ GridManager: Constructor called');
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(): void {
|
private init(): void {
|
||||||
// Initialize the factory
|
|
||||||
CalendarTypeFactory.initialize();
|
|
||||||
|
|
||||||
this.findElements();
|
this.findElements();
|
||||||
this.subscribeToEvents();
|
this.subscribeToEvents();
|
||||||
|
|
||||||
|
|
@ -43,8 +42,8 @@ export class GridManager {
|
||||||
if (!this.currentWeek) {
|
if (!this.currentWeek) {
|
||||||
this.currentWeek = this.getWeekStart(new Date());
|
this.currentWeek = this.getWeekStart(new Date());
|
||||||
console.log('GridManager: Set initial currentWeek to', this.currentWeek);
|
console.log('GridManager: Set initial currentWeek to', this.currentWeek);
|
||||||
// Render initial grid
|
// Don't render immediately - wait for proper initialization event
|
||||||
this.render();
|
console.log('GridManager: Waiting for initialization complete before rendering');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +57,13 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToEvents(): void {
|
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
|
// Re-render grid on config changes
|
||||||
eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => {
|
eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => {
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
|
|
@ -96,18 +102,15 @@ export class GridManager {
|
||||||
this.updateAllDayEvents(detail.events);
|
this.updateAllDayEvents(detail.events);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle resource data loaded
|
// Handle data loaded for resource mode
|
||||||
eventBus.on(EventTypes.RESOURCE_DATA_LOADED, (e: Event) => {
|
eventBus.on(StateEvents.DATA_LOADED, (e: Event) => {
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
this.resourceData = detail.resourceData;
|
console.log(`GridManager: Received DATA_LOADED`);
|
||||||
console.log(`GridManager: Received resource data for ${this.resourceData!.resources.length} resources`);
|
|
||||||
|
|
||||||
// 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();
|
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.updateGridStyles();
|
||||||
this.renderGrid();
|
this.renderGrid();
|
||||||
|
|
||||||
// Emit grid rendered event
|
// Emit state-driven grid rendered event
|
||||||
|
const columnCount = this.getColumnCount();
|
||||||
console.log('GridManager: Emitting GRID_RENDERED event');
|
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');
|
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
|
* Render the complete grid using POC structure
|
||||||
*/
|
*/
|
||||||
|
|
@ -148,10 +193,10 @@ export class GridManager {
|
||||||
// Only clear and rebuild if grid is empty (first render)
|
// Only clear and rebuild if grid is empty (first render)
|
||||||
if (this.grid.children.length === 0) {
|
if (this.grid.children.length === 0) {
|
||||||
console.log('GridManager: First render - creating grid structure');
|
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.createHeaderSpacer();
|
||||||
this.createTimeAxis();
|
this.createTimeAxis();
|
||||||
this.createWeekContainer();
|
this.createGridContainer();
|
||||||
} else {
|
} else {
|
||||||
console.log('GridManager: Re-render - updating existing structure');
|
console.log('GridManager: Re-render - updating existing structure');
|
||||||
// Just update the calendar header for all-day events
|
// 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 {
|
private createTimeAxis(): void {
|
||||||
if (!this.grid) return;
|
if (!this.grid) return;
|
||||||
|
|
||||||
const timeAxis = document.createElement('swp-time-axis');
|
const timeAxis = document.createElement('swp-time-axis');
|
||||||
const timeAxisContent = document.createElement('swp-time-axis-content');
|
const timeAxisContent = document.createElement('swp-time-axis-content');
|
||||||
const startHour = calendarConfig.get('dayStartHour');
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const endHour = calendarConfig.get('dayEndHour');
|
const startHour = gridSettings.dayStartHour;
|
||||||
|
const endHour = gridSettings.dayEndHour;
|
||||||
console.log('GridManager: Creating time axis - startHour:', startHour, 'endHour:', endHour);
|
console.log('GridManager: Creating time axis - startHour:', startHour, 'endHour:', endHour);
|
||||||
|
|
||||||
for (let hour = startHour; hour < endHour; hour++) {
|
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;
|
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
|
// Create calendar header using Strategy Pattern
|
||||||
const calendarHeader = document.createElement('swp-calendar-header');
|
const calendarHeader = document.createElement('swp-calendar-header');
|
||||||
this.renderCalendarHeader(calendarHeader);
|
this.renderCalendarHeader(calendarHeader);
|
||||||
weekContainer.appendChild(calendarHeader);
|
gridContainer.appendChild(calendarHeader);
|
||||||
|
|
||||||
// Create scrollable content
|
// Create scrollable content
|
||||||
const scrollableContent = document.createElement('swp-scrollable-content');
|
const scrollableContent = document.createElement('swp-scrollable-content');
|
||||||
|
|
@ -222,9 +268,9 @@ export class GridManager {
|
||||||
timeGrid.appendChild(columnContainer);
|
timeGrid.appendChild(columnContainer);
|
||||||
|
|
||||||
scrollableContent.appendChild(timeGrid);
|
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 {
|
private renderCalendarHeader(calendarHeader: HTMLElement): void {
|
||||||
if (!this.currentWeek) return;
|
if (!this.currentWeek) return;
|
||||||
|
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||||
|
|
||||||
const context: HeaderRenderContext = {
|
const context: HeaderRenderContext = {
|
||||||
|
|
@ -256,7 +302,7 @@ export class GridManager {
|
||||||
if (!this.currentWeek) return;
|
if (!this.currentWeek) return;
|
||||||
|
|
||||||
console.log('GridManager: renderColumnContainer called');
|
console.log('GridManager: renderColumnContainer called');
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
||||||
|
|
||||||
const context: ColumnRenderContext = {
|
const context: ColumnRenderContext = {
|
||||||
|
|
@ -330,30 +376,44 @@ export class GridManager {
|
||||||
*/
|
*/
|
||||||
private updateGridStyles(): void {
|
private updateGridStyles(): void {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const config = calendarConfig.getAll();
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const calendar = document.querySelector('swp-calendar') as HTMLElement;
|
const calendar = document.querySelector('swp-calendar') as HTMLElement;
|
||||||
const calendarType = calendarConfig.getCalendarType();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
|
|
||||||
// Set CSS variables
|
// Set CSS variables
|
||||||
root.style.setProperty('--hour-height', `${config.hourHeight}px`);
|
root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`);
|
||||||
root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`);
|
root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`);
|
||||||
root.style.setProperty('--snap-interval', config.snapInterval.toString());
|
root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString());
|
||||||
root.style.setProperty('--day-start-hour', config.dayStartHour.toString());
|
root.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString());
|
||||||
root.style.setProperty('--day-end-hour', config.dayEndHour.toString());
|
root.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString());
|
||||||
root.style.setProperty('--work-start-hour', config.workStartHour.toString());
|
root.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString());
|
||||||
root.style.setProperty('--work-end-hour', config.workEndHour.toString());
|
root.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString());
|
||||||
|
|
||||||
// Set number of columns based on calendar type
|
// Set number of columns based on calendar type
|
||||||
let columnCount = 7; // Default for date mode
|
let columnCount = 7; // Default for date mode
|
||||||
if (calendarType === 'resource' && this.resourceData) {
|
if (calendarType === 'resource' && this.resourceData) {
|
||||||
columnCount = this.resourceData.resources.length;
|
columnCount = this.resourceData.resources.length;
|
||||||
} else if (calendarType === 'date') {
|
} 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());
|
root.style.setProperty('--grid-columns', columnCount.toString());
|
||||||
|
|
||||||
// Set day column min width based on fitToWidth setting
|
// 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
|
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
|
||||||
} else {
|
} else {
|
||||||
root.style.setProperty('--day-column-min-width', '250px'); // Default min-width for horizontal scroll mode
|
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
|
// Set fitToWidth data attribute for CSS targeting
|
||||||
if (calendar) {
|
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');
|
console.log('GridManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
|
||||||
|
|
@ -419,10 +479,11 @@ export class GridManager {
|
||||||
const rect = dayColumn.getBoundingClientRect();
|
const rect = dayColumn.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
const hourHeight = calendarConfig.get('hourHeight');
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
|
const hourHeight = gridSettings.hourHeight;
|
||||||
const minuteHeight = hourHeight / 60;
|
const minuteHeight = hourHeight / 60;
|
||||||
const snapInterval = calendarConfig.get('snapInterval');
|
const snapInterval = gridSettings.snapInterval;
|
||||||
const dayStartHour = calendarConfig.get('dayStartHour');
|
const dayStartHour = gridSettings.dayStartHour;
|
||||||
|
|
||||||
// Calculate total minutes from day start
|
// Calculate total minutes from day start
|
||||||
let totalMinutes = Math.floor(y / minuteHeight);
|
let totalMinutes = Math.floor(y / minuteHeight);
|
||||||
|
|
@ -446,8 +507,9 @@ export class GridManager {
|
||||||
scrollToHour(hour: number): void {
|
scrollToHour(hour: number): void {
|
||||||
if (!this.grid) return;
|
if (!this.grid) return;
|
||||||
|
|
||||||
const hourHeight = calendarConfig.get('hourHeight');
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const dayStartHour = calendarConfig.get('dayStartHour');
|
const hourHeight = gridSettings.hourHeight;
|
||||||
|
const dayStartHour = gridSettings.dayStartHour;
|
||||||
const headerHeight = 80; // Header row height
|
const headerHeight = 80; // Header row height
|
||||||
const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight);
|
const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export class NavigationManager {
|
||||||
private animationQueue: number = 0;
|
private animationQueue: number = 0;
|
||||||
|
|
||||||
constructor(eventBus: IEventBus) {
|
constructor(eventBus: IEventBus) {
|
||||||
|
console.log('🧭 NavigationManager: Constructor called');
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
||||||
this.targetWeek = new Date(this.currentWeek);
|
this.targetWeek = new Date(this.currentWeek);
|
||||||
|
|
@ -21,10 +22,17 @@ export class NavigationManager {
|
||||||
|
|
||||||
private init(): void {
|
private init(): void {
|
||||||
this.setupEventListeners();
|
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 {
|
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
|
// Listen for navigation button clicks
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|
@ -157,10 +165,16 @@ export class NavigationManager {
|
||||||
|
|
||||||
if (weekNumberElement) {
|
if (weekNumberElement) {
|
||||||
weekNumberElement.textContent = `Week ${weekNumber}`;
|
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) {
|
if (dateRangeElement) {
|
||||||
dateRangeElement.textContent = dateRange;
|
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
|
// Notify other managers about week info update
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { EventTypes } from '../constants/EventTypes';
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { StateEvents } from '../types/CalendarState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages scrolling functionality for the calendar using native scrollbars
|
* Manages scrolling functionality for the calendar using native scrollbars
|
||||||
|
|
@ -15,6 +16,7 @@ export class ScrollManager {
|
||||||
private resizeObserver: ResizeObserver | null = null;
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
console.log('📜 ScrollManager: Constructor called');
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,19 +26,29 @@ export class ScrollManager {
|
||||||
|
|
||||||
private subscribeToEvents(): void {
|
private subscribeToEvents(): void {
|
||||||
// Initialize scroll when grid is rendered
|
// Initialize scroll when grid is rendered
|
||||||
eventBus.on(EventTypes.GRID_RENDERED, () => {
|
eventBus.on(StateEvents.GRID_RENDERED, () => {
|
||||||
console.log('ScrollManager: Received GRID_RENDERED event');
|
console.log('ScrollManager: Received GRID_RENDERED event');
|
||||||
this.setupScrolling();
|
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
|
// Handle window resize
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.updateScrollableHeight();
|
this.updateScrollableHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle config updates for scrollbar styling
|
// Handle config updates for scrollbar styling
|
||||||
eventBus.on(EventTypes.CONFIG_UPDATE, (event: CustomEvent) => {
|
eventBus.on(EventTypes.CONFIG_UPDATE, (event: Event) => {
|
||||||
const { key } = event.detail;
|
const { key } = (event as CustomEvent).detail;
|
||||||
if (key.startsWith('scrollbar')) {
|
if (key.startsWith('scrollbar')) {
|
||||||
this.applyScrollbarStyling();
|
this.applyScrollbarStyling();
|
||||||
}
|
}
|
||||||
|
|
@ -131,8 +143,9 @@ export class ScrollManager {
|
||||||
* Scroll to specific hour
|
* Scroll to specific hour
|
||||||
*/
|
*/
|
||||||
scrollToHour(hour: number): void {
|
scrollToHour(hour: number): void {
|
||||||
const hourHeight = calendarConfig.get('hourHeight');
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const dayStartHour = calendarConfig.get('dayStartHour');
|
const hourHeight = gridSettings.hourHeight;
|
||||||
|
const dayStartHour = gridSettings.dayStartHour;
|
||||||
const scrollTop = (hour - dayStartHour) * hourHeight;
|
const scrollTop = (hour - dayStartHour) * hourHeight;
|
||||||
|
|
||||||
this.scrollTo(scrollTop);
|
this.scrollTo(scrollTop);
|
||||||
|
|
|
||||||
170
src/types/CalendarState.ts
Normal file
170
src/types/CalendarState.ts
Normal 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
|
||||||
|
};
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
// Calendar type definitions
|
// Calendar type definitions
|
||||||
|
|
||||||
export type ViewType = 'day' | 'week' | 'month';
|
// Time period view types (how much time to display)
|
||||||
export type CalendarView = ViewType; // Alias for compatibility
|
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';
|
export type SyncStatus = 'synced' | 'pending' | 'error';
|
||||||
|
|
||||||
|
|
@ -27,37 +32,22 @@ export interface CalendarEvent {
|
||||||
title: string;
|
title: string;
|
||||||
start: string; // ISO 8601
|
start: string; // ISO 8601
|
||||||
end: string; // ISO 8601
|
end: string; // ISO 8601
|
||||||
type: EventType;
|
type: string; // Flexible event type - can be any string value
|
||||||
allDay: boolean;
|
allDay: boolean;
|
||||||
syncStatus: SyncStatus;
|
syncStatus: SyncStatus;
|
||||||
|
|
||||||
// Resource information (only present in resource calendar mode)
|
// Resource information (only present in resource calendar mode)
|
||||||
resourceName?: string;
|
resource?: {
|
||||||
resourceDisplayName?: string;
|
name: string;
|
||||||
resourceEmployeeId?: string;
|
displayName: string;
|
||||||
|
employeeId: string;
|
||||||
|
};
|
||||||
|
|
||||||
recurringId?: string;
|
recurringId?: string;
|
||||||
resources?: string[];
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendarConfig {
|
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
|
// Scrollbar styling
|
||||||
scrollbarWidth: number; // Width of scrollbar in pixels
|
scrollbarWidth: number; // Width of scrollbar in pixels
|
||||||
scrollbarColor: string; // Scrollbar thumb color
|
scrollbarColor: string; // Scrollbar thumb color
|
||||||
|
|
@ -116,7 +106,7 @@ export interface GridPosition {
|
||||||
export interface Period {
|
export interface Period {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
view: ViewType;
|
mode?: CalendarMode; // Optional: which calendar mode this period is for
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventData {
|
export interface EventData {
|
||||||
|
|
@ -124,7 +114,30 @@ export interface EventData {
|
||||||
meta: {
|
meta: {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
view: ViewType;
|
|
||||||
total: number;
|
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;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { CalendarConfig } from '../core/CalendarConfig.js';
|
import { CalendarConfig } from '../core/CalendarConfig.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PositionUtils - Utility funktioner til pixel/minut konvertering
|
* PositionUtils - Utility functions for pixel/minute conversion
|
||||||
* Håndterer positionering og størrelse beregninger for calendar events
|
* Handles positioning and size calculations for calendar events
|
||||||
*/
|
*/
|
||||||
export class PositionUtils {
|
export class PositionUtils {
|
||||||
private config: CalendarConfig;
|
private config: CalendarConfig;
|
||||||
|
|
@ -12,41 +12,45 @@ export class PositionUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konverter minutter til pixels
|
* Convert minutes to pixels
|
||||||
*/
|
*/
|
||||||
public minutesToPixels(minutes: number): number {
|
public minutesToPixels(minutes: number): number {
|
||||||
const pixelsPerHour = this.config.get('hourHeight');
|
const gridSettings = this.config.getGridSettings();
|
||||||
|
const pixelsPerHour = gridSettings.hourHeight;
|
||||||
return (minutes / 60) * pixelsPerHour;
|
return (minutes / 60) * pixelsPerHour;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konverter pixels til minutter
|
* Convert pixels to minutes
|
||||||
*/
|
*/
|
||||||
public pixelsToMinutes(pixels: number): number {
|
public pixelsToMinutes(pixels: number): number {
|
||||||
const pixelsPerHour = this.config.get('hourHeight');
|
const gridSettings = this.config.getGridSettings();
|
||||||
|
const pixelsPerHour = gridSettings.hourHeight;
|
||||||
return (pixels / pixelsPerHour) * 60;
|
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 {
|
public timeToPixels(timeString: string): number {
|
||||||
const [hours, minutes] = timeString.split(':').map(Number);
|
const [hours, minutes] = timeString.split(':').map(Number);
|
||||||
const totalMinutes = (hours * 60) + minutes;
|
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;
|
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
||||||
|
|
||||||
return this.minutesToPixels(minutesFromDayStart);
|
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 {
|
public dateToPixels(date: Date): number {
|
||||||
const hours = date.getHours();
|
const hours = date.getHours();
|
||||||
const minutes = date.getMinutes();
|
const minutes = date.getMinutes();
|
||||||
const totalMinutes = (hours * 60) + minutes;
|
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;
|
const minutesFromDayStart = totalMinutes - dayStartMinutes;
|
||||||
|
|
||||||
return this.minutesToPixels(minutesFromDayStart);
|
return this.minutesToPixels(minutesFromDayStart);
|
||||||
|
|
@ -57,7 +61,8 @@ export class PositionUtils {
|
||||||
*/
|
*/
|
||||||
public pixelsToTime(pixels: number): string {
|
public pixelsToTime(pixels: number): string {
|
||||||
const minutes = this.pixelsToMinutes(pixels);
|
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 totalMinutes = dayStartMinutes + minutes;
|
||||||
|
|
||||||
const hours = Math.floor(totalMinutes / 60);
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
|
@ -103,7 +108,8 @@ export class PositionUtils {
|
||||||
* Snap position til grid interval
|
* Snap position til grid interval
|
||||||
*/
|
*/
|
||||||
public snapToGrid(pixels: number): number {
|
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);
|
const snapPixels = this.minutesToPixels(snapInterval);
|
||||||
|
|
||||||
return Math.round(pixels / snapPixels) * snapPixels;
|
return Math.round(pixels / snapPixels) * snapPixels;
|
||||||
|
|
@ -115,7 +121,8 @@ export class PositionUtils {
|
||||||
public snapTimeToInterval(timeString: string): string {
|
public snapTimeToInterval(timeString: string): string {
|
||||||
const [hours, minutes] = timeString.split(':').map(Number);
|
const [hours, minutes] = timeString.split(':').map(Number);
|
||||||
const totalMinutes = (hours * 60) + minutes;
|
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 snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
|
||||||
const snappedHours = Math.floor(snappedMinutes / 60);
|
const snappedHours = Math.floor(snappedMinutes / 60);
|
||||||
|
|
@ -186,7 +193,8 @@ export class PositionUtils {
|
||||||
*/
|
*/
|
||||||
public isWithinWorkHours(timeString: string): boolean {
|
public isWithinWorkHours(timeString: string): boolean {
|
||||||
const [hours] = timeString.split(':').map(Number);
|
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 {
|
public isWithinDayBounds(timeString: string): boolean {
|
||||||
const [hours] = timeString.split(':').map(Number);
|
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)
|
* Hent maksimum event højde i pixels (hele dagen)
|
||||||
*/
|
*/
|
||||||
public getMaximumEventHeight(): number {
|
public getMaximumEventHeight(): number {
|
||||||
const dayDurationHours = this.config.get('dayEndHour') - this.config.get('dayStartHour');
|
const gridSettings = this.config.getGridSettings();
|
||||||
return dayDurationHours * this.config.get('hourHeight');
|
const dayDurationHours = gridSettings.dayEndHour - gridSettings.dayStartHour;
|
||||||
|
return dayDurationHours * gridSettings.hourHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue