feat(calendar): add birthday integration from contacts service

- Add birthdaysStore to fetch and manage birthdays from contacts API
- Add BirthdayPopover component with contact details and link to contacts app
- Integrate birthdays into WeekView, MonthView, and DayView as all-day events
- Add settings for showBirthdays and showBirthdayAge toggles
- Add reactive $effect in layout to load birthdays when setting is enabled
- Add /contacts/birthdays endpoint to contacts backend
- Configure PUBLIC_CONTACTS_API_URL env variable for calendar app

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-14 20:49:08 +01:00
parent 4b6a4c73ae
commit cdc3cd3ec8
12 changed files with 995 additions and 85 deletions

View file

@ -234,6 +234,16 @@ export class ContactController {
return { contacts, total };
}
/**
* Get all contacts with birthdays (for calendar integration)
* Returns lightweight data: id, displayName, firstName, lastName, birthday, photoUrl
*/
@Get('birthdays')
async getBirthdays(@CurrentUser() user: CurrentUserData) {
const contacts = await this.contactService.findWithBirthdays(user.userId);
return { contacts };
}
@Get(':id')
async findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
const contact = await this.contactService.findById(id, user.userId);

View file

@ -1,10 +1,19 @@
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { eq, and, or, ilike, desc, sql } from 'drizzle-orm';
import { eq, and, or, ilike, desc, sql, isNotNull } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { Database } from '../db/connection';
import { contacts } from '../db/schema';
import type { Contact, NewContact } from '../db/schema';
export interface ContactBirthdaySummary {
id: string;
displayName: string | null;
firstName: string | null;
lastName: string | null;
birthday: string;
photoUrl: string | null;
}
export interface ContactFilters {
search?: string;
isFavorite?: boolean;
@ -148,4 +157,34 @@ export class ContactService {
return Number(result[0]?.count || 0);
}
/**
* Find all contacts with birthdays (for calendar integration)
* Returns only essential fields for lightweight transfer
*/
async findWithBirthdays(userId: string): Promise<ContactBirthdaySummary[]> {
const result = await this.db
.select({
id: contacts.id,
displayName: contacts.displayName,
firstName: contacts.firstName,
lastName: contacts.lastName,
birthday: contacts.birthday,
photoUrl: contacts.photoUrl,
})
.from(contacts)
.where(
and(
eq(contacts.userId, userId),
eq(contacts.isArchived, false),
isNotNull(contacts.birthday)
)
)
.orderBy(contacts.lastName, contacts.firstName);
return result.map((c) => ({
...c,
birthday: c.birthday || '',
}));
}
}