feat: add CalDAV synchronization with automatic sync

- Add CaldavService with tsdav/ical.js for CalDAV server communication
- Add CaldavController, CaldavRepository, and caldav routes
- Add client-side CaldavConfigService with sync(), config CRUD
- Add CalDAV settings UI with config load/save in settings screen
- Sync on login, auto-login (AuthGuard), periodic timer (calendar), and sync button
- Push single events to CalDAV on server-side create/update/delete
- Push all events to CalDAV after chat event confirmation
- Refactor ChatService to use EventService instead of direct EventRepository
- Rename CalDav/calDav to Caldav/caldav for consistent naming
- Add Radicale Docker setup for local CalDAV testing
- Update PlantUML diagrams and CLAUDE.md with CalDAV architecture
This commit is contained in:
2026-02-08 19:24:59 +01:00
parent 81221d8b70
commit 325246826a
44 changed files with 7074 additions and 126 deletions

View File

@@ -6,7 +6,7 @@ import {
MONTH_TO_GERMAN,
ExpandedEvent,
} from "@calchat/shared";
import { EventRepository } from "../services/interfaces";
import { EventService } from "../services/EventService";
import { expandRecurringEvents } from "./recurrenceExpander";
// Private formatting helpers
@@ -107,13 +107,13 @@ function formatMonthText(events: ExpandedEvent[], monthName: string): string {
* Recurring events are expanded to show all occurrences within the range.
*/
export async function getWeeksOverview(
eventRepo: EventRepository,
eventService: EventService,
userId: string,
weeks: number,
): Promise<string> {
const now = new Date();
const endDate = new Date(now.getTime() + weeks * 7 * 24 * 60 * 60 * 1000);
const events = await eventRepo.findByUserId(userId);
const events = await eventService.getAll(userId);
const expanded = expandRecurringEvents(events, now, endDate);
return formatWeeksText(expanded, weeks);
}
@@ -123,14 +123,14 @@ export async function getWeeksOverview(
* Recurring events are expanded to show all occurrences within the month.
*/
export async function getMonthOverview(
eventRepo: EventRepository,
eventService: EventService,
userId: string,
year: number,
month: number,
): Promise<string> {
const startOfMonth = new Date(year, month, 1);
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
const events = await eventRepo.findByUserId(userId);
const events = await eventService.getAll(userId);
const expanded = expandRecurringEvents(events, startOfMonth, endOfMonth);
const monthName = MONTH_TO_GERMAN[MONTHS[month]];
return formatMonthText(expanded, monthName);

View File

@@ -1,5 +1,5 @@
import { RRule, rrulestr } from "rrule";
import { CalendarEvent, ExpandedEvent } from "@calchat/shared";
import { CalendarEvent, ExpandedEvent, formatDateKey } from "@calchat/shared";
// Convert local time to "fake UTC" for rrule
// rrule interprets all dates as UTC internally, so we need to trick it
@@ -133,11 +133,3 @@ function formatRRuleDateString(date: Date): string {
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}${month}${day}T${hours}${minutes}${seconds}`;
}
// Format date as YYYY-MM-DD for exception date comparison
function formatDateKey(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}