import { INodeType, INodeTypeDescription, IPollFunctions, INodeExecutionData, NodeApiError, NodeOperationError, } from 'n8n-workflow'; interface LibreBookingSession { sessionToken: string; userId: number; sessionExpires: string; } interface ReservationData { referenceNumber: string; startDate: string; endDate: string; title: string; resourceId: number; userId: number; [key: string]: any; } /** * Authentifizierung bei LibreBooking */ async function authenticateTrigger( pollFunctions: IPollFunctions, baseUrl: string, username: string, password: string, ): Promise { try { const response = await pollFunctions.helpers.httpRequest({ method: 'POST', url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, headers: { 'Content-Type': 'application/json' }, body: { username, password }, json: true, }); if (!response.isAuthenticated) { throw new NodeOperationError( pollFunctions.getNode(), 'Authentifizierung fehlgeschlagen', ); } return { sessionToken: response.sessionToken, userId: response.userId, sessionExpires: response.sessionExpires, }; } catch (error: any) { throw new NodeApiError(pollFunctions.getNode(), error, { message: 'Authentifizierung fehlgeschlagen', }); } } /** * Abmeldung */ async function signOutTrigger( pollFunctions: IPollFunctions, baseUrl: string, session: LibreBookingSession, ): Promise { try { await pollFunctions.helpers.httpRequest({ method: 'POST', url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, headers: { 'Content-Type': 'application/json' }, body: { userId: session.userId, sessionToken: session.sessionToken, }, json: true, }); } catch (error) { // Ignoriere SignOut-Fehler } } /** * Reservierungen abrufen */ async function getReservations( pollFunctions: IPollFunctions, baseUrl: string, session: LibreBookingSession, startDateTime: string, endDateTime: string, filters: any, ): Promise { const qs: any = { startDateTime, endDateTime, }; if (filters.resourceId) qs.resourceId = filters.resourceId; if (filters.scheduleId) qs.scheduleId = filters.scheduleId; if (filters.userId) qs.userId = filters.userId; const response = await pollFunctions.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/Web/Services/index.php/Reservations/`, headers: { 'Content-Type': 'application/json', 'X-Booked-SessionToken': session.sessionToken, 'X-Booked-UserId': session.userId.toString(), }, qs, json: true, }); return response.reservations || []; } /** * Detaillierte Reservierungsdaten abrufen */ async function getReservationDetails( pollFunctions: IPollFunctions, baseUrl: string, session: LibreBookingSession, referenceNumber: string, ): Promise { const response = await pollFunctions.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`, headers: { 'Content-Type': 'application/json', 'X-Booked-SessionToken': session.sessionToken, 'X-Booked-UserId': session.userId.toString(), }, json: true, }); return response; } /** * Zeitfenster berechnen */ function getTimeWindow(timeWindow: string): { start: string; end: string } { const now = new Date(); const start = now.toISOString(); let endDate = new Date(now); switch (timeWindow) { case '7days': endDate.setDate(endDate.getDate() + 7); break; case '14days': endDate.setDate(endDate.getDate() + 14); break; case '30days': endDate.setDate(endDate.getDate() + 30); break; case '90days': endDate.setDate(endDate.getDate() + 90); break; default: endDate.setDate(endDate.getDate() + 14); } return { start, end: endDate.toISOString(), }; } /** * Eindeutigen Schlüssel für Reservierung generieren */ function getReservationKey(reservation: ReservationData): string { return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`; } /** * LibreBooking Trigger Node */ export class LibreBookingTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'LibreBooking Trigger', name: 'libreBookingTrigger', icon: 'file:librebooking.svg', group: ['trigger'], version: 1, description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst', subtitle: '={{$parameter["event"]}}', defaults: { name: 'LibreBooking Trigger', }, inputs: [], outputs: ['main'], credentials: [ { name: 'libreBookingApi', required: true, }, ], polling: true, properties: [ { displayName: 'Event', name: 'event', type: 'options', options: [ { name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' }, { name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' }, { name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' }, ], default: 'newReservation', }, { displayName: 'Filter', name: 'filters', type: 'collection', placeholder: 'Filter hinzufügen', default: {}, options: [ { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, ], }, { displayName: 'Zeitfenster', name: 'timeWindow', type: 'options', options: [ { name: 'Nächste 7 Tage', value: '7days' }, { name: 'Nächste 14 Tage', value: '14days' }, { name: 'Nächste 30 Tage', value: '30days' }, { name: 'Nächste 90 Tage', value: '90days' }, ], default: '14days', }, { displayName: 'Optionen', name: 'options', type: 'collection', placeholder: 'Option hinzufügen', default: {}, options: [ { displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false }, ], }, ], }; async poll(this: IPollFunctions): Promise { const credentials = await this.getCredentials('libreBookingApi'); const baseUrl = (credentials.url as string).replace(/\/$/, ''); const username = credentials.username as string; const password = credentials.password as string; const event = this.getNodeParameter('event') as string; const filters = this.getNodeParameter('filters', {}) as any; const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; const options = this.getNodeParameter('options', {}) as any; const workflowStaticData = this.getWorkflowStaticData('node'); const previousReservations = (workflowStaticData.reservations as Record) || {}; let session: LibreBookingSession; try { session = await authenticateTrigger(this, baseUrl, username, password); } catch (error) { throw error; } try { const { start, end } = getTimeWindow(timeWindow); const reservations = await getReservations( this, baseUrl, session, start, end, filters, ); const returnData: INodeExecutionData[] = []; const currentReservations: Record = {}; for (const reservation of reservations) { const refNumber = reservation.referenceNumber; const reservationKey = getReservationKey(reservation); currentReservations[refNumber] = reservationKey; const isNew = !previousReservations[refNumber]; const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey; let shouldTrigger = false; let eventType = ''; if (event === 'newReservation' && isNew) { shouldTrigger = true; eventType = 'new'; } else if (event === 'updatedReservation' && isUpdated) { shouldTrigger = true; eventType = 'updated'; } else if (event === 'allReservations' && (isNew || isUpdated)) { shouldTrigger = true; eventType = isNew ? 'new' : 'updated'; } if (shouldTrigger) { let reservationData = reservation; if (options.fetchDetails) { try { reservationData = await getReservationDetails( this, baseUrl, session, refNumber, ); } catch (error) { reservationData = reservation; } } returnData.push({ json: { ...reservationData, _eventType: eventType, _triggeredAt: new Date().toISOString(), }, }); } } workflowStaticData.reservations = currentReservations; if (returnData.length === 0) { return null; } return [returnData]; } finally { await signOutTrigger(this, baseUrl, session); } } }