Version 1.2.2
This commit is contained in:
parent
e59aa0241f
commit
ed9b1046b9
File diff suppressed because one or more lines are too long
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -2,6 +2,43 @@
|
|||
|
||||
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
|
||||
|
||||
### Behoben
|
||||
|
|
|
|||
|
|
@ -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** | ✅ |
|
||||
Binary file not shown.
|
|
@ -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"}
|
||||
|
|
@ -105,16 +105,77 @@ async function getReservationDetails(pollFunctions, baseUrl, session, referenceN
|
|||
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
|
||||
*/
|
||||
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) {
|
||||
return {
|
||||
start: new Date(customStartDate).toISOString(),
|
||||
end: new Date(customEndDate).toISOString(),
|
||||
};
|
||||
}
|
||||
// Fallback: Ab heute für defaultDays
|
||||
const now = new Date();
|
||||
const endDate = new Date(now);
|
||||
endDate.setDate(endDate.getDate() + defaultDays);
|
||||
|
|
@ -143,6 +204,9 @@ function getTimeWindowForPolling(timeWindow) {
|
|||
case '90days':
|
||||
endDate.setDate(endDate.getDate() + 90);
|
||||
break;
|
||||
case '180days':
|
||||
endDate.setDate(endDate.getDate() + 180);
|
||||
break;
|
||||
default:
|
||||
endDate.setDate(endDate.getDate() + 14);
|
||||
}
|
||||
|
|
@ -151,14 +215,46 @@ function getTimeWindowForPolling(timeWindow) {
|
|||
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)
|
||||
*/
|
||||
function getReservationHash(reservation) {
|
||||
const relevantData = {
|
||||
referenceNumber: reservation.referenceNumber,
|
||||
startDate: reservation.startDate,
|
||||
endDate: reservation.endDate,
|
||||
startDate: reservation.startDate || reservation.startDateTime,
|
||||
endDate: reservation.endDate || reservation.endDateTime,
|
||||
title: reservation.title || '',
|
||||
description: reservation.description || '',
|
||||
resourceId: reservation.resourceId,
|
||||
|
|
@ -167,6 +263,7 @@ function getReservationHash(reservation) {
|
|||
requiresApproval: reservation.requiresApproval,
|
||||
participants: reservation.participants || [],
|
||||
invitees: reservation.invitees || [],
|
||||
statusId: reservation.statusId,
|
||||
};
|
||||
return JSON.stringify(relevantData);
|
||||
}
|
||||
|
|
@ -229,8 +326,52 @@ class LibreBookingTrigger {
|
|||
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',
|
||||
name: 'startDate',
|
||||
|
|
@ -238,6 +379,7 @@ class LibreBookingTrigger {
|
|||
displayOptions: {
|
||||
show: {
|
||||
triggerMode: ['getAll'],
|
||||
dateRange: ['custom'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
|
@ -250,6 +392,7 @@ class LibreBookingTrigger {
|
|||
displayOptions: {
|
||||
show: {
|
||||
triggerMode: ['getAll'],
|
||||
dateRange: ['custom'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
|
@ -272,10 +415,48 @@ class LibreBookingTrigger {
|
|||
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||
{ name: 'Nächste 30 Tage', value: '30days' },
|
||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
|
||||
],
|
||||
default: '14days',
|
||||
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',
|
||||
name: 'pollingNotice',
|
||||
|
|
@ -376,25 +557,32 @@ class LibreBookingTrigger {
|
|||
// MODE: Get All (One-Time / Every Poll)
|
||||
// ==========================================
|
||||
if (triggerMode === 'getAll') {
|
||||
const dateRange = this.getNodeParameter('dateRange', 'custom');
|
||||
const startDate = this.getNodeParameter('startDate', '');
|
||||
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);
|
||||
if (debugMode) {
|
||||
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 (debugMode) {
|
||||
return [[{
|
||||
return [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
_debug: true,
|
||||
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
||||
_dateRange: dateRange,
|
||||
_startDate: start,
|
||||
_endDate: end,
|
||||
_count: 0,
|
||||
},
|
||||
}]];
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -416,6 +604,7 @@ class LibreBookingTrigger {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'getAll',
|
||||
_dateRange: dateRange,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
@ -426,6 +615,7 @@ class LibreBookingTrigger {
|
|||
// ==========================================
|
||||
else if (triggerMode === 'newReservations') {
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days');
|
||||
const timeFilter = this.getNodeParameter('timeFilter', 'all');
|
||||
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||
// Initialisiere seenIds beim ersten Poll
|
||||
|
|
@ -437,6 +627,7 @@ class LibreBookingTrigger {
|
|||
if (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] New Reservations Mode`);
|
||||
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}`);
|
||||
}
|
||||
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
||||
|
|
@ -445,7 +636,9 @@ class LibreBookingTrigger {
|
|||
webhookData.isFirstPoll = false;
|
||||
webhookData.lastPollTime = new Date().toISOString();
|
||||
if (debugMode) {
|
||||
return [[{
|
||||
return [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
_debug: true,
|
||||
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
||||
|
|
@ -453,19 +646,29 @@ class LibreBookingTrigger {
|
|||
_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) => !webhookData.seenIds.includes(r.referenceNumber));
|
||||
let newReservations = reservations.filter((r) => !webhookData.seenIds.includes(r.referenceNumber));
|
||||
// Update seenIds mit allen aktuellen IDs
|
||||
webhookData.seenIds = currentIds;
|
||||
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 (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No new reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Neue Reservierungen verarbeiten
|
||||
|
|
@ -486,6 +689,7 @@ class LibreBookingTrigger {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'new',
|
||||
_timeFilter: timeFilter,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
@ -499,6 +703,7 @@ class LibreBookingTrigger {
|
|||
// ==========================================
|
||||
else if (triggerMode === 'updatedReservations') {
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days');
|
||||
const timeFilter = this.getNodeParameter('timeFilter', 'all');
|
||||
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||
// Initialisiere reservationHashes beim ersten Poll
|
||||
|
|
@ -509,29 +714,35 @@ class LibreBookingTrigger {
|
|||
if (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
||||
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}`);
|
||||
}
|
||||
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
|
||||
if (webhookData.isFirstPoll) {
|
||||
for (const reservation of reservations) {
|
||||
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation);
|
||||
webhookData.reservationHashes[reservation.referenceNumber] =
|
||||
getReservationHash(reservation);
|
||||
}
|
||||
webhookData.isFirstPoll = false;
|
||||
webhookData.lastPollTime = new Date().toISOString();
|
||||
if (debugMode) {
|
||||
return [[{
|
||||
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 = [];
|
||||
let updatedReservations = [];
|
||||
const newHashes = {};
|
||||
for (const reservation of reservations) {
|
||||
const currentHash = getReservationHash(reservation);
|
||||
|
|
@ -547,10 +758,18 @@ class LibreBookingTrigger {
|
|||
// Update Hashes mit allen aktuellen Reservierungen
|
||||
webhookData.reservationHashes = newHashes;
|
||||
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 (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Geänderte Reservierungen verarbeiten
|
||||
|
|
@ -571,6 +790,7 @@ class LibreBookingTrigger {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'updated',
|
||||
_timeFilter: timeFilter,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,8 @@ interface ReservationData {
|
|||
userId: number;
|
||||
description?: string;
|
||||
resourceName?: string;
|
||||
startDateTime?: string;
|
||||
endDateTime?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
@ -162,14 +164,84 @@ 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
|
||||
*/
|
||||
function getTimeWindowForGetAll(
|
||||
dateRange: string,
|
||||
customStartDate?: string,
|
||||
customEndDate?: string,
|
||||
defaultDays: number = 14
|
||||
defaultDays: number = 14,
|
||||
): { 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) {
|
||||
return {
|
||||
start: new Date(customStartDate).toISOString(),
|
||||
|
|
@ -177,6 +249,7 @@ function getTimeWindowForGetAll(
|
|||
};
|
||||
}
|
||||
|
||||
// Fallback: Ab heute für defaultDays
|
||||
const now = new Date();
|
||||
const endDate = new Date(now);
|
||||
endDate.setDate(endDate.getDate() + defaultDays);
|
||||
|
|
@ -208,6 +281,9 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
|
|||
case '90days':
|
||||
endDate.setDate(endDate.getDate() + 90);
|
||||
break;
|
||||
case '180days':
|
||||
endDate.setDate(endDate.getDate() + 180);
|
||||
break;
|
||||
default:
|
||||
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)
|
||||
*/
|
||||
function getReservationHash(reservation: ReservationData): string {
|
||||
const relevantData = {
|
||||
referenceNumber: reservation.referenceNumber,
|
||||
startDate: reservation.startDate,
|
||||
endDate: reservation.endDate,
|
||||
startDate: reservation.startDate || reservation.startDateTime,
|
||||
endDate: reservation.endDate || reservation.endDateTime,
|
||||
title: reservation.title || '',
|
||||
description: reservation.description || '',
|
||||
resourceId: reservation.resourceId,
|
||||
|
|
@ -234,6 +349,7 @@ function getReservationHash(reservation: ReservationData): string {
|
|||
requiresApproval: reservation.requiresApproval,
|
||||
participants: reservation.participants || [],
|
||||
invitees: reservation.invitees || [],
|
||||
statusId: reservation.statusId,
|
||||
};
|
||||
return JSON.stringify(relevantData);
|
||||
}
|
||||
|
|
@ -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',
|
||||
name: 'startDate',
|
||||
|
|
@ -306,6 +466,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
displayOptions: {
|
||||
show: {
|
||||
triggerMode: ['getAll'],
|
||||
dateRange: ['custom'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
|
@ -318,6 +479,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
displayOptions: {
|
||||
show: {
|
||||
triggerMode: ['getAll'],
|
||||
dateRange: ['custom'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
|
@ -341,10 +503,49 @@ export class LibreBookingTrigger implements INodeType {
|
|||
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||
{ name: 'Nächste 30 Tage', value: '30days' },
|
||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
|
||||
],
|
||||
default: '14days',
|
||||
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',
|
||||
name: 'pollingNotice',
|
||||
|
|
@ -355,7 +556,8 @@ export class LibreBookingTrigger implements INodeType {
|
|||
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',
|
||||
type: 'boolean',
|
||||
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',
|
||||
|
|
@ -452,40 +655,43 @@ export class LibreBookingTrigger implements INodeType {
|
|||
// MODE: Get All (One-Time / Every Poll)
|
||||
// ==========================================
|
||||
if (triggerMode === 'getAll') {
|
||||
const dateRange = this.getNodeParameter('dateRange', 'custom') as string;
|
||||
const startDate = this.getNodeParameter('startDate', '') as string;
|
||||
const endDate = this.getNodeParameter('endDate', '') as string;
|
||||
|
||||
const { start, end } = getTimeWindowForGetAll(
|
||||
dateRange,
|
||||
startDate || undefined,
|
||||
endDate || undefined,
|
||||
14
|
||||
14,
|
||||
);
|
||||
|
||||
const reservations = await getReservations(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
start,
|
||||
end,
|
||||
filters,
|
||||
);
|
||||
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}`);
|
||||
console.log(
|
||||
`[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 (debugMode) {
|
||||
return [[{
|
||||
json: {
|
||||
_debug: true,
|
||||
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
||||
_startDate: start,
|
||||
_endDate: end,
|
||||
_count: 0,
|
||||
},
|
||||
}]];
|
||||
return [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
_debug: true,
|
||||
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
||||
_dateRange: dateRange,
|
||||
_startDate: start,
|
||||
_endDate: end,
|
||||
_count: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -514,6 +720,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'getAll',
|
||||
_dateRange: dateRange,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
@ -525,16 +732,10 @@ export class LibreBookingTrigger implements INodeType {
|
|||
// ==========================================
|
||||
else if (triggerMode === 'newReservations') {
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
|
||||
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
|
||||
if (!webhookData.seenIds) {
|
||||
|
|
@ -547,7 +748,10 @@ export class LibreBookingTrigger implements INodeType {
|
|||
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}`);
|
||||
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
|
||||
|
|
@ -557,33 +761,53 @@ export class LibreBookingTrigger implements INodeType {
|
|||
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 [
|
||||
[
|
||||
{
|
||||
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)
|
||||
let 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 (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 (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No new reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -611,6 +835,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'new',
|
||||
_timeFilter: timeFilter,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
@ -626,16 +851,10 @@ export class LibreBookingTrigger implements INodeType {
|
|||
// ==========================================
|
||||
else if (triggerMode === 'updatedReservations') {
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
|
||||
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
|
||||
if (!webhookData.reservationHashes) {
|
||||
|
|
@ -646,33 +865,41 @@ export class LibreBookingTrigger implements INodeType {
|
|||
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}`);
|
||||
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
|
||||
if (webhookData.isFirstPoll) {
|
||||
for (const reservation of reservations) {
|
||||
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation);
|
||||
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 [
|
||||
[
|
||||
{
|
||||
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[] = [];
|
||||
let updatedReservations: ReservationData[] = [];
|
||||
const newHashes: Record<string, string> = {};
|
||||
|
||||
for (const reservation of reservations) {
|
||||
|
|
@ -692,10 +919,26 @@ export class LibreBookingTrigger implements INodeType {
|
|||
webhookData.reservationHashes = newHashes;
|
||||
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 (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -723,13 +966,16 @@ export class LibreBookingTrigger implements INodeType {
|
|||
json: {
|
||||
...reservationData,
|
||||
_eventType: 'updated',
|
||||
_timeFilter: timeFilter,
|
||||
_triggeredAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
} finally {
|
||||
await signOutTrigger(this, baseUrl, session);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-librebooking",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
|
||||
"keywords": [
|
||||
"n8n-community-node-package",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue