Version 1.2.2

This commit is contained in:
Sebastian Zell 2026-01-25 23:57:56 +01:00
parent e59aa0241f
commit ed9b1046b9
10 changed files with 1302 additions and 103 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,43 @@
Alle wichtigen Änderungen werden hier dokumentiert. Alle wichtigen Änderungen werden hier dokumentiert.
## [1.2.2] - 2026-01-25
### Hinzugefügt
- ⭐ **Vordefinierte Zeitraum-Optionen für "Alle Abrufen"**:
- "Diese Woche" - Montag bis Sonntag der aktuellen Woche
- "Nächste 2 Wochen" - Ab heute bis 14 Tage in die Zukunft
- "Dieser Monat" - 1. bis letzter Tag des aktuellen Monats
- "Nächste 2 Monate" - Ab heute bis 2 Monate in die Zukunft
- "Dieses Jahr" - 1. Januar bis 31. Dezember
- "Benutzerdefiniert" - Manuelle Start-/Enddatum-Eingabe
- ⭐ **Zeit-Filter für "Neue" und "Geänderte" Trigger**:
- "Alle (Kein Filter)" - Alle Reservierungen, unabhängig vom Datum
- "Nur Heute" - Nur Reservierungen, die heute stattfinden
- "Nächste 3 Tage" - Reservierungen in den nächsten 3 Tagen
- "Nächste 7 Tage" - Reservierungen in den nächsten 7 Tagen
- Use Case: Agent benachrichtigt nur bei Änderungen an heutigen Terminen
- ⭐ **Erweitertes Zeitfenster für Polling**:
- Neuer Option: "Nächste 180 Tage (6 Monate)" für längere Überwachungszeiträume
- 📋 **test-triggers.ts**: Umfassendes Test-Skript für alle Trigger-Funktionen:
- Date Range Berechnungen
- Time Filter Logik
- Create/Update/Delete Reservierung
- Änderungserkennung mit Hash-Vergleich
### Getestet
- ✅ 18 Tests erfolgreich bestanden
- ✅ Date Range Berechnungen: thisWeek, next2Weeks, thisMonth, next2Months, thisYear
- ✅ Time Filter: today, next3Days, next7Days
- ✅ Create, Update, Delete Reservierung mit echten API-Calls
- ✅ Änderungserkennung funktioniert korrekt
- **Test-URL**: https://librebooking.zell-cloud.de
---
## [1.2.1] - 2026-01-25 ## [1.2.1] - 2026-01-25
### Behoben ### Behoben

212
TRIGGER-GUIDE.md Normal file
View File

@ -0,0 +1,212 @@
# LibreBooking Trigger Node - Anleitung
Dieses Dokument beschreibt die drei Trigger-Modi und deren Konfiguration.
## Übersicht
Der LibreBooking Trigger Node bietet drei Modi:
| Modus | Beschreibung | Use Case |
|-------|-------------|----------|
| **Alle Abrufen** | Alle Reservierungen für einen Zeitraum | Täglicher Report, Dashboard |
| **Neue Reservierungen** | Triggert bei neuen Buchungen | Benachrichtigung, Bestätigung |
| **Geänderte Reservierungen** | Triggert bei Änderungen | Konfliktprüfung, Update-Mail |
---
## 1. Modus: Alle Abrufen (Einmalig)
### Beschreibung
Dieser Modus ruft bei jedem Poll **alle** Reservierungen im angegebenen Zeitraum ab.
### Zeitraum-Optionen
| Option | Beschreibung | Beispiel (25.01.2026) |
|--------|-------------|----------------------|
| **Benutzerdefiniert** | Manuelle Eingabe | Frei wählbar |
| **Diese Woche** | Mo-So der aktuellen Woche | 19.01. - 25.01.2026 |
| **Nächste 2 Wochen** | Ab heute + 14 Tage | 25.01. - 08.02.2026 |
| **Dieser Monat** | 1. bis letzter Tag | 01.01. - 31.01.2026 |
| **Nächste 2 Monate** | Ab heute + 2 Monate | 25.01. - 25.03.2026 |
| **Dieses Jahr** | 1. Jan bis 31. Dez | 01.01. - 31.12.2026 |
### Beispiel-Workflow
```
[LibreBooking Trigger] --> [Format] --> [E-Mail senden]
Alle Abrufen Tabellenformat Täglicher Report
Diese Woche
```
---
## 2. Modus: Neue Reservierungen (Polling)
### Beschreibung
Triggert **nur** wenn eine neue Reservierung erstellt wird.
### Wichtig
- **Erster Poll**: Speichert existierende IDs, triggert NICHT
- **Folgende Polls**: Triggert nur bei wirklich neuen Reservierungen
### Zeit-Filter
Filtert getriggerte Reservierungen nach Startdatum:
| Filter | Beschreibung |
|--------|-------------|
| **Alle (Kein Filter)** | Alle neuen Reservierungen, unabhängig vom Startdatum |
| **Nur Heute** | Nur wenn die Reservierung heute stattfindet |
| **Nächste 3 Tage** | Reservierung startet in den nächsten 3 Tagen |
| **Nächste 7 Tage** | Reservierung startet in den nächsten 7 Tagen |
### Beispiel-Workflow
```
[LibreBooking Trigger] --> [IF] --> [E-Mail]
Neue Reservierungen Prüfe Bestätigung senden
Nächste 3 Tage Ressource
```
### Use Case: Sofortige Buchungsbestätigung
- Trigger-Modus: **Neue Reservierungen**
- Zeit-Filter: **Alle (Kein Filter)**
- Aktion: E-Mail an Benutzer mit Buchungsdetails
---
## 3. Modus: Geänderte Reservierungen (Polling)
### Beschreibung
Triggert **nur** wenn eine bestehende Reservierung geändert wird.
### Änderungserkennung
Folgende Felder werden überwacht:
- `title` (Titel)
- `description` (Beschreibung)
- `startDate` / `endDate` (Zeitraum)
- `resourceId` / `resourceName` (Ressource)
- `userId` (Benutzer)
- `statusId` (Status)
- `participants` / `invitees` (Teilnehmer)
### Zeit-Filter
Gleiche Optionen wie bei "Neue Reservierungen":
| Filter | Beschreibung | Use Case |
|--------|-------------|----------|
| **Nur Heute** | Änderungen an heutigen Terminen | Tagesaktueller Agent |
| **Nächste 3 Tage** | Kurzfristige Änderungen | Dringende Benachrichtigungen |
### Beispiel-Workflow
```
[LibreBooking Trigger] --> [Compare] --> [Slack]
Geänderte Vorher/ Benachrichtigung
Reservierungen Nachher "Termin wurde geändert"
Nur Heute
```
### Use Case: Agent für Tagesänderungen
- Trigger-Modus: **Geänderte Reservierungen**
- Zeit-Filter: **Nur Heute**
- Aktion: Slack/E-Mail wenn sich ein heutiger Termin ändert
---
## Allgemeine Einstellungen
### Filter (für alle Modi)
| Filter | Beschreibung |
|--------|-------------|
| **Ressourcen-ID** | Nur Reservierungen für diese Ressource |
| **Zeitplan-ID** | Nur Reservierungen für diesen Zeitplan |
| **Benutzer-ID** | Nur Reservierungen für diesen Benutzer |
### Optionen (für alle Modi)
| Option | Beschreibung |
|--------|-------------|
| **Detaillierte Daten Abrufen** | Holt vollständige Daten inkl. Custom Attributes |
| **Debug-Modus** | Gibt Debug-Informationen aus (für Entwicklung) |
---
## Polling-Intervall
Das Polling-Intervall wird in den n8n Workflow-Einstellungen konfiguriert:
- **Empfohlen für "Neue" und "Geänderte"**: 1-5 Minuten
- **Empfohlen für "Alle Abrufen"**: 15-60 Minuten (je nach Report-Bedarf)
---
## Fehlerbehebung
### Trigger triggert nicht bei neuen/geänderten Reservierungen
1. **Prüfen**: Ist es der erste Poll? → Triggert absichtlich nicht
2. **Prüfen**: Ist der Zeit-Filter zu restriktiv?
3. **Aktivieren**: Debug-Modus für detaillierte Logs
### Zu viele Events werden getriggert
1. **Verwenden**: Zeit-Filter ("Nur Heute", "Nächste 3 Tage")
2. **Filtern**: Nach Ressourcen-ID, Benutzer-ID
### Performance-Probleme
1. **Reduzieren**: Zeitfenster (z.B. 7 statt 90 Tage)
2. **Deaktivieren**: "Detaillierte Daten Abrufen" wenn nicht benötigt
---
## Praxisbeispiele
### 1. Täglicher Reservierungsreport
```yaml
Modus: Alle Abrufen
Zeitraum: Diese Woche
Poll-Intervall: Täglich um 07:00
Aktion: E-Mail mit Wochenübersicht
```
### 2. Sofortige Buchungsbestätigung
```yaml
Modus: Neue Reservierungen
Zeitfenster: Nächste 90 Tage
Zeit-Filter: Alle
Poll-Intervall: 1 Minute
Aktion: E-Mail an Buchenden
```
### 3. Kurzfristige Änderungsbenachrichtigung
```yaml
Modus: Geänderte Reservierungen
Zeitfenster: Nächste 14 Tage
Zeit-Filter: Nächste 3 Tage
Poll-Intervall: 5 Minuten
Aktion: Slack-Nachricht an Team
```
### 4. Tagesaktueller Terminagent
```yaml
Modus: Geänderte Reservierungen
Zeitfenster: Nächste 7 Tage
Zeit-Filter: Nur Heute
Poll-Intervall: 2 Minuten
Aktion: E-Mail an betroffene Teilnehmer
```
---
## Test-Ergebnisse
Stand: 25.01.2026
| Test | Status |
|------|--------|
| Date Range: thisWeek | ✅ |
| Date Range: next2Weeks | ✅ |
| Date Range: thisMonth | ✅ |
| Date Range: next2Months | ✅ |
| Date Range: thisYear | ✅ |
| Time Filter: today | ✅ |
| Time Filter: next3Days | ✅ |
| Time Filter: next7Days | ✅ |
| Create Reservation Detection | ✅ |
| Update Reservation Detection | ✅ |
| Delete Reservation | ✅ |
| **Gesamt: 18/18 Tests bestanden** | ✅ |

