Adds IndexedDB and operation queue for robust offline synchronization Introduces SyncManager to handle background data synchronization Supports local event operations with automatic remote sync queuing Enhances application reliability and user experience in low/no connectivity scenarios
197 lines
6.1 KiB
TypeScript
197 lines
6.1 KiB
TypeScript
import { IEventBus, ICalendarEvent } from '../types/CalendarTypes';
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
|
import { Configuration } from '../configurations/CalendarConfig';
|
|
import { DateService } from '../utils/DateService';
|
|
import { IEventRepository } from '../repositories/IEventRepository';
|
|
|
|
/**
|
|
* EventManager - Event lifecycle and CRUD operations
|
|
* Delegates all data operations to IEventRepository
|
|
* No longer maintains in-memory cache - repository is single source of truth
|
|
*/
|
|
export class EventManager {
|
|
|
|
private dateService: DateService;
|
|
private config: Configuration;
|
|
private repository: IEventRepository;
|
|
|
|
constructor(
|
|
private eventBus: IEventBus,
|
|
dateService: DateService,
|
|
config: Configuration,
|
|
repository: IEventRepository
|
|
) {
|
|
this.dateService = dateService;
|
|
this.config = config;
|
|
this.repository = repository;
|
|
}
|
|
|
|
/**
|
|
* Load event data from repository
|
|
* No longer caches - delegates to repository
|
|
*/
|
|
public async loadData(): Promise<void> {
|
|
try {
|
|
// Just ensure repository is ready - no caching
|
|
await this.repository.loadEvents();
|
|
} catch (error) {
|
|
console.error('Failed to load event data:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all events from repository
|
|
*/
|
|
public async getEvents(copy: boolean = false): Promise<ICalendarEvent[]> {
|
|
const events = await this.repository.loadEvents();
|
|
return copy ? [...events] : events;
|
|
}
|
|
|
|
/**
|
|
* Get event by ID from repository
|
|
*/
|
|
public async getEventById(id: string): Promise<ICalendarEvent | undefined> {
|
|
const events = await this.repository.loadEvents();
|
|
return 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 async getEventForNavigation(id: string): Promise<{ event: ICalendarEvent; eventDate: Date } | null> {
|
|
const event = await 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 async navigateToEvent(eventId: string): Promise<boolean> {
|
|
const eventInfo = await 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;
|
|
}
|
|
|
|
/**
|
|
* Get events that overlap with a given time period
|
|
*/
|
|
public async getEventsForPeriod(startDate: Date, endDate: Date): Promise<ICalendarEvent[]> {
|
|
const events = await this.repository.loadEvents();
|
|
// Event overlaps period if it starts before period ends AND ends after period starts
|
|
return events.filter(event => {
|
|
return event.start <= endDate && event.end >= startDate;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new event and add it to the calendar
|
|
* Delegates to repository with source='local'
|
|
*/
|
|
public async addEvent(event: Omit<ICalendarEvent, 'id'>): Promise<ICalendarEvent> {
|
|
const newEvent = await this.repository.createEvent(event, 'local');
|
|
|
|
this.eventBus.emit(CoreEvents.EVENT_CREATED, {
|
|
event: newEvent
|
|
});
|
|
|
|
return newEvent;
|
|
}
|
|
|
|
/**
|
|
* Update an existing event
|
|
* Delegates to repository with source='local'
|
|
*/
|
|
public async updateEvent(id: string, updates: Partial<ICalendarEvent>): Promise<ICalendarEvent | null> {
|
|
try {
|
|
const updatedEvent = await this.repository.updateEvent(id, updates, 'local');
|
|
|
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
|
event: updatedEvent
|
|
});
|
|
|
|
return updatedEvent;
|
|
} catch (error) {
|
|
console.error(`Failed to update event ${id}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an event
|
|
* Delegates to repository with source='local'
|
|
*/
|
|
public async deleteEvent(id: string): Promise<boolean> {
|
|
try {
|
|
await this.repository.deleteEvent(id, 'local');
|
|
|
|
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
|
|
eventId: id
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Failed to delete event ${id}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle remote update from SignalR
|
|
* Delegates to repository with source='remote'
|
|
*/
|
|
public async handleRemoteUpdate(event: ICalendarEvent): Promise<void> {
|
|
try {
|
|
await this.repository.updateEvent(event.id, event, 'remote');
|
|
|
|
this.eventBus.emit(CoreEvents.REMOTE_UPDATE_RECEIVED, {
|
|
event
|
|
});
|
|
|
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
|
event
|
|
});
|
|
} catch (error) {
|
|
console.error(`Failed to handle remote update for event ${event.id}:`, error);
|
|
}
|
|
}
|
|
}
|