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; } /** * Get upcoming birthdays for widget (sorted by next occurrence) */ async getUpcomingBirthdays(hashedUserId, limit = 10) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const userAge = await this.getUserAge(user.id); const today = new Date(); const currentYear = today.getFullYear(); // 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) { const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id; 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; const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible'; if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue; const friend = await User.findOne({ where: { id: friendId }, attributes: ['username', 'hashedId'] }); if (!friend) continue; const birthdate = new Date(birthdateParam.value); if (isNaN(birthdate.getTime())) continue; // Calculate next birthday let nextBirthday = new Date(currentYear, birthdate.getMonth(), birthdate.getDate()); if (nextBirthday < today) { nextBirthday = new Date(currentYear + 1, birthdate.getMonth(), birthdate.getDate()); } // Calculate days until birthday const diffTime = nextBirthday.getTime() - today.getTime(); const daysUntil = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // Calculate age they will turn const turningAge = nextBirthday.getFullYear() - birthdate.getFullYear(); birthdays.push({ username: friend.username, hashedId: friend.hashedId, date: `${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`, nextDate: nextBirthday.toISOString().split('T')[0], daysUntil, turningAge }); } // Sort by days until birthday birthdays.sort((a, b) => a.daysUntil - b.daysUntil); return birthdays.slice(0, limit); } /** * Get upcoming events for widget */ async getUpcomingEvents(hashedUserId, limit = 10) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const today = new Date(); const todayStr = today.toISOString().split('T')[0]; const events = await CalendarEvent.findAll({ where: { userId: user.id, [Op.or]: [ { startDate: { [Op.gte]: todayStr } }, { endDate: { [Op.gte]: todayStr } } ] }, order: [['startDate', 'ASC'], ['startTime', 'ASC']], limit }); return events.map(e => ({ id: e.id, titel: e.title, datum: e.startDate, beschreibung: e.description || null, categoryId: e.categoryId, allDay: e.allDay, startTime: e.startTime ? e.startTime.substring(0, 5) : null, endDate: e.endDate })); } /** * Get mini calendar data for widget */ async getMiniCalendarData(hashedUserId) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) { throw new Error('User not found'); } const today = new Date(); const year = today.getFullYear(); const month = today.getMonth(); // Get first and last day of month const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startStr = firstDay.toISOString().split('T')[0]; const endStr = lastDay.toISOString().split('T')[0]; // Get user events for this month const events = await CalendarEvent.findAll({ where: { userId: user.id, [Op.or]: [ { startDate: { [Op.between]: [startStr, endStr] } }, { endDate: { [Op.between]: [startStr, endStr] } }, { [Op.and]: [ { startDate: { [Op.lte]: startStr } }, { endDate: { [Op.gte]: endStr } } ] } ] } }); // Get birthdays for this month const birthdays = await this.getFriendsBirthdays(hashedUserId, year); const monthBirthdays = birthdays.filter(b => { const bMonth = parseInt(b.startDate.split('-')[1]); return bMonth === month + 1; }); // Build days with events const daysWithEvents = {}; for (const event of events) { const start = new Date(event.startDate); const end = event.endDate ? new Date(event.endDate) : start; for (let d = new Date(start); d <= end && d <= lastDay; d.setDate(d.getDate() + 1)) { if (d >= firstDay) { const dayNum = d.getDate(); if (!daysWithEvents[dayNum]) { daysWithEvents[dayNum] = { events: 0, birthdays: 0 }; } daysWithEvents[dayNum].events++; } } } for (const birthday of monthBirthdays) { const dayNum = parseInt(birthday.startDate.split('-')[2]); if (!daysWithEvents[dayNum]) { daysWithEvents[dayNum] = { events: 0, birthdays: 0 }; } daysWithEvents[dayNum].birthdays++; } return { year, month: month + 1, today: today.getDate(), firstDayOfWeek: firstDay.getDay() === 0 ? 7 : firstDay.getDay(), // Monday = 1 daysInMonth: lastDay.getDate(), daysWithEvents }; } /** * 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();