Compare commits
No commits in common. "main" and "master" have entirely different histories.
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Generated PDFs
|
||||||
|
*.pdf
|
||||||
BIN
ARCHIV-INFO.pdf
BIN
ARCHIV-INFO.pdf
Binary file not shown.
92
CHANGELOG.md
92
CHANGELOG.md
|
|
@ -2,98 +2,6 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
### Behoben
|
|
||||||
- 🐛 **allowParticipation Fehler**: API-Fehler "Undefined property: stdClass::$allowParticipation" behoben. Das Feld wird jetzt immer im Request-Body gesendet.
|
|
||||||
- 🐛 **Trigger "Alle Abrufen" funktioniert nicht**: Trigger-Modi komplett überarbeitet mit drei klaren Optionen:
|
|
||||||
- "Alle Abrufen (Einmalig)" - Ruft alle Reservierungen für einen Zeitraum ab
|
|
||||||
- "Neue Reservierungen (Polling)" - Erkennt neue Reservierungen
|
|
||||||
- "Geänderte Reservierungen (Polling)" - Erkennt Änderungen
|
|
||||||
- 🐛 **Custom Attributes bei GetAll**: Option fehlt
|
|
||||||
|
|
||||||
### Hinzugefügt
|
|
||||||
- ⭐ **Include Custom Attributes Option**: Neues "Custom Attributes Einschließen" Checkbox bei:
|
|
||||||
- Reservierungen → Alle Abrufen
|
|
||||||
- Ressourcen → Alle Abrufen
|
|
||||||
- Benutzer → Alle Abrufen
|
|
||||||
- 📋 **TEST-RESULTS.md**: Detaillierte Test-Dokumentation mit echten API-Tests
|
|
||||||
- 📋 **test-api.ts**: Verbessertes Test-Skript für alle API-Endpunkte
|
|
||||||
|
|
||||||
### Geändert
|
|
||||||
- **Trigger Node**: Komplett überarbeitete UI mit klarerer Trennung der Modi
|
|
||||||
- **Trigger Zeitraum**: Optionale Start-/Enddatum-Felder für "Alle Abrufen" Mode
|
|
||||||
- **Reservierung erstellen/aktualisieren**: allowParticipation wird immer gesetzt (API-Pflichtfeld)
|
|
||||||
|
|
||||||
### Getestet
|
|
||||||
- ✅ 19 API-Tests erfolgreich bestanden
|
|
||||||
- ✅ Alle Trigger-Modi getestet
|
|
||||||
- ✅ Custom Attributes Integration getestet
|
|
||||||
- **Test-URL**: https://librebooking.zell-cloud.de
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-25
|
|
||||||
|
|
||||||
### Hinzugefügt
|
|
||||||
- ⭐ **Pflichtfeld `termsAccepted`**: Neues erforderliches Feld bei Reservierungserstellung
|
|
||||||
- ⭐ **Custom Attributes Support**: Benutzerdefinierte Attribute können jetzt bei Reservierungen, Ressourcen, Benutzern und Accounts gesetzt werden
|
|
||||||
- ⭐ **LibreBooking Config Node**: Neuer optionaler Config-Credential für zentrale Standardwerte
|
|
||||||
- **Debug-Modus**: Neuer Debug-Modus im Trigger Node für Fehlerdiagnose
|
|
||||||
- `CUSTOM-ATTRIBUTES.md`: Dokumentation zur Verwendung von benutzerdefinierten Attributen
|
|
||||||
- `CONFIG-NODE.md`: Dokumentation zum Config Node
|
|
||||||
|
|
||||||
### Geändert
|
|
||||||
- **Trigger "Neue Reservierungen"**: Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert
|
|
||||||
- **Trigger "Geänderte Reservierungen"**: Verbesserter Hash-Vergleich für zuverlässige Änderungserkennung
|
|
||||||
- Verbesserte Standardwerte für Zeitzone und Sprache bei Benutzererstellung
|
|
||||||
|
|
||||||
### Behoben
|
|
||||||
- 🐛 **Trigger triggert alle existierenden Events**: Jetzt werden beim ersten Poll nur IDs/Hashes gespeichert
|
|
||||||
- 🐛 **Trigger für geänderte Events funktioniert nicht**: Komplette Neuimplementierung mit Hash-Vergleich
|
|
||||||
|
|
||||||
### Technisch
|
|
||||||
- Neuer Credential-Typ: `libreBookingConfig`
|
|
||||||
- Erweiterte `WorkflowStaticData` für besseres State-Management im Trigger
|
|
||||||
- `getConfigDefaults()` Hilfsfunktion für Config-Integration
|
|
||||||
|
|
||||||
## [1.1.0] - 2026-01-25
|
## [1.1.0] - 2026-01-25
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
|
||||||
144
CONFIG-NODE.md
144
CONFIG-NODE.md
|
|
@ -1,144 +0,0 @@
|
||||||
# LibreBooking Config Node
|
|
||||||
|
|
||||||
Der Config Node ermöglicht die zentrale Konfiguration von Standardwerten, die in allen LibreBooking Operationen verwendet werden können.
|
|
||||||
|
|
||||||
## Überblick
|
|
||||||
|
|
||||||
Der LibreBooking Config Credential ist **optional** und dient dazu:
|
|
||||||
- Standardwerte zentral zu definieren
|
|
||||||
- Wiederholte Eingaben zu vermeiden
|
|
||||||
- Konsistente Einstellungen sicherzustellen
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Der Config Node wird automatisch mit der LibreBooking Node installiert. Er erscheint unter **Credentials** als "LibreBooking Config".
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
### 1. Config Credential anlegen
|
|
||||||
|
|
||||||
1. Gehen Sie zu **Credentials** in n8n
|
|
||||||
2. Klicken Sie auf **Add Credential**
|
|
||||||
3. Suchen Sie nach **LibreBooking Config**
|
|
||||||
4. Klicken Sie auf **Create**
|
|
||||||
|
|
||||||
### 2. Standardwerte definieren
|
|
||||||
|
|
||||||
| Einstellung | Beschreibung | Standard |
|
|
||||||
|-------------|--------------|----------|
|
|
||||||
| Standard Nutzungsbedingungen Akzeptiert | Vorauswahl für termsAccepted | `true` |
|
|
||||||
| Standard Teilnahme Erlauben | Vorauswahl für allowParticipation | `false` |
|
|
||||||
| Standard Ressourcen-ID | Standard-Ressource für Reservierungen | `0` (keine) |
|
|
||||||
| Standard Benutzer-ID | Standard-Benutzer für Reservierungen | `0` (angemeldeter Benutzer) |
|
|
||||||
| Standard Zeitplan-ID | Standard-Zeitplan für Ressourcen | `0` (keine) |
|
|
||||||
| Standard Zeitzone | Zeitzone für neue Benutzer | `Europe/Berlin` |
|
|
||||||
| Standard Sprache | Sprache für neue Benutzer | `de_de` |
|
|
||||||
|
|
||||||
### 3. Config mit Node verbinden
|
|
||||||
|
|
||||||
1. Öffnen Sie einen LibreBooking Node
|
|
||||||
2. Bei Ressourcen wie Reservierung, Ressource, Benutzer oder Konto erscheint ein optionales Credential-Feld für **LibreBooking Config**
|
|
||||||
3. Wählen Sie Ihre Config Credential aus
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
### Beispiel: Reservierung erstellen
|
|
||||||
|
|
||||||
**Ohne Config Node:**
|
|
||||||
```
|
|
||||||
- Ressourcen-ID: 1
|
|
||||||
- Startzeit: ...
|
|
||||||
- Endzeit: ...
|
|
||||||
- Nutzungsbedingungen Akzeptiert: true (manuell)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mit Config Node:**
|
|
||||||
```
|
|
||||||
- Ressourcen-ID: 1 (oder aus Config wenn 0 = übernehmen)
|
|
||||||
- Startzeit: ...
|
|
||||||
- Endzeit: ...
|
|
||||||
- Nutzungsbedingungen Akzeptiert: (automatisch aus Config)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Priorität der Werte
|
|
||||||
|
|
||||||
1. **Höchste Priorität**: Werte direkt im Node eingegeben
|
|
||||||
2. **Niedrigere Priorität**: Werte aus dem Config Node
|
|
||||||
3. **Fallback**: Eingebaute Standardwerte
|
|
||||||
|
|
||||||
## Anwendungsfälle
|
|
||||||
|
|
||||||
### 1. Automatisierte Buchungen
|
|
||||||
|
|
||||||
Wenn Sie einen Workflow haben, der automatisch Buchungen erstellt:
|
|
||||||
|
|
||||||
```
|
|
||||||
Config Node:
|
|
||||||
- Standard Nutzungsbedingungen Akzeptiert: true
|
|
||||||
- Standard Teilnahme Erlauben: false
|
|
||||||
```
|
|
||||||
|
|
||||||
So müssen Sie diese Werte nicht in jedem Create-Node angeben.
|
|
||||||
|
|
||||||
### 2. Standardressource für Abteilung
|
|
||||||
|
|
||||||
```
|
|
||||||
Config Node für Abteilung A:
|
|
||||||
- Standard Ressourcen-ID: 5 (Konferenzraum A)
|
|
||||||
|
|
||||||
Config Node für Abteilung B:
|
|
||||||
- Standard Ressourcen-ID: 8 (Konferenzraum B)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Mehrsprachige Umgebung
|
|
||||||
|
|
||||||
```
|
|
||||||
Config Node für deutschsprachige Workflows:
|
|
||||||
- Standard Zeitzone: Europe/Berlin
|
|
||||||
- Standard Sprache: de_de
|
|
||||||
|
|
||||||
Config Node für englischsprachige Workflows:
|
|
||||||
- Standard Zeitzone: Europe/London
|
|
||||||
- Standard Sprache: en_us
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Benennung
|
|
||||||
|
|
||||||
Verwenden Sie aussagekräftige Namen für Ihre Config Credentials:
|
|
||||||
- `LibreBooking Config - Produktion`
|
|
||||||
- `LibreBooking Config - Test`
|
|
||||||
- `LibreBooking Config - Abteilung Marketing`
|
|
||||||
|
|
||||||
### 2. Dokumentation
|
|
||||||
|
|
||||||
Dokumentieren Sie Ihre Config-Einstellungen für Ihr Team.
|
|
||||||
|
|
||||||
### 3. Umgebungstrennung
|
|
||||||
|
|
||||||
Erstellen Sie separate Configs für verschiedene Umgebungen (Test/Produktion).
|
|
||||||
|
|
||||||
## Fehlerbehebung
|
|
||||||
|
|
||||||
### Config wird nicht angewendet
|
|
||||||
|
|
||||||
- Stellen Sie sicher, dass der Config Node mit dem LibreBooking Node verbunden ist
|
|
||||||
- Prüfen Sie, ob die Ressource den Config Node unterstützt (nur Reservierung, Ressource, Benutzer, Konto)
|
|
||||||
|
|
||||||
### Werte werden überschrieben
|
|
||||||
|
|
||||||
- Direkt im Node eingegebene Werte haben immer Vorrang
|
|
||||||
- Lassen Sie Felder leer, wenn der Config-Wert verwendet werden soll
|
|
||||||
|
|
||||||
## Technische Details
|
|
||||||
|
|
||||||
Der Config Node wird als n8n Credential implementiert, ist aber kein echtes Authentifizierungs-Credential. Er speichert lediglich Konfigurationswerte.
|
|
||||||
|
|
||||||
**Credential-Name**: `libreBookingConfig`
|
|
||||||
|
|
||||||
**Unterstützte Ressourcen**:
|
|
||||||
- Reservierung (`reservation`)
|
|
||||||
- Ressource (`resource`)
|
|
||||||
- Benutzer (`user`)
|
|
||||||
- Konto (`account`)
|
|
||||||
BIN
CONFIG-NODE.pdf
BIN
CONFIG-NODE.pdf
Binary file not shown.
|
|
@ -1,215 +0,0 @@
|
||||||
# Benutzerdefinierte Attribute (Custom Attributes)
|
|
||||||
|
|
||||||
Diese Dokumentation erklärt, wie Sie benutzerdefinierte Attribute in LibreBooking über die n8n Nodes verwenden können.
|
|
||||||
|
|
||||||
## Überblick
|
|
||||||
|
|
||||||
LibreBooking unterstützt benutzerdefinierte Attribute für:
|
|
||||||
- **Reservierungen** (Kategorie-ID: 1)
|
|
||||||
- **Benutzer** (Kategorie-ID: 2)
|
|
||||||
- **Ressourcen** (Kategorie-ID: 4)
|
|
||||||
- **Ressourcen-Typen** (Kategorie-ID: 5)
|
|
||||||
|
|
||||||
## Attribut-Typen
|
|
||||||
|
|
||||||
| Typ | Beschreibung | Wert |
|
|
||||||
|-----|--------------|------|
|
|
||||||
| Einzeilig | Einfaches Textfeld | 1 |
|
|
||||||
| Mehrzeilig | Textbereich | 2 |
|
|
||||||
| Auswahlliste | Dropdown-Menü | 3 |
|
|
||||||
| Checkbox | Ja/Nein Feld | 4 |
|
|
||||||
| Datum/Zeit | Datums-/Zeitauswahl | 5 |
|
|
||||||
|
|
||||||
## Attribute abrufen
|
|
||||||
|
|
||||||
### Alle Attribute einer Kategorie abrufen
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Attribut`
|
|
||||||
2. Wählen Sie **Operation**: `Nach Kategorie Abrufen`
|
|
||||||
3. Wählen Sie **Kategorie-ID**: z.B. `Reservierung`
|
|
||||||
|
|
||||||
Die Antwort enthält alle Attribute mit ihren IDs und Eigenschaften.
|
|
||||||
|
|
||||||
### Einzelnes Attribut abrufen
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Attribut`
|
|
||||||
2. Wählen Sie **Operation**: `Abrufen`
|
|
||||||
3. Geben Sie die **Attribut-ID** ein
|
|
||||||
|
|
||||||
## Attribute bei Reservierungen setzen
|
|
||||||
|
|
||||||
### Bei Erstellen einer Reservierung
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Reservierung`
|
|
||||||
2. Wählen Sie **Operation**: `Erstellen`
|
|
||||||
3. Füllen Sie die Pflichtfelder aus
|
|
||||||
4. Unter **Benutzerdefinierte Attribute**:
|
|
||||||
- Klicken Sie auf "Attribut hinzufügen"
|
|
||||||
- Geben Sie die **Attribut-ID** ein (z.B. `1`)
|
|
||||||
- Geben Sie den **Wert** ein (z.B. `Meetingraum-Konfiguration`)
|
|
||||||
|
|
||||||
### Bei Aktualisieren einer Reservierung
|
|
||||||
|
|
||||||
Gleiche Vorgehensweise wie beim Erstellen.
|
|
||||||
|
|
||||||
### Beispiel JSON für API-Request
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"resourceId": 1,
|
|
||||||
"startDateTime": "2024-01-15T10:00:00+01:00",
|
|
||||||
"endDateTime": "2024-01-15T11:00:00+01:00",
|
|
||||||
"title": "Team Meeting",
|
|
||||||
"termsAccepted": true,
|
|
||||||
"customAttributes": [
|
|
||||||
{
|
|
||||||
"attributeId": 1,
|
|
||||||
"attributeValue": "Standard-Setup"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"attributeId": 2,
|
|
||||||
"attributeValue": "10"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attribute bei Ressourcen setzen
|
|
||||||
|
|
||||||
### Bei Erstellen einer Ressource
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Ressource`
|
|
||||||
2. Wählen Sie **Operation**: `Erstellen`
|
|
||||||
3. Füllen Sie die Pflichtfelder aus (Name, Zeitplan-ID)
|
|
||||||
4. Unter **Benutzerdefinierte Attribute**:
|
|
||||||
- Klicken Sie auf "Attribut hinzufügen"
|
|
||||||
- Geben Sie die **Attribut-ID** und den **Wert** ein
|
|
||||||
|
|
||||||
### Beispiel: Raum mit Ausstattung
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Konferenzraum A",
|
|
||||||
"scheduleId": 1,
|
|
||||||
"customAttributes": [
|
|
||||||
{
|
|
||||||
"attributeId": 10,
|
|
||||||
"attributeValue": "Beamer, Whiteboard"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"attributeId": 11,
|
|
||||||
"attributeValue": "20"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attribute bei Benutzern setzen
|
|
||||||
|
|
||||||
### Bei Erstellen eines Benutzers
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Benutzer`
|
|
||||||
2. Wählen Sie **Operation**: `Erstellen`
|
|
||||||
3. Füllen Sie die Pflichtfelder aus
|
|
||||||
4. Unter **Benutzerdefinierte Attribute**:
|
|
||||||
- Klicken Sie auf "Attribut hinzufügen"
|
|
||||||
- Geben Sie die **Attribut-ID** und den **Wert** ein
|
|
||||||
|
|
||||||
## Neue Attribute erstellen (Admin)
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Attribut`
|
|
||||||
2. Wählen Sie **Operation**: `Erstellen`
|
|
||||||
3. Füllen Sie aus:
|
|
||||||
- **Kategorie-ID**: Ziel-Kategorie (1, 2, 4 oder 5)
|
|
||||||
- **Attribut-Label**: Anzeigename
|
|
||||||
- **Attribut-Typ**: Feldtyp
|
|
||||||
4. Optional unter **Attribut-Optionen**:
|
|
||||||
- **Erforderlich**: Pflichtfeld?
|
|
||||||
- **Nur Admin**: Nur für Admins sichtbar?
|
|
||||||
- **Mögliche Werte**: Für Auswahllisten (komma-getrennt)
|
|
||||||
|
|
||||||
## Elegante Lösung: Attribute automatisch abrufen (NEU in v1.2.1)
|
|
||||||
|
|
||||||
### Das Problem
|
|
||||||
Bisher musste man Attribut-IDs manuell eingeben, was umständlich war.
|
|
||||||
|
|
||||||
### Die Lösung: "Custom Attributes Einschließen"
|
|
||||||
Bei den GetAll-Operationen gibt es jetzt eine neue Option, die automatisch die Custom Attribute Values für jeden Eintrag abruft.
|
|
||||||
|
|
||||||
### Verwendung für Reservierungen
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Reservierung`
|
|
||||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
|
||||||
3. Unter **Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
|
||||||
|
|
||||||
**Ergebnis:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"reservations": [
|
|
||||||
{
|
|
||||||
"referenceNumber": "abc123",
|
|
||||||
"title": "Meeting",
|
|
||||||
"startDate": "2026-02-07T10:00:00",
|
|
||||||
"customAttributes": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"label": "Mietername",
|
|
||||||
"value": "Max Mustermann"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"label": "Adresse",
|
|
||||||
"value": "Hauptstraße 1, 12345 Stadt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verwendung für Ressourcen
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Ressource`
|
|
||||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
|
||||||
3. Unter **Ressourcen-Abruf-Optionen** aktivieren Sie **Custom Attributes Einschließen** ✅
|
|
||||||
|
|
||||||
### Verwendung für Benutzer
|
|
||||||
|
|
||||||
1. Wählen Sie **Ressource**: `Benutzer`
|
|
||||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
|
||||||
3. Unter **Benutzer-Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
|
||||||
|
|
||||||
### Wichtiger Hinweis
|
|
||||||
Diese Option führt für jeden Eintrag einen zusätzlichen API-Call durch. Bei vielen Einträgen kann dies länger dauern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tipps
|
|
||||||
|
|
||||||
### Attribut-IDs herausfinden
|
|
||||||
|
|
||||||
1. Nutzen Sie die Operation "Nach Kategorie Abrufen"
|
|
||||||
2. Notieren Sie sich die `id` der benötigten Attribute
|
|
||||||
|
|
||||||
### Checkbox-Attribute
|
|
||||||
|
|
||||||
Für Checkbox-Attribute verwenden Sie:
|
|
||||||
- `"1"` oder `"true"` für aktiviert
|
|
||||||
- `"0"` oder `"false"` für deaktiviert
|
|
||||||
|
|
||||||
### Auswahllisten
|
|
||||||
|
|
||||||
Der Wert muss exakt einem der möglichen Werte entsprechen.
|
|
||||||
|
|
||||||
## Fehlerbehebung
|
|
||||||
|
|
||||||
### Attribut wird nicht gespeichert
|
|
||||||
|
|
||||||
- Prüfen Sie, ob die Attribut-ID korrekt ist
|
|
||||||
- Prüfen Sie, ob das Attribut für diese Kategorie gilt
|
|
||||||
- Prüfen Sie, ob der Wert dem Attribut-Typ entspricht
|
|
||||||
|
|
||||||
### Zugriff verweigert
|
|
||||||
|
|
||||||
- Einige Attribute sind nur für Admins verfügbar
|
|
||||||
- Prüfen Sie die Berechtigungen in LibreBooking
|
|
||||||
Binary file not shown.
Binary file not shown.
BIN
GIT-COMMANDS.pdf
BIN
GIT-COMMANDS.pdf
Binary file not shown.
107
GIT-UPLOAD.md
107
GIT-UPLOAD.md
|
|
@ -1,107 +0,0 @@
|
||||||
# Git Upload Anleitung
|
|
||||||
|
|
||||||
Diese Anleitung erklärt, wie Sie das LibreBooking n8n Node Repository auf verschiedene Git-Plattformen hochladen können.
|
|
||||||
|
|
||||||
## Option 1: GitHub/GitLab/Bitbucket (Web Interface)
|
|
||||||
|
|
||||||
1. Erstelle ein neues Repository auf GitHub/GitLab/Bitbucket
|
|
||||||
2. Entpacke das Archiv lokal
|
|
||||||
3. Folge den Anweisungen auf der Plattform
|
|
||||||
|
|
||||||
## Option 2: Command Line (Empfohlen)
|
|
||||||
|
|
||||||
### GitHub
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Repository erstellen auf github.com
|
|
||||||
# 2. Dann lokal:
|
|
||||||
cd librebooking_n8n_node
|
|
||||||
git remote add origin https://github.com/USERNAME/n8n-nodes-librebooking.git
|
|
||||||
git branch -M main
|
|
||||||
git push -u origin main
|
|
||||||
git push origin v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### GitLab
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd librebooking_n8n_node
|
|
||||||
git remote add origin https://gitlab.com/USERNAME/n8n-nodes-librebooking.git
|
|
||||||
git branch -M main
|
|
||||||
git push -u origin main
|
|
||||||
git push origin v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bitbucket
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd librebooking_n8n_node
|
|
||||||
git remote add origin https://bitbucket.org/USERNAME/n8n-nodes-librebooking.git
|
|
||||||
git branch -M main
|
|
||||||
git push -u origin main
|
|
||||||
git push origin v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Option 3: Git Bundle verwenden
|
|
||||||
|
|
||||||
Wenn Sie das Git Bundle (.bundle Datei) erhalten haben:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Bundle entpacken (klonen)
|
|
||||||
git clone librebooking-n8n-node-v1.2.0.bundle librebooking_n8n_node
|
|
||||||
cd librebooking_n8n_node
|
|
||||||
|
|
||||||
# Remote hinzufügen
|
|
||||||
git remote add origin YOUR_REMOTE_URL
|
|
||||||
|
|
||||||
# Pushen mit Tags
|
|
||||||
git push -u origin main --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wichtige Hinweise
|
|
||||||
|
|
||||||
### Vor dem Upload prüfen
|
|
||||||
|
|
||||||
- [ ] Keine sensiblen Daten (API Keys, Passwörter) im Repository
|
|
||||||
- [ ] `.gitignore` ist korrekt konfiguriert
|
|
||||||
- [ ] `node_modules/` und `dist/` sind nicht im Repository
|
|
||||||
- [ ] Alle Dokumentation ist aktuell
|
|
||||||
|
|
||||||
### Nach dem Upload
|
|
||||||
|
|
||||||
- [ ] Repository ist erreichbar
|
|
||||||
- [ ] Alle Dateien sind vorhanden
|
|
||||||
- [ ] Tags sind sichtbar
|
|
||||||
- [ ] README wird korrekt angezeigt
|
|
||||||
|
|
||||||
## SSH vs HTTPS
|
|
||||||
|
|
||||||
### HTTPS (einfacher)
|
|
||||||
```bash
|
|
||||||
git remote add origin https://github.com/USERNAME/REPO.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSH (empfohlen für regelmäßige Nutzung)
|
|
||||||
```bash
|
|
||||||
git remote add origin git@github.com:USERNAME/REPO.git
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fehlerbehandlung
|
|
||||||
|
|
||||||
### "fatal: remote origin already exists"
|
|
||||||
```bash
|
|
||||||
git remote remove origin
|
|
||||||
git remote add origin NEW_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Updates were rejected"
|
|
||||||
```bash
|
|
||||||
# VORSICHT: Nur wenn Sie sicher sind
|
|
||||||
git push -f origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Siehe auch
|
|
||||||
|
|
||||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Beitragen zum Projekt
|
|
||||||
- [README.md](README.md) - Hauptdokumentation
|
|
||||||
- [RELEASE-NOTES.md](RELEASE-NOTES.md) - Versionshinweise
|
|
||||||
BIN
GIT-UPLOAD.pdf
BIN
GIT-UPLOAD.pdf
Binary file not shown.
Binary file not shown.
|
|
@ -1,151 +0,0 @@
|
||||||
# Package Contents
|
|
||||||
|
|
||||||
Übersicht aller Dateien im LibreBooking n8n Node Paket.
|
|
||||||
|
|
||||||
## 📁 Struktur
|
|
||||||
|
|
||||||
```
|
|
||||||
librebooking_n8n_node/
|
|
||||||
├── 📄 Hauptdateien
|
|
||||||
├── 📁 nodes/ # n8n Nodes
|
|
||||||
├── 📁 credentials/ # Credentials
|
|
||||||
├── 📁 workflows/ # Beispiel Workflows
|
|
||||||
├── 📁 test/ # Test Dateien
|
|
||||||
└── 📄 Dokumentation & Skripte
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Hauptdateien
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `package.json` | Node Package Konfiguration |
|
|
||||||
| `tsconfig.json` | TypeScript Konfiguration |
|
|
||||||
| `index.ts` | Haupt-Export Datei |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Nodes
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `nodes/LibreBooking/LibreBooking.node.ts` | Haupt-Node für alle CRUD Operationen |
|
|
||||||
| `nodes/LibreBooking/librebooking.svg` | Node Icon |
|
|
||||||
| `nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts` | Trigger Node für Events |
|
|
||||||
| `nodes/LibreBookingTrigger/librebooking.svg` | Trigger Node Icon |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 Credentials
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `credentials/LibreBookingApi.credentials.ts` | API Credentials (URL, Benutzer, Passwort) |
|
|
||||||
| `credentials/LibreBookingConfig.credentials.ts` | Config Node für Standardwerte |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Dokumentation
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `README.md` | Hauptdokumentation |
|
|
||||||
| `INSTALLATION.md` | Detaillierte Installationsanleitung |
|
|
||||||
| `SCHNELLSTART.md` | Quick Start Guide |
|
|
||||||
| `CUSTOM-ATTRIBUTES.md` | Custom Attributes Anleitung |
|
|
||||||
| `CONFIG-NODE.md` | Config Node Guide |
|
|
||||||
| `TROUBLESHOOTING.md` | Problemlösungen |
|
|
||||||
| `DOCKER-INTEGRATION.md` | Docker Dokumentation |
|
|
||||||
| `SECURITY.md` | Sicherheitshinweise |
|
|
||||||
| `CHANGELOG.md` | Versionshistorie |
|
|
||||||
| `CONTRIBUTING.md` | Contribution Guide |
|
|
||||||
| `LICENSE` | MIT Lizenz |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Git-spezifische Dateien
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `GIT-UPLOAD.md` | Git Upload Anleitung |
|
|
||||||
| `RELEASE-NOTES.md` | Release Notes v1.2.0 |
|
|
||||||
| `PACKAGE-CONTENTS.md` | Diese Datei |
|
|
||||||
| `.gitignore` | Git Ignore Konfiguration |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Skripte
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `install.sh` | Linux/Mac Installation |
|
|
||||||
| `install.ps1` | Windows PowerShell Installation |
|
|
||||||
| `quick-install.sh` | Schnellinstallation |
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `install-docker.sh` | Docker Installation |
|
|
||||||
| `install-docker-manual.sh` | Manuelle Docker Installation |
|
|
||||||
| `install-in-container.sh` | Installation im Container |
|
|
||||||
| `build-on-host.sh` | Host-seitiges Bauen |
|
|
||||||
|
|
||||||
### Wartung
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `update-node.sh` | Update Skript |
|
|
||||||
| `check-installation.sh` | Installation prüfen |
|
|
||||||
| `fix-node-installation.sh` | Installation reparieren |
|
|
||||||
| `update-dependencies.sh` | Dependencies aktualisieren |
|
|
||||||
| `upload-to-git.sh` | Git Upload Helper |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐳 Docker Konfiguration
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `Dockerfile` | Docker Image Definition |
|
|
||||||
| `docker-compose.yml` | Standard Docker Compose |
|
|
||||||
| `docker-compose.override.yml` | Override für Entwicklung |
|
|
||||||
| `docker-compose.readonly.yml` | Read-only Volume Konfiguration |
|
|
||||||
| `.dockerignore` | Docker Build Ausschlüsse |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Test
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `test/test-api.ts` | API Test Script |
|
|
||||||
| `workflows/example-workflows.json` | Beispiel n8n Workflows |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Konfiguration
|
|
||||||
|
|
||||||
| Datei | Beschreibung |
|
|
||||||
|-------|-------------|
|
|
||||||
| `.npmrc` | npm Konfiguration |
|
|
||||||
| `.npmignore` | npm Publish Ausschlüsse |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Build Output (nicht im Repository)
|
|
||||||
|
|
||||||
| Verzeichnis | Beschreibung |
|
|
||||||
|-------------|-------------|
|
|
||||||
| `dist/` | Kompilierte JavaScript Dateien |
|
|
||||||
| `node_modules/` | npm Dependencies |
|
|
||||||
| `dist-for-docker/` | Host-Build für Docker |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dateigröße
|
|
||||||
|
|
||||||
- **Quellcode**: ~50 KB
|
|
||||||
- **Mit node_modules**: ~50 MB
|
|
||||||
- **Mit dist**: ~100 KB zusätzlich
|
|
||||||
- **Git Repository**: ~1 MB (ohne node_modules)
|
|
||||||
Binary file not shown.
60
README.md
60
README.md
|
|
@ -1,44 +1,7 @@
|
||||||
# LibreBooking n8n Node
|
# LibreBooking n8n Node
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Integration von LibreBooking in n8n für automatisierte Reservierungs- und Ressourcenverwaltung.
|
Integration von LibreBooking in n8n für automatisierte Reservierungs- und Ressourcenverwaltung.
|
||||||
|
|
||||||
## 📦 Installation via Git
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Repository klonen
|
|
||||||
git clone https://github.com/YOUR-USERNAME/n8n-nodes-librebooking.git
|
|
||||||
cd n8n-nodes-librebooking
|
|
||||||
|
|
||||||
# Dependencies installieren
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Bauen
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Start nach Git Clone
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Option 1: Automatische Installation
|
|
||||||
./quick-install.sh n8n
|
|
||||||
|
|
||||||
# Option 2: Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Option 3: Manuell in bestehenden n8n Container
|
|
||||||
docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
|
||||||
docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
|
||||||
docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
|
||||||
docker restart n8n
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ Schnellstart (EMPFOHLEN)
|
## ⚡ Schnellstart (EMPFOHLEN)
|
||||||
|
|
||||||
**Die einfachste Methode: Auf dem Host bauen, in Docker kopieren**
|
**Die einfachste Methode: Auf dem Host bauen, in Docker kopieren**
|
||||||
|
|
@ -79,6 +42,13 @@ npm run docker:copy # Kopiert in Container
|
||||||
npm run docker:restart # Startet Container neu
|
npm run docker:restart # Startet Container neu
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📚 Dokumentation
|
||||||
|
|
||||||
|
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
|
||||||
|
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
|
||||||
|
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
|
||||||
|
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
|
||||||
|
|
||||||
## 🔑 Credentials einrichten
|
## 🔑 Credentials einrichten
|
||||||
|
|
||||||
1. Öffne n8n: http://localhost:5678
|
1. Öffne n8n: http://localhost:5678
|
||||||
|
|
@ -96,27 +66,11 @@ npm run docker:restart # Startet Container neu
|
||||||
- Ressourcen und Verfügbarkeit verwalten
|
- Ressourcen und Verfügbarkeit verwalten
|
||||||
- Benutzer und Gruppen administrieren
|
- Benutzer und Gruppen administrieren
|
||||||
- Zeitpläne und Zubehör konfigurieren
|
- Zeitpläne und Zubehör konfigurieren
|
||||||
- **NEU v1.2.0**: Benutzerdefinierte Attribute setzen
|
|
||||||
|
|
||||||
### LibreBooking Trigger Node
|
### LibreBooking Trigger Node
|
||||||
- Neue Reservierungen überwachen
|
- Neue Reservierungen überwachen
|
||||||
- Geänderte Reservierungen erfassen
|
- Geänderte Reservierungen erfassen
|
||||||
- Filter nach Ressource/Zeitplan/Benutzer
|
- Filter nach Ressource/Zeitplan/Benutzer
|
||||||
- **NEU v1.2.0**: Korrektes Verhalten beim ersten Poll (keine Altdaten)
|
|
||||||
- **NEU v1.2.0**: Zuverlässige Änderungserkennung via Hash-Vergleich
|
|
||||||
|
|
||||||
### LibreBooking Config (v1.2.0)
|
|
||||||
- Optionales Credential für zentrale Standardwerte
|
|
||||||
- Konfigurierbar: termsAccepted, allowParticipation, Zeitzone, Sprache
|
|
||||||
|
|
||||||
## 📚 Dokumentation
|
|
||||||
|
|
||||||
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
|
|
||||||
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
|
|
||||||
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
|
|
||||||
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
|
|
||||||
- **[CUSTOM-ATTRIBUTES.md](CUSTOM-ATTRIBUTES.md)** - Benutzerdefinierte Attribute verwenden
|
|
||||||
- **[CONFIG-NODE.md](CONFIG-NODE.md)** - Config Node für Standardwerte
|
|
||||||
|
|
||||||
## 🔄 Updates
|
## 🔄 Updates
|
||||||
|
|
||||||
|
|
|
||||||
135
RELEASE-NOTES.md
135
RELEASE-NOTES.md
|
|
@ -1,135 +0,0 @@
|
||||||
# Release Notes v1.2.0
|
|
||||||
|
|
||||||
**Release Datum:** Januar 2026
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Diese Version bringt wichtige neue Features, Verbesserungen und Bugfixes für den LibreBooking n8n Node.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
### 🏷️ Custom Attributes Support
|
|
||||||
- Setzen von benutzerdefinierten Attributen für:
|
|
||||||
- Reservierungen
|
|
||||||
- Ressourcen
|
|
||||||
- Benutzer
|
|
||||||
- Accounts
|
|
||||||
- Flexible Konfiguration über fixedCollection
|
|
||||||
- Unterstützung für verschiedene Attributtypen
|
|
||||||
|
|
||||||
### ⚙️ Config Node
|
|
||||||
- Zentraler Konfigurationsknoten für Standardwerte
|
|
||||||
- Optionale Verwendung - bestehende Workflows funktionieren weiterhin
|
|
||||||
- Reduziert manuelle Eingaben bei wiederkehrenden Werten
|
|
||||||
- Konfigurierbare Defaults für:
|
|
||||||
- Standard-Ressource
|
|
||||||
- Standard-Zeitplan
|
|
||||||
- Standardwerte für neue Reservierungen
|
|
||||||
|
|
||||||
### 🔄 Verbesserte Trigger
|
|
||||||
- Neue Events triggern nicht mehr mit existierenden Daten
|
|
||||||
- Geänderte Events werden korrekt erkannt
|
|
||||||
- Hash-basierte Änderungserkennung
|
|
||||||
- Verbesserte Deduplizierung
|
|
||||||
|
|
||||||
### ✅ Pflichtfelder
|
|
||||||
- `termsAccepted` als Pflichtfeld bei Reservation Create
|
|
||||||
- Alle Pflichtfelder gemäß API-Dokumentation geprüft und ergänzt
|
|
||||||
- Bessere Validierung vor API-Aufrufen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verbesserungen
|
|
||||||
|
|
||||||
### 📚 Dokumentation
|
|
||||||
- Neue CUSTOM-ATTRIBUTES.md Anleitung
|
|
||||||
- CONFIG-NODE.md Dokumentation
|
|
||||||
- Erweiterte TROUBLESHOOTING.md
|
|
||||||
- SECURITY.md für Sicherheitshinweise
|
|
||||||
|
|
||||||
### 🐳 Docker Integration
|
|
||||||
- Read-only Volume Problem gelöst
|
|
||||||
- build-on-host.sh für Host-seitiges Bauen
|
|
||||||
- docker-compose.readonly.yml für sichere Deployments
|
|
||||||
- Verbesserte Fehlermeldungen
|
|
||||||
|
|
||||||
### 🔒 Sicherheit
|
|
||||||
- npm audit Vulnerabilities dokumentiert
|
|
||||||
- package.json overrides für sichere Dependencies
|
|
||||||
- .npmrc zur Unterdrückung von Warnungen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bugfixes
|
|
||||||
|
|
||||||
- Trigger löst bei neuen Events nicht mehr mit alten Daten aus
|
|
||||||
- Korrektes Handling von leeren API-Responses
|
|
||||||
- Verbesserte Fehlerbehandlung bei Authentifizierung
|
|
||||||
- Session-Timeout wird korrekt behandelt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Siehe [INSTALLATION.md](INSTALLATION.md) für detaillierte Anweisungen.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Quick Install
|
|
||||||
git clone https://github.com/YOUR-USERNAME/n8n-nodes-librebooking.git
|
|
||||||
cd n8n-nodes-librebooking
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Upgrade von v1.1.0
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/n8n/custom-nodes/n8n-nodes-librebooking
|
|
||||||
git pull
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Bei Docker:
|
|
||||||
docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
|
||||||
docker restart n8n
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
**Keine** - Diese Version ist vollständig abwärtskompatibel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- npm audit zeigt Vulnerabilities von n8n-workflow Dependencies (siehe SECURITY.md)
|
|
||||||
- Read-only Docker Volumes erfordern Host-seitiges Bauen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nächste Version (Roadmap)
|
|
||||||
|
|
||||||
- [ ] Webhook Support für Echtzeit-Events
|
|
||||||
- [ ] Batch-Operationen für mehrere Reservierungen
|
|
||||||
- [ ] Erweiterte Filteroptionen
|
|
||||||
- [ ] npm Registry Veröffentlichung
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Danksagungen
|
|
||||||
|
|
||||||
Vielen Dank an alle Contributors und Tester!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [GitHub Repository](https://github.com/YOUR-USERNAME/n8n-nodes-librebooking)
|
|
||||||
- [LibreBooking](https://github.com/LibreBooking/app)
|
|
||||||
- [n8n](https://n8n.io)
|
|
||||||
Binary file not shown.
Binary file not shown.
BIN
SCHNELLSTART.pdf
BIN
SCHNELLSTART.pdf
Binary file not shown.
BIN
SECURITY.pdf
BIN
SECURITY.pdf
Binary file not shown.
143
TEST-RESULTS.md
143
TEST-RESULTS.md
|
|
@ -1,143 +0,0 @@
|
||||||
# LibreBooking n8n Node - Test Results
|
|
||||||
|
|
||||||
## Test Datum: 25.01.2026
|
|
||||||
|
|
||||||
### Test-Umgebung
|
|
||||||
- **URL**: https://librebooking.zell-cloud.de
|
|
||||||
- **Benutzer**: sebastian.zell@zell-aufmass.de
|
|
||||||
- **n8n Node Version**: 1.2.1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test-Ergebnisse
|
|
||||||
|
|
||||||
### 1. Authentifizierung ✅
|
|
||||||
- Login erfolgreich
|
|
||||||
- Session Token wird korrekt generiert
|
|
||||||
- User ID wird zurückgegeben
|
|
||||||
|
|
||||||
### 2. Reservierungen ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 12 Reservierungen gefunden |
|
|
||||||
| Get All (mit Datumsfilter) | ✅ | Filtert korrekt nach Zeitraum |
|
|
||||||
| Get (Einzeln) | ✅ | Custom Attributes werden zurückgegeben |
|
|
||||||
| Create | ✅ | allowParticipation wird korrekt gesetzt |
|
|
||||||
| Update | ✅ | Änderungen werden übernommen |
|
|
||||||
| Delete | ✅ | Reservierung wird gelöscht |
|
|
||||||
|
|
||||||
**Custom Attributes für Reservierungen (9 gefunden):**
|
|
||||||
- Mietername (ID: 1, Typ: Text, Pflicht: ✅)
|
|
||||||
- Telefon (ID: 2, Typ: Text, Pflicht: ❌)
|
|
||||||
- Adresse (ID: 3, Typ: Text, Pflicht: ✅)
|
|
||||||
- Lage der Wohnung – Gebäudeart (ID: 11, Typ: Auswahl)
|
|
||||||
- Geschoss (ID: 9, Typ: Auswahl)
|
|
||||||
- Lage der Wohnung – Lage im Grundriss (ID: 10, Typ: Auswahl)
|
|
||||||
- Quadratmeter (ID: 12, Typ: Text)
|
|
||||||
- Clustername (ID: 4, Typ: Text, Pflicht: ✅)
|
|
||||||
- Status (ID: 8, Typ: Auswahl, Pflicht: ✅)
|
|
||||||
|
|
||||||
### 3. Ressourcen ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 4 Ressourcen gefunden |
|
|
||||||
| Get (Einzeln) | ✅ | Details werden abgerufen |
|
|
||||||
|
|
||||||
**Ressourcen:**
|
|
||||||
- Aufmass Team 1 (ID: 1)
|
|
||||||
- Aufmass Team 2 (ID: 2)
|
|
||||||
- Aufmass Team 3 (ID: 3)
|
|
||||||
- Aufmass Team 4 (ID: 4)
|
|
||||||
|
|
||||||
### 4. Benutzer ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 3 Benutzer gefunden |
|
|
||||||
| Get (Einzeln) | ✅ | Details werden abgerufen |
|
|
||||||
|
|
||||||
### 5. Zeitpläne ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 1 Zeitplan gefunden |
|
|
||||||
|
|
||||||
### 6. Attribute (nach Kategorie) ✅
|
|
||||||
|
|
||||||
| Kategorie | Anzahl |
|
|
||||||
|-----------|--------|
|
|
||||||
| Reservierung (1) | 9 |
|
|
||||||
| Benutzer (2) | 0 |
|
|
||||||
| Ressource (4) | 0 |
|
|
||||||
| Ressourcen-Typ (5) | 0 |
|
|
||||||
|
|
||||||
### 7. Gruppen ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 2 Gruppen gefunden |
|
|
||||||
|
|
||||||
### 8. Zubehör ✅
|
|
||||||
|
|
||||||
| Operation | Status | Details |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Get All | ✅ | 0 Zubehörteile (keine konfiguriert) |
|
|
||||||
|
|
||||||
### 9. Sign Out ✅
|
|
||||||
- Session wird korrekt beendet
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Trigger Node Tests
|
|
||||||
|
|
||||||
### "Alle Abrufen" (Get All) Mode ✅
|
|
||||||
- Ruft alle Reservierungen für den angegebenen Zeitraum ab
|
|
||||||
- Optionale Start-/Enddatum-Filter funktionieren
|
|
||||||
- "Detaillierte Daten Abrufen" Option lädt Custom Attributes
|
|
||||||
|
|
||||||
### "Neue Reservierungen" (Poll) Mode ✅
|
|
||||||
- Erster Poll: Speichert IDs, triggert nicht
|
|
||||||
- Folgende Polls: Erkennt neue Reservierungen
|
|
||||||
- Debug-Modus zeigt gespeicherte IDs an
|
|
||||||
|
|
||||||
### "Geänderte Reservierungen" (Poll) Mode ✅
|
|
||||||
- Erster Poll: Speichert Hashes, triggert nicht
|
|
||||||
- Folgende Polls: Erkennt Änderungen durch Hash-Vergleich
|
|
||||||
- Änderungen an Titel, Beschreibung, Zeitraum werden erkannt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Behobene Probleme
|
|
||||||
|
|
||||||
### 1. allowParticipation Fehler ✅
|
|
||||||
**Problem**: API-Fehler "Undefined property: stdClass::$allowParticipation"
|
|
||||||
|
|
||||||
**Lösung**: `allowParticipation` wird jetzt immer im Request-Body gesendet (ist ein Pflichtfeld).
|
|
||||||
|
|
||||||
### 2. Trigger "Alle Abrufen" funktioniert nicht ✅
|
|
||||||
**Problem**: Mode war unklar, nutzte Polling-Logik
|
|
||||||
|
|
||||||
**Lösung**: Neuer "Alle Abrufen (Einmalig)" Mode mit optionalen Datum-Parametern.
|
|
||||||
|
|
||||||
### 3. Custom Attributes nicht elegant abrufbar ✅
|
|
||||||
**Problem**: Manuelles Eingeben von Attribut-IDs nötig
|
|
||||||
|
|
||||||
**Lösung**: "Custom Attributes Einschließen" Option bei GetAll-Operationen für:
|
|
||||||
- Reservierungen
|
|
||||||
- Ressourcen
|
|
||||||
- Benutzer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test-Zusammenfassung
|
|
||||||
|
|
||||||
| Kategorie | Tests | Bestanden | Fehlgeschlagen |
|
|
||||||
|-----------|-------|-----------|----------------|
|
|
||||||
| API-Endpunkte | 19 | 19 | 0 |
|
|
||||||
| Trigger Modes | 3 | 3 | 0 |
|
|
||||||
| Custom Attributes | 4 | 4 | 0 |
|
|
||||||
| **Gesamt** | **26** | **26** | **0** |
|
|
||||||
|
|
||||||
✅ **Alle Tests erfolgreich bestanden!**
|
|
||||||
BIN
TEST-RESULTS.pdf
BIN
TEST-RESULTS.pdf
Binary file not shown.
212
TRIGGER-GUIDE.md
212
TRIGGER-GUIDE.md
|
|
@ -1,212 +0,0 @@
|
||||||
# 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.
Binary file not shown.
|
|
@ -1,75 +0,0 @@
|
||||||
import {
|
|
||||||
ICredentialType,
|
|
||||||
INodeProperties,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LibreBooking Config Credential
|
|
||||||
*
|
|
||||||
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
|
||||||
* die in allen LibreBooking Nodes verwendet werden können.
|
|
||||||
*/
|
|
||||||
export class LibreBookingConfig implements ICredentialType {
|
|
||||||
name = 'libreBookingConfig';
|
|
||||||
displayName = 'LibreBooking Config';
|
|
||||||
documentationUrl = 'https://librebooking.org';
|
|
||||||
|
|
||||||
properties: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Hinweis',
|
|
||||||
name: 'notice',
|
|
||||||
type: 'notice',
|
|
||||||
default: '',
|
|
||||||
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
|
|
||||||
name: 'defaultTermsAccepted',
|
|
||||||
type: 'boolean',
|
|
||||||
default: true,
|
|
||||||
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Teilnahme Erlauben',
|
|
||||||
name: 'defaultAllowParticipation',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Ressourcen-ID',
|
|
||||||
name: 'defaultResourceId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Benutzer-ID',
|
|
||||||
name: 'defaultUserId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Zeitplan-ID',
|
|
||||||
name: 'defaultScheduleId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Zeitzone',
|
|
||||||
name: 'defaultTimezone',
|
|
||||||
type: 'string',
|
|
||||||
default: 'Europe/Berlin',
|
|
||||||
description: 'Standard-Zeitzone für neue Benutzer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Sprache',
|
|
||||||
name: 'defaultLanguage',
|
|
||||||
type: 'string',
|
|
||||||
default: 'de_de',
|
|
||||||
description: 'Standard-Sprache für neue Benutzer',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
||||||
/**
|
|
||||||
* LibreBooking API Credentials
|
|
||||||
*
|
|
||||||
* LibreBooking verwendet Session-basierte Authentifizierung.
|
|
||||||
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
|
|
||||||
*/
|
|
||||||
export declare class LibreBookingApi implements ICredentialType {
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
documentationUrl: string;
|
|
||||||
properties: INodeProperties[];
|
|
||||||
test: ICredentialTestRequest;
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=LibreBookingApi.credentials.d.ts.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"LibreBookingApi.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,sBAAsB,EACtB,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACtD,IAAI,SAAqB;IACzB,WAAW,SAAsB;IACjC,gBAAgB,SAAuC;IAEvD,UAAU,EAAE,eAAe,EAAE,CA6B3B;IAGF,IAAI,EAAE,sBAAsB,CAa1B;CACF"}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.LibreBookingApi = void 0;
|
|
||||||
/**
|
|
||||||
* LibreBooking API Credentials
|
|
||||||
*
|
|
||||||
* LibreBooking verwendet Session-basierte Authentifizierung.
|
|
||||||
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
|
|
||||||
*/
|
|
||||||
class LibreBookingApi {
|
|
||||||
constructor() {
|
|
||||||
this.name = 'libreBookingApi';
|
|
||||||
this.displayName = 'LibreBooking API';
|
|
||||||
this.documentationUrl = 'https://librebooking.org/docs/api';
|
|
||||||
this.properties = [
|
|
||||||
{
|
|
||||||
displayName: 'LibreBooking URL',
|
|
||||||
name: 'url',
|
|
||||||
type: 'string',
|
|
||||||
default: '',
|
|
||||||
placeholder: 'https://booking.example.com',
|
|
||||||
required: true,
|
|
||||||
description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Benutzername',
|
|
||||||
name: 'username',
|
|
||||||
type: 'string',
|
|
||||||
default: '',
|
|
||||||
required: true,
|
|
||||||
description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Passwort',
|
|
||||||
name: 'password',
|
|
||||||
type: 'string',
|
|
||||||
typeOptions: {
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
required: true,
|
|
||||||
description: 'Ihr LibreBooking-Passwort',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// Test-Request um die Credentials zu validieren
|
|
||||||
this.test = {
|
|
||||||
request: {
|
|
||||||
baseURL: '={{$credentials.url}}',
|
|
||||||
url: '/Web/Services/index.php/Authentication/Authenticate',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
username: '={{$credentials.username}}',
|
|
||||||
password: '={{$credentials.password}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.LibreBookingApi = LibreBookingApi;
|
|
||||||
//# sourceMappingURL=LibreBookingApi.credentials.js.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"LibreBookingApi.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":";;;AAOA;;;;;GAKG;AACH,MAAa,eAAe;IAA5B;QACC,SAAI,GAAG,iBAAiB,CAAC;QACzB,gBAAW,GAAG,kBAAkB,CAAC;QACjC,qBAAgB,GAAG,mCAAmC,CAAC;QAEvD,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,6BAA6B;gBAC1C,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,oEAAoE;aACjF;YACD;gBACC,WAAW,EAAE,cAAc;gBAC3B,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,mDAAmD;aAChE;YACD;gBACC,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE;oBACZ,QAAQ,EAAE,IAAI;iBACd;gBACD,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,2BAA2B;aACxC;SACD,CAAC;QAEF,gDAAgD;QAChD,SAAI,GAA2B;YAC9B,OAAO,EAAE;gBACR,OAAO,EAAE,uBAAuB;gBAChC,GAAG,EAAE,qDAAqD;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACR,cAAc,EAAE,kBAAkB;iBAClC;gBACD,IAAI,EAAE;oBACL,QAAQ,EAAE,4BAA4B;oBACtC,QAAQ,EAAE,4BAA4B;iBACtC;aACD;SACD,CAAC;IACH,CAAC;CAAA;AAnDD,0CAmDC"}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
||||||
/**
|
|
||||||
* LibreBooking Config Credential
|
|
||||||
*
|
|
||||||
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
|
||||||
* die in allen LibreBooking Nodes verwendet werden können.
|
|
||||||
*/
|
|
||||||
export declare class LibreBookingConfig implements ICredentialType {
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
documentationUrl: string;
|
|
||||||
properties: INodeProperties[];
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=LibreBookingConfig.credentials.d.ts.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"LibreBookingConfig.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACzD,IAAI,SAAwB;IAC5B,WAAW,SAAyB;IACpC,gBAAgB,SAA8B;IAE9C,UAAU,EAAE,eAAe,EAAE,CAyD3B;CACF"}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.LibreBookingConfig = void 0;
|
|
||||||
/**
|
|
||||||
* LibreBooking Config Credential
|
|
||||||
*
|
|
||||||
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
|
||||||
* die in allen LibreBooking Nodes verwendet werden können.
|
|
||||||
*/
|
|
||||||
class LibreBookingConfig {
|
|
||||||
constructor() {
|
|
||||||
this.name = 'libreBookingConfig';
|
|
||||||
this.displayName = 'LibreBooking Config';
|
|
||||||
this.documentationUrl = 'https://librebooking.org';
|
|
||||||
this.properties = [
|
|
||||||
{
|
|
||||||
displayName: 'Hinweis',
|
|
||||||
name: 'notice',
|
|
||||||
type: 'notice',
|
|
||||||
default: '',
|
|
||||||
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
|
|
||||||
name: 'defaultTermsAccepted',
|
|
||||||
type: 'boolean',
|
|
||||||
default: true,
|
|
||||||
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Teilnahme Erlauben',
|
|
||||||
name: 'defaultAllowParticipation',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Ressourcen-ID',
|
|
||||||
name: 'defaultResourceId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Benutzer-ID',
|
|
||||||
name: 'defaultUserId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Zeitplan-ID',
|
|
||||||
name: 'defaultScheduleId',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Zeitzone',
|
|
||||||
name: 'defaultTimezone',
|
|
||||||
type: 'string',
|
|
||||||
default: 'Europe/Berlin',
|
|
||||||
description: 'Standard-Zeitzone für neue Benutzer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Standard Sprache',
|
|
||||||
name: 'defaultLanguage',
|
|
||||||
type: 'string',
|
|
||||||
default: 'de_de',
|
|
||||||
description: 'Standard-Sprache für neue Benutzer',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.LibreBookingConfig = LibreBookingConfig;
|
|
||||||
//# sourceMappingURL=LibreBookingConfig.credentials.js.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"LibreBookingConfig.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":";;;AAKA;;;;;GAKG;AACH,MAAa,kBAAkB;IAA/B;QACC,SAAI,GAAG,oBAAoB,CAAC;QAC5B,gBAAW,GAAG,qBAAqB,CAAC;QACpC,qBAAgB,GAAG,0BAA0B,CAAC;QAE9C,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,SAAS;gBACtB,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,4JAA4J;aACzK;YACD;gBACC,WAAW,EAAE,yCAAyC;gBACtD,IAAI,EAAE,sBAAsB;gBAC5B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,2EAA2E;aACxF;YACD;gBACC,WAAW,EAAE,6BAA6B;gBAC1C,IAAI,EAAE,2BAA2B;gBACjC,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,KAAK;gBACd,WAAW,EAAE,6DAA6D;aAC1E;YACD;gBACC,WAAW,EAAE,wBAAwB;gBACrC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,yEAAyE;aACtF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,eAAe;gBACrB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,qEAAqE;aAClF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,8EAA8E;aAC3F;YACD;gBACC,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,eAAe;gBACxB,WAAW,EAAE,qCAAqC;aAClD;YACD;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,OAAO;gBAChB,WAAW,EAAE,oCAAoC;aACjD;SACD,CAAC;IACH,CAAC;CAAA;AA/DD,gDA+DC"}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
/**
|
|
||||||
* LibreBooking n8n Node
|
|
||||||
*
|
|
||||||
* Vollständige Integration für die LibreBooking API.
|
|
||||||
* Unterstützt alle wichtigen Ressourcen und Operationen.
|
|
||||||
*/
|
|
||||||
export declare class LibreBooking implements INodeType {
|
|
||||||
description: INodeTypeDescription;
|
|
||||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=LibreBooking.node.d.ts.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"LibreBooking.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBooking/LibreBooking.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACC,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAI3B,MAAM,cAAc,CAAC;AA8LtB;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,SAAS;IACtC,WAAW,EAAE,oBAAoB,CAw2B/B;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAmjB9E"}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
|
|
||||||
|
|
||||||
<!-- Calendar base -->
|
|
||||||
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
|
|
||||||
|
|
||||||
<!-- Calendar header -->
|
|
||||||
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
|
|
||||||
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
|
|
||||||
|
|
||||||
<!-- Calendar rings -->
|
|
||||||
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
|
||||||
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
|
||||||
|
|
||||||
<!-- Grid lines -->
|
|
||||||
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
|
|
||||||
<!-- Booking indicator -->
|
|
||||||
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
|
|
||||||
|
|
||||||
<!-- Check mark -->
|
|
||||||
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,14 +0,0 @@
|
||||||
import { INodeType, INodeTypeDescription, IPollFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
/**
|
|
||||||
* LibreBooking Trigger Node
|
|
||||||
*
|
|
||||||
* Drei Modi:
|
|
||||||
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
|
||||||
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
|
||||||
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
|
||||||
*/
|
|
||||||
export declare class LibreBookingTrigger implements INodeType {
|
|
||||||
description: INodeTypeDescription;
|
|
||||||
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=LibreBookingTrigger.node.d.ts.map
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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"}
|
|
||||||
|
|
@ -1,813 +0,0 @@
|
||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.LibreBookingTrigger = void 0;
|
|
||||||
const n8n_workflow_1 = require("n8n-workflow");
|
|
||||||
/**
|
|
||||||
* Authentifizierung bei LibreBooking
|
|
||||||
*/
|
|
||||||
async function authenticateTrigger(pollFunctions, baseUrl, username, password) {
|
|
||||||
try {
|
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
|
||||||
method: 'POST',
|
|
||||||
url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: { username, password },
|
|
||||||
json: true,
|
|
||||||
});
|
|
||||||
if (!response.isAuthenticated) {
|
|
||||||
throw new n8n_workflow_1.NodeOperationError(pollFunctions.getNode(), 'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
sessionToken: response.sessionToken,
|
|
||||||
userId: response.userId,
|
|
||||||
sessionExpires: response.sessionExpires,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
|
|
||||||
message: 'Authentifizierung fehlgeschlagen',
|
|
||||||
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Abmeldung
|
|
||||||
*/
|
|
||||||
async function signOutTrigger(pollFunctions, baseUrl, session) {
|
|
||||||
try {
|
|
||||||
await pollFunctions.helpers.httpRequest({
|
|
||||||
method: 'POST',
|
|
||||||
url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: {
|
|
||||||
userId: session.userId,
|
|
||||||
sessionToken: session.sessionToken,
|
|
||||||
},
|
|
||||||
json: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Ignoriere SignOut-Fehler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Reservierungen abrufen
|
|
||||||
*/
|
|
||||||
async function getReservations(pollFunctions, baseUrl, session, startDateTime, endDateTime, filters) {
|
|
||||||
const qs = {
|
|
||||||
startDateTime,
|
|
||||||
endDateTime,
|
|
||||||
};
|
|
||||||
if (filters.resourceId)
|
|
||||||
qs.resourceId = filters.resourceId;
|
|
||||||
if (filters.scheduleId)
|
|
||||||
qs.scheduleId = filters.scheduleId;
|
|
||||||
if (filters.userId)
|
|
||||||
qs.userId = filters.userId;
|
|
||||||
try {
|
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
|
||||||
method: 'GET',
|
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Booked-SessionToken': session.sessionToken,
|
|
||||||
'X-Booked-UserId': session.userId.toString(),
|
|
||||||
},
|
|
||||||
qs,
|
|
||||||
json: true,
|
|
||||||
});
|
|
||||||
return response.reservations || [];
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
|
|
||||||
message: 'Fehler beim Abrufen der Reservierungen',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
|
||||||
*/
|
|
||||||
async function getReservationDetails(pollFunctions, baseUrl, session, referenceNumber) {
|
|
||||||
try {
|
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
|
||||||
method: 'GET',
|
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Booked-SessionToken': session.sessionToken,
|
|
||||||
'X-Booked-UserId': session.userId.toString(),
|
|
||||||
},
|
|
||||||
json: true,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 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(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);
|
|
||||||
return {
|
|
||||||
start: now.toISOString(),
|
|
||||||
end: endDate.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Zeitfenster berechnen für Polling
|
|
||||||
*/
|
|
||||||
function getTimeWindowForPolling(timeWindow) {
|
|
||||||
const now = new Date();
|
|
||||||
const start = now.toISOString();
|
|
||||||
let endDate = new Date(now);
|
|
||||||
switch (timeWindow) {
|
|
||||||
case '7days':
|
|
||||||
endDate.setDate(endDate.getDate() + 7);
|
|
||||||
break;
|
|
||||||
case '14days':
|
|
||||||
endDate.setDate(endDate.getDate() + 14);
|
|
||||||
break;
|
|
||||||
case '30days':
|
|
||||||
endDate.setDate(endDate.getDate() + 30);
|
|
||||||
break;
|
|
||||||
case '90days':
|
|
||||||
endDate.setDate(endDate.getDate() + 90);
|
|
||||||
break;
|
|
||||||
case '180days':
|
|
||||||
endDate.setDate(endDate.getDate() + 180);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
endDate.setDate(endDate.getDate() + 14);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
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 || reservation.startDateTime,
|
|
||||||
endDate: reservation.endDate || reservation.endDateTime,
|
|
||||||
title: reservation.title || '',
|
|
||||||
description: reservation.description || '',
|
|
||||||
resourceId: reservation.resourceId,
|
|
||||||
resourceName: reservation.resourceName || '',
|
|
||||||
userId: reservation.userId,
|
|
||||||
requiresApproval: reservation.requiresApproval,
|
|
||||||
participants: reservation.participants || [],
|
|
||||||
invitees: reservation.invitees || [],
|
|
||||||
statusId: reservation.statusId,
|
|
||||||
};
|
|
||||||
return JSON.stringify(relevantData);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* LibreBooking Trigger Node
|
|
||||||
*
|
|
||||||
* Drei Modi:
|
|
||||||
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
|
||||||
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
|
||||||
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
|
||||||
*/
|
|
||||||
class LibreBookingTrigger {
|
|
||||||
constructor() {
|
|
||||||
this.description = {
|
|
||||||
displayName: 'LibreBooking Trigger',
|
|
||||||
name: 'libreBookingTrigger',
|
|
||||||
icon: 'file:librebooking.svg',
|
|
||||||
group: ['trigger'],
|
|
||||||
version: 1,
|
|
||||||
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
|
||||||
subtitle: '={{$parameter["triggerMode"]}}',
|
|
||||||
defaults: {
|
|
||||||
name: 'LibreBooking Trigger',
|
|
||||||
},
|
|
||||||
inputs: [],
|
|
||||||
outputs: ['main'],
|
|
||||||
credentials: [
|
|
||||||
{
|
|
||||||
name: 'libreBookingApi',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
polling: true,
|
|
||||||
properties: [
|
|
||||||
// =====================================================
|
|
||||||
// TRIGGER MODE SELECTOR
|
|
||||||
// =====================================================
|
|
||||||
{
|
|
||||||
displayName: 'Trigger-Modus',
|
|
||||||
name: 'triggerMode',
|
|
||||||
type: 'options',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Alle Abrufen (Einmalig)',
|
|
||||||
value: 'getAll',
|
|
||||||
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Neue Reservierungen (Polling)',
|
|
||||||
value: 'newReservations',
|
|
||||||
description: 'Nur bei neuen Reservierungen triggern',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Geänderte Reservierungen (Polling)',
|
|
||||||
value: 'updatedReservations',
|
|
||||||
description: 'Nur bei geänderten Reservierungen triggern',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'getAll',
|
|
||||||
description: 'Wählen Sie den Trigger-Modus',
|
|
||||||
},
|
|
||||||
// =====================================================
|
|
||||||
// GET ALL MODE - DATE RANGE 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',
|
|
||||||
type: 'dateTime',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['getAll'],
|
|
||||||
dateRange: ['custom'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'Startdatum für den Abruf (leer = heute)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Enddatum',
|
|
||||||
name: 'endDate',
|
|
||||||
type: 'dateTime',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['getAll'],
|
|
||||||
dateRange: ['custom'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
|
|
||||||
},
|
|
||||||
// =====================================================
|
|
||||||
// POLLING MODE - TIME WINDOW
|
|
||||||
// =====================================================
|
|
||||||
{
|
|
||||||
displayName: 'Zeitfenster',
|
|
||||||
name: 'timeWindow',
|
|
||||||
type: 'options',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['newReservations', 'updatedReservations'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{ name: 'Nächste 7 Tage', value: '7days' },
|
|
||||||
{ name: 'Nächste 14 Tage', value: '14days' },
|
|
||||||
{ name: 'Nächste 30 Tage', value: '30days' },
|
|
||||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
|
||||||
{ 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',
|
|
||||||
type: 'notice',
|
|
||||||
default: '',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['newReservations', 'updatedReservations'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
|
|
||||||
},
|
|
||||||
// =====================================================
|
|
||||||
// FILTERS (ALL MODES)
|
|
||||||
// =====================================================
|
|
||||||
{
|
|
||||||
displayName: 'Filter',
|
|
||||||
name: 'filters',
|
|
||||||
type: 'collection',
|
|
||||||
placeholder: 'Filter hinzufügen',
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
displayName: 'Ressourcen-ID',
|
|
||||||
name: 'resourceId',
|
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diese Ressource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Zeitplan-ID',
|
|
||||||
name: 'scheduleId',
|
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diesen Zeitplan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Benutzer-ID',
|
|
||||||
name: 'userId',
|
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diesen Benutzer',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// =====================================================
|
|
||||||
// OPTIONS (ALL MODES)
|
|
||||||
// =====================================================
|
|
||||||
{
|
|
||||||
displayName: 'Optionen',
|
|
||||||
name: 'options',
|
|
||||||
type: 'collection',
|
|
||||||
placeholder: 'Option hinzufügen',
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
displayName: 'Detaillierte Daten Abrufen',
|
|
||||||
name: 'fetchDetails',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Debug-Modus',
|
|
||||||
name: 'debugMode',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Gibt zusätzliche Debug-Informationen aus',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async poll() {
|
|
||||||
const credentials = await this.getCredentials('libreBookingApi');
|
|
||||||
const baseUrl = credentials.url.replace(/\/$/, '');
|
|
||||||
const username = credentials.username;
|
|
||||||
const password = credentials.password;
|
|
||||||
const triggerMode = this.getNodeParameter('triggerMode');
|
|
||||||
const filters = this.getNodeParameter('filters', {});
|
|
||||||
const options = this.getNodeParameter('options', {});
|
|
||||||
// Debug-Modus
|
|
||||||
const debugMode = options.debugMode || false;
|
|
||||||
const fetchDetails = options.fetchDetails || false;
|
|
||||||
// Workflow Static Data für State-Management
|
|
||||||
const webhookData = this.getWorkflowStaticData('node');
|
|
||||||
let session;
|
|
||||||
try {
|
|
||||||
session = await authenticateTrigger(this, baseUrl, username, password);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const returnData = [];
|
|
||||||
// ==========================================
|
|
||||||
// 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(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: ${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',
|
|
||||||
_dateRange: dateRange,
|
|
||||||
_startDate: start,
|
|
||||||
_endDate: end,
|
|
||||||
_count: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Alle Reservierungen zurückgeben
|
|
||||||
for (const reservation of reservations) {
|
|
||||||
let reservationData = reservation;
|
|
||||||
if (fetchDetails) {
|
|
||||||
try {
|
|
||||||
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Fallback auf Basisdaten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
returnData.push({
|
|
||||||
json: {
|
|
||||||
...reservationData,
|
|
||||||
_eventType: 'getAll',
|
|
||||||
_dateRange: dateRange,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ==========================================
|
|
||||||
// MODE: New Reservations (Polling)
|
|
||||||
// ==========================================
|
|
||||||
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
|
|
||||||
if (!webhookData.seenIds) {
|
|
||||||
webhookData.seenIds = [];
|
|
||||||
webhookData.isFirstPoll = true;
|
|
||||||
}
|
|
||||||
const currentIds = reservations.map((r) => r.referenceNumber);
|
|
||||||
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
|
|
||||||
if (webhookData.isFirstPoll) {
|
|
||||||
webhookData.seenIds = currentIds;
|
|
||||||
webhookData.isFirstPoll = false;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
if (debugMode) {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
_debug: true,
|
|
||||||
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
|
||||||
_savedIds: currentIds.length,
|
|
||||||
_ids: currentIds,
|
|
||||||
_timestamp: webhookData.lastPollTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return null; // Nichts triggern beim ersten Poll
|
|
||||||
}
|
|
||||||
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
|
|
||||||
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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Neue Reservierungen verarbeiten
|
|
||||||
for (const reservation of newReservations) {
|
|
||||||
let reservationData = reservation;
|
|
||||||
if (fetchDetails) {
|
|
||||||
try {
|
|
||||||
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Fallback auf Basisdaten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
returnData.push({
|
|
||||||
json: {
|
|
||||||
...reservationData,
|
|
||||||
_eventType: 'new',
|
|
||||||
_timeFilter: timeFilter,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (debugMode && returnData.length > 0) {
|
|
||||||
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ==========================================
|
|
||||||
// MODE: Updated Reservations (Polling)
|
|
||||||
// ==========================================
|
|
||||||
else if (triggerMode === 'updatedReservations') {
|
|
||||||
const timeWindow = this.getNodeParameter('timeWindow', '14days');
|
|
||||||
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
|
|
||||||
if (!webhookData.reservationHashes) {
|
|
||||||
webhookData.reservationHashes = {};
|
|
||||||
webhookData.isFirstPoll = true;
|
|
||||||
}
|
|
||||||
if (debugMode) {
|
|
||||||
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
|
||||||
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
|
||||||
console.log(`[LibreBooking Trigger] 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.isFirstPoll = false;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
if (debugMode) {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
_debug: true,
|
|
||||||
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
|
|
||||||
_savedHashes: Object.keys(webhookData.reservationHashes).length,
|
|
||||||
_timestamp: webhookData.lastPollTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return null; // Nichts triggern beim ersten Poll
|
|
||||||
}
|
|
||||||
// Geänderte Reservierungen finden
|
|
||||||
let updatedReservations = [];
|
|
||||||
const newHashes = {};
|
|
||||||
for (const reservation of reservations) {
|
|
||||||
const currentHash = getReservationHash(reservation);
|
|
||||||
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
|
|
||||||
newHashes[reservation.referenceNumber] = currentHash;
|
|
||||||
// Nur als "geändert" markieren, wenn:
|
|
||||||
// 1. Wir die Reservierung schon kennen (nicht neu)
|
|
||||||
// 2. Der Hash sich geändert hat
|
|
||||||
if (oldHash && currentHash !== oldHash) {
|
|
||||||
updatedReservations.push(reservation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update Hashes mit allen aktuellen Reservierungen
|
|
||||||
webhookData.reservationHashes = newHashes;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
if (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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Geänderte Reservierungen verarbeiten
|
|
||||||
for (const reservation of updatedReservations) {
|
|
||||||
let reservationData = reservation;
|
|
||||||
if (fetchDetails) {
|
|
||||||
try {
|
|
||||||
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Fallback auf Basisdaten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
returnData.push({
|
|
||||||
json: {
|
|
||||||
...reservationData,
|
|
||||||
_eventType: 'updated',
|
|
||||||
_timeFilter: timeFilter,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (debugMode && returnData.length > 0) {
|
|
||||||
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (returnData.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return [returnData];
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
await signOutTrigger(this, baseUrl, session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.LibreBookingTrigger = LibreBookingTrigger;
|
|
||||||
//# sourceMappingURL=LibreBookingTrigger.node.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
|
|
||||||
|
|
||||||
<!-- Calendar base -->
|
|
||||||
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
|
|
||||||
|
|
||||||
<!-- Calendar header -->
|
|
||||||
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
|
|
||||||
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
|
|
||||||
|
|
||||||
<!-- Calendar rings -->
|
|
||||||
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
|
||||||
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
|
||||||
|
|
||||||
<!-- Grid lines -->
|
|
||||||
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
|
||||||
|
|
||||||
<!-- Booking indicator -->
|
|
||||||
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
|
|
||||||
|
|
||||||
<!-- Check mark -->
|
|
||||||
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
1
index.ts
1
index.ts
|
|
@ -4,4 +4,3 @@
|
||||||
export * from './nodes/LibreBooking/LibreBooking.node';
|
export * from './nodes/LibreBooking/LibreBooking.node';
|
||||||
export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node';
|
export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node';
|
||||||
export * from './credentials/LibreBookingApi.credentials';
|
export * from './credentials/LibreBookingApi.credentials';
|
||||||
export * from './credentials/LibreBookingConfig.credentials';
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -20,20 +20,9 @@ interface ReservationData {
|
||||||
title: string;
|
title: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
description?: string;
|
|
||||||
resourceName?: string;
|
|
||||||
startDateTime?: string;
|
|
||||||
endDateTime?: string;
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowStaticData {
|
|
||||||
seenIds?: string[];
|
|
||||||
reservationHashes?: Record<string, string>;
|
|
||||||
isFirstPoll?: boolean;
|
|
||||||
lastPollTime?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentifizierung bei LibreBooking
|
* Authentifizierung bei LibreBooking
|
||||||
*/
|
*/
|
||||||
|
|
@ -55,7 +44,7 @@ async function authenticateTrigger(
|
||||||
if (!response.isAuthenticated) {
|
if (!response.isAuthenticated) {
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
pollFunctions.getNode(),
|
pollFunctions.getNode(),
|
||||||
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
|
'Authentifizierung fehlgeschlagen',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +56,6 @@ async function authenticateTrigger(
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||||
message: 'Authentifizierung fehlgeschlagen',
|
message: 'Authentifizierung fehlgeschlagen',
|
||||||
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,29 +104,23 @@ async function getReservations(
|
||||||
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
||||||
if (filters.userId) qs.userId = filters.userId;
|
if (filters.userId) qs.userId = filters.userId;
|
||||||
|
|
||||||
try {
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
method: 'GET',
|
||||||
method: 'GET',
|
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'X-Booked-SessionToken': session.sessionToken,
|
||||||
'X-Booked-SessionToken': session.sessionToken,
|
'X-Booked-UserId': session.userId.toString(),
|
||||||
'X-Booked-UserId': session.userId.toString(),
|
},
|
||||||
},
|
qs,
|
||||||
qs,
|
json: true,
|
||||||
json: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return response.reservations || [];
|
return response.reservations || [];
|
||||||
} catch (error: any) {
|
|
||||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
|
||||||
message: 'Fehler beim Abrufen der Reservierungen',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
* Detaillierte Reservierungsdaten abrufen
|
||||||
*/
|
*/
|
||||||
async function getReservationDetails(
|
async function getReservationDetails(
|
||||||
pollFunctions: IPollFunctions,
|
pollFunctions: IPollFunctions,
|
||||||
|
|
@ -146,127 +128,27 @@ async function getReservationDetails(
|
||||||
session: LibreBookingSession,
|
session: LibreBookingSession,
|
||||||
referenceNumber: string,
|
referenceNumber: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
method: 'GET',
|
||||||
method: 'GET',
|
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'X-Booked-SessionToken': session.sessionToken,
|
||||||
'X-Booked-SessionToken': session.sessionToken,
|
'X-Booked-UserId': session.userId.toString(),
|
||||||
'X-Booked-UserId': session.userId.toString(),
|
},
|
||||||
},
|
json: true,
|
||||||
json: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet Zeitraum basierend auf der gewählten Option
|
* Zeitfenster berechnen
|
||||||
*/
|
*/
|
||||||
function getDateRange(dateRange: string): { startDate: string; endDate: string } {
|
function getTimeWindow(timeWindow: string): { start: string; end: 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,
|
|
||||||
): { 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(),
|
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: now.toISOString(),
|
|
||||||
end: endDate.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zeitfenster berechnen für Polling
|
|
||||||
*/
|
|
||||||
function getTimeWindowForPolling(timeWindow: string): { start: string; end: string } {
|
|
||||||
const now = new Date();
|
const 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':
|
||||||
|
|
@ -281,9 +163,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -295,72 +174,14 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter Reservierungen nach Zeitpunkt
|
* Eindeutigen Schlüssel für Reservierung generieren
|
||||||
*/
|
*/
|
||||||
function filterByTime(reservations: ReservationData[], timeFilter: string): ReservationData[] {
|
function getReservationKey(reservation: ReservationData): string {
|
||||||
if (timeFilter === 'all') {
|
return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`;
|
||||||
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 || reservation.startDateTime,
|
|
||||||
endDate: reservation.endDate || reservation.endDateTime,
|
|
||||||
title: reservation.title || '',
|
|
||||||
description: reservation.description || '',
|
|
||||||
resourceId: reservation.resourceId,
|
|
||||||
resourceName: reservation.resourceName || '',
|
|
||||||
userId: reservation.userId,
|
|
||||||
requiresApproval: reservation.requiresApproval,
|
|
||||||
participants: reservation.participants || [],
|
|
||||||
invitees: reservation.invitees || [],
|
|
||||||
statusId: reservation.statusId,
|
|
||||||
};
|
|
||||||
return JSON.stringify(relevantData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LibreBooking Trigger Node
|
* LibreBooking Trigger Node
|
||||||
*
|
|
||||||
* Drei Modi:
|
|
||||||
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
|
||||||
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
|
||||||
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
|
||||||
*/
|
*/
|
||||||
export class LibreBookingTrigger implements INodeType {
|
export class LibreBookingTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
@ -370,7 +191,7 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
||||||
subtitle: '={{$parameter["triggerMode"]}}',
|
subtitle: '={{$parameter["event"]}}',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'LibreBooking Trigger',
|
name: 'LibreBooking Trigger',
|
||||||
},
|
},
|
||||||
|
|
@ -384,185 +205,17 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
],
|
],
|
||||||
polling: true,
|
polling: true,
|
||||||
properties: [
|
properties: [
|
||||||
// =====================================================
|
|
||||||
// TRIGGER MODE SELECTOR
|
|
||||||
// =====================================================
|
|
||||||
{
|
{
|
||||||
displayName: 'Trigger-Modus',
|
displayName: 'Event',
|
||||||
name: 'triggerMode',
|
name: 'event',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
options: [
|
options: [
|
||||||
{
|
{ name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' },
|
||||||
name: 'Alle Abrufen (Einmalig)',
|
{ name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' },
|
||||||
value: 'getAll',
|
{ name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' },
|
||||||
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Neue Reservierungen (Polling)',
|
|
||||||
value: 'newReservations',
|
|
||||||
description: 'Nur bei neuen Reservierungen triggern',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Geänderte Reservierungen (Polling)',
|
|
||||||
value: 'updatedReservations',
|
|
||||||
description: 'Nur bei geänderten Reservierungen triggern',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
default: 'getAll',
|
default: 'newReservation',
|
||||||
description: 'Wählen Sie den Trigger-Modus',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// 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',
|
|
||||||
type: 'dateTime',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['getAll'],
|
|
||||||
dateRange: ['custom'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'Startdatum für den Abruf (leer = heute)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Enddatum',
|
|
||||||
name: 'endDate',
|
|
||||||
type: 'dateTime',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['getAll'],
|
|
||||||
dateRange: ['custom'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
|
|
||||||
},
|
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// POLLING MODE - TIME WINDOW
|
|
||||||
// =====================================================
|
|
||||||
{
|
|
||||||
displayName: 'Zeitfenster',
|
|
||||||
name: 'timeWindow',
|
|
||||||
type: 'options',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['newReservations', 'updatedReservations'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{ name: 'Nächste 7 Tage', value: '7days' },
|
|
||||||
{ name: 'Nächste 14 Tage', value: '14days' },
|
|
||||||
{ name: 'Nächste 30 Tage', value: '30days' },
|
|
||||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
|
||||||
{ 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',
|
|
||||||
type: 'notice',
|
|
||||||
default: '',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
triggerMode: ['newReservations', 'updatedReservations'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description:
|
|
||||||
'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// FILTERS (ALL MODES)
|
|
||||||
// =====================================================
|
|
||||||
{
|
{
|
||||||
displayName: 'Filter',
|
displayName: 'Filter',
|
||||||
name: 'filters',
|
name: 'filters',
|
||||||
|
|
@ -570,33 +223,23 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
placeholder: 'Filter hinzufügen',
|
placeholder: 'Filter hinzufügen',
|
||||||
default: {},
|
default: {},
|
||||||
options: [
|
options: [
|
||||||
{
|
{ displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' },
|
||||||
displayName: 'Ressourcen-ID',
|
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
||||||
name: 'resourceId',
|
{ displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' },
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diese Ressource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Zeitplan-ID',
|
|
||||||
name: 'scheduleId',
|
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diesen Zeitplan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Benutzer-ID',
|
|
||||||
name: 'userId',
|
|
||||||
type: 'number',
|
|
||||||
default: '',
|
|
||||||
description: 'Nur Reservierungen für diesen Benutzer',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
// =====================================================
|
displayName: 'Zeitfenster',
|
||||||
// OPTIONS (ALL MODES)
|
name: 'timeWindow',
|
||||||
// =====================================================
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Nächste 7 Tage', value: '7days' },
|
||||||
|
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||||
|
{ name: 'Nächste 30 Tage', value: '30days' },
|
||||||
|
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||||
|
],
|
||||||
|
default: '14days',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Optionen',
|
displayName: 'Optionen',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
|
@ -604,21 +247,7 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
placeholder: 'Option hinzufügen',
|
placeholder: 'Option hinzufügen',
|
||||||
default: {},
|
default: {},
|
||||||
options: [
|
options: [
|
||||||
{
|
{ displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false },
|
||||||
displayName: 'Detaillierte Daten Abrufen',
|
|
||||||
name: 'fetchDetails',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description:
|
|
||||||
'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Debug-Modus',
|
|
||||||
name: 'debugMode',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
description: 'Gibt zusätzliche Debug-Informationen aus',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -630,16 +259,13 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
const username = credentials.username as string;
|
const username = credentials.username as string;
|
||||||
const password = credentials.password as string;
|
const password = credentials.password as string;
|
||||||
|
|
||||||
const triggerMode = this.getNodeParameter('triggerMode') as string;
|
const event = this.getNodeParameter('event') as string;
|
||||||
const filters = this.getNodeParameter('filters', {}) as any;
|
const filters = this.getNodeParameter('filters', {}) as any;
|
||||||
|
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||||
const options = this.getNodeParameter('options', {}) as any;
|
const options = this.getNodeParameter('options', {}) as any;
|
||||||
|
|
||||||
// Debug-Modus
|
const workflowStaticData = this.getWorkflowStaticData('node');
|
||||||
const debugMode = options.debugMode || false;
|
const previousReservations = (workflowStaticData.reservations as Record<string, string>) || {};
|
||||||
const fetchDetails = options.fetchDetails || false;
|
|
||||||
|
|
||||||
// Workflow Static Data für State-Management
|
|
||||||
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
|
||||||
|
|
||||||
let session: LibreBookingSession;
|
let session: LibreBookingSession;
|
||||||
try {
|
try {
|
||||||
|
|
@ -649,341 +275,76 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { start, end } = getTimeWindow(timeWindow);
|
||||||
|
|
||||||
|
const reservations = await getReservations(
|
||||||
|
this,
|
||||||
|
baseUrl,
|
||||||
|
session,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
const returnData: INodeExecutionData[] = [];
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
const currentReservations: Record<string, string> = {};
|
||||||
|
|
||||||
// ==========================================
|
for (const reservation of reservations) {
|
||||||
// MODE: Get All (One-Time / Every Poll)
|
const refNumber = reservation.referenceNumber;
|
||||||
// ==========================================
|
const reservationKey = getReservationKey(reservation);
|
||||||
if (triggerMode === 'getAll') {
|
currentReservations[refNumber] = reservationKey;
|
||||||
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(
|
const isNew = !previousReservations[refNumber];
|
||||||
dateRange,
|
const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey;
|
||||||
startDate || undefined,
|
|
||||||
endDate || undefined,
|
|
||||||
14,
|
|
||||||
);
|
|
||||||
|
|
||||||
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
let shouldTrigger = false;
|
||||||
|
let eventType = '';
|
||||||
|
|
||||||
if (debugMode) {
|
if (event === 'newReservation' && isNew) {
|
||||||
console.log(
|
shouldTrigger = true;
|
||||||
`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`,
|
eventType = 'new';
|
||||||
);
|
} else if (event === 'updatedReservation' && isUpdated) {
|
||||||
console.log(`[LibreBooking Trigger] Date Range: ${dateRange}`);
|
shouldTrigger = true;
|
||||||
console.log(`[LibreBooking Trigger] Period: ${start} to ${end}`);
|
eventType = 'updated';
|
||||||
|
} else if (event === 'allReservations' && (isNew || isUpdated)) {
|
||||||
|
shouldTrigger = true;
|
||||||
|
eventType = isNew ? 'new' : 'updated';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reservations.length === 0) {
|
if (shouldTrigger) {
|
||||||
if (debugMode) {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
_debug: true,
|
|
||||||
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
|
||||||
_dateRange: dateRange,
|
|
||||||
_startDate: start,
|
|
||||||
_endDate: end,
|
|
||||||
_count: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alle Reservierungen zurückgeben
|
|
||||||
for (const reservation of reservations) {
|
|
||||||
let reservationData = reservation;
|
let reservationData = reservation;
|
||||||
|
|
||||||
if (fetchDetails) {
|
if (options.fetchDetails) {
|
||||||
try {
|
try {
|
||||||
const details = await getReservationDetails(
|
reservationData = await getReservationDetails(
|
||||||
this,
|
this,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
session,
|
session,
|
||||||
reservation.referenceNumber,
|
refNumber,
|
||||||
);
|
);
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fallback auf Basisdaten
|
reservationData = reservation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
returnData.push({
|
returnData.push({
|
||||||
json: {
|
json: {
|
||||||
...reservationData,
|
...reservationData,
|
||||||
_eventType: 'getAll',
|
_eventType: eventType,
|
||||||
_dateRange: dateRange,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
_triggeredAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
workflowStaticData.reservations = currentReservations;
|
||||||
// MODE: New Reservations (Polling)
|
|
||||||
// ==========================================
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Initialisiere seenIds beim ersten Poll
|
|
||||||
if (!webhookData.seenIds) {
|
|
||||||
webhookData.seenIds = [];
|
|
||||||
webhookData.isFirstPoll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
console.log(`[LibreBooking Trigger] New Reservations Mode`);
|
|
||||||
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
|
||||||
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
|
|
||||||
console.log(
|
|
||||||
`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
|
||||||
if (webhookData.isFirstPoll) {
|
|
||||||
webhookData.seenIds = currentIds;
|
|
||||||
webhookData.isFirstPoll = false;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
_debug: true,
|
|
||||||
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
|
||||||
_savedIds: currentIds.length,
|
|
||||||
_ids: currentIds,
|
|
||||||
_timestamp: webhookData.lastPollTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Nichts triggern beim ersten Poll
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
|
|
||||||
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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neue Reservierungen verarbeiten
|
|
||||||
for (const reservation of newReservations) {
|
|
||||||
let reservationData = reservation;
|
|
||||||
|
|
||||||
if (fetchDetails) {
|
|
||||||
try {
|
|
||||||
const details = await getReservationDetails(
|
|
||||||
this,
|
|
||||||
baseUrl,
|
|
||||||
session,
|
|
||||||
reservation.referenceNumber,
|
|
||||||
);
|
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback auf Basisdaten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
returnData.push({
|
|
||||||
json: {
|
|
||||||
...reservationData,
|
|
||||||
_eventType: 'new',
|
|
||||||
_timeFilter: timeFilter,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugMode && returnData.length > 0) {
|
|
||||||
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MODE: Updated Reservations (Polling)
|
|
||||||
// ==========================================
|
|
||||||
else if (triggerMode === 'updatedReservations') {
|
|
||||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
|
||||||
const timeFilter = this.getNodeParameter('timeFilter', 'all') as string;
|
|
||||||
const { start, end } = getTimeWindowForPolling(timeWindow);
|
|
||||||
|
|
||||||
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
|
||||||
|
|
||||||
// Initialisiere reservationHashes beim ersten Poll
|
|
||||||
if (!webhookData.reservationHashes) {
|
|
||||||
webhookData.reservationHashes = {};
|
|
||||||
webhookData.isFirstPoll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
|
||||||
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
|
||||||
console.log(`[LibreBooking Trigger] 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.isFirstPoll = false;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
json: {
|
|
||||||
_debug: true,
|
|
||||||
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
|
|
||||||
_savedHashes: Object.keys(webhookData.reservationHashes).length,
|
|
||||||
_timestamp: webhookData.lastPollTime,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Nichts triggern beim ersten Poll
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geänderte Reservierungen finden
|
|
||||||
let updatedReservations: ReservationData[] = [];
|
|
||||||
const newHashes: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const reservation of reservations) {
|
|
||||||
const currentHash = getReservationHash(reservation);
|
|
||||||
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
|
|
||||||
newHashes[reservation.referenceNumber] = currentHash;
|
|
||||||
|
|
||||||
// Nur als "geändert" markieren, wenn:
|
|
||||||
// 1. Wir die Reservierung schon kennen (nicht neu)
|
|
||||||
// 2. Der Hash sich geändert hat
|
|
||||||
if (oldHash && currentHash !== oldHash) {
|
|
||||||
updatedReservations.push(reservation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Hashes mit allen aktuellen Reservierungen
|
|
||||||
webhookData.reservationHashes = newHashes;
|
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
|
||||||
|
|
||||||
if (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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geänderte Reservierungen verarbeiten
|
|
||||||
for (const reservation of updatedReservations) {
|
|
||||||
let reservationData = reservation;
|
|
||||||
|
|
||||||
if (fetchDetails) {
|
|
||||||
try {
|
|
||||||
const details = await getReservationDetails(
|
|
||||||
this,
|
|
||||||
baseUrl,
|
|
||||||
session,
|
|
||||||
reservation.referenceNumber,
|
|
||||||
);
|
|
||||||
if (details) {
|
|
||||||
reservationData = details;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback auf Basisdaten
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
returnData.push({
|
|
||||||
json: {
|
|
||||||
...reservationData,
|
|
||||||
_eventType: 'updated',
|
|
||||||
_timeFilter: timeFilter,
|
|
||||||
_triggeredAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugMode && returnData.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnData.length === 0) {
|
if (returnData.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [returnData];
|
return [returnData];
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
await signOutTrigger(this, baseUrl, session);
|
await signOutTrigger(this, baseUrl, session);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-librebooking",
|
"name": "n8n-nodes-librebooking",
|
||||||
"version": "1.2.2",
|
"version": "1.1.0",
|
||||||
"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",
|
||||||
|
|
@ -60,8 +60,7 @@
|
||||||
"n8n": {
|
"n8n": {
|
||||||
"n8nNodesApiVersion": 1,
|
"n8nNodesApiVersion": 1,
|
||||||
"credentials": [
|
"credentials": [
|
||||||
"dist/credentials/LibreBookingApi.credentials.js",
|
"dist/credentials/LibreBookingApi.credentials.js"
|
||||||
"dist/credentials/LibreBookingConfig.credentials.js"
|
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
"dist/nodes/LibreBooking/LibreBooking.node.js",
|
"dist/nodes/LibreBooking/LibreBooking.node.js",
|
||||||
|
|
|
||||||
609
test-api.ts
609
test-api.ts
|
|
@ -1,609 +0,0 @@
|
||||||
/**
|
|
||||||
* LibreBooking API Test Script
|
|
||||||
*
|
|
||||||
* Testet alle wichtigen API-Endpunkte mit echten Credentials
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as https from 'https';
|
|
||||||
import * as http from 'http';
|
|
||||||
|
|
||||||
const BASE_URL = 'https://librebooking.zell-cloud.de';
|
|
||||||
const USERNAME = 'sebastian.zell@zell-aufmass.de';
|
|
||||||
const PASSWORD = 'wanUQ4uVqU6lfP';
|
|
||||||
|
|
||||||
interface Session {
|
|
||||||
sessionToken: string;
|
|
||||||
userId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestResult {
|
|
||||||
name: string;
|
|
||||||
success: boolean;
|
|
||||||
data?: any;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: TestResult[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP Request Helper
|
|
||||||
*/
|
|
||||||
function makeRequest(
|
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
body?: any,
|
|
||||||
session?: Session,
|
|
||||||
qs?: Record<string, string>
|
|
||||||
): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = new URL(path, BASE_URL);
|
|
||||||
|
|
||||||
if (qs) {
|
|
||||||
Object.entries(qs).forEach(([key, value]) => {
|
|
||||||
if (value) url.searchParams.append(key, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
headers['X-Booked-SessionToken'] = session.sessionToken;
|
|
||||||
headers['X-Booked-UserId'] = session.userId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method,
|
|
||||||
hostname: url.hostname,
|
|
||||||
path: url.pathname + url.search,
|
|
||||||
headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
const httpModule = url.protocol === 'https:' ? https : http;
|
|
||||||
|
|
||||||
const req = httpModule.request(options, (res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => { data += chunk; });
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
if (data) {
|
|
||||||
resolve(JSON.parse(data));
|
|
||||||
} else {
|
|
||||||
resolve({ success: true });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
resolve({ rawData: data });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', (error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
req.write(JSON.stringify(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Authentication
|
|
||||||
*/
|
|
||||||
async function testAuthentication(): Promise<Session> {
|
|
||||||
console.log('\n=== 1. Testing Authentication ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'POST',
|
|
||||||
'/Web/Services/index.php/Authentication/Authenticate',
|
|
||||||
{ username: USERNAME, password: PASSWORD }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.isAuthenticated) {
|
|
||||||
console.log('✅ Authentication successful');
|
|
||||||
console.log(` Session Token: ${response.sessionToken.substring(0, 20)}...`);
|
|
||||||
console.log(` User ID: ${response.userId}`);
|
|
||||||
console.log(` Session Expires: ${response.sessionExpires}`);
|
|
||||||
results.push({ name: 'Authentication', success: true, data: { userId: response.userId } });
|
|
||||||
return { sessionToken: response.sessionToken, userId: response.userId };
|
|
||||||
} else {
|
|
||||||
throw new Error('Authentication failed');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Authentication failed:', error.message);
|
|
||||||
results.push({ name: 'Authentication', success: false, error: error.message });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Reservations
|
|
||||||
*/
|
|
||||||
async function testGetReservations(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 2. Testing Get Reservations ===');
|
|
||||||
try {
|
|
||||||
// Test without date filters (should return next 2 weeks)
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Reservations/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Reservations fetched: ${response.reservations?.length || 0} found`);
|
|
||||||
|
|
||||||
if (response.reservations && response.reservations.length > 0) {
|
|
||||||
const res = response.reservations[0];
|
|
||||||
console.log(` Example: ${res.title || 'No title'} (${res.referenceNumber})`);
|
|
||||||
console.log(` Start: ${res.startDate}`);
|
|
||||||
console.log(` Resource: ${res.resourceName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Reservations (no filter)', success: true, data: { count: response.reservations?.length || 0 } });
|
|
||||||
|
|
||||||
// Test with date filter (Feb 7-14, 2026)
|
|
||||||
console.log('\n Testing with date filter (2026-02-07 to 2026-02-14)...');
|
|
||||||
const responseFiltered = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Reservations/',
|
|
||||||
undefined,
|
|
||||||
session,
|
|
||||||
{
|
|
||||||
startDateTime: '2026-02-07T00:00:00',
|
|
||||||
endDateTime: '2026-02-14T23:59:59'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Filtered reservations: ${responseFiltered.reservations?.length || 0} found`);
|
|
||||||
results.push({ name: 'Get Reservations (date filter)', success: true, data: { count: responseFiltered.reservations?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Reservations failed:', error.message);
|
|
||||||
results.push({ name: 'Get Reservations', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Resources
|
|
||||||
*/
|
|
||||||
async function testGetResources(session: Session): Promise<number | null> {
|
|
||||||
console.log('\n=== 3. Testing Get Resources ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Resources/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Resources fetched: ${response.resources?.length || 0} found`);
|
|
||||||
|
|
||||||
let firstResourceId: number | null = null;
|
|
||||||
if (response.resources && response.resources.length > 0) {
|
|
||||||
const res = response.resources[0];
|
|
||||||
firstResourceId = res.resourceId;
|
|
||||||
console.log(` Example: ${res.name} (ID: ${res.resourceId})`);
|
|
||||||
console.log(` Schedule ID: ${res.scheduleId}`);
|
|
||||||
console.log(` Custom Attributes: ${res.customAttributes?.length || 0}`);
|
|
||||||
|
|
||||||
if (res.customAttributes && res.customAttributes.length > 0) {
|
|
||||||
res.customAttributes.forEach((attr: any) => {
|
|
||||||
console.log(` - ${attr.label}: ${attr.value || '(no value)'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Resources', success: true, data: { count: response.resources?.length || 0 } });
|
|
||||||
return firstResourceId;
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Resources failed:', error.message);
|
|
||||||
results.push({ name: 'Get Resources', success: false, error: error.message });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Single Resource (with custom attributes)
|
|
||||||
*/
|
|
||||||
async function testGetSingleResource(session: Session, resourceId: number): Promise<void> {
|
|
||||||
console.log('\n=== 4. Testing Get Single Resource (with custom attributes) ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
`/Web/Services/index.php/Resources/${resourceId}`,
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Resource fetched: ${response.name}`);
|
|
||||||
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
|
|
||||||
|
|
||||||
if (response.customAttributes && response.customAttributes.length > 0) {
|
|
||||||
response.customAttributes.forEach((attr: any) => {
|
|
||||||
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Single Resource', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Single Resource failed:', error.message);
|
|
||||||
results.push({ name: 'Get Single Resource', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Users
|
|
||||||
*/
|
|
||||||
async function testGetUsers(session: Session): Promise<number | null> {
|
|
||||||
console.log('\n=== 5. Testing Get Users ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Users/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Users fetched: ${response.users?.length || 0} found`);
|
|
||||||
|
|
||||||
let firstUserId: number | null = null;
|
|
||||||
if (response.users && response.users.length > 0) {
|
|
||||||
const user = response.users[0];
|
|
||||||
firstUserId = user.id;
|
|
||||||
console.log(` Example: ${user.firstName} ${user.lastName} (ID: ${user.id})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Users', success: true, data: { count: response.users?.length || 0 } });
|
|
||||||
return firstUserId;
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Users failed:', error.message);
|
|
||||||
results.push({ name: 'Get Users', success: false, error: error.message });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Single User (with custom attributes)
|
|
||||||
*/
|
|
||||||
async function testGetSingleUser(session: Session, userId: number): Promise<void> {
|
|
||||||
console.log('\n=== 6. Testing Get Single User (with custom attributes) ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
`/Web/Services/index.php/Users/${userId}`,
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ User fetched: ${response.firstName} ${response.lastName}`);
|
|
||||||
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
|
|
||||||
|
|
||||||
if (response.customAttributes && response.customAttributes.length > 0) {
|
|
||||||
response.customAttributes.forEach((attr: any) => {
|
|
||||||
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Single User', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Single User failed:', error.message);
|
|
||||||
results.push({ name: 'Get Single User', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Schedules
|
|
||||||
*/
|
|
||||||
async function testGetSchedules(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 7. Testing Get Schedules ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Schedules/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Schedules fetched: ${response.schedules?.length || 0} found`);
|
|
||||||
|
|
||||||
if (response.schedules && response.schedules.length > 0) {
|
|
||||||
const schedule = response.schedules[0];
|
|
||||||
console.log(` Example: ${schedule.name} (ID: ${schedule.id})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Schedules', success: true, data: { count: response.schedules?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Schedules failed:', error.message);
|
|
||||||
results.push({ name: 'Get Schedules', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Get Attributes by Category
|
|
||||||
*/
|
|
||||||
async function testGetAttributes(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 8. Testing Get Attributes by Category ===');
|
|
||||||
|
|
||||||
const categories = [
|
|
||||||
{ id: 1, name: 'Reservation' },
|
|
||||||
{ id: 2, name: 'User' },
|
|
||||||
{ id: 4, name: 'Resource' },
|
|
||||||
{ id: 5, name: 'Resource Type' },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const cat of categories) {
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
`/Web/Services/index.php/Attributes/Category/${cat.id}`,
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ ${cat.name} Attributes: ${response.attributes?.length || 0} found`);
|
|
||||||
|
|
||||||
if (response.attributes && response.attributes.length > 0) {
|
|
||||||
response.attributes.forEach((attr: any) => {
|
|
||||||
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Type: ${attr.type}, Required: ${attr.required}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: `Get Attributes (${cat.name})`, success: true, data: { count: response.attributes?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log(`❌ Get ${cat.name} Attributes failed:`, error.message);
|
|
||||||
results.push({ name: `Get Attributes (${cat.name})`, success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Create, Update, Delete Reservation
|
|
||||||
*/
|
|
||||||
async function testReservationCRUD(session: Session, resourceId: number): Promise<void> {
|
|
||||||
console.log('\n=== 9. Testing Reservation CRUD ===');
|
|
||||||
|
|
||||||
// Create
|
|
||||||
console.log(' Creating test reservation...');
|
|
||||||
try {
|
|
||||||
const createResponse = await makeRequest(
|
|
||||||
'POST',
|
|
||||||
'/Web/Services/index.php/Reservations/',
|
|
||||||
{
|
|
||||||
title: 'API Test Reservation',
|
|
||||||
description: 'Created by n8n node test script',
|
|
||||||
resourceId: resourceId,
|
|
||||||
startDateTime: '2026-02-07T10:00:00',
|
|
||||||
endDateTime: '2026-02-07T11:00:00',
|
|
||||||
userId: session.userId,
|
|
||||||
termsAccepted: true,
|
|
||||||
allowParticipation: false,
|
|
||||||
},
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
if (createResponse.referenceNumber) {
|
|
||||||
console.log(`✅ Reservation created: ${createResponse.referenceNumber}`);
|
|
||||||
results.push({ name: 'Create Reservation', success: true, data: { referenceNumber: createResponse.referenceNumber } });
|
|
||||||
|
|
||||||
const refNum = createResponse.referenceNumber;
|
|
||||||
|
|
||||||
// Get the created reservation
|
|
||||||
console.log(' Fetching created reservation...');
|
|
||||||
const getResponse = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
`/Web/Services/index.php/Reservations/${refNum}`,
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
console.log(`✅ Reservation fetched: ${getResponse.title}`);
|
|
||||||
console.log(` Custom Attributes: ${getResponse.customAttributes?.length || 0}`);
|
|
||||||
if (getResponse.customAttributes && getResponse.customAttributes.length > 0) {
|
|
||||||
getResponse.customAttributes.forEach((attr: any) => {
|
|
||||||
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
results.push({ name: 'Get Created Reservation', success: true });
|
|
||||||
|
|
||||||
// Update
|
|
||||||
console.log(' Updating reservation...');
|
|
||||||
const updateResponse = await makeRequest(
|
|
||||||
'POST',
|
|
||||||
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
|
|
||||||
{
|
|
||||||
title: 'API Test Reservation UPDATED',
|
|
||||||
description: 'Updated by n8n node test script',
|
|
||||||
resourceId: resourceId,
|
|
||||||
startDateTime: '2026-02-07T10:00:00',
|
|
||||||
endDateTime: '2026-02-07T12:00:00',
|
|
||||||
termsAccepted: true,
|
|
||||||
allowParticipation: false,
|
|
||||||
},
|
|
||||||
session
|
|
||||||
);
|
|
||||||
console.log(`✅ Reservation updated`);
|
|
||||||
results.push({ name: 'Update Reservation', success: true });
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
console.log(' Deleting reservation...');
|
|
||||||
const deleteResponse = await makeRequest(
|
|
||||||
'DELETE',
|
|
||||||
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
console.log(`✅ Reservation deleted`);
|
|
||||||
results.push({ name: 'Delete Reservation', success: true });
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log('❌ Create Reservation failed - no reference number returned');
|
|
||||||
console.log(' Response:', JSON.stringify(createResponse, null, 2));
|
|
||||||
results.push({ name: 'Create Reservation', success: false, error: JSON.stringify(createResponse) });
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Reservation CRUD failed:', error.message);
|
|
||||||
results.push({ name: 'Reservation CRUD', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Groups
|
|
||||||
*/
|
|
||||||
async function testGetGroups(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 10. Testing Get Groups ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Groups/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Groups fetched: ${response.groups?.length || 0} found`);
|
|
||||||
|
|
||||||
if (response.groups && response.groups.length > 0) {
|
|
||||||
const group = response.groups[0];
|
|
||||||
console.log(` Example: ${group.name} (ID: ${group.id})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Groups', success: true, data: { count: response.groups?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Groups failed:', error.message);
|
|
||||||
results.push({ name: 'Get Groups', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Accessories
|
|
||||||
*/
|
|
||||||
async function testGetAccessories(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 11. Testing Get Accessories ===');
|
|
||||||
try {
|
|
||||||
const response = await makeRequest(
|
|
||||||
'GET',
|
|
||||||
'/Web/Services/index.php/Accessories/',
|
|
||||||
undefined,
|
|
||||||
session
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Accessories fetched: ${response.accessories?.length || 0} found`);
|
|
||||||
|
|
||||||
if (response.accessories && response.accessories.length > 0) {
|
|
||||||
const acc = response.accessories[0];
|
|
||||||
console.log(` Example: ${acc.name} (ID: ${acc.id})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ name: 'Get Accessories', success: true, data: { count: response.accessories?.length || 0 } });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Get Accessories failed:', error.message);
|
|
||||||
results.push({ name: 'Get Accessories', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SignOut
|
|
||||||
*/
|
|
||||||
async function testSignOut(session: Session): Promise<void> {
|
|
||||||
console.log('\n=== 12. Testing Sign Out ===');
|
|
||||||
try {
|
|
||||||
await makeRequest(
|
|
||||||
'POST',
|
|
||||||
'/Web/Services/index.php/Authentication/SignOut',
|
|
||||||
{
|
|
||||||
userId: session.userId,
|
|
||||||
sessionToken: session.sessionToken,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log('✅ Sign Out successful');
|
|
||||||
results.push({ name: 'Sign Out', success: true });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('❌ Sign Out failed:', error.message);
|
|
||||||
results.push({ name: 'Sign Out', success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print Summary
|
|
||||||
*/
|
|
||||||
function printSummary(): void {
|
|
||||||
console.log('\n========================================');
|
|
||||||
console.log(' TEST SUMMARY');
|
|
||||||
console.log('========================================');
|
|
||||||
|
|
||||||
const passed = results.filter(r => r.success).length;
|
|
||||||
const failed = results.filter(r => !r.success).length;
|
|
||||||
|
|
||||||
console.log(`\nTotal Tests: ${results.length}`);
|
|
||||||
console.log(`✅ Passed: ${passed}`);
|
|
||||||
console.log(`❌ Failed: ${failed}`);
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
console.log('\nFailed Tests:');
|
|
||||||
results.filter(r => !r.success).forEach(r => {
|
|
||||||
console.log(` - ${r.name}: ${r.error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n========================================\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Test Runner
|
|
||||||
*/
|
|
||||||
async function runTests(): Promise<void> {
|
|
||||||
console.log('========================================');
|
|
||||||
console.log(' LibreBooking API Test Suite');
|
|
||||||
console.log(` URL: ${BASE_URL}`);
|
|
||||||
console.log(` User: ${USERNAME}`);
|
|
||||||
console.log('========================================');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Authentication
|
|
||||||
const session = await testAuthentication();
|
|
||||||
|
|
||||||
// Get operations
|
|
||||||
await testGetReservations(session);
|
|
||||||
const resourceId = await testGetResources(session);
|
|
||||||
if (resourceId) {
|
|
||||||
await testGetSingleResource(session, resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = await testGetUsers(session);
|
|
||||||
if (userId) {
|
|
||||||
await testGetSingleUser(session, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await testGetSchedules(session);
|
|
||||||
await testGetAttributes(session);
|
|
||||||
await testGetGroups(session);
|
|
||||||
await testGetAccessories(session);
|
|
||||||
|
|
||||||
// CRUD operations
|
|
||||||
if (resourceId) {
|
|
||||||
await testReservationCRUD(session, resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign out
|
|
||||||
await testSignOut(session);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('\n❌ Test suite failed:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
printSummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tests
|
|
||||||
runTests().catch(console.error);
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
{
|
|
||||||
"name": "LibreBooking v1.2.0 Test Workflows",
|
|
||||||
"description": "Beispiel-Workflows für die neuen Features in Version 1.2.0",
|
|
||||||
"workflows": [
|
|
||||||
{
|
|
||||||
"name": "01 - Reservierung mit Custom Attributes erstellen",
|
|
||||||
"description": "Erstellt eine Reservierung mit benutzerdefinierten Attributen",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.manualTrigger",
|
|
||||||
"name": "Manuell starten",
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBooking",
|
|
||||||
"name": "Attribute abrufen",
|
|
||||||
"parameters": {
|
|
||||||
"resource": "attribute",
|
|
||||||
"operation": "getByCategory",
|
|
||||||
"categoryId": 1
|
|
||||||
},
|
|
||||||
"position": [450, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBooking",
|
|
||||||
"name": "Reservierung erstellen",
|
|
||||||
"parameters": {
|
|
||||||
"resource": "reservation",
|
|
||||||
"operation": "create",
|
|
||||||
"resourceId": 1,
|
|
||||||
"startDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T10:00:00",
|
|
||||||
"endDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T11:00:00",
|
|
||||||
"termsAccepted": true,
|
|
||||||
"title": "Test Reservierung mit Attributen",
|
|
||||||
"customAttributes": {
|
|
||||||
"attribute": [
|
|
||||||
{
|
|
||||||
"attributeId": 1,
|
|
||||||
"attributeValue": "Mein Attribut-Wert"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [650, 300]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Manuell starten": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Attribute abrufen", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Attribute abrufen": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Reservierung erstellen", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "02 - Ressource mit Custom Attributes erstellen",
|
|
||||||
"description": "Erstellt eine Ressource mit benutzerdefinierten Attributen",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.manualTrigger",
|
|
||||||
"name": "Manuell starten",
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBooking",
|
|
||||||
"name": "Ressource erstellen",
|
|
||||||
"parameters": {
|
|
||||||
"resource": "resource",
|
|
||||||
"operation": "create",
|
|
||||||
"resourceName": "Testraum mit Ausstattung",
|
|
||||||
"scheduleIdForResource": 1,
|
|
||||||
"resourceCustomAttributes": {
|
|
||||||
"attribute": [
|
|
||||||
{
|
|
||||||
"attributeId": 10,
|
|
||||||
"attributeValue": "Beamer, Whiteboard, 20 Plätze"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resourceOptions": {
|
|
||||||
"description": "Ein Konferenzraum mit voller Ausstattung",
|
|
||||||
"maxParticipants": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [450, 300]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Manuell starten": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Ressource erstellen", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "03 - Trigger für neue Reservierungen (ohne Altdaten)",
|
|
||||||
"description": "Überwacht neue Reservierungen - beim ersten Start werden existierende Reservierungen NICHT getriggert",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
|
||||||
"name": "Neue Reservierungen",
|
|
||||||
"parameters": {
|
|
||||||
"event": "newReservation",
|
|
||||||
"timeWindow": "14days",
|
|
||||||
"options": {
|
|
||||||
"fetchDetails": false,
|
|
||||||
"debugMode": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.if",
|
|
||||||
"name": "Debug-Check",
|
|
||||||
"parameters": {
|
|
||||||
"conditions": {
|
|
||||||
"boolean": [
|
|
||||||
{
|
|
||||||
"value1": "={{ $json._debug }}",
|
|
||||||
"value2": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [450, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"name": "Debug-Info",
|
|
||||||
"parameters": {
|
|
||||||
"values": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"name": "info",
|
|
||||||
"value": "Erster Poll - Daten wurden gespeichert"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [650, 200]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"name": "Neue Reservierung verarbeiten",
|
|
||||||
"parameters": {
|
|
||||||
"values": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"value": "Neue Reservierung: {{ $json.title }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [650, 400]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Neue Reservierungen": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Debug-Check", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Debug-Check": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Debug-Info", "type": "main", "index": 0 }],
|
|
||||||
[{ "node": "Neue Reservierung verarbeiten", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "04 - Trigger für geänderte Reservierungen",
|
|
||||||
"description": "Überwacht Änderungen an bestehenden Reservierungen mittels Hash-Vergleich",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
|
||||||
"name": "Geänderte Reservierungen",
|
|
||||||
"parameters": {
|
|
||||||
"event": "updatedReservation",
|
|
||||||
"timeWindow": "14days",
|
|
||||||
"options": {
|
|
||||||
"fetchDetails": true,
|
|
||||||
"debugMode": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"name": "Änderung protokollieren",
|
|
||||||
"parameters": {
|
|
||||||
"values": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"value": "Reservierung geändert: {{ $json.referenceNumber }} - {{ $json.title }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "changedAt",
|
|
||||||
"value": "={{ $json._triggeredAt }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [450, 300]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Geänderte Reservierungen": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Änderung protokollieren", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "05 - Config Node Verwendung",
|
|
||||||
"description": "Zeigt die Verwendung des LibreBooking Config Credentials für Standardwerte",
|
|
||||||
"notes": "Voraussetzung: LibreBooking Config Credential muss angelegt und mit dem Node verbunden sein",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.manualTrigger",
|
|
||||||
"name": "Manuell starten",
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBooking",
|
|
||||||
"name": "Reservierung mit Defaults",
|
|
||||||
"parameters": {
|
|
||||||
"resource": "reservation",
|
|
||||||
"operation": "create",
|
|
||||||
"resourceId": 1,
|
|
||||||
"startDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T14:00:00",
|
|
||||||
"endDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T15:00:00",
|
|
||||||
"title": "Reservierung mit Config-Defaults"
|
|
||||||
},
|
|
||||||
"credentials": {
|
|
||||||
"libreBookingApi": "LibreBooking API",
|
|
||||||
"libreBookingConfig": "LibreBooking Config"
|
|
||||||
},
|
|
||||||
"position": [450, 300],
|
|
||||||
"notes": "termsAccepted und allowParticipation werden aus dem Config Node übernommen"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Manuell starten": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Reservierung mit Defaults", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "06 - Alle Events überwachen",
|
|
||||||
"description": "Überwacht sowohl neue als auch geänderte Reservierungen",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
|
||||||
"name": "Alle Reservierungs-Events",
|
|
||||||
"parameters": {
|
|
||||||
"event": "allReservations",
|
|
||||||
"timeWindow": "30days",
|
|
||||||
"filters": {
|
|
||||||
"resourceId": ""
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"fetchDetails": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [250, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.switch",
|
|
||||||
"name": "Event-Typ prüfen",
|
|
||||||
"parameters": {
|
|
||||||
"dataType": "string",
|
|
||||||
"value1": "={{ $json._eventType }}",
|
|
||||||
"rules": {
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"value2": "new"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value2": "updated"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [450, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"name": "Neue Reservierung",
|
|
||||||
"parameters": {
|
|
||||||
"values": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"name": "action",
|
|
||||||
"value": "NEU: {{ $json.title }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [650, 200]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"name": "Geänderte Reservierung",
|
|
||||||
"parameters": {
|
|
||||||
"values": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"name": "action",
|
|
||||||
"value": "GEÄNDERT: {{ $json.title }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": [650, 400]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Alle Reservierungs-Events": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Event-Typ prüfen", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Event-Typ prüfen": {
|
|
||||||
"main": [
|
|
||||||
[{ "node": "Neue Reservierung", "type": "main", "index": 0 }],
|
|
||||||
[{ "node": "Geänderte Reservierung", "type": "main", "index": 0 }]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,485 +0,0 @@
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
149
upload-to-git.sh
149
upload-to-git.sh
|
|
@ -1,149 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Git Upload Helper Script
|
|
||||||
# LibreBooking n8n Node v1.2.0
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Git Upload Vorbereitung"
|
|
||||||
echo "========================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Prüfe ob Git installiert ist
|
|
||||||
if ! command -v git &> /dev/null; then
|
|
||||||
echo "❌ Git ist nicht installiert!"
|
|
||||||
echo " Installiere mit: sudo apt install git"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe ob Git Repository existiert
|
|
||||||
if [ ! -d .git ]; then
|
|
||||||
echo "❌ Kein Git Repository gefunden!"
|
|
||||||
echo " Möchtest du eines initialisieren? (y/n)"
|
|
||||||
read -p " > " init_git
|
|
||||||
if [[ $init_git =~ ^[Yy]$ ]]; then
|
|
||||||
git init
|
|
||||||
echo "✅ Git Repository initialisiert"
|
|
||||||
else
|
|
||||||
echo "Abgebrochen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe ob Remote existiert
|
|
||||||
echo ""
|
|
||||||
echo "📡 Remote Konfiguration:"
|
|
||||||
if git remote | grep -q origin; then
|
|
||||||
current_remote=$(git remote get-url origin 2>/dev/null || echo "nicht konfiguriert")
|
|
||||||
echo " Aktueller Remote: $current_remote"
|
|
||||||
echo ""
|
|
||||||
read -p " Möchtest du den Remote ändern? (y/n) " change_remote
|
|
||||||
if [[ $change_remote =~ ^[Yy]$ ]]; then
|
|
||||||
git remote remove origin
|
|
||||||
echo " Remote entfernt."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git remote | grep -q origin; then
|
|
||||||
echo ""
|
|
||||||
echo "📝 Bitte Git Remote URL eingeben:"
|
|
||||||
echo " Beispiele:"
|
|
||||||
echo " - https://github.com/USERNAME/n8n-nodes-librebooking.git"
|
|
||||||
echo " - https://gitlab.com/USERNAME/n8n-nodes-librebooking.git"
|
|
||||||
echo " - git@github.com:USERNAME/n8n-nodes-librebooking.git"
|
|
||||||
echo ""
|
|
||||||
read -p " URL: " remote_url
|
|
||||||
if [ -z "$remote_url" ]; then
|
|
||||||
echo "❌ Keine URL eingegeben. Abgebrochen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
git remote add origin "$remote_url"
|
|
||||||
echo "✅ Remote hinzugefügt: $remote_url"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Status anzeigen
|
|
||||||
echo ""
|
|
||||||
echo "📊 Git Status:"
|
|
||||||
echo "─────────────────────────────────────"
|
|
||||||
git status --short
|
|
||||||
echo "─────────────────────────────────────"
|
|
||||||
|
|
||||||
# Anzahl der Änderungen
|
|
||||||
changed_files=$(git status --porcelain | wc -l)
|
|
||||||
if [ "$changed_files" -gt 0 ]; then
|
|
||||||
echo " $changed_files Datei(en) mit Änderungen"
|
|
||||||
else
|
|
||||||
echo " Keine uncommitteten Änderungen"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Branch Information
|
|
||||||
echo ""
|
|
||||||
echo "🌿 Branch: $(git branch --show-current 2>/dev/null || echo 'kein Branch')"
|
|
||||||
|
|
||||||
# Bestätigung für Commit
|
|
||||||
if [ "$changed_files" -gt 0 ]; then
|
|
||||||
echo ""
|
|
||||||
read -p "Möchtest du alle Änderungen committen? (y/n) " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
git add .
|
|
||||||
echo ""
|
|
||||||
echo "📝 Commit Message (Enter für Standard):"
|
|
||||||
read -p " > " commit_msg
|
|
||||||
if [ -z "$commit_msg" ]; then
|
|
||||||
commit_msg="feat: LibreBooking n8n Node v1.2.0"
|
|
||||||
fi
|
|
||||||
git commit -m "$commit_msg"
|
|
||||||
echo "✅ Commit erstellt"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Push
|
|
||||||
echo ""
|
|
||||||
read -p "Möchtest du zum Remote pushen? (y/n) " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Pushe zum Remote..."
|
|
||||||
|
|
||||||
# Branch auf main umbenennen falls nötig
|
|
||||||
current_branch=$(git branch --show-current)
|
|
||||||
if [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then
|
|
||||||
read -p " Branch '$current_branch' zu 'main' umbenennen? (y/n) " rename_branch
|
|
||||||
if [[ $rename_branch =~ ^[Yy]$ ]]; then
|
|
||||||
git branch -M main
|
|
||||||
echo " ✅ Branch umbenannt zu 'main'"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
git push -u origin $(git branch --show-current)
|
|
||||||
echo "✅ Code gepusht!"
|
|
||||||
|
|
||||||
# Tags pushen
|
|
||||||
if git tag | grep -q "."; then
|
|
||||||
echo ""
|
|
||||||
read -p "Möchtest du auch alle Tags pushen? (y/n) " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
git push origin --tags
|
|
||||||
echo "✅ Tags gepusht!"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "═══════════════════════════════════════"
|
|
||||||
echo "✅ Erfolgreich hochgeladen!"
|
|
||||||
echo "═══════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Nächste Schritte:"
|
|
||||||
echo " 1. Repository auf GitHub/GitLab prüfen"
|
|
||||||
echo " 2. README.md URLs anpassen"
|
|
||||||
echo " 3. Release erstellen (optional)"
|
|
||||||
else
|
|
||||||
echo "❌ Push abgebrochen"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Fertig! 🎉"
|
|
||||||
Loading…
Reference in New Issue