254 lines
7.1 KiB
TypeScript
254 lines
7.1 KiB
TypeScript
import { createDAVClient } from 'tsdav';
|
|
|
|
export interface CalendarEvent {
|
|
uid: string;
|
|
summary: string;
|
|
description?: string;
|
|
location?: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
status?: string;
|
|
}
|
|
|
|
export class NextcloudCalendarService {
|
|
private client: any;
|
|
private initialized: boolean = false;
|
|
private initPromise: Promise<void> | null = null;
|
|
|
|
async initialize() {
|
|
// Wenn bereits am Initialisieren, warte auf Abschluss
|
|
if (this.initPromise) {
|
|
return this.initPromise;
|
|
}
|
|
|
|
// Wenn bereits initialisiert, fertig
|
|
if (this.initialized && this.client) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Neue Initialisierung starten
|
|
this.initPromise = this._doInitialize();
|
|
|
|
try {
|
|
await this.initPromise;
|
|
} finally {
|
|
this.initPromise = null;
|
|
}
|
|
}
|
|
|
|
private async _doInitialize() {
|
|
const serverUrl = process.env.NEXTCLOUD_URL;
|
|
const username = process.env.NEXTCLOUD_USERNAME;
|
|
const password = process.env.NEXTCLOUD_PASSWORD;
|
|
|
|
console.log('🔍 Nextcloud credentials check:');
|
|
console.log(' URL:', serverUrl);
|
|
console.log(' Username:', username);
|
|
console.log(' Password length:', password?.length, 'chars');
|
|
|
|
if (!serverUrl || !username || !password) {
|
|
throw new Error('Nextcloud credentials not configured');
|
|
}
|
|
|
|
try {
|
|
console.log('⏳ Creating Nextcloud CalDAV client...');
|
|
|
|
this.client = await createDAVClient({
|
|
serverUrl: `${serverUrl}/remote.php/dav`,
|
|
credentials: {
|
|
username,
|
|
password,
|
|
},
|
|
authMethod: 'Basic',
|
|
defaultAccountType: 'caldav',
|
|
});
|
|
|
|
this.initialized = true;
|
|
console.log('✅ Nextcloud CalDAV client initialized successfully');
|
|
} catch (error: any) {
|
|
console.error('❌ Failed to initialize Nextcloud CalDAV client:', error);
|
|
this.initialized = false;
|
|
this.client = null;
|
|
throw new Error(`Nextcloud initialization failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async getCalendars() {
|
|
await this.initialize();
|
|
try {
|
|
const calendars = await this.client.fetchCalendars();
|
|
return calendars;
|
|
} catch (error) {
|
|
console.error('Error fetching calendars:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createEvent(calendarUrl: string, event: CalendarEvent) {
|
|
await this.initialize();
|
|
|
|
const icsContent = this.generateICS(event);
|
|
|
|
try {
|
|
const result = await this.client.createCalendarObject({
|
|
calendar: { url: calendarUrl },
|
|
filename: `${event.uid}.ics`,
|
|
iCalString: icsContent,
|
|
});
|
|
|
|
console.log('✅ Event created in Nextcloud:', event.summary);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error creating event:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateEvent(calendarUrl: string, event: CalendarEvent) {
|
|
await this.initialize();
|
|
|
|
const icsContent = this.generateICS(event);
|
|
|
|
try {
|
|
const result = await this.client.updateCalendarObject({
|
|
calendar: { url: calendarUrl },
|
|
filename: `${event.uid}.ics`,
|
|
iCalString: icsContent,
|
|
});
|
|
|
|
console.log('✅ Event updated in Nextcloud:', event.summary);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error updating event:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async deleteEvent(calendarUrl: string, eventUid: string) {
|
|
await this.initialize();
|
|
|
|
try {
|
|
await this.client.deleteCalendarObject({
|
|
calendar: { url: calendarUrl },
|
|
filename: `${eventUid}.ics`,
|
|
});
|
|
|
|
console.log('✅ Event deleted from Nextcloud:', eventUid);
|
|
} catch (error) {
|
|
console.error('Error deleting event:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async syncBookingToCalendar(booking: any) {
|
|
await this.initialize();
|
|
|
|
const calendars = await this.getCalendars();
|
|
|
|
if (calendars.length === 0) {
|
|
throw new Error('No calendars found in Nextcloud');
|
|
}
|
|
|
|
// Suche nach "Buchungen" Kalender, sonst verwende ersten
|
|
let calendar = calendars.find((cal: any) =>
|
|
cal.displayName?.toLowerCase().includes('veranstaltung')
|
|
);
|
|
|
|
if (!calendar) {
|
|
console.warn('⚠️ Kein "Veranstaltungen"-Kalender gefunden, verwende:', calendars[0].displayName);
|
|
calendar = calendars[0];
|
|
} else {
|
|
console.log('✅ Verwende Kalender:', calendar.displayName);
|
|
}
|
|
|
|
const event: CalendarEvent = {
|
|
uid: `savethemoment-booking-${booking.id}`,
|
|
summary: `${booking.customerName} - ${booking.location?.name || 'Unbekannt'}`,
|
|
description: `
|
|
Buchung #${booking.bookingNumber || booking.id}
|
|
Kunde: ${booking.customerName}
|
|
E-Mail: ${booking.customerEmail}
|
|
Telefon: ${booking.customerPhone || 'N/A'}
|
|
Event-Location: ${booking.eventLocation || booking.eventAddress}
|
|
Status: ${booking.status}
|
|
Fotobox: ${booking.photobox?.model || 'Keine Box'}
|
|
Standort: ${booking.location?.name || 'Unbekannt'}
|
|
Preis: ${booking.calculatedPrice || 0}€
|
|
`.trim(),
|
|
location: `${booking.eventAddress || ''}, ${booking.eventZip || ''} ${booking.eventCity || ''}`.trim(),
|
|
startDate: new Date(booking.eventDate),
|
|
endDate: new Date(new Date(booking.eventDate).getTime() + 4 * 60 * 60 * 1000),
|
|
status: booking.status,
|
|
};
|
|
|
|
try {
|
|
await this.createEvent(calendar.url, event);
|
|
console.log('✅ Event in Nextcloud erstellt:', event.summary);
|
|
return event;
|
|
} catch (error: any) {
|
|
if (error.message?.includes('already exists') || error.response?.status === 412) {
|
|
await this.updateEvent(calendar.url, event);
|
|
console.log('✅ Event in Nextcloud aktualisiert:', event.summary);
|
|
return event;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async removeBookingFromCalendar(bookingId: string) {
|
|
await this.initialize();
|
|
|
|
const calendars = await this.getCalendars();
|
|
|
|
if (calendars.length === 0) {
|
|
throw new Error('No calendars found in Nextcloud');
|
|
}
|
|
|
|
// Suche nach "Buchungen" Kalender, sonst verwende ersten
|
|
let calendar = calendars.find((cal: any) =>
|
|
cal.displayName?.toLowerCase().includes('buchung')
|
|
);
|
|
|
|
if (!calendar) {
|
|
calendar = calendars[0];
|
|
}
|
|
|
|
const eventUid = `savethemoment-booking-${bookingId}`;
|
|
|
|
try {
|
|
await this.deleteEvent(calendar.url, eventUid);
|
|
console.log('✅ Event aus Nextcloud gelöscht:', eventUid);
|
|
} catch (error) {
|
|
console.error('Error removing booking from calendar:', error);
|
|
}
|
|
}
|
|
|
|
private generateICS(event: CalendarEvent): string {
|
|
const formatDate = (date: Date) => {
|
|
return date.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
};
|
|
|
|
const now = new Date();
|
|
const dtstamp = formatDate(now);
|
|
const dtstart = formatDate(event.startDate);
|
|
const dtend = formatDate(event.endDate);
|
|
|
|
return `BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//SaveTheMoment Atlas//DE
|
|
BEGIN:VEVENT
|
|
UID:${event.uid}
|
|
DTSTAMP:${dtstamp}
|
|
DTSTART:${dtstart}
|
|
DTEND:${dtend}
|
|
SUMMARY:${event.summary}
|
|
DESCRIPTION:${event.description || ''}
|
|
LOCATION:${event.location || ''}
|
|
STATUS:${event.status === 'CONFIRMED' ? 'CONFIRMED' : 'TENTATIVE'}
|
|
END:VEVENT
|
|
END:VCALENDAR`;
|
|
}
|
|
}
|
|
|
|
export const nextcloudCalendar = new NextcloudCalendarService();
|