// Data management and API communication import { eventBus } from '../core/EventBus'; import { EventTypes } from '../constants/EventTypes'; import { CalendarEvent, EventData, Period } from '../types/CalendarTypes'; /** * Event creation data interface */ interface EventCreateData { title: string; type: string; start: string; end: string; allDay: boolean; description?: string; } /** * Event update data interface */ interface EventUpdateData { eventId: string; changes: Partial; } /** * Manages data fetching and API communication * Currently uses mock data until backend is implemented */ export class DataManager { private baseUrl: string = '/api/events'; private useMockData: boolean = true; // Toggle this when backend is ready private cache: Map = new Map(); constructor() { this.init(); } private init(): void { this.subscribeToEvents(); } private subscribeToEvents(): void { // Listen for period changes to fetch new data eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => { this.fetchEventsForPeriod((e as CustomEvent).detail); }); // Listen for event updates eventBus.on(EventTypes.EVENT_UPDATE, (e: Event) => { this.updateEvent((e as CustomEvent).detail); }); // Listen for event creation eventBus.on(EventTypes.EVENT_CREATE, (e: Event) => { this.createEvent((e as CustomEvent).detail); }); // Listen for event deletion eventBus.on(EventTypes.EVENT_DELETE, (e: Event) => { this.deleteEvent((e as CustomEvent).detail.eventId); }); } /** * Fetch events for a specific period */ async fetchEventsForPeriod(period: Period): Promise { const cacheKey = `${period.start}-${period.end}`; // Check cache first if (this.cache.has(cacheKey)) { const cachedData = this.cache.get(cacheKey)!; eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, cachedData); return cachedData; } // Emit loading start eventBus.emit(EventTypes.DATA_FETCH_START, { period }); try { let data: EventData; if (this.useMockData) { // Simulate network delay await this.delay(300); data = this.getMockData(period); } else { // Real API call const params = new URLSearchParams({ start: period.start, end: period.end }); const response = await fetch(`${this.baseUrl}?${params}`); if (!response.ok) throw new Error('Failed to fetch events'); data = await response.json(); } // Cache the data this.cache.set(cacheKey, data); // Emit success eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, data); return data; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: errorMessage }); throw error; } } /** * Create a new event */ async createEvent(eventData: EventCreateData): Promise { eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' }); try { if (this.useMockData) { await this.delay(200); const newEvent: CalendarEvent = { id: `evt-${Date.now()}`, title: eventData.title, start: eventData.start, end: eventData.end, type: eventData.type, allDay: eventData.allDay, syncStatus: 'synced', metadata: eventData.description ? { description: eventData.description } : undefined }; // Clear cache to force refresh this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'create', event: newEvent }); return newEvent; } else { // Real API call const response = await fetch(this.baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(eventData) }); if (!response.ok) throw new Error('Failed to create event'); const newEvent = await response.json(); this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'create', event: newEvent }); return newEvent; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; eventBus.emit(EventTypes.DATA_SYNC_ERROR, { action: 'create', error: errorMessage }); throw error; } } /** * Update an existing event */ async updateEvent(updateData: EventUpdateData): Promise { eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'update' }); try { if (this.useMockData) { await this.delay(200); // Clear cache to force refresh this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'update', eventId: updateData.eventId, changes: updateData.changes }); return true; } else { // Real API call const response = await fetch(`${this.baseUrl}/${updateData.eventId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData.changes) }); if (!response.ok) throw new Error('Failed to update event'); this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'update', eventId: updateData.eventId }); return true; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; eventBus.emit(EventTypes.DATA_SYNC_ERROR, { action: 'update', error: errorMessage, eventId: updateData.eventId }); throw error; } } /** * Delete an event */ async deleteEvent(eventId: string): Promise { eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'delete' }); try { if (this.useMockData) { await this.delay(200); // Clear cache to force refresh this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'delete', eventId }); return true; } else { // Real API call const response = await fetch(`${this.baseUrl}/${eventId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete event'); this.cache.clear(); eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, { action: 'delete', eventId }); return true; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; eventBus.emit(EventTypes.DATA_SYNC_ERROR, { action: 'delete', error: errorMessage, eventId }); throw error; } } /** * Generate mock data for testing */ private getMockData(period: Period): EventData { const events: CalendarEvent[] = []; const types: string[] = ['meeting', 'meal', 'work', 'milestone']; const titles: Record = { meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'], meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'], work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'], milestone: ['Project Deadline', 'Release Day', 'Demo Day'] }; // Parse dates const startDate = new Date(period.start); const endDate = new Date(period.end); // Generate some events for each day for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { // Skip weekends for most events const dayOfWeek = d.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; if (isWeekend) { // Maybe one or two events on weekends if (Math.random() > 0.7) { const type: string = 'meal'; const title = titles[type][Math.floor(Math.random() * titles[type].length)]; const hour = 12 + Math.floor(Math.random() * 4); events.push({ id: `evt-${events.length + 1}`, title, type, start: `${this.formatDate(d)}T${hour}:00:00`, end: `${this.formatDate(d)}T${hour + 1}:00:00`, allDay: false, syncStatus: 'synced' }); } } else { // Regular workday events // Morning standup if (Math.random() > 0.3) { events.push({ id: `evt-${events.length + 1}`, title: 'Team Standup', type: 'meeting', start: `${this.formatDate(d)}T09:00:00`, end: `${this.formatDate(d)}T09:30:00`, allDay: false, syncStatus: 'synced' }); } // Lunch events.push({ id: `evt-${events.length + 1}`, title: 'Lunch', type: 'meal', start: `${this.formatDate(d)}T12:00:00`, end: `${this.formatDate(d)}T13:00:00`, allDay: false, syncStatus: 'synced' }); // Random afternoon events const numAfternoonEvents = Math.floor(Math.random() * 3) + 1; for (let i = 0; i < numAfternoonEvents; i++) { const type = types[Math.floor(Math.random() * types.length)]; const title = titles[type][Math.floor(Math.random() * titles[type].length)]; const startHour = 13 + Math.floor(Math.random() * 4); const duration = 1 + Math.floor(Math.random() * 2); events.push({ id: `evt-${events.length + 1}`, title, type, start: `${this.formatDate(d)}T${startHour}:${Math.random() > 0.5 ? '00' : '30'}:00`, end: `${this.formatDate(d)}T${startHour + duration}:00:00`, allDay: false, syncStatus: Math.random() > 0.9 ? 'pending' : 'synced' }); } } } // Add a multi-day event if period spans multiple days const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff > 1) { const midWeek = new Date(startDate); midWeek.setDate(midWeek.getDate() + Math.min(2, daysDiff - 1)); events.push({ id: `evt-${events.length + 1}`, title: 'Project Sprint', type: 'milestone', start: `${this.formatDate(startDate)}T00:00:00`, end: `${this.formatDate(midWeek)}T23:59:59`, allDay: true, syncStatus: 'synced' }); } return { events, meta: { start: period.start, end: period.end, total: events.length } }; } /** * Utility methods */ private formatDate(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Clear all cached data */ clearCache(): void { this.cache.clear(); } /** * Toggle between mock and real data */ setUseMockData(useMock: boolean): void { this.useMockData = useMock; this.clearCache(); } }