Calendar/src/managers/EventManager.ts
Janus C. H. Knudsen 9bc082eed4 Improves date handling and event stacking
Enhances date validation and timezone handling using DateService, ensuring data integrity and consistency.

Refactors event rendering and dragging to correctly handle date transformations.

Adds a test plan for event stacking and z-index management.

Fixes edge cases in navigation and date calculations for week/year boundaries and DST transitions.
2025-10-04 00:32:26 +02:00

292 lines
No EOL
9.3 KiB
TypeScript

import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
import { DateService } from '../utils/DateService';
import { ResourceData } from '../types/ManagerTypes';
interface RawEventData {
id: string;
title: string;
start: string | Date;
end: string | Date;
type : string;
color?: string;
allDay?: boolean;
[key: string]: unknown;
}
/**
* EventManager - Optimized event lifecycle and CRUD operations
* Handles data loading with improved performance and caching
*/
export class EventManager {
private eventBus: IEventBus;
private events: CalendarEvent[] = [];
private rawData: ResourceCalendarData | RawEventData[] | null = null;
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
private lastCacheKey: string = '';
private dateService: DateService;
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
}
/**
* Optimized data loading with better error handling
*/
public async loadData(): Promise<void> {
try {
await this.loadMockData();
this.clearCache(); // Clear cache when new data is loaded
} catch (error) {
console.error('Failed to load event data:', error);
this.events = [];
this.rawData = null;
}
}
/**
* Optimized mock data loading with better resource handling
*/
private async loadMockData(): Promise<void> {
const calendarType = calendarConfig.getCalendarMode();
const jsonFile = calendarType === 'resource'
? '/src/data/mock-resource-events.json'
: '/src/data/mock-events.json';
const response = await fetch(jsonFile);
if (!response.ok) {
throw new Error(`Failed to load mock events: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// Store raw data and process in one operation
this.rawData = data;
this.events = this.processCalendarData(calendarType, data);
}
/**
* Optimized data processing with better type safety
*/
private processCalendarData(calendarType: string, data: ResourceCalendarData | RawEventData[]): CalendarEvent[] {
if (calendarType === 'resource') {
const resourceData = data as ResourceCalendarData;
return resourceData.resources.flatMap(resource =>
resource.events.map(event => ({
...event,
start: new Date(event.start),
end: new Date(event.end),
resourceName: resource.name,
resourceDisplayName: resource.displayName,
resourceEmployeeId: resource.employeeId
}))
);
}
const eventData = data as RawEventData[];
return eventData.map((event): CalendarEvent => ({
...event,
start: new Date(event.start),
end: new Date(event.end),
type : event.type,
allDay: event.allDay || false,
syncStatus: 'synced' as const
}));
}
/**
* Clear event cache when data changes
*/
private clearCache(): void {
this.eventCache.clear();
this.lastCacheKey = '';
}
/**
* Get events with optional copying for performance
*/
public getEvents(copy: boolean = false): CalendarEvent[] {
return copy ? [...this.events] : this.events;
}
/**
* Get raw resource data for resource calendar mode
*/
public getResourceData(): ResourceData | null {
if (!this.rawData || !('resources' in this.rawData)) {
return null;
}
return {
resources: this.rawData.resources.map(r => ({
id: r.employeeId || r.name, // Use employeeId as id, fallback to name
name: r.name,
type: r.employeeId ? 'employee' : 'resource',
color: 'blue' // Default color since Resource interface doesn't have color
}))
};
}
/**
* Get raw data for compatibility
*/
public getRawData(): ResourceCalendarData | RawEventData[] | null {
return this.rawData;
}
/**
* Optimized event lookup with early return
*/
public getEventById(id: string): CalendarEvent | undefined {
// Use find for better performance than filter + first
return this.events.find(event => event.id === id);
}
/**
* Get event by ID and return event info for navigation
* @param id Event ID to find
* @returns Event with navigation info or null if not found
*/
public getEventForNavigation(id: string): { event: CalendarEvent; eventDate: Date } | null {
const event = this.getEventById(id);
if (!event) {
return null;
}
// Validate event dates
const validation = this.dateService.validateDate(event.start);
if (!validation.valid) {
console.warn(`EventManager: Invalid event start date for event ${id}:`, validation.error);
return null;
}
// Validate date range
if (!this.dateService.isValidRange(event.start, event.end)) {
console.warn(`EventManager: Invalid date range for event ${id}: start must be before end`);
return null;
}
return {
event,
eventDate: event.start
};
}
/**
* Navigate to specific event by ID
* Emits navigation events for other managers to handle
* @param eventId Event ID to navigate to
* @returns true if event found and navigation initiated, false otherwise
*/
public navigateToEvent(eventId: string): boolean {
const eventInfo = this.getEventForNavigation(eventId);
if (!eventInfo) {
console.warn(`EventManager: Event with ID ${eventId} not found`);
return false;
}
const { event, eventDate } = eventInfo;
// Emit navigation request event
this.eventBus.emit(CoreEvents.NAVIGATE_TO_EVENT, {
eventId,
event,
eventDate,
eventStartTime: event.start
});
return true;
}
/**
* Optimized events for period with caching and DateService
*/
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
// Create cache key using DateService for consistent formatting
const cacheKey = `${this.dateService.formatISODate(startDate)}_${this.dateService.formatISODate(endDate)}`;
// Return cached result if available
if (this.lastCacheKey === cacheKey && this.eventCache.has(cacheKey)) {
return this.eventCache.get(cacheKey)!;
}
// Filter events using optimized date operations
const filteredEvents = this.events.filter(event => {
// Event overlaps period if it starts before period ends AND ends after period starts
return event.start <= endDate && event.end >= startDate;
});
// Cache the result
this.eventCache.set(cacheKey, filteredEvents);
this.lastCacheKey = cacheKey;
return filteredEvents;
}
/**
* Optimized event creation with better ID generation
*/
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent {
const newEvent: CalendarEvent = {
...event,
id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
};
this.events.push(newEvent);
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_CREATED, {
event: newEvent
});
return newEvent;
}
/**
* Optimized event update with validation
*/
public updateEvent(id: string, updates: Partial<CalendarEvent>): CalendarEvent | null {
const eventIndex = this.events.findIndex(event => event.id === id);
if (eventIndex === -1) return null;
const updatedEvent = { ...this.events[eventIndex], ...updates };
this.events[eventIndex] = updatedEvent;
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
event: updatedEvent
});
return updatedEvent;
}
/**
* Optimized event deletion with better error handling
*/
public deleteEvent(id: string): boolean {
const eventIndex = this.events.findIndex(event => event.id === id);
if (eventIndex === -1) return false;
const deletedEvent = this.events[eventIndex];
this.events.splice(eventIndex, 1);
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
event: deletedEvent
});
return true;
}
/**
* Refresh data by reloading from source
*/
public async refresh(): Promise<void> {
await this.loadData();
}
}