Files
yourpart3/backend/services/calendarService.js

508 lines
17 KiB
JavaScript

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();