Calendar/src/managers/EventManager.ts

295 lines
9.2 KiB
TypeScript
Raw Normal View History

2025-08-07 00:15:44 +02:00
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 events: CalendarEvent[] = [];
private rawData: ResourceCalendarData | RawEventData[] | null = null;
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
private lastCacheKey: string = '';
private dateService: DateService;
private config: CalendarConfig;
constructor(
private eventBus: IEventBus,
dateService: DateService,
config: CalendarConfig
) {
this.dateService = dateService;
this.config = config;
2025-08-09 01:16:04 +02:00
}
2025-08-09 01:16:04 +02:00
/**
* Optimized data loading with better error handling
2025-08-09 01:16:04 +02:00
*/
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;
2025-08-09 01:16:04 +02:00
}
}
/**
* Optimized mock data loading with better resource handling
*/
2025-08-07 00:15:44 +02:00
private async loadMockData(): Promise<void> {
const calendarType = this.config.getCalendarMode();
const jsonFile = calendarType === 'resource'
? '/data/mock-resource-events.json'
: '/data/mock-events.json';
const response = await fetch(jsonFile);
if (!response.ok) {
throw new Error(`Failed to load mock events: ${response.status} ${response.statusText}`);
2025-08-07 00:15:44 +02:00
}
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,
2025-09-09 14:35:21 +02:00
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 => ({
2025-09-09 14:35:21 +02:00
...event,
start: new Date(event.start),
end: new Date(event.end),
type : event.type,
allDay: event.allDay || false,
syncStatus: 'synced' as const
2025-09-09 14:35:21 +02:00
}));
}
/**
* 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
2025-09-09 14:35:21 +02:00
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();
}
}