BIN
TRIGGER-GUIDE.pdf Normal file

Binary file not shown.

View File

@ -1 +1 @@
{"version":3,"file":"LibreBookingTrigger.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAGlB,MAAM,cAAc,CAAC;AAyOtB;;;;;;;GAOG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IACpD,WAAW,EAAE,oBAAoB,CA4K/B;IAEI,IAAI,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,GAAG,IAAI,CAAC;CAkUxE"} {"version":3,"file":"LibreBookingTrigger.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAGlB,MAAM,cAAc,CAAC;AA6VtB;;;;;;;GAOG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IACpD,WAAW,EAAE,oBAAoB,CAmQ/B;IAEI,IAAI,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,GAAG,IAAI,CAAC;CA4WxE"}

View File

@ -105,16 +105,77 @@ async function getReservationDetails(pollFunctions, baseUrl, session, referenceN
return null; return null;
} }
} }
/**
* Berechnet Zeitraum basierend auf der gewählten Option
*/
function getDateRange(dateRange) {
const now = new Date();
let startDate;
let endDate;
switch (dateRange) {
case 'thisWeek':
// Montag dieser Woche (ISO: Montag = 1, Sonntag = 0)
startDate = new Date(now);
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
startDate.setDate(now.getDate() + diffToMonday);
startDate.setHours(0, 0, 0, 0);
// Sonntag dieser Woche
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Weeks':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setDate(now.getDate() + 14);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisMonth':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Months':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setMonth(now.getMonth() + 2);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisYear':
startDate = new Date(now.getFullYear(), 0, 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), 11, 31);
endDate.setHours(23, 59, 59, 999);
break;
default: // custom
return { startDate: '', endDate: '' };
}
return {
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
};
}
/** /**
* Zeitfenster berechnen für "Get All" Mode * Zeitfenster berechnen für "Get All" Mode
*/ */
function getTimeWindowForGetAll(customStartDate, customEndDate, defaultDays = 14) { function getTimeWindowForGetAll(dateRange, customStartDate, customEndDate, defaultDays = 14) {
// Wenn ein vordefinierter Zeitraum gewählt wurde
if (dateRange && dateRange !== 'custom') {
const { startDate, endDate } = getDateRange(dateRange);
return { start: startDate, end: endDate };
}
// Custom Modus mit manuellen Daten
if (customStartDate && customEndDate) { if (customStartDate && customEndDate) {
return { return {
start: new Date(customStartDate).toISOString(), start: new Date(customStartDate).toISOString(),
end: new Date(customEndDate).toISOString(), end: new Date(customEndDate).toISOString(),
}; };
} }
// Fallback: Ab heute für defaultDays
const now = new Date(); const now = new Date();
const endDate = new Date(now); const endDate = new Date(now);
endDate.setDate(endDate.getDate() + defaultDays); endDate.setDate(endDate.getDate() + defaultDays);
@ -143,6 +204,9 @@ function getTimeWindowForPolling(timeWindow) {
case '90days': case '90days':
endDate.setDate(endDate.getDate() + 90); endDate.setDate(endDate.getDate() + 90);
break; break;
case '180days':
endDate.setDate(endDate.getDate() + 180);
break;
default: default:
endDate.setDate(endDate.getDate() + 14); endDate.setDate(endDate.getDate() + 14);
} }
@ -151,14 +215,46 @@ function getTimeWindowForPolling(timeWindow) {
end: endDate.toISOString(), end: endDate.toISOString(),
}; };
} }
/**
* Filter Reservierungen nach Zeitpunkt
*/
function filterByTime(reservations, timeFilter) {
if (timeFilter === 'all') {
return reservations;
}
const now = new Date();
now.setHours(0, 0, 0, 0);
return reservations.filter((reservation) => {
// Verwende startDate oder startDateTime
const dateStr = reservation.startDateTime || reservation.startDate;
if (!dateStr)
return true;
const startDate = new Date(dateStr);
startDate.setHours(0, 0, 0, 0);
if (timeFilter === 'today') {
return startDate.getTime() === now.getTime();
}
if (timeFilter === 'next3Days') {
const threeDaysFromNow = new Date(now);
threeDaysFromNow.setDate(now.getDate() + 3);
return startDate >= now && startDate <= threeDaysFromNow;
}
if (timeFilter === 'next7Days') {
const sevenDaysFromNow = new Date(now);
sevenDaysFromNow.setDate(now.getDate() + 7);
return startDate >= now && startDate <= sevenDaysFromNow;
}
return true;
});
}
/** /**
* Hash für Reservierung generieren (für Änderungserkennung) * Hash für Reservierung generieren (für Änderungserkennung)
*/ */
function getReservationHash(reservation) { function getReservationHash(reservation) {
const relevantData = { const relevantData = {
referenceNumber: reservation.referenceNumber, referenceNumber: reservation.referenceNumber,
startDate: reservation.startDate, startDate: reservation.startDate || reservation.startDateTime,
endDate: reservation.endDate, endDate: reservation.endDate || reservation.endDateTime,
title: reservation.title || '', title: reservation.title || '',
description: reservation.description || '', description: reservation.description || '',
resourceId: reservation.resourceId, resourceId: reservation.resourceId,
@ -167,6 +263,7 @@ function getReservationHash(reservation) {
requiresApproval: reservation.requiresApproval, requiresApproval: reservation.requiresApproval,
participants: reservation.participants || [], participants: reservation.participants || [],
invitees: reservation.invitees || [], invitees: reservation.invitees || [],
statusId: reservation.statusId,
}; };
return JSON.stringify(relevantData); return JSON.stringify(relevantData);
} }
@ -229,8 +326,52 @@ class LibreBookingTrigger {
description: 'Wählen Sie den Trigger-Modus', description: 'Wählen Sie den Trigger-Modus',
}, },
// ===================================================== // =====================================================
// GET ALL MODE - DATE RANGE // GET ALL MODE - DATE RANGE SELECTOR
// ===================================================== // =====================================================
{
displayName: 'Zeitraum',
name: 'dateRange',
type: 'options',
displayOptions: {
show: {
triggerMode: ['getAll'],
},
},
options: [
{
name: 'Benutzerdefiniert',
value: 'custom',
description: 'Start- und Enddatum manuell angeben',
},
{
name: 'Diese Woche',
value: 'thisWeek',
description: 'Von Montag bis Sonntag der aktuellen Woche',
},
{
name: 'Nächste 2 Wochen',
value: 'next2Weeks',
description: 'Ab heute bis 14 Tage in die Zukunft',
},
{
name: 'Dieser Monat',
value: 'thisMonth',
description: 'Vom 1. bis zum letzten Tag des aktuellen Monats',
},
{
name: 'Nächste 2 Monate',
value: 'next2Months',
description: 'Ab heute bis 2 Monate in die Zukunft',
},
{
name: 'Dieses Jahr',
value: 'thisYear',
description: 'Vom 1. Januar bis 31. Dezember des aktuellen Jahres',
},
],
default: 'custom',
description: 'Vordefinierter Zeitraum für den Abruf',
},
{ {
displayName: 'Startdatum', displayName: 'Startdatum',
name: 'startDate', name: 'startDate',
@ -238,6 +379,7 @@ class LibreBookingTrigger {
displayOptions: { displayOptions: {
show: { show: {
triggerMode: ['getAll'], triggerMode: ['getAll'],
dateRange: ['custom'],
}, },
}, },
default: '', default: '',
@ -250,6 +392,7 @@ class LibreBookingTrigger {
displayOptions: { displayOptions: {
show: { show: {
triggerMode: ['getAll'], triggerMode: ['getAll'],
dateRange: ['custom'],
}, },
}, },
default: '', default: '',
@ -272,10 +415,48 @@ class LibreBookingTrigger {
{ name: 'Nächste 14 Tage', value: '14days' }, { name: 'Nächste 14 Tage', value: '14days' },
{ name: 'Nächste 30 Tage', value: '30days' }, { name: 'Nächste 30 Tage', value: '30days' },
{ name: 'Nächste 90 Tage', value: '90days' }, { name: 'Nächste 90 Tage', value: '90days' },
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
], ],
default: '14days', default: '14days',
description: 'Zeitfenster für die Überwachung von Reservierungen', description: 'Zeitfenster für die Überwachung von Reservierungen',
}, },
// =====================================================
// TIME FILTER FOR NEW/UPDATED MODES
// =====================================================
{
displayName: 'Zeit-Filter',
name: 'timeFilter',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{
name: 'Alle (Kein Filter)',
value: 'all',
description: 'Alle neuen/geänderten Reservierungen, unabhängig vom Datum',
},
{
name: 'Nur Heute',
value: 'today',
description: 'Nur Reservierungen, die heute stattfinden',
},
{
name: 'Nächste 3 Tage',
value: 'next3Days',
description: 'Nur Reservierungen, die in den nächsten 3 Tagen stattfinden',
},
{
name: 'Nächste 7 Tage',
value: 'next7Days',
description: 'Nur Reservierungen, die in den nächsten 7 Tagen stattfinden',
},
],
default: 'all',
description: 'Filtert Reservierungen nach ihrem Startdatum',
},
{ {
displayName: 'Hinweis', displayName: 'Hinweis',
name: 'pollingNotice', name: 'pollingNotice',
@ -376,25 +557,32 @@ class LibreBookingTrigger {
// MODE: Get All (One-Time / Every Poll) // MODE: Get All (One-Time / Every Poll)
// ========================================== // ==========================================
if (triggerMode === 'getAll') { if (triggerMode === 'getAll') {
const dateRange = this.getNodeParameter('dateRange', 'custom');
const startDate = this.getNodeParameter('startDate', ''); const startDate = this.getNodeParameter('startDate', '');
const endDate = this.getNodeParameter('endDate', ''); const endDate = this.getNodeParameter('endDate', '');
const { start, end } = getTimeWindowForGetAll(startDate || undefined, endDate || undefined, 14); const { start, end } = getTimeWindowForGetAll(dateRange, startDate || undefined, endDate || undefined, 14);
const reservations = await getReservations(this, baseUrl, session, start, end, filters); const reservations = await getReservations(this, baseUrl, session, start, end, filters);
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`); console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`);
console.log(`[LibreBooking Trigger] Date Range: ${start} to ${end}`); console.log(`[LibreBooking Trigger] Date Range: ${dateRange}`);
console.log(`[LibreBooking Trigger] Period: ${start} to ${end}`);
} }
if (reservations.length === 0) { if (reservations.length === 0) {
if (debugMode) { if (debugMode) {
return [[{ return [
[
{
json: { json: {
_debug: true, _debug: true,
_message: 'Keine Reservierungen im Zeitraum gefunden', _message: 'Keine Reservierungen im Zeitraum gefunden',
_dateRange: dateRange,
_startDate: start, _startDate: start,
_endDate: end, _endDate: end,
_count: 0, _count: 0,
}, },
}]]; },
],
];
} }
return null; return null;
} }
@ -416,6 +604,7 @@ class LibreBookingTrigger {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'getAll', _eventType: 'getAll',
_dateRange: dateRange,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });
@ -426,6 +615,7 @@ class LibreBookingTrigger {
// ========================================== // ==========================================
else if (triggerMode === 'newReservations') { else if (triggerMode === 'newReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days'); const timeWindow = this.getNodeParameter('timeWindow', '14days');
const timeFilter = this.getNodeParameter('timeFilter', 'all');
const { start, end } = getTimeWindowForPolling(timeWindow); const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters); const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere seenIds beim ersten Poll // Initialisiere seenIds beim ersten Poll
@ -437,6 +627,7 @@ class LibreBookingTrigger {
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] New Reservations Mode`); console.log(`[LibreBooking Trigger] New Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`); console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`); console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`);
} }
// Beim ersten Poll: Nur IDs speichern, NICHT triggern // Beim ersten Poll: Nur IDs speichern, NICHT triggern
@ -445,7 +636,9 @@ class LibreBookingTrigger {
webhookData.isFirstPoll = false; webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) { if (debugMode) {
return [[{ return [
[
{
json: { json: {
_debug: true, _debug: true,
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert', _message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
@ -453,19 +646,29 @@ class LibreBookingTrigger {
_ids: currentIds, _ids: currentIds,
_timestamp: webhookData.lastPollTime, _timestamp: webhookData.lastPollTime,
}, },
}]]; },
],
];
} }
return null; // Nichts triggern beim ersten Poll return null; // Nichts triggern beim ersten Poll
} }
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben) // Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
const newReservations = reservations.filter((r) => !webhookData.seenIds.includes(r.referenceNumber)); let newReservations = reservations.filter((r) => !webhookData.seenIds.includes(r.referenceNumber));
// Update seenIds mit allen aktuellen IDs // Update seenIds mit allen aktuellen IDs
webhookData.seenIds = currentIds; webhookData.seenIds = currentIds;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(`[LibreBooking Trigger] Found ${newReservations.length} new reservations before filter`);
}
if (newReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
newReservations = filterByTime(newReservations, timeFilter);
if (debugMode) {
console.log(`[LibreBooking Trigger] ${newReservations.length} reservations after time filter (${timeFilter})`);
}
if (newReservations.length === 0) { if (newReservations.length === 0) {
if (debugMode) {
console.log(`[LibreBooking Trigger] No new reservations found`);
}
return null; return null;
} }
// Neue Reservierungen verarbeiten // Neue Reservierungen verarbeiten
@ -486,6 +689,7 @@ class LibreBookingTrigger {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'new', _eventType: 'new',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });
@ -499,6 +703,7 @@ class LibreBookingTrigger {
// ========================================== // ==========================================
else if (triggerMode === 'updatedReservations') { else if (triggerMode === 'updatedReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days'); const timeWindow = this.getNodeParameter('timeWindow', '14days');
const timeFilter = this.getNodeParameter('timeFilter', 'all');
const { start, end } = getTimeWindowForPolling(timeWindow); const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters); const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere reservationHashes beim ersten Poll // Initialisiere reservationHashes beim ersten Poll
@ -509,29 +714,35 @@ class LibreBookingTrigger {
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] Updated Reservations Mode`); console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`); console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`); console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`);
} }
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern // Beim ersten Poll: Nur Hashes speichern, NICHT triggern
if (webhookData.isFirstPoll) { if (webhookData.isFirstPoll) {
for (const reservation of reservations) { for (const reservation of reservations) {
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation); webhookData.reservationHashes[reservation.referenceNumber] =
getReservationHash(reservation);
} }
webhookData.isFirstPoll = false; webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) { if (debugMode) {
return [[{ return [
[
{
json: { json: {
_debug: true, _debug: true,
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert', _message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
_savedHashes: Object.keys(webhookData.reservationHashes).length, _savedHashes: Object.keys(webhookData.reservationHashes).length,
_timestamp: webhookData.lastPollTime, _timestamp: webhookData.lastPollTime,
}, },
}]]; },
],
];
} }
return null; // Nichts triggern beim ersten Poll return null; // Nichts triggern beim ersten Poll
} }
// Geänderte Reservierungen finden // Geänderte Reservierungen finden
const updatedReservations = []; let updatedReservations = [];
const newHashes = {}; const newHashes = {};
for (const reservation of reservations) { for (const reservation of reservations) {
const currentHash = getReservationHash(reservation); const currentHash = getReservationHash(reservation);
@ -547,10 +758,18 @@ class LibreBookingTrigger {
// Update Hashes mit allen aktuellen Reservierungen // Update Hashes mit allen aktuellen Reservierungen
webhookData.reservationHashes = newHashes; webhookData.reservationHashes = newHashes;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(`[LibreBooking Trigger] Found ${updatedReservations.length} updated reservations before filter`);
}
if (updatedReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
updatedReservations = filterByTime(updatedReservations, timeFilter);
if (debugMode) {
console.log(`[LibreBooking Trigger] ${updatedReservations.length} reservations after time filter (${timeFilter})`);
}
if (updatedReservations.length === 0) { if (updatedReservations.length === 0) {
if (debugMode) {
console.log(`[LibreBooking Trigger] No updated reservations found`);
}
return null; return null;
} }
// Geänderte Reservierungen verarbeiten // Geänderte Reservierungen verarbeiten
@ -571,6 +790,7 @@ class LibreBookingTrigger {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'updated', _eventType: 'updated',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,8 @@ interface ReservationData {
userId: number; userId: number;
description?: string; description?: string;
resourceName?: string; resourceName?: string;
startDateTime?: string;
endDateTime?: string;
[key: string]: any; [key: string]: any;
} }
@ -162,25 +164,96 @@ async function getReservationDetails(
} }
} }
/**
* Berechnet Zeitraum basierend auf der gewählten Option
*/
function getDateRange(dateRange: string): { startDate: string; endDate: string } {
const now = new Date();
let startDate: Date;
let endDate: Date;
switch (dateRange) {
case 'thisWeek':
// Montag dieser Woche (ISO: Montag = 1, Sonntag = 0)
startDate = new Date(now);
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
startDate.setDate(now.getDate() + diffToMonday);
startDate.setHours(0, 0, 0, 0);
// Sonntag dieser Woche
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Weeks':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setDate(now.getDate() + 14);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisMonth':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Months':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setMonth(now.getMonth() + 2);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisYear':
startDate = new Date(now.getFullYear(), 0, 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), 11, 31);
endDate.setHours(23, 59, 59, 999);
break;
default: // custom
return { startDate: '', endDate: '' };
}
return {
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
};
}
/** /**
* Zeitfenster berechnen für "Get All" Mode * Zeitfenster berechnen für "Get All" Mode
*/ */
function getTimeWindowForGetAll( function getTimeWindowForGetAll(
dateRange: string,
customStartDate?: string, customStartDate?: string,
customEndDate?: string, customEndDate?: string,
defaultDays: number = 14 defaultDays: number = 14,
): { start: string; end: string } { ): { start: string; end: string } {
// Wenn ein vordefinierter Zeitraum gewählt wurde
if (dateRange && dateRange !== 'custom') {
const { startDate, endDate } = getDateRange(dateRange);
return { start: startDate, end: endDate };
}
// Custom Modus mit manuellen Daten
if (customStartDate && customEndDate) { if (customStartDate && customEndDate) {
return { return {
start: new Date(customStartDate).toISOString(), start: new Date(customStartDate).toISOString(),
end: new Date(customEndDate).toISOString(), end: new Date(customEndDate).toISOString(),
}; };
} }
// Fallback: Ab heute für defaultDays
const now = new Date(); const now = new Date();
const endDate = new Date(now); const endDate = new Date(now);
endDate.setDate(endDate.getDate() + defaultDays); endDate.setDate(endDate.getDate() + defaultDays);
return { return {
start: now.toISOString(), start: now.toISOString(),
end: endDate.toISOString(), end: endDate.toISOString(),
@ -193,7 +266,7 @@ function getTimeWindowForGetAll(
function getTimeWindowForPolling(timeWindow: string): { start: string; end: string } { function getTimeWindowForPolling(timeWindow: string): { start: string; end: string } {
const now = new Date(); const now = new Date();
const start = now.toISOString(); const start = now.toISOString();
let endDate = new Date(now); let endDate = new Date(now);
switch (timeWindow) { switch (timeWindow) {
case '7days': case '7days':
@ -208,6 +281,9 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
case '90days': case '90days':
endDate.setDate(endDate.getDate() + 90); endDate.setDate(endDate.getDate() + 90);
break; break;
case '180days':
endDate.setDate(endDate.getDate() + 180);
break;
default: default:
endDate.setDate(endDate.getDate() + 14); endDate.setDate(endDate.getDate() + 14);
} }
@ -218,14 +294,53 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
}; };
} }
/**
* Filter Reservierungen nach Zeitpunkt
*/
function filterByTime(reservations: ReservationData[], timeFilter: string): ReservationData[] {
if (timeFilter === 'all') {
return reservations;
}
const now = new Date();
now.setHours(0, 0, 0, 0);
return reservations.filter((reservation) => {
// Verwende startDate oder startDateTime
const dateStr = reservation.startDateTime || reservation.startDate;
if (!dateStr) return true;
const startDate = new Date(dateStr);
startDate.setHours(0, 0, 0, 0);
if (timeFilter === 'today') {
return startDate.getTime() === now.getTime();
}
if (timeFilter === 'next3Days') {
const threeDaysFromNow = new Date(now);
threeDaysFromNow.setDate(now.getDate() + 3);
return startDate >= now && startDate <= threeDaysFromNow;
}
if (timeFilter === 'next7Days') {
const sevenDaysFromNow = new Date(now);
sevenDaysFromNow.setDate(now.getDate() + 7);
return startDate >= now && startDate <= sevenDaysFromNow;
}
return true;
});
}
/** /**
* Hash für Reservierung generieren (für Änderungserkennung) * Hash für Reservierung generieren (für Änderungserkennung)
*/ */
function getReservationHash(reservation: ReservationData): string { function getReservationHash(reservation: ReservationData): string {
const relevantData = { const relevantData = {
referenceNumber: reservation.referenceNumber, referenceNumber: reservation.referenceNumber,
startDate: reservation.startDate, startDate: reservation.startDate || reservation.startDateTime,
endDate: reservation.endDate, endDate: reservation.endDate || reservation.endDateTime,
title: reservation.title || '', title: reservation.title || '',
description: reservation.description || '', description: reservation.description || '',
resourceId: reservation.resourceId, resourceId: reservation.resourceId,
@ -234,13 +349,14 @@ function getReservationHash(reservation: ReservationData): string {
requiresApproval: reservation.requiresApproval, requiresApproval: reservation.requiresApproval,
participants: reservation.participants || [], participants: reservation.participants || [],
invitees: reservation.invitees || [], invitees: reservation.invitees || [],
statusId: reservation.statusId,
}; };
return JSON.stringify(relevantData); return JSON.stringify(relevantData);
} }
/** /**
* LibreBooking Trigger Node * LibreBooking Trigger Node
* *
* Drei Modi: * Drei Modi:
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen * 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern * 2. New Reservations (Poll): Bei neuen Reservierungen triggern
@ -297,8 +413,52 @@ export class LibreBookingTrigger implements INodeType {
}, },
// ===================================================== // =====================================================
// GET ALL MODE - DATE RANGE // GET ALL MODE - DATE RANGE SELECTOR
// ===================================================== // =====================================================
{
displayName: 'Zeitraum',
name: 'dateRange',
type: 'options',
displayOptions: {
show: {
triggerMode: ['getAll'],
},
},
options: [
{
name: 'Benutzerdefiniert',
value: 'custom',
description: 'Start- und Enddatum manuell angeben',
},
{
name: 'Diese Woche',
value: 'thisWeek',
description: 'Von Montag bis Sonntag der aktuellen Woche',
},
{
name: 'Nächste 2 Wochen',
value: 'next2Weeks',
description: 'Ab heute bis 14 Tage in die Zukunft',
},
{
name: 'Dieser Monat',
value: 'thisMonth',
description: 'Vom 1. bis zum letzten Tag des aktuellen Monats',
},
{
name: 'Nächste 2 Monate',
value: 'next2Months',
description: 'Ab heute bis 2 Monate in die Zukunft',
},
{
name: 'Dieses Jahr',
value: 'thisYear',
description: 'Vom 1. Januar bis 31. Dezember des aktuellen Jahres',
},
],
default: 'custom',
description: 'Vordefinierter Zeitraum für den Abruf',
},
{ {
displayName: 'Startdatum', displayName: 'Startdatum',
name: 'startDate', name: 'startDate',
@ -306,6 +466,7 @@ export class LibreBookingTrigger implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
triggerMode: ['getAll'], triggerMode: ['getAll'],
dateRange: ['custom'],
}, },
}, },
default: '', default: '',
@ -318,6 +479,7 @@ export class LibreBookingTrigger implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
triggerMode: ['getAll'], triggerMode: ['getAll'],
dateRange: ['custom'],
}, },
}, },
default: '', default: '',
@ -341,10 +503,49 @@ export class LibreBookingTrigger implements INodeType {
{ name: 'Nächste 14 Tage', value: '14days' }, { name: 'Nächste 14 Tage', value: '14days' },
{ name: 'Nächste 30 Tage', value: '30days' }, { name: 'Nächste 30 Tage', value: '30days' },
{ name: 'Nächste 90 Tage', value: '90days' }, { name: 'Nächste 90 Tage', value: '90days' },
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
], ],
default: '14days', default: '14days',
description: 'Zeitfenster für die Überwachung von Reservierungen', description: 'Zeitfenster für die Überwachung von Reservierungen',
}, },
// =====================================================
// TIME FILTER FOR NEW/UPDATED MODES
// =====================================================
{
displayName: 'Zeit-Filter',
name: 'timeFilter',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{
name: 'Alle (Kein Filter)',
value: 'all',
description: 'Alle neuen/geänderten Reservierungen, unabhängig vom Datum',
},
{
name: 'Nur Heute',
value: 'today',
description: 'Nur Reservierungen, die heute stattfinden',
},
{
name: 'Nächste 3 Tage',
value: 'next3Days',
description: 'Nur Reservierungen, die in den nächsten 3 Tagen stattfinden',
},
{
name: 'Nächste 7 Tage',
value: 'next7Days',
description: 'Nur Reservierungen, die in den nächsten 7 Tagen stattfinden',
},
],
default: 'all',
description: 'Filtert Reservierungen nach ihrem Startdatum',
},
{ {
displayName: 'Hinweis', displayName: 'Hinweis',
name: 'pollingNotice', name: 'pollingNotice',
@ -355,7 +556,8 @@ export class LibreBookingTrigger implements INodeType {
triggerMode: ['newReservations', 'updatedReservations'], triggerMode: ['newReservations', 'updatedReservations'],
}, },
}, },
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.', description:
'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
}, },
// ===================================================== // =====================================================
@ -407,7 +609,8 @@ export class LibreBookingTrigger implements INodeType {
name: 'fetchDetails', name: 'fetchDetails',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)', description:
'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
}, },
{ {
displayName: 'Debug-Modus', displayName: 'Debug-Modus',
@ -452,40 +655,43 @@ export class LibreBookingTrigger implements INodeType {
// MODE: Get All (One-Time / Every Poll) // MODE: Get All (One-Time / Every Poll)
// ========================================== // ==========================================
if (triggerMode === 'getAll') { if (triggerMode === 'getAll') {
const dateRange = this.getNodeParameter('dateRange', 'custom') as string;
const startDate = this.getNodeParameter('startDate', '') as string; const startDate = this.getNodeParameter('startDate', '') as string;
const endDate = this.getNodeParameter('endDate', '') as string; const endDate = this.getNodeParameter('endDate', '') as string;
const { start, end } = getTimeWindowForGetAll( const { start, end } = getTimeWindowForGetAll(
dateRange,
startDate || undefined, startDate || undefined,
endDate || undefined, endDate || undefined,
14 14,
); );
const reservations = await getReservations( const reservations = await getReservations(this, baseUrl, session, start, end, filters);
this,
baseUrl,
session,
start,
end,
filters,
);
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`); console.log(
console.log(`[LibreBooking Trigger] Date Range: ${start} to ${end}`); `[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`,
);
console.log(`[LibreBooking Trigger] Date Range: ${dateRange}`);
console.log(`[LibreBooking Trigger] Period: ${start} to ${end}`);
} }
if (reservations.length === 0) { if (reservations.length === 0) {
if (debugMode) { if (debugMode) {
return [[{ return [
json: { [
_debug: true, {
_message: 'Keine Reservierungen im Zeitraum gefunden', json: {
_startDate: start, _debug: true,
_endDate: end, _message: 'Keine Reservierungen im Zeitraum gefunden',
_count: 0, _dateRange: dateRange,
}, _startDate: start,
}]]; _endDate: end,
_count: 0,
},
},
],
];
} }
return null; return null;
} }
@ -514,6 +720,7 @@ export class LibreBookingTrigger implements INodeType {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'getAll', _eventType: 'getAll',
_dateRange: dateRange,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });
@ -525,16 +732,10 @@ export class LibreBookingTrigger implements INodeType {
// ========================================== // ==========================================
else if (triggerMode === 'newReservations') { else if (triggerMode === 'newReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
const { start, end } = getTimeWindowForPolling(timeWindow); const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations( const reservations = await getReservations(this, baseUrl, session, start, end, filters);
this,
baseUrl,
session,
start,
end,
filters,
);
// Initialisiere seenIds beim ersten Poll // Initialisiere seenIds beim ersten Poll
if (!webhookData.seenIds) { if (!webhookData.seenIds) {
@ -547,7 +748,10 @@ export class LibreBookingTrigger implements INodeType {
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] New Reservations Mode`); console.log(`[LibreBooking Trigger] New Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`); console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`); console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(
`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`,
);
} }
// Beim ersten Poll: Nur IDs speichern, NICHT triggern // Beim ersten Poll: Nur IDs speichern, NICHT triggern
@ -557,33 +761,53 @@ export class LibreBookingTrigger implements INodeType {
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) { if (debugMode) {
return [[{ return [
json: { [
_debug: true, {
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert', json: {
_savedIds: currentIds.length, _debug: true,
_ids: currentIds, _message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
_timestamp: webhookData.lastPollTime, _savedIds: currentIds.length,
}, _ids: currentIds,
}]]; _timestamp: webhookData.lastPollTime,
},
},
],
];
} }
return null; // Nichts triggern beim ersten Poll return null; // Nichts triggern beim ersten Poll
} }
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben) // Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
const newReservations = reservations.filter((r: ReservationData) => let newReservations = reservations.filter(
!webhookData.seenIds!.includes(r.referenceNumber) (r: ReservationData) => !webhookData.seenIds!.includes(r.referenceNumber),
); );
// Update seenIds mit allen aktuellen IDs // Update seenIds mit allen aktuellen IDs
webhookData.seenIds = currentIds; webhookData.seenIds = currentIds;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(
`[LibreBooking Trigger] Found ${newReservations.length} new reservations before filter`,
);
}
if (newReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
newReservations = filterByTime(newReservations, timeFilter);
if (debugMode) {
console.log(
`[LibreBooking Trigger] ${newReservations.length} reservations after time filter (${timeFilter})`,
);
}
if (newReservations.length === 0) { if (newReservations.length === 0) {
if (debugMode) {
console.log(`[LibreBooking Trigger] No new reservations found`);
}
return null; return null;
} }
@ -611,6 +835,7 @@ export class LibreBookingTrigger implements INodeType {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'new', _eventType: 'new',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });
@ -626,16 +851,10 @@ export class LibreBookingTrigger implements INodeType {
// ========================================== // ==========================================
else if (triggerMode === 'updatedReservations') { else if (triggerMode === 'updatedReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
const { start, end } = getTimeWindowForPolling(timeWindow); const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations( const reservations = await getReservations(this, baseUrl, session, start, end, filters);
this,
baseUrl,
session,
start,
end,
filters,
);
// Initialisiere reservationHashes beim ersten Poll // Initialisiere reservationHashes beim ersten Poll
if (!webhookData.reservationHashes) { if (!webhookData.reservationHashes) {
@ -646,33 +865,41 @@ export class LibreBookingTrigger implements INodeType {
if (debugMode) { if (debugMode) {
console.log(`[LibreBooking Trigger] Updated Reservations Mode`); console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`); console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`); console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(
`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`,
);
} }
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern // Beim ersten Poll: Nur Hashes speichern, NICHT triggern
if (webhookData.isFirstPoll) { if (webhookData.isFirstPoll) {
for (const reservation of reservations) { for (const reservation of reservations) {
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation); webhookData.reservationHashes[reservation.referenceNumber] =
getReservationHash(reservation);
} }
webhookData.isFirstPoll = false; webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) { if (debugMode) {
return [[{ return [
json: { [
_debug: true, {
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert', json: {
_savedHashes: Object.keys(webhookData.reservationHashes).length, _debug: true,
_timestamp: webhookData.lastPollTime, _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 return null; // Nichts triggern beim ersten Poll
} }
// Geänderte Reservierungen finden // Geänderte Reservierungen finden
const updatedReservations: ReservationData[] = []; let updatedReservations: ReservationData[] = [];
const newHashes: Record<string, string> = {}; const newHashes: Record<string, string> = {};
for (const reservation of reservations) { for (const reservation of reservations) {
@ -692,10 +919,26 @@ export class LibreBookingTrigger implements INodeType {
webhookData.reservationHashes = newHashes; webhookData.reservationHashes = newHashes;
webhookData.lastPollTime = new Date().toISOString(); webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(
`[LibreBooking Trigger] Found ${updatedReservations.length} updated reservations before filter`,
);
}
if (updatedReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
updatedReservations = filterByTime(updatedReservations, timeFilter);
if (debugMode) {
console.log(
`[LibreBooking Trigger] ${updatedReservations.length} reservations after time filter (${timeFilter})`,
);
}
if (updatedReservations.length === 0) { if (updatedReservations.length === 0) {
if (debugMode) {
console.log(`[LibreBooking Trigger] No updated reservations found`);
}
return null; return null;
} }
@ -723,13 +966,16 @@ export class LibreBookingTrigger implements INodeType {
json: { json: {
...reservationData, ...reservationData,
_eventType: 'updated', _eventType: 'updated',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(), _triggeredAt: new Date().toISOString(),
}, },
}); });
} }
if (debugMode && returnData.length > 0) { if (debugMode && returnData.length > 0) {
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`); console.log(
`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`,
);
} }
} }
@ -738,7 +984,6 @@ export class LibreBookingTrigger implements INodeType {
} }
return [returnData]; return [returnData];
} finally { } finally {
await signOutTrigger(this, baseUrl, session); await signOutTrigger(this, baseUrl, session);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-librebooking", "name": "n8n-nodes-librebooking",
"version": "1.2.1", "version": "1.2.2",
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung", "description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
"keywords": [ "keywords": [
"n8n-community-node-package", "n8n-community-node-package",

485
test/test-triggers.ts Normal file
View File

@ -0,0 +1,485 @@
/**
* Comprehensive Trigger Test Script for LibreBooking
*
* Tests:
* 1. Authentication
* 2. Get All Reservations
* 3. Create New Reservation (triggers "New" mode)
* 4. Update Reservation (triggers "Updated" mode)
* 5. Delete Reservation
* 6. Date Range calculations
* 7. Time Filter logic
*/
import * as https from 'https';
const BASE_URL = 'https://librebooking.zell-cloud.de';
const USERNAME = 'sebastian.zell@zell-aufmass.de';
const PASSWORD = 'wanUQ4uVqU6lfP';
// Test date: 7.2.2026
const TEST_DATE = '2026-02-07';
interface TestResult {
name: string;
success: boolean;
message: string;
data?: any;
}
interface Session {
sessionToken: string;
userId: number;
}
const results: TestResult[] = [];
function log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
function logSuccess(name: string, message: string, data?: any) {
log(`${name}: ${message}`);
results.push({ name, success: true, message, data });
}
function logError(name: string, message: string, data?: any) {
log(`${name}: ${message}`);
results.push({ name, success: false, message, data });
}
async function makeRequest(
method: string,
path: string,
body?: any,
headers?: Record<string, string>
): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(`${BASE_URL}${path}`);
const options: https.RequestOptions = {
hostname: url.hostname,
port: 443,
path: url.pathname + url.search,
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json);
} catch (e) {
resolve(data);
}
});
});
req.on('error', reject);
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
// ===== AUTHENTICATION =====
async function authenticate(): Promise<Session> {
const response = await makeRequest(
'POST',
'/Web/Services/index.php/Authentication/Authenticate',
{ username: USERNAME, password: PASSWORD }
);
if (!response.isAuthenticated) {
throw new Error('Authentication failed');
}
return {
sessionToken: response.sessionToken,
userId: response.userId,
};
}
async function signOut(session: Session): Promise<void> {
await makeRequest(
'POST',
'/Web/Services/index.php/Authentication/SignOut',
{ userId: session.userId, sessionToken: session.sessionToken }
);
}
// ===== API REQUESTS =====
async function getReservations(
session: Session,
startDate: string,
endDate: string
): Promise<any[]> {
const response = await makeRequest(
'GET',
`/Web/Services/index.php/Reservations/?startDateTime=${startDate}T00:00:00&endDateTime=${endDate}T23:59:59`,
undefined,
{
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
}
);
return response.reservations || [];
}
async function createReservation(
session: Session,
data: {
title: string;
description: string;
resourceId: number;
startDateTime: string;
endDateTime: string;
}
): Promise<any> {
const response = await makeRequest(
'POST',
'/Web/Services/index.php/Reservations/',
{
...data,
userId: session.userId,
termsAccepted: true,
allowParticipation: false,
},
{
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
}
);
return response;
}
async function updateReservation(
session: Session,
referenceNumber: string,
data: {
title: string;
description: string;
resourceId: number;
startDateTime: string;
endDateTime: string;
}
): Promise<any> {
const response = await makeRequest(
'POST',
`/Web/Services/index.php/Reservations/${referenceNumber}`,
{
...data,
userId: session.userId,
termsAccepted: true,
allowParticipation: false,
},
{
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
}
);
return response;
}
async function deleteReservation(session: Session, referenceNumber: string): Promise<any> {
const response = await makeRequest(
'DELETE',
`/Web/Services/index.php/Reservations/${referenceNumber}`,
undefined,
{
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
}
);
return response;
}
async function getResources(session: Session): Promise<any[]> {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Resources/',
undefined,
{
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
}
);
return response.resources || [];
}
// ===== DATE RANGE TESTS =====
function testDateRange() {
log('\n📅 Testing Date Range Calculations...\n');
const now = new Date('2026-01-25'); // Simulate current date
// Test thisWeek
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
const monday = new Date(now);
monday.setDate(now.getDate() + diffToMonday);
monday.setHours(0, 0, 0, 0);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
logSuccess(
'thisWeek',
`Monday: ${monday.toISOString().split('T')[0]}, Sunday: ${sunday.toISOString().split('T')[0]}`
);
// Test next2Weeks
const next2Weeks = new Date(now);
next2Weeks.setDate(now.getDate() + 14);
logSuccess(
'next2Weeks',
`From: ${now.toISOString().split('T')[0]}, To: ${next2Weeks.toISOString().split('T')[0]}`
);
// Test thisMonth
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
logSuccess(
'thisMonth',
`From: ${monthStart.toISOString().split('T')[0]}, To: ${monthEnd.toISOString().split('T')[0]}`
);
// Test next2Months
const next2Months = new Date(now);
next2Months.setMonth(now.getMonth() + 2);
logSuccess(
'next2Months',
`From: ${now.toISOString().split('T')[0]}, To: ${next2Months.toISOString().split('T')[0]}`
);
// Test thisYear
const yearStart = new Date(now.getFullYear(), 0, 1);
const yearEnd = new Date(now.getFullYear(), 11, 31);
logSuccess(
'thisYear',
`From: ${yearStart.toISOString().split('T')[0]}, To: ${yearEnd.toISOString().split('T')[0]}`
);
}
// ===== TIME FILTER TESTS =====
function testTimeFilter() {
log('\n⏰ Testing Time Filter Logic...\n');
const today = new Date('2026-02-07');
today.setHours(0, 0, 0, 0);
const reservations = [
{ title: 'Today', startDateTime: '2026-02-07T10:00:00' },
{ title: 'Tomorrow', startDateTime: '2026-02-08T10:00:00' },
{ title: 'In 3 days', startDateTime: '2026-02-10T10:00:00' },
{ title: 'In 7 days', startDateTime: '2026-02-14T10:00:00' },
{ title: 'In 10 days', startDateTime: '2026-02-17T10:00:00' },
];
// Filter: today
const todayOnly = reservations.filter((r) => {
const startDate = new Date(r.startDateTime);
startDate.setHours(0, 0, 0, 0);
return startDate.getTime() === today.getTime();
});
logSuccess('timeFilter=today', `Found ${todayOnly.length} reservation(s): ${todayOnly.map(r => r.title).join(', ')}`);
// Filter: next3Days
const threeDays = new Date(today);
threeDays.setDate(today.getDate() + 3);
const next3Days = reservations.filter((r) => {
const startDate = new Date(r.startDateTime);
startDate.setHours(0, 0, 0, 0);
return startDate >= today && startDate <= threeDays;
});
logSuccess('timeFilter=next3Days', `Found ${next3Days.length} reservation(s): ${next3Days.map(r => r.title).join(', ')}`);
// Filter: next7Days
const sevenDays = new Date(today);
sevenDays.setDate(today.getDate() + 7);
const next7Days = reservations.filter((r) => {
const startDate = new Date(r.startDateTime);
startDate.setHours(0, 0, 0, 0);
return startDate >= today && startDate <= sevenDays;
});
logSuccess('timeFilter=next7Days', `Found ${next7Days.length} reservation(s): ${next7Days.map(r => r.title).join(', ')}`);
}
// ===== MAIN TEST =====
async function runTests() {
console.log('🧪 LibreBooking Trigger Test Suite\n');
console.log('═'.repeat(60) + '\n');
// Test date calculations first (no API needed)
testDateRange();
testTimeFilter();
log('\n🔌 Testing API Operations...\n');
let session: Session | null = null;
let createdRefNumber: string | null = null;
try {
// 1. Authentication
log('1⃣ Authenticating...');
session = await authenticate();
logSuccess('Authentication', `Session: ${session.sessionToken.substring(0, 20)}...`);
// 2. Get Resources (to find available resource)
log('\n2⃣ Getting Resources...');
const resources = await getResources(session);
if (resources.length === 0) {
logError('GetResources', 'No resources found');
return;
}
logSuccess('GetResources', `Found ${resources.length} resources`);
const testResourceId = resources[0].resourceId;
log(` Using resource: ${resources[0].name} (ID: ${testResourceId})`);
// 3. Get Initial Reservations
log('\n3⃣ Getting initial reservations...');
const initialReservations = await getReservations(session, TEST_DATE, '2026-02-14');
logSuccess('GetReservations', `Found ${initialReservations.length} reservations`);
// Store initial IDs (simulating first poll)
const seenIds = initialReservations.map((r: any) => r.referenceNumber);
log(` Stored ${seenIds.length} IDs (simulating first poll)`);
// 4. Create New Reservation
log('\n4⃣ Creating new reservation...');
const createResult = await createReservation(session, {
title: 'TEST: Neue Reservierung für Trigger-Test',
description: 'Diese Reservierung wird erstellt, um den "Neue Objekte" Trigger zu testen',
resourceId: testResourceId,
startDateTime: `${TEST_DATE}T14:00:00`,
endDateTime: `${TEST_DATE}T15:00:00`,
});
if (createResult.referenceNumber) {
createdRefNumber = createResult.referenceNumber;
logSuccess('CreateReservation', `Created: ${createdRefNumber}`);
} else {
logError('CreateReservation', `Failed: ${JSON.stringify(createResult)}`);
}
// 5. Get Reservations Again (simulate second poll)
log('\n5⃣ Simulating second poll...');
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 seconds
const afterCreateReservations = await getReservations(session, TEST_DATE, '2026-02-14');
const currentIds = afterCreateReservations.map((r: any) => r.referenceNumber);
const newIds = currentIds.filter((id: string) => !seenIds.includes(id));
logSuccess(
'NewReservationDetection',
`Found ${newIds.length} new reservation(s): ${newIds.join(', ')}`
);
// 6. Update Reservation
if (createdRefNumber) {
log('\n6⃣ Updating reservation...');
// Store hash before update
const reservationBefore = afterCreateReservations.find(
(r: any) => r.referenceNumber === createdRefNumber
);
const hashBefore = JSON.stringify({
title: reservationBefore?.title,
description: reservationBefore?.description,
});
// Update - must include all required fields
const updateResult = await updateReservation(session, createdRefNumber, {
title: 'TEST: GEÄNDERTE Reservierung',
description: 'Diese Reservierung wurde geändert - Update-Trigger sollte feuern',
resourceId: testResourceId,
startDateTime: `${TEST_DATE}T14:00:00`,
endDateTime: `${TEST_DATE}T15:00:00`,
});
if (updateResult.referenceNumber) {
logSuccess('UpdateReservation', `Updated: ${createdRefNumber}`);
} else {
logError('UpdateReservation', `Failed: ${JSON.stringify(updateResult)}`);
}
// 7. Get Reservations Again (check for changes)
log('\n7⃣ Checking for changes...');
await new Promise((resolve) => setTimeout(resolve, 2000));
const afterUpdateReservations = await getReservations(session, TEST_DATE, '2026-02-14');
const reservationAfter = afterUpdateReservations.find(
(r: any) => r.referenceNumber === createdRefNumber
);
const hashAfter = JSON.stringify({
title: reservationAfter?.title,
description: reservationAfter?.description,
});
if (hashBefore !== hashAfter) {
logSuccess(
'ChangeDetection',
`Change detected! Title: "${reservationAfter?.title}"`
);
} else {
logError('ChangeDetection', 'No change detected (hash unchanged)');
}
// 8. Delete Reservation
log('\n8⃣ Deleting reservation...');
await deleteReservation(session, createdRefNumber);
logSuccess('DeleteReservation', `Deleted: ${createdRefNumber}`);
// Verify deletion
await new Promise((resolve) => setTimeout(resolve, 1000));
const afterDeleteReservations = await getReservations(session, TEST_DATE, '2026-02-14');
const stillExists = afterDeleteReservations.some(
(r: any) => r.referenceNumber === createdRefNumber
);
if (!stillExists) {
logSuccess('DeletionVerified', 'Reservation successfully deleted');
} else {
logError('DeletionVerified', 'Reservation still exists!');
}
}
// 9. Sign Out
log('\n9⃣ Signing out...');
await signOut(session);
logSuccess('SignOut', 'Successfully signed out');
} catch (error: any) {
logError('TestSuite', `Error: ${error.message}`);
}
// Summary
console.log('\n' + '═'.repeat(60));
console.log('📊 TEST SUMMARY\n');
const passed = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
console.log(` ✅ Passed: ${passed}`);
console.log(` ❌ Failed: ${failed}`);
console.log(` 📝 Total: ${results.length}`);
if (failed > 0) {
console.log('\n Failed Tests:');
results
.filter((r) => !r.success)
.forEach((r) => console.log(` - ${r.name}: ${r.message}`));
}
console.log('\n' + '═'.repeat(60));
// Exit with appropriate code
process.exit(failed > 0 ? 1 : 0);
}
runTests();