Add widget functionality for birthdays, upcoming events, and mini calendar: Implement new API endpoints in calendarController and calendarService to retrieve upcoming birthdays and events, as well as mini calendar data. Update calendarRouter to include widget routes and enhance DashboardWidget to dynamically render new widget components. This update improves user experience by providing quick access to important calendar information.

This commit is contained in:
Torsten Schulz (local)
2026-01-30 15:14:37 +01:00
parent f65d3385ec
commit 7ed284d74b
8 changed files with 774 additions and 2 deletions

View File

@@ -274,6 +274,216 @@ class CalendarService {
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
*/