Calendar/src/managers/DataManager.ts

453 lines
No EOL
13 KiB
TypeScript

// 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<CalendarEvent>;
}
/**
* 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<string, EventData> = 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<EventData> {
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;
}
}
/**
* Filter events to only include those within the specified period
*/
public filterEventsForPeriod(events: CalendarEvent[], period: Period): CalendarEvent[] {
const startDate = new Date(period.start);
const endDate = new Date(period.end);
return events.filter(event => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
// Include event if it overlaps with the period
return eventStart <= endDate && eventEnd >= startDate;
});
}
/**
* Get events filtered by period and optionally by all-day status
*/
public getFilteredEvents(period: Period, excludeAllDay: boolean = false): CalendarEvent[] {
const cacheKey = `${period.start}-${period.end}`;
const cachedData = this.cache.get(cacheKey);
if (!cachedData) {
console.warn('DataManager: No cached data found for period', period);
return [];
}
let filteredEvents = this.filterEventsForPeriod(cachedData.events, period);
if (excludeAllDay) {
filteredEvents = filteredEvents.filter(event => !event.allDay);
console.log(`DataManager: Filtered out all-day events, ${filteredEvents.length} non-all-day events remaining`);
}
return filteredEvents;
}
/**
* Create a new event
*/
async createEvent(eventData: EventCreateData): Promise<CalendarEvent> {
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<boolean> {
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<boolean> {
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 - only generates events within the specified period
*/
private getMockData(period: Period): EventData {
const events: CalendarEvent[] = [];
const types: string[] = ['meeting', 'meal', 'work', 'milestone'];
const titles: Record<string, string[]> = {
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 - only generate events within this exact period
const startDate = new Date(period.start);
const endDate = new Date(period.end);
console.log(`DataManager: Generating mock events for period ${period.start} to ${period.end}`);
// Generate some events for each day within the period
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<void> {
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();
}
}