453 lines
No EOL
13 KiB
TypeScript
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();
|
|
}
|
|
} |