385 lines
10 KiB
JavaScript
385 lines
10 KiB
JavaScript
|
|
// js/managers/DataManager.js
|
||
|
|
|
||
|
|
import { eventBus } from '../core/EventBus.js';
|
||
|
|
import { EventTypes } from '../types/EventTypes.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Manages data fetching and API communication
|
||
|
|
* Currently uses mock data until backend is implemented
|
||
|
|
*/
|
||
|
|
export class DataManager {
|
||
|
|
constructor() {
|
||
|
|
this.baseUrl = '/api/events';
|
||
|
|
this.useMockData = true; // Toggle this when backend is ready
|
||
|
|
this.cache = new Map();
|
||
|
|
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
init() {
|
||
|
|
this.subscribeToEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
subscribeToEvents() {
|
||
|
|
// Listen for period changes to fetch new data
|
||
|
|
eventBus.on(EventTypes.PERIOD_CHANGE, (e) => {
|
||
|
|
this.fetchEventsForPeriod(e.detail);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for event updates
|
||
|
|
eventBus.on(EventTypes.EVENT_UPDATE, (e) => {
|
||
|
|
this.updateEvent(e.detail);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for event creation
|
||
|
|
eventBus.on(EventTypes.EVENT_CREATE, (e) => {
|
||
|
|
this.createEvent(e.detail);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for event deletion
|
||
|
|
eventBus.on(EventTypes.EVENT_DELETE, (e) => {
|
||
|
|
this.deleteEvent(e.detail.eventId);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch events for a specific period
|
||
|
|
* @param {Object} period - Contains start, end, view
|
||
|
|
*/
|
||
|
|
async fetchEventsForPeriod(period) {
|
||
|
|
const cacheKey = `${period.start}-${period.end}-${period.view}`;
|
||
|
|
|
||
|
|
// 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;
|
||
|
|
|
||
|
|
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,
|
||
|
|
view: period.view
|
||
|
|
});
|
||
|
|
|
||
|
|
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) {
|
||
|
|
eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: error.message });
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new event
|
||
|
|
*/
|
||
|
|
async createEvent(eventData) {
|
||
|
|
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (this.useMockData) {
|
||
|
|
await this.delay(200);
|
||
|
|
const newEvent = {
|
||
|
|
id: `evt-${Date.now()}`,
|
||
|
|
...eventData,
|
||
|
|
syncStatus: 'synced'
|
||
|
|
};
|
||
|
|
|
||
|
|
// 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) {
|
||
|
|
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||
|
|
action: 'create',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update an existing event
|
||
|
|
*/
|
||
|
|
async updateEvent(updateData) {
|
||
|
|
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) {
|
||
|
|
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||
|
|
action: 'update',
|
||
|
|
error: error.message,
|
||
|
|
eventId: updateData.eventId
|
||
|
|
});
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete an event
|
||
|
|
*/
|
||
|
|
async deleteEvent(eventId) {
|
||
|
|
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) {
|
||
|
|
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||
|
|
action: 'delete',
|
||
|
|
error: error.message,
|
||
|
|
eventId
|
||
|
|
});
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate mock data for testing
|
||
|
|
*/
|
||
|
|
getMockData(period) {
|
||
|
|
const events = [];
|
||
|
|
const types = ['meeting', 'meal', 'work', 'milestone'];
|
||
|
|
const titles = {
|
||
|
|
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 = '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.view === 'week') {
|
||
|
|
const midWeek = new Date(startDate);
|
||
|
|
midWeek.setDate(midWeek.getDate() + 2);
|
||
|
|
|
||
|
|
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,
|
||
|
|
view: period.view,
|
||
|
|
total: events.length
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Utility methods
|
||
|
|
*/
|
||
|
|
|
||
|
|
formatDate(date) {
|
||
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
delay(ms) {
|
||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear all cached data
|
||
|
|
*/
|
||
|
|
clearCache() {
|
||
|
|
this.cache.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Toggle between mock and real data
|
||
|
|
*/
|
||
|
|
setUseMockData(useMock) {
|
||
|
|
this.useMockData = useMock;
|
||
|
|
this.clearCache();
|
||
|
|
}
|
||
|
|
}
|