747 lines
19 KiB
TypeScript
747 lines
19 KiB
TypeScript
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;
|
|
description?: string;
|
|
resourceName?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface WorkflowStaticData {
|
|
seenIds?: string[];
|
|
reservationHashes?: Record<string, string>;
|
|
isFirstPoll?: boolean;
|
|
lastPollTime?: string;
|
|
}
|
|
|
|
/**
|
|
* Authentifizierung bei LibreBooking
|
|
*/
|
|
async function authenticateTrigger(
|
|
pollFunctions: IPollFunctions,
|
|
baseUrl: string,
|
|
username: string,
|
|
password: string,
|
|
): Promise<LibreBookingSession> {
|
|
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. Überprüfen Sie Ihre Zugangsdaten.',
|
|
);
|
|
}
|
|
|
|
return {
|
|
sessionToken: response.sessionToken,
|
|
userId: response.userId,
|
|
sessionExpires: response.sessionExpires,
|
|
};
|
|
} catch (error: any) {
|
|
throw new NodeApiError(pollFunctions.getNode(), error, {
|
|
message: 'Authentifizierung fehlgeschlagen',
|
|
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abmeldung
|
|
*/
|
|
async function signOutTrigger(
|
|
pollFunctions: IPollFunctions,
|
|
baseUrl: string,
|
|
session: LibreBookingSession,
|
|
): Promise<void> {
|
|
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<ReservationData[]> {
|
|
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;
|
|
|
|
try {
|
|
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 || [];
|
|
} catch (error: any) {
|
|
throw new NodeApiError(pollFunctions.getNode(), error, {
|
|
message: 'Fehler beim Abrufen der Reservierungen',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
|
*/
|
|
async function getReservationDetails(
|
|
pollFunctions: IPollFunctions,
|
|
baseUrl: string,
|
|
session: LibreBookingSession,
|
|
referenceNumber: string,
|
|
): Promise<any> {
|
|
try {
|
|
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;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zeitfenster berechnen für "Get All" Mode
|
|
*/
|
|
function getTimeWindowForGetAll(
|
|
customStartDate?: string,
|
|
customEndDate?: string,
|
|
defaultDays: number = 14
|
|
): { start: string; end: string } {
|
|
if (customStartDate && customEndDate) {
|
|
return {
|
|
start: new Date(customStartDate).toISOString(),
|
|
end: new Date(customEndDate).toISOString(),
|
|
};
|
|
}
|
|
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setDate(endDate.getDate() + defaultDays);
|
|
|
|
return {
|
|
start: now.toISOString(),
|
|
end: endDate.toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Zeitfenster berechnen für Polling
|
|
*/
|
|
function getTimeWindowForPolling(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(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hash für Reservierung generieren (für Änderungserkennung)
|
|
*/
|
|
function getReservationHash(reservation: ReservationData): string {
|
|
const relevantData = {
|
|
referenceNumber: reservation.referenceNumber,
|
|
startDate: reservation.startDate,
|
|
endDate: reservation.endDate,
|
|
title: reservation.title || '',
|
|
description: reservation.description || '',
|
|
resourceId: reservation.resourceId,
|
|
resourceName: reservation.resourceName || '',
|
|
userId: reservation.userId,
|
|
requiresApproval: reservation.requiresApproval,
|
|
participants: reservation.participants || [],
|
|
invitees: reservation.invitees || [],
|
|
};
|
|
return JSON.stringify(relevantData);
|
|
}
|
|
|
|
/**
|
|
* LibreBooking Trigger Node
|
|
*
|
|
* Drei Modi:
|
|
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
|
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
|
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
|
*/
|
|
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["triggerMode"]}}',
|
|
defaults: {
|
|
name: 'LibreBooking Trigger',
|
|
},
|
|
inputs: [],
|
|
outputs: ['main'],
|
|
credentials: [
|
|
{
|
|
name: 'libreBookingApi',
|
|
required: true,
|
|
},
|
|
],
|
|
polling: true,
|
|
properties: [
|
|
// =====================================================
|
|
// TRIGGER MODE SELECTOR
|
|
// =====================================================
|
|
{
|
|
displayName: 'Trigger-Modus',
|
|
name: 'triggerMode',
|
|
type: 'options',
|
|
options: [
|
|
{
|
|
name: 'Alle Abrufen (Einmalig)',
|
|
value: 'getAll',
|
|
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
|
},
|
|
{
|
|
name: 'Neue Reservierungen (Polling)',
|
|
value: 'newReservations',
|
|
description: 'Nur bei neuen Reservierungen triggern',
|
|
},
|
|
{
|
|
name: 'Geänderte Reservierungen (Polling)',
|
|
value: 'updatedReservations',
|
|
description: 'Nur bei geänderten Reservierungen triggern',
|
|
},
|
|
],
|
|
default: 'getAll',
|
|
description: 'Wählen Sie den Trigger-Modus',
|
|
},
|
|
|
|
// =====================================================
|
|
// GET ALL MODE - DATE RANGE
|
|
// =====================================================
|
|
{
|
|
displayName: 'Startdatum',
|
|
name: 'startDate',
|
|
type: 'dateTime',
|
|
displayOptions: {
|
|
show: {
|
|
triggerMode: ['getAll'],
|
|
},
|
|
},
|
|
default: '',
|
|
description: 'Startdatum für den Abruf (leer = heute)',
|
|
},
|
|
{
|
|
displayName: 'Enddatum',
|
|
name: 'endDate',
|
|
type: 'dateTime',
|
|
displayOptions: {
|
|
show: {
|
|
triggerMode: ['getAll'],
|
|
},
|
|
},
|
|
default: '',
|
|
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
|
|
},
|
|
|
|
// =====================================================
|
|
// POLLING MODE - TIME WINDOW
|
|
// =====================================================
|
|
{
|
|
displayName: 'Zeitfenster',
|
|
name: 'timeWindow',
|
|
type: 'options',
|
|
displayOptions: {
|
|
show: {
|
|
triggerMode: ['newReservations', 'updatedReservations'],
|
|
},
|
|
},
|
|
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',
|
|
description: 'Zeitfenster für die Überwachung von Reservierungen',
|
|
},
|
|
{
|
|
displayName: 'Hinweis',
|
|
name: 'pollingNotice',
|
|
type: 'notice',
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
triggerMode: ['newReservations', 'updatedReservations'],
|
|
},
|
|
},
|
|
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
|
|
},
|
|
|
|
// =====================================================
|
|
// FILTERS (ALL MODES)
|
|
// =====================================================
|
|
{
|
|
displayName: 'Filter',
|
|
name: 'filters',
|
|
type: 'collection',
|
|
placeholder: 'Filter hinzufügen',
|
|
default: {},
|
|
options: [
|
|
{
|
|
displayName: 'Ressourcen-ID',
|
|
name: 'resourceId',
|
|
type: 'number',
|
|
default: '',
|
|
description: 'Nur Reservierungen für diese Ressource',
|
|
},
|
|
{
|
|
displayName: 'Zeitplan-ID',
|
|
name: 'scheduleId',
|
|
type: 'number',
|
|
default: '',
|
|
description: 'Nur Reservierungen für diesen Zeitplan',
|
|
},
|
|
{
|
|
displayName: 'Benutzer-ID',
|
|
name: 'userId',
|
|
type: 'number',
|
|
default: '',
|
|
description: 'Nur Reservierungen für diesen Benutzer',
|
|
},
|
|
],
|
|
},
|
|
|
|
// =====================================================
|
|
// OPTIONS (ALL MODES)
|
|
// =====================================================
|
|
{
|
|
displayName: 'Optionen',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Option hinzufügen',
|
|
default: {},
|
|
options: [
|
|
{
|
|
displayName: 'Detaillierte Daten Abrufen',
|
|
name: 'fetchDetails',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
|
|
},
|
|
{
|
|
displayName: 'Debug-Modus',
|
|
name: 'debugMode',
|
|
type: 'boolean',
|
|
default: false,
|
|
description: 'Gibt zusätzliche Debug-Informationen aus',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
|
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 triggerMode = this.getNodeParameter('triggerMode') as string;
|
|
const filters = this.getNodeParameter('filters', {}) as any;
|
|
const options = this.getNodeParameter('options', {}) as any;
|
|
|
|
// Debug-Modus
|
|
const debugMode = options.debugMode || false;
|
|
const fetchDetails = options.fetchDetails || false;
|
|
|
|
// Workflow Static Data für State-Management
|
|
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
|
|
|
let session: LibreBookingSession;
|
|
try {
|
|
session = await authenticateTrigger(this, baseUrl, username, password);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const returnData: INodeExecutionData[] = [];
|
|
|
|
// ==========================================
|
|
// MODE: Get All (One-Time / Every Poll)
|
|
// ==========================================
|
|
if (triggerMode === 'getAll') {
|
|
const startDate = this.getNodeParameter('startDate', '') as string;
|
|
const endDate = this.getNodeParameter('endDate', '') as string;
|
|
|
|
const { start, end } = getTimeWindowForGetAll(
|
|
startDate || undefined,
|
|
endDate || undefined,
|
|
14
|
|
);
|
|
|
|
const reservations = await getReservations(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
start,
|
|
end,
|
|
filters,
|
|
);
|
|
|
|
if (debugMode) {
|
|
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`);
|
|
console.log(`[LibreBooking Trigger] Date Range: ${start} to ${end}`);
|
|
}
|
|
|
|
if (reservations.length === 0) {
|
|
if (debugMode) {
|
|
return [[{
|
|
json: {
|
|
_debug: true,
|
|
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
|
_startDate: start,
|
|
_endDate: end,
|
|
_count: 0,
|
|
},
|
|
}]];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Alle Reservierungen zurückgeben
|
|
for (const reservation of reservations) {
|
|
let reservationData = reservation;
|
|
|
|
if (fetchDetails) {
|
|
try {
|
|
const details = await getReservationDetails(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
reservation.referenceNumber,
|
|
);
|
|
if (details) {
|
|
reservationData = details;
|
|
}
|
|
} catch (error) {
|
|
// Fallback auf Basisdaten
|
|
}
|
|
}
|
|
|
|
returnData.push({
|
|
json: {
|
|
...reservationData,
|
|
_eventType: 'getAll',
|
|
_triggeredAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// MODE: New Reservations (Polling)
|
|
// ==========================================
|
|
else if (triggerMode === 'newReservations') {
|
|
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
|
|
|
const reservations = await getReservations(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
start,
|
|
end,
|
|
filters,
|
|
);
|
|
|
|
// Initialisiere seenIds beim ersten Poll
|
|
if (!webhookData.seenIds) {
|
|
webhookData.seenIds = [];
|
|
webhookData.isFirstPoll = true;
|
|
}
|
|
|
|
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
|
|
|
|
if (debugMode) {
|
|
console.log(`[LibreBooking Trigger] New Reservations Mode`);
|
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
|
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`);
|
|
}
|
|
|
|
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
|
if (webhookData.isFirstPoll) {
|
|
webhookData.seenIds = currentIds;
|
|
webhookData.isFirstPoll = false;
|
|
webhookData.lastPollTime = new Date().toISOString();
|
|
|
|
if (debugMode) {
|
|
return [[{
|
|
json: {
|
|
_debug: true,
|
|
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
|
_savedIds: currentIds.length,
|
|
_ids: currentIds,
|
|
_timestamp: webhookData.lastPollTime,
|
|
},
|
|
}]];
|
|
}
|
|
|
|
return null; // Nichts triggern beim ersten Poll
|
|
}
|
|
|
|
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
|
|
const newReservations = reservations.filter((r: ReservationData) =>
|
|
!webhookData.seenIds!.includes(r.referenceNumber)
|
|
);
|
|
|
|
// Update seenIds mit allen aktuellen IDs
|
|
webhookData.seenIds = currentIds;
|
|
webhookData.lastPollTime = new Date().toISOString();
|
|
|
|
if (newReservations.length === 0) {
|
|
if (debugMode) {
|
|
console.log(`[LibreBooking Trigger] No new reservations found`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Neue Reservierungen verarbeiten
|
|
for (const reservation of newReservations) {
|
|
let reservationData = reservation;
|
|
|
|
if (fetchDetails) {
|
|
try {
|
|
const details = await getReservationDetails(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
reservation.referenceNumber,
|
|
);
|
|
if (details) {
|
|
reservationData = details;
|
|
}
|
|
} catch (error) {
|
|
// Fallback auf Basisdaten
|
|
}
|
|
}
|
|
|
|
returnData.push({
|
|
json: {
|
|
...reservationData,
|
|
_eventType: 'new',
|
|
_triggeredAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (debugMode && returnData.length > 0) {
|
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// MODE: Updated Reservations (Polling)
|
|
// ==========================================
|
|
else if (triggerMode === 'updatedReservations') {
|
|
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
|
|
|
const reservations = await getReservations(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
start,
|
|
end,
|
|
filters,
|
|
);
|
|
|
|
// Initialisiere reservationHashes beim ersten Poll
|
|
if (!webhookData.reservationHashes) {
|
|
webhookData.reservationHashes = {};
|
|
webhookData.isFirstPoll = true;
|
|
}
|
|
|
|
if (debugMode) {
|
|
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
|
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`);
|
|
}
|
|
|
|
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
|
|
if (webhookData.isFirstPoll) {
|
|
for (const reservation of reservations) {
|
|
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation);
|
|
}
|
|
webhookData.isFirstPoll = false;
|
|
webhookData.lastPollTime = new Date().toISOString();
|
|
|
|
if (debugMode) {
|
|
return [[{
|
|
json: {
|
|
_debug: true,
|
|
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
|
|
_savedHashes: Object.keys(webhookData.reservationHashes).length,
|
|
_timestamp: webhookData.lastPollTime,
|
|
},
|
|
}]];
|
|
}
|
|
|
|
return null; // Nichts triggern beim ersten Poll
|
|
}
|
|
|
|
// Geänderte Reservierungen finden
|
|
const updatedReservations: ReservationData[] = [];
|
|
const newHashes: Record<string, string> = {};
|
|
|
|
for (const reservation of reservations) {
|
|
const currentHash = getReservationHash(reservation);
|
|
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
|
|
newHashes[reservation.referenceNumber] = currentHash;
|
|
|
|
// Nur als "geändert" markieren, wenn:
|
|
// 1. Wir die Reservierung schon kennen (nicht neu)
|
|
// 2. Der Hash sich geändert hat
|
|
if (oldHash && currentHash !== oldHash) {
|
|
updatedReservations.push(reservation);
|
|
}
|
|
}
|
|
|
|
// Update Hashes mit allen aktuellen Reservierungen
|
|
webhookData.reservationHashes = newHashes;
|
|
webhookData.lastPollTime = new Date().toISOString();
|
|
|
|
if (updatedReservations.length === 0) {
|
|
if (debugMode) {
|
|
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Geänderte Reservierungen verarbeiten
|
|
for (const reservation of updatedReservations) {
|
|
let reservationData = reservation;
|
|
|
|
if (fetchDetails) {
|
|
try {
|
|
const details = await getReservationDetails(
|
|
this,
|
|
baseUrl,
|
|
session,
|
|
reservation.referenceNumber,
|
|
);
|
|
if (details) {
|
|
reservationData = details;
|
|
}
|
|
} catch (error) {
|
|
// Fallback auf Basisdaten
|
|
}
|
|
}
|
|
|
|
returnData.push({
|
|
json: {
|
|
...reservationData,
|
|
_eventType: 'updated',
|
|
_triggeredAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (debugMode && returnData.length > 0) {
|
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
|
|
}
|
|
}
|
|
|
|
if (returnData.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return [returnData];
|
|
|
|
} finally {
|
|
await signOutTrigger(this, baseUrl, session);
|
|
}
|
|
}
|
|
}
|