import CalendarEvent from '../models/community/calendar_event.js'; import User from '../models/community/user.js'; import Friendship from '../models/community/friendship.js'; import UserParam from '../models/community/user_param.js'; import UserParamType from '../models/type/user_param.js'; import UserParamVisibility from '../models/community/user_param_visibility.js'; import UserParamVisibilityType from '../models/type/user_param_visibility.js'; import { Op } from 'sequelize'; class CalendarService { /** * Get all calendar events for a user * @param {string} hashedUserId - The user's hashed ID * @param {object} options - Optional filters (startDate, endDate) */ async getEvents(hashedUserId, options = {}) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const where = { userId: user.id }; // Filter by date range if provided if (options.startDate || options.endDate) { where[Op.or] = []; if (options.startDate && options.endDate) { // Events that overlap with the requested range where[Op.or].push({ startDate: { [Op.between]: [options.startDate, options.endDate] } }); where[Op.or].push({ endDate: { [Op.between]: [options.startDate, options.endDate] } }); where[Op.or].push({ [Op.and]: [ { startDate: { [Op.lte]: options.startDate } }, { endDate: { [Op.gte]: options.endDate } } ] }); } else if (options.startDate) { where[Op.or].push({ startDate: { [Op.gte]: options.startDate } }); where[Op.or].push({ endDate: { [Op.gte]: options.startDate } }); } else if (options.endDate) { where[Op.or].push({ startDate: { [Op.lte]: options.endDate } }); } } const events = await CalendarEvent.findAll({ where, order: [['startDate', 'ASC'], ['startTime', 'ASC']] }); return events.map(e => this.formatEvent(e)); } /** * Get a single event by ID */ async getEvent(hashedUserId, eventId) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const event = await CalendarEvent.findOne({ where: { id: eventId, userId: user.id } }); if (!event) { throw new Error('Event not found'); } return this.formatEvent(event); } /** * Create a new calendar event */ async createEvent(hashedUserId, eventData) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const event = await CalendarEvent.create({ userId: user.id, title: eventData.title, description: eventData.description || null, categoryId: eventData.categoryId || 'personal', startDate: eventData.startDate, endDate: eventData.endDate || eventData.startDate, startTime: eventData.allDay ? null : eventData.startTime, endTime: eventData.allDay ? null : eventData.endTime, allDay: eventData.allDay || false }); return this.formatEvent(event); } /** * Update an existing calendar event */ async updateEvent(hashedUserId, eventId, eventData) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const event = await CalendarEvent.findOne({ where: { id: eventId, userId: user.id } }); if (!event) { throw new Error('Event not found'); } await event.update({ title: eventData.title, description: eventData.description || null, categoryId: eventData.categoryId || 'personal', startDate: eventData.startDate, endDate: eventData.endDate || eventData.startDate, startTime: eventData.allDay ? null : eventData.startTime, endTime: eventData.allDay ? null : eventData.endTime, allDay: eventData.allDay || false }); return this.formatEvent(event); } /** * Delete a calendar event */ async deleteEvent(hashedUserId, eventId) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const event = await CalendarEvent.findOne({ where: { id: eventId, userId: user.id } }); if (!event) { throw new Error('Event not found'); } await event.destroy(); return { success: true }; } /** * Get friends' birthdays that are visible to the user * @param {string} hashedUserId - The user's hashed ID * @param {number} year - The year to get birthdays for */ async getFriendsBirthdays(hashedUserId, year) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } // Get user's age for visibility check const userAge = await this.getUserAge(user.id); // Get all accepted friendships const friendships = await Friendship.findAll({ where: { accepted: true, withdrawn: false, denied: false, [Op.or]: [ { user1Id: user.id }, { user2Id: user.id } ] } }); const birthdays = []; for (const friendship of friendships) { // Get the friend's user ID const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id; // Get the friend's birthdate param with visibility const birthdateParam = await UserParam.findOne({ where: { userId: friendId }, include: [ { model: UserParamType, as: 'paramType', where: { description: 'birthdate' } }, { model: UserParamVisibility, as: 'param_visibilities', include: [{ model: UserParamVisibilityType, as: 'visibility_type' }] } ] }); if (!birthdateParam || !birthdateParam.value) continue; // Check visibility const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible'; if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue; // Get friend's username const friend = await User.findOne({ where: { id: friendId }, attributes: ['username', 'hashedId'] }); if (!friend) continue; // Parse birthdate and create birthday event for the requested year const birthdate = new Date(birthdateParam.value); if (isNaN(birthdate.getTime())) continue; const birthdayDate = `${year}-${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`; birthdays.push({ id: `birthday-${friend.hashedId}-${year}`, title: friend.username, categoryId: 'birthday', startDate: birthdayDate, endDate: birthdayDate, allDay: true, isBirthday: true, friendHashedId: friend.hashedId }); } return birthdays; } /** * Check if birthdate is visible to a friend */ isBirthdayVisibleToFriend(visibility, requestingUserAge) { // Visible to friends if visibility is 'All', 'Friends', or 'FriendsAndAdults' (if adult) return visibility === 'All' || visibility === 'Friends' || (visibility === 'FriendsAndAdults' && requestingUserAge >= 18); } /** * Get user's age from birthdate */ async getUserAge(userId) { const birthdateParam = await UserParam.findOne({ where: { userId }, include: [{ model: UserParamType, as: 'paramType', where: { description: 'birthdate' } }] }); if (!birthdateParam || !birthdateParam.value) return 0; const birthdate = new Date(birthdateParam.value); const today = new Date(); let age = today.getFullYear() - birthdate.getFullYear(); const monthDiff = today.getMonth() - birthdate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthdate.getDate())) { age--; } return age; } /** * Format event for API response */ formatEvent(event) { return { id: event.id, title: event.title, description: event.description, categoryId: event.categoryId, startDate: event.startDate, endDate: event.endDate, startTime: event.startTime ? event.startTime.substring(0, 5) : null, // HH:MM format endTime: event.endTime ? event.endTime.substring(0, 5) : null, allDay: event.allDay, createdAt: event.createdAt, updatedAt: event.updatedAt }; } } export default new CalendarService();