mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
323 lines
8.6 KiB
TypeScript
323 lines
8.6 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import {
|
|
CalendarEvent,
|
|
Calendar,
|
|
CreateEventInput,
|
|
UpdateEventInput,
|
|
ParsedEventInput,
|
|
} from './types';
|
|
import { parseGermanDateKeyword, getTodayISO, addDays } from '../shared/utils';
|
|
|
|
/**
|
|
* Calendar API Service
|
|
*
|
|
* Connects to the calendar-backend API for event management.
|
|
* This service is used when the user is logged in and has a valid JWT token.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Get events for a user (requires JWT token)
|
|
* const events = await calendarApiService.getEvents(token, { start: '2024-01-01', end: '2024-01-31' });
|
|
*
|
|
* // Create an event
|
|
* const event = await calendarApiService.createEvent(token, {
|
|
* title: 'Meeting',
|
|
* startTime: new Date('2024-01-15T10:00:00'),
|
|
* endTime: new Date('2024-01-15T11:00:00'),
|
|
* });
|
|
* ```
|
|
*/
|
|
@Injectable()
|
|
export class CalendarApiService {
|
|
private readonly logger = new Logger(CalendarApiService.name);
|
|
private readonly baseUrl: string;
|
|
|
|
constructor(baseUrl = 'http://localhost:3014') {
|
|
this.baseUrl = baseUrl;
|
|
this.logger.log(`Calendar API Service initialized with URL: ${baseUrl}`);
|
|
}
|
|
|
|
// ===== Event Operations =====
|
|
|
|
/**
|
|
* Get events within a date range
|
|
* Note: The calendar backend doesn't support date filtering via query params,
|
|
* so we fetch all events and filter client-side.
|
|
*/
|
|
async getEvents(
|
|
token: string,
|
|
filter?: { start?: string; end?: string; calendarId?: string }
|
|
): Promise<CalendarEvent[]> {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
// Only calendarId is supported as query param
|
|
if (filter?.calendarId) params.append('calendarId', filter.calendarId);
|
|
|
|
const queryString = params.toString();
|
|
const url = queryString
|
|
? `${this.baseUrl}/api/v1/events?${queryString}`
|
|
: `${this.baseUrl}/api/v1/events`;
|
|
|
|
const response = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { events?: unknown[] };
|
|
let events = this.mapApiEvents(data.events || []);
|
|
|
|
// Client-side date filtering
|
|
if (filter?.start || filter?.end) {
|
|
const startDate = filter.start ? new Date(filter.start + 'T00:00:00') : null;
|
|
const endDate = filter.end ? new Date(filter.end + 'T23:59:59') : null;
|
|
|
|
events = events.filter((event) => {
|
|
const eventStart = new Date(event.startTime);
|
|
const eventEnd = new Date(event.endTime);
|
|
|
|
if (startDate && eventEnd < startDate) return false;
|
|
if (endDate && eventStart > endDate) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return events;
|
|
} catch (error) {
|
|
this.logger.error('Failed to get events:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get today's events
|
|
*/
|
|
async getTodayEvents(token: string): Promise<CalendarEvent[]> {
|
|
const today = getTodayISO();
|
|
return this.getEvents(token, { start: today, end: today });
|
|
}
|
|
|
|
/**
|
|
* Get upcoming events (next 7 days)
|
|
*/
|
|
async getUpcomingEvents(token: string, days = 7): Promise<CalendarEvent[]> {
|
|
const today = getTodayISO();
|
|
const end = addDays(new Date(), days).toISOString().split('T')[0];
|
|
return this.getEvents(token, { start: today, end });
|
|
}
|
|
|
|
/**
|
|
* Create a new event
|
|
*/
|
|
async createEvent(token: string, input: CreateEventInput): Promise<CalendarEvent | null> {
|
|
try {
|
|
const body: Record<string, unknown> = {
|
|
title: input.title,
|
|
startTime:
|
|
input.startTime instanceof Date ? input.startTime.toISOString() : input.startTime,
|
|
endTime: input.endTime instanceof Date ? input.endTime.toISOString() : input.endTime,
|
|
isAllDay: input.isAllDay || false,
|
|
};
|
|
|
|
if (input.description) body.description = input.description;
|
|
if (input.location) body.location = input.location;
|
|
if (input.calendarId) body.calendarId = input.calendarId;
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/v1/events`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { event: unknown };
|
|
return this.mapApiEvent(data.event);
|
|
} catch (error) {
|
|
this.logger.error('Failed to create event:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an event
|
|
*/
|
|
async updateEvent(
|
|
token: string,
|
|
eventId: string,
|
|
input: UpdateEventInput
|
|
): Promise<CalendarEvent | null> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/api/v1/events/${eventId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(input),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { event: unknown };
|
|
return this.mapApiEvent(data.event);
|
|
} catch (error) {
|
|
this.logger.error('Failed to update event:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an event
|
|
*/
|
|
async deleteEvent(token: string, eventId: string): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/api/v1/events/${eventId}`, {
|
|
method: 'DELETE',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (error) {
|
|
this.logger.error('Failed to delete event:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ===== Calendar Operations =====
|
|
|
|
/**
|
|
* Get all calendars
|
|
*/
|
|
async getCalendars(token: string): Promise<Calendar[]> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/api/v1/calendars`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API error: ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { calendars?: any[] };
|
|
return (data.calendars || []).map((c: any) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
color: c.color,
|
|
userId: c.userId || '',
|
|
isDefault: c.isDefault || false,
|
|
}));
|
|
} catch (error) {
|
|
this.logger.error('Failed to get calendars:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ===== Parsing =====
|
|
|
|
/**
|
|
* Parse natural language event input
|
|
*/
|
|
parseEventInput(input: string): ParsedEventInput {
|
|
let title = input;
|
|
let startTime: Date | null = null;
|
|
let endTime: Date | null = null;
|
|
let isAllDay = false;
|
|
let location: string | null = null;
|
|
|
|
// Extract date (@heute, @morgen, etc.)
|
|
const dateMatch = title.match(/@(\S+)/);
|
|
if (dateMatch) {
|
|
const dateStr = dateMatch[1].toLowerCase();
|
|
const parsedDate = parseGermanDateKeyword(dateStr);
|
|
|
|
if (parsedDate) {
|
|
// Default to 9:00-10:00 for the parsed date
|
|
startTime = new Date(`${parsedDate}T09:00:00`);
|
|
endTime = new Date(`${parsedDate}T10:00:00`);
|
|
}
|
|
title = title.replace(dateMatch[0], '').trim();
|
|
}
|
|
|
|
// Extract time (um 14 Uhr, 14:00, etc.)
|
|
const timeMatch = title.match(/(?:um\s+)?(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?/i);
|
|
if (timeMatch) {
|
|
const hours = parseInt(timeMatch[1]);
|
|
const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0;
|
|
|
|
if (startTime) {
|
|
startTime.setHours(hours, minutes, 0, 0);
|
|
endTime = new Date(startTime);
|
|
endTime.setHours(hours + 1); // Default 1 hour duration
|
|
} else {
|
|
// If no date specified, assume today
|
|
startTime = new Date();
|
|
startTime.setHours(hours, minutes, 0, 0);
|
|
endTime = new Date(startTime);
|
|
endTime.setHours(hours + 1);
|
|
}
|
|
title = title.replace(timeMatch[0], '').trim();
|
|
}
|
|
|
|
// Extract location (in ...)
|
|
const locationMatch = title.match(/\bin\s+([^,]+)/i);
|
|
if (locationMatch) {
|
|
location = locationMatch[1].trim();
|
|
title = title.replace(locationMatch[0], '').trim();
|
|
}
|
|
|
|
// If no time specified, treat as all-day event
|
|
if (!startTime) {
|
|
startTime = new Date();
|
|
startTime.setHours(0, 0, 0, 0);
|
|
endTime = new Date(startTime);
|
|
endTime.setHours(23, 59, 59, 999);
|
|
isAllDay = true;
|
|
}
|
|
|
|
return {
|
|
title: title.trim(),
|
|
startTime: startTime!,
|
|
endTime: endTime!,
|
|
isAllDay,
|
|
location,
|
|
};
|
|
}
|
|
|
|
// ===== Private Helpers =====
|
|
|
|
/**
|
|
* Map API event format to internal CalendarEvent format
|
|
*/
|
|
private mapApiEvent(apiEvent: any): CalendarEvent {
|
|
return {
|
|
id: apiEvent.id,
|
|
userId: apiEvent.userId || '',
|
|
calendarId: apiEvent.calendarId,
|
|
calendarName: apiEvent.calendar?.name || 'Kalender',
|
|
title: apiEvent.title,
|
|
description: apiEvent.description || null,
|
|
location: apiEvent.location || null,
|
|
startTime: apiEvent.startTime,
|
|
endTime: apiEvent.endTime,
|
|
isAllDay: apiEvent.isAllDay || false,
|
|
createdAt: apiEvent.createdAt,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Map array of API events
|
|
*/
|
|
private mapApiEvents(apiEvents: any[]): CalendarEvent[] {
|
|
return apiEvents.map((e) => this.mapApiEvent(e));
|
|
}
|
|
}
|