Compare commits
No commits in common. "master" and "main" have entirely different histories.
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
|
||||
# Generated PDFs
|
||||
*.pdf
|
||||
Binary file not shown.
92
CHANGELOG.md
92
CHANGELOG.md
|
|
@ -2,6 +2,98 @@
|
|||
|
||||
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
|
||||
|
||||
### Geändert
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
# 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`)
|
||||
Binary file not shown.
|
|
@ -0,0 +1,215 @@
|
|||
# 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.
Binary file not shown.
|
|
@ -0,0 +1,107 @@
|
|||
# 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
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,151 @@
|
|||
# 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,7 +1,44 @@
|
|||
# LibreBooking n8n Node
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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)
|
||||
|
||||
**Die einfachste Methode: Auf dem Host bauen, in Docker kopieren**
|
||||
|
|
@ -42,13 +79,6 @@ npm run docker:copy # Kopiert in Container
|
|||
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
|
||||
|
||||
1. Öffne n8n: http://localhost:5678
|
||||
|
|
@ -66,11 +96,27 @@ npm run docker:restart # Startet Container neu
|
|||
- Ressourcen und Verfügbarkeit verwalten
|
||||
- Benutzer und Gruppen administrieren
|
||||
- Zeitpläne und Zubehör konfigurieren
|
||||
- **NEU v1.2.0**: Benutzerdefinierte Attribute setzen
|
||||
|
||||
### LibreBooking Trigger Node
|
||||
- Neue Reservierungen überwachen
|
||||
- Geänderte Reservierungen erfassen
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
# 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.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,143 @@
|
|||
# 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!**
|
||||
Binary file not shown.
|
|
@ -0,0 +1,212 @@
|
|||
# LibreBooking Trigger Node - Anleitung
|
||||
|
||||
Dieses Dokument beschreibt die drei Trigger-Modi und deren Konfiguration.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der LibreBooking Trigger Node bietet drei Modi:
|
||||
|
||||
| Modus | Beschreibung | Use Case |
|
||||
|-------|-------------|----------|
|
||||
| **Alle Abrufen** | Alle Reservierungen für einen Zeitraum | Täglicher Report, Dashboard |
|
||||
| **Neue Reservierungen** | Triggert bei neuen Buchungen | Benachrichtigung, Bestätigung |
|
||||
| **Geänderte Reservierungen** | Triggert bei Änderungen | Konfliktprüfung, Update-Mail |
|
||||
|
||||
---
|
||||
|
||||
## 1. Modus: Alle Abrufen (Einmalig)
|
||||
|
||||
### Beschreibung
|
||||
Dieser Modus ruft bei jedem Poll **alle** Reservierungen im angegebenen Zeitraum ab.
|
||||
|
||||
### Zeitraum-Optionen
|
||||
|
||||
| Option | Beschreibung | Beispiel (25.01.2026) |
|
||||
|--------|-------------|----------------------|
|
||||
| **Benutzerdefiniert** | Manuelle Eingabe | Frei wählbar |
|
||||
| **Diese Woche** | Mo-So der aktuellen Woche | 19.01. - 25.01.2026 |
|
||||
| **Nächste 2 Wochen** | Ab heute + 14 Tage | 25.01. - 08.02.2026 |
|
||||
| **Dieser Monat** | 1. bis letzter Tag | 01.01. - 31.01.2026 |
|
||||
| **Nächste 2 Monate** | Ab heute + 2 Monate | 25.01. - 25.03.2026 |
|
||||
| **Dieses Jahr** | 1. Jan bis 31. Dez | 01.01. - 31.12.2026 |
|
||||
|
||||
### Beispiel-Workflow
|
||||
```
|
||||
[LibreBooking Trigger] --> [Format] --> [E-Mail senden]
|
||||
Alle Abrufen Tabellenformat Täglicher Report
|
||||
Diese Woche
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Modus: Neue Reservierungen (Polling)
|
||||
|
||||
### Beschreibung
|
||||
Triggert **nur** wenn eine neue Reservierung erstellt wird.
|
||||
|
||||
### Wichtig
|
||||
- **Erster Poll**: Speichert existierende IDs, triggert NICHT
|
||||
- **Folgende Polls**: Triggert nur bei wirklich neuen Reservierungen
|
||||
|
||||
### Zeit-Filter
|
||||
Filtert getriggerte Reservierungen nach Startdatum:
|
||||
|
||||
| Filter | Beschreibung |
|
||||
|--------|-------------|
|
||||
| **Alle (Kein Filter)** | Alle neuen Reservierungen, unabhängig vom Startdatum |
|
||||
| **Nur Heute** | Nur wenn die Reservierung heute stattfindet |
|
||||
| **Nächste 3 Tage** | Reservierung startet in den nächsten 3 Tagen |
|
||||
| **Nächste 7 Tage** | Reservierung startet in den nächsten 7 Tagen |
|
||||
|
||||
### Beispiel-Workflow
|
||||
```
|
||||
[LibreBooking Trigger] --> [IF] --> [E-Mail]
|
||||
Neue Reservierungen Prüfe Bestätigung senden
|
||||
Nächste 3 Tage Ressource
|
||||
```
|
||||
|
||||
### Use Case: Sofortige Buchungsbestätigung
|
||||
- Trigger-Modus: **Neue Reservierungen**
|
||||
- Zeit-Filter: **Alle (Kein Filter)**
|
||||
- Aktion: E-Mail an Benutzer mit Buchungsdetails
|
||||
|
||||
---
|
||||
|
||||
## 3. Modus: Geänderte Reservierungen (Polling)
|
||||
|
||||
### Beschreibung
|
||||
Triggert **nur** wenn eine bestehende Reservierung geändert wird.
|
||||
|
||||
### Änderungserkennung
|
||||
Folgende Felder werden überwacht:
|
||||
- `title` (Titel)
|
||||
- `description` (Beschreibung)
|
||||
- `startDate` / `endDate` (Zeitraum)
|
||||
- `resourceId` / `resourceName` (Ressource)
|
||||
- `userId` (Benutzer)
|
||||
- `statusId` (Status)
|
||||
- `participants` / `invitees` (Teilnehmer)
|
||||
|
||||
### Zeit-Filter
|
||||
Gleiche Optionen wie bei "Neue Reservierungen":
|
||||
|
||||
| Filter | Beschreibung | Use Case |
|
||||
|--------|-------------|----------|
|
||||
| **Nur Heute** | Änderungen an heutigen Terminen | Tagesaktueller Agent |
|
||||
| **Nächste 3 Tage** | Kurzfristige Änderungen | Dringende Benachrichtigungen |
|
||||
|
||||
### Beispiel-Workflow
|
||||
```
|
||||
[LibreBooking Trigger] --> [Compare] --> [Slack]
|
||||
Geänderte Vorher/ Benachrichtigung
|
||||
Reservierungen Nachher "Termin wurde geändert"
|
||||
Nur Heute
|
||||
```
|
||||
|
||||
### Use Case: Agent für Tagesänderungen
|
||||
- Trigger-Modus: **Geänderte Reservierungen**
|
||||
- Zeit-Filter: **Nur Heute**
|
||||
- Aktion: Slack/E-Mail wenn sich ein heutiger Termin ändert
|
||||
|
||||
---
|
||||
|
||||
## Allgemeine Einstellungen
|
||||
|
||||
### Filter (für alle Modi)
|
||||
| Filter | Beschreibung |
|
||||
|--------|-------------|
|
||||
| **Ressourcen-ID** | Nur Reservierungen für diese Ressource |
|
||||
| **Zeitplan-ID** | Nur Reservierungen für diesen Zeitplan |
|
||||
| **Benutzer-ID** | Nur Reservierungen für diesen Benutzer |
|
||||
|
||||
### Optionen (für alle Modi)
|
||||
| Option | Beschreibung |
|
||||
|--------|-------------|
|
||||
| **Detaillierte Daten Abrufen** | Holt vollständige Daten inkl. Custom Attributes |
|
||||
| **Debug-Modus** | Gibt Debug-Informationen aus (für Entwicklung) |
|
||||
|
||||
---
|
||||
|
||||
## Polling-Intervall
|
||||
|
||||
Das Polling-Intervall wird in den n8n Workflow-Einstellungen konfiguriert:
|
||||
|
||||
- **Empfohlen für "Neue" und "Geänderte"**: 1-5 Minuten
|
||||
- **Empfohlen für "Alle Abrufen"**: 15-60 Minuten (je nach Report-Bedarf)
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Trigger triggert nicht bei neuen/geänderten Reservierungen
|
||||
1. **Prüfen**: Ist es der erste Poll? → Triggert absichtlich nicht
|
||||
2. **Prüfen**: Ist der Zeit-Filter zu restriktiv?
|
||||
3. **Aktivieren**: Debug-Modus für detaillierte Logs
|
||||
|
||||
### Zu viele Events werden getriggert
|
||||
1. **Verwenden**: Zeit-Filter ("Nur Heute", "Nächste 3 Tage")
|
||||
2. **Filtern**: Nach Ressourcen-ID, Benutzer-ID
|
||||
|
||||
### Performance-Probleme
|
||||
1. **Reduzieren**: Zeitfenster (z.B. 7 statt 90 Tage)
|
||||
2. **Deaktivieren**: "Detaillierte Daten Abrufen" wenn nicht benötigt
|
||||
|
||||
---
|
||||
|
||||
## Praxisbeispiele
|
||||
|
||||
### 1. Täglicher Reservierungsreport
|
||||
```yaml
|
||||
Modus: Alle Abrufen
|
||||
Zeitraum: Diese Woche
|
||||
Poll-Intervall: Täglich um 07:00
|
||||
Aktion: E-Mail mit Wochenübersicht
|
||||
```
|
||||
|
||||
### 2. Sofortige Buchungsbestätigung
|
||||
```yaml
|
||||
Modus: Neue Reservierungen
|
||||
Zeitfenster: Nächste 90 Tage
|
||||
Zeit-Filter: Alle
|
||||
Poll-Intervall: 1 Minute
|
||||
Aktion: E-Mail an Buchenden
|
||||
```
|
||||
|
||||
### 3. Kurzfristige Änderungsbenachrichtigung
|
||||
```yaml
|
||||
Modus: Geänderte Reservierungen
|
||||
Zeitfenster: Nächste 14 Tage
|
||||
Zeit-Filter: Nächste 3 Tage
|
||||
Poll-Intervall: 5 Minuten
|
||||
Aktion: Slack-Nachricht an Team
|
||||
```
|
||||
|
||||
### 4. Tagesaktueller Terminagent
|
||||
```yaml
|
||||
Modus: Geänderte Reservierungen
|
||||
Zeitfenster: Nächste 7 Tage
|
||||
Zeit-Filter: Nur Heute
|
||||
Poll-Intervall: 2 Minuten
|
||||
Aktion: E-Mail an betroffene Teilnehmer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test-Ergebnisse
|
||||
|
||||
Stand: 25.01.2026
|
||||
|
||||
| Test | Status |
|
||||
|------|--------|
|
||||
| Date Range: thisWeek | ✅ |
|
||||
| Date Range: next2Weeks | ✅ |
|
||||
| Date Range: thisMonth | ✅ |
|
||||
| Date Range: next2Months | ✅ |
|
||||
| Date Range: thisYear | ✅ |
|
||||
| Time Filter: today | ✅ |
|
||||
| Time Filter: next3Days | ✅ |
|
||||
| Time Filter: next7Days | ✅ |
|
||||
| Create Reservation Detection | ✅ |
|
||||
| Update Reservation Detection | ✅ |
|
||||
| Delete Reservation | ✅ |
|
||||
| **Gesamt: 18/18 Tests bestanden** | ✅ |
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,75 @@
|
|||
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',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"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"}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
"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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"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"}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"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"}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
"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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"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"}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"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
|
|
@ -0,0 +1,28 @@
|
|||
<?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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"LibreBookingTrigger.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAGlB,MAAM,cAAc,CAAC;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"}
|
||||
|
|
@ -0,0 +1,813 @@
|
|||
"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
|
|
@ -0,0 +1,28 @@
|
|||
<?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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
index.ts
1
index.ts
|
|
@ -4,3 +4,4 @@
|
|||
export * from './nodes/LibreBooking/LibreBooking.node';
|
||||
export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node';
|
||||
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,9 +20,20 @@ interface ReservationData {
|
|||
title: string;
|
||||
resourceId: number;
|
||||
userId: number;
|
||||
description?: string;
|
||||
resourceName?: string;
|
||||
startDateTime?: string;
|
||||
endDateTime?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface WorkflowStaticData {
|
||||
seenIds?: string[];
|
||||
reservationHashes?: Record<string, string>;
|
||||
isFirstPoll?: boolean;
|
||||
lastPollTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentifizierung bei LibreBooking
|
||||
*/
|
||||
|
|
@ -44,7 +55,7 @@ async function authenticateTrigger(
|
|||
if (!response.isAuthenticated) {
|
||||
throw new NodeOperationError(
|
||||
pollFunctions.getNode(),
|
||||
'Authentifizierung fehlgeschlagen',
|
||||
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +67,7 @@ async function authenticateTrigger(
|
|||
} catch (error: any) {
|
||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||
message: 'Authentifizierung fehlgeschlagen',
|
||||
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -104,23 +116,29 @@ async function getReservations(
|
|||
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
||||
if (filters.userId) qs.userId = filters.userId;
|
||||
|
||||
const response = await pollFunctions.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
},
|
||||
qs,
|
||||
json: true,
|
||||
});
|
||||
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 || [];
|
||||
return response.reservations || [];
|
||||
} catch (error: any) {
|
||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||
message: 'Fehler beim Abrufen der Reservierungen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaillierte Reservierungsdaten abrufen
|
||||
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
||||
*/
|
||||
async function getReservationDetails(
|
||||
pollFunctions: IPollFunctions,
|
||||
|
|
@ -128,27 +146,127 @@ async function getReservationDetails(
|
|||
session: LibreBookingSession,
|
||||
referenceNumber: string,
|
||||
): Promise<any> {
|
||||
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,
|
||||
});
|
||||
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;
|
||||
return response;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeitfenster berechnen
|
||||
* Berechnet Zeitraum basierend auf der gewählten Option
|
||||
*/
|
||||
function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
||||
function getDateRange(dateRange: string): { startDate: string; endDate: string } {
|
||||
const now = new Date();
|
||||
let startDate: Date;
|
||||
let endDate: Date;
|
||||
|
||||
switch (dateRange) {
|
||||
case 'thisWeek':
|
||||
// Montag dieser Woche (ISO: Montag = 1, Sonntag = 0)
|
||||
startDate = new Date(now);
|
||||
const dayOfWeek = now.getDay();
|
||||
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
startDate.setDate(now.getDate() + diffToMonday);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
// Sonntag dieser Woche
|
||||
endDate = new Date(startDate);
|
||||
endDate.setDate(startDate.getDate() + 6);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
|
||||
case 'next2Weeks':
|
||||
startDate = new Date(now);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate = new Date(now);
|
||||
endDate.setDate(now.getDate() + 14);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
|
||||
case 'thisMonth':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
|
||||
case 'next2Months':
|
||||
startDate = new Date(now);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate = new Date(now);
|
||||
endDate.setMonth(now.getMonth() + 2);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
|
||||
case 'thisYear':
|
||||
startDate = new Date(now.getFullYear(), 0, 1);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
endDate = new Date(now.getFullYear(), 11, 31);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
|
||||
default: // custom
|
||||
return { startDate: '', endDate: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeitfenster berechnen für "Get All" Mode
|
||||
*/
|
||||
function getTimeWindowForGetAll(
|
||||
dateRange: string,
|
||||
customStartDate?: string,
|
||||
customEndDate?: string,
|
||||
defaultDays: number = 14,
|
||||
): { 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 start = now.toISOString();
|
||||
|
||||
|
||||
let endDate = new Date(now);
|
||||
switch (timeWindow) {
|
||||
case '7days':
|
||||
|
|
@ -163,6 +281,9 @@ function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
|||
case '90days':
|
||||
endDate.setDate(endDate.getDate() + 90);
|
||||
break;
|
||||
case '180days':
|
||||
endDate.setDate(endDate.getDate() + 180);
|
||||
break;
|
||||
default:
|
||||
endDate.setDate(endDate.getDate() + 14);
|
||||
}
|
||||
|
|
@ -174,14 +295,72 @@ function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
|||
}
|
||||
|
||||
/**
|
||||
* Eindeutigen Schlüssel für Reservierung generieren
|
||||
* Filter Reservierungen nach Zeitpunkt
|
||||
*/
|
||||
function getReservationKey(reservation: ReservationData): string {
|
||||
return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`;
|
||||
function filterByTime(reservations: ReservationData[], timeFilter: string): ReservationData[] {
|
||||
if (timeFilter === 'all') {
|
||||
return reservations;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
return reservations.filter((reservation) => {
|
||||
// Verwende startDate oder startDateTime
|
||||
const dateStr = reservation.startDateTime || reservation.startDate;
|
||||
if (!dateStr) return true;
|
||||
|
||||
const startDate = new Date(dateStr);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (timeFilter === 'today') {
|
||||
return startDate.getTime() === now.getTime();
|
||||
}
|
||||
|
||||
if (timeFilter === 'next3Days') {
|
||||
const threeDaysFromNow = new Date(now);
|
||||
threeDaysFromNow.setDate(now.getDate() + 3);
|
||||
return startDate >= now && startDate <= threeDaysFromNow;
|
||||
}
|
||||
|
||||
if (timeFilter === 'next7Days') {
|
||||
const sevenDaysFromNow = new Date(now);
|
||||
sevenDaysFromNow.setDate(now.getDate() + 7);
|
||||
return startDate >= now && startDate <= sevenDaysFromNow;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash für Reservierung generieren (für Änderungserkennung)
|
||||
*/
|
||||
function getReservationHash(reservation: ReservationData): string {
|
||||
const relevantData = {
|
||||
referenceNumber: reservation.referenceNumber,
|
||||
startDate: reservation.startDate || 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
|
||||
*/
|
||||
export class LibreBookingTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
@ -191,7 +370,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
subtitle: '={{$parameter["triggerMode"]}}',
|
||||
defaults: {
|
||||
name: 'LibreBooking Trigger',
|
||||
},
|
||||
|
|
@ -205,17 +384,185 @@ export class LibreBookingTrigger implements INodeType {
|
|||
],
|
||||
polling: true,
|
||||
properties: [
|
||||
// =====================================================
|
||||
// TRIGGER MODE SELECTOR
|
||||
// =====================================================
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
displayName: 'Trigger-Modus',
|
||||
name: 'triggerMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' },
|
||||
{ name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' },
|
||||
{ name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' },
|
||||
{
|
||||
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: 'newReservation',
|
||||
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',
|
||||
|
|
@ -223,23 +570,33 @@ export class LibreBookingTrigger implements INodeType {
|
|||
placeholder: 'Filter hinzufügen',
|
||||
default: {},
|
||||
options: [
|
||||
{ displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' },
|
||||
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
||||
{ displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' },
|
||||
{
|
||||
displayName: '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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Zeitfenster',
|
||||
name: 'timeWindow',
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'Nächste 7 Tage', value: '7days' },
|
||||
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||
{ name: 'Nächste 30 Tage', value: '30days' },
|
||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||
],
|
||||
default: '14days',
|
||||
},
|
||||
|
||||
// =====================================================
|
||||
// OPTIONS (ALL MODES)
|
||||
// =====================================================
|
||||
{
|
||||
displayName: 'Optionen',
|
||||
name: 'options',
|
||||
|
|
@ -247,7 +604,21 @@ export class LibreBookingTrigger implements INodeType {
|
|||
placeholder: 'Option hinzufügen',
|
||||
default: {},
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -259,13 +630,16 @@ export class LibreBookingTrigger implements INodeType {
|
|||
const username = credentials.username as string;
|
||||
const password = credentials.password as string;
|
||||
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const triggerMode = this.getNodeParameter('triggerMode') as string;
|
||||
const filters = this.getNodeParameter('filters', {}) as any;
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||
const options = this.getNodeParameter('options', {}) as any;
|
||||
|
||||
const workflowStaticData = this.getWorkflowStaticData('node');
|
||||
const previousReservations = (workflowStaticData.reservations as Record<string, string>) || {};
|
||||
// Debug-Modus
|
||||
const debugMode = options.debugMode || false;
|
||||
const fetchDetails = options.fetchDetails || false;
|
||||
|
||||
// Workflow Static Data für State-Management
|
||||
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
||||
|
||||
let session: LibreBookingSession;
|
||||
try {
|
||||
|
|
@ -275,76 +649,341 @@ export class LibreBookingTrigger implements INodeType {
|
|||
}
|
||||
|
||||
try {
|
||||
const { start, end } = getTimeWindow(timeWindow);
|
||||
|
||||
const reservations = await getReservations(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
start,
|
||||
end,
|
||||
filters,
|
||||
);
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const currentReservations: Record<string, string> = {};
|
||||
|
||||
for (const reservation of reservations) {
|
||||
const refNumber = reservation.referenceNumber;
|
||||
const reservationKey = getReservationKey(reservation);
|
||||
currentReservations[refNumber] = reservationKey;
|
||||
// ==========================================
|
||||
// MODE: Get All (One-Time / Every Poll)
|
||||
// ==========================================
|
||||
if (triggerMode === 'getAll') {
|
||||
const dateRange = this.getNodeParameter('dateRange', 'custom') as string;
|
||||
const startDate = this.getNodeParameter('startDate', '') as string;
|
||||
const endDate = this.getNodeParameter('endDate', '') as string;
|
||||
|
||||
const isNew = !previousReservations[refNumber];
|
||||
const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey;
|
||||
const { start, end } = getTimeWindowForGetAll(
|
||||
dateRange,
|
||||
startDate || undefined,
|
||||
endDate || undefined,
|
||||
14,
|
||||
);
|
||||
|
||||
let shouldTrigger = false;
|
||||
let eventType = '';
|
||||
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||
|
||||
if (event === 'newReservation' && isNew) {
|
||||
shouldTrigger = true;
|
||||
eventType = 'new';
|
||||
} else if (event === 'updatedReservation' && isUpdated) {
|
||||
shouldTrigger = true;
|
||||
eventType = 'updated';
|
||||
} else if (event === 'allReservations' && (isNew || isUpdated)) {
|
||||
shouldTrigger = true;
|
||||
eventType = isNew ? 'new' : 'updated';
|
||||
if (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 (shouldTrigger) {
|
||||
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 (options.fetchDetails) {
|
||||
if (fetchDetails) {
|
||||
try {
|
||||
reservationData = await getReservationDetails(
|
||||
const details = await getReservationDetails(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
refNumber,
|
||||
reservation.referenceNumber,
|
||||
);
|
||||
if (details) {
|
||||
reservationData = details;
|
||||
}
|
||||
} catch (error) {
|
||||
reservationData = reservation;
|
||||
// Fallback auf Basisdaten
|
||||
}
|
||||
}
|
||||
|
||||
returnData.push({
|
||||
json: {
|
||||
...reservationData,
|
||||
_eventType: eventType,
|
||||
_eventType: 'getAll',
|
||||
_dateRange: dateRange,
|
||||
_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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
|
||||
} finally {
|
||||
await signOutTrigger(this, baseUrl, session);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-librebooking",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.2",
|
||||
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
|
||||
"keywords": [
|
||||
"n8n-community-node-package",
|
||||
|
|
@ -60,7 +60,8 @@
|
|||
"n8n": {
|
||||
"n8nNodesApiVersion": 1,
|
||||
"credentials": [
|
||||
"dist/credentials/LibreBookingApi.credentials.js"
|
||||
"dist/credentials/LibreBookingApi.credentials.js",
|
||||
"dist/credentials/LibreBookingConfig.credentials.js"
|
||||
],
|
||||
"nodes": [
|
||||
"dist/nodes/LibreBooking/LibreBooking.node.js",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,609 @@
|
|||
/**
|
||||
* 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);
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
{
|
||||
"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 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
/**
|
||||
* Comprehensive Trigger Test Script for LibreBooking
|
||||
*
|
||||
* Tests:
|
||||
* 1. Authentication
|
||||
* 2. Get All Reservations
|
||||
* 3. Create New Reservation (triggers "New" mode)
|
||||
* 4. Update Reservation (triggers "Updated" mode)
|
||||
* 5. Delete Reservation
|
||||
* 6. Date Range calculations
|
||||
* 7. Time Filter logic
|
||||
*/
|
||||
|
||||
import * as https from 'https';
|
||||
|
||||
const BASE_URL = 'https://librebooking.zell-cloud.de';
|
||||
const USERNAME = 'sebastian.zell@zell-aufmass.de';
|
||||
const PASSWORD = 'wanUQ4uVqU6lfP';
|
||||
|
||||
// Test date: 7.2.2026
|
||||
const TEST_DATE = '2026-02-07';
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
sessionToken: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
const results: TestResult[] = [];
|
||||
|
||||
function log(message: string) {
|
||||
console.log(`[${new Date().toISOString()}] ${message}`);
|
||||
}
|
||||
|
||||
function logSuccess(name: string, message: string, data?: any) {
|
||||
log(`✅ ${name}: ${message}`);
|
||||
results.push({ name, success: true, message, data });
|
||||
}
|
||||
|
||||
function logError(name: string, message: string, data?: any) {
|
||||
log(`❌ ${name}: ${message}`);
|
||||
results.push({ name, success: false, message, data });
|
||||
}
|
||||
|
||||
async function makeRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: any,
|
||||
headers?: Record<string, string>
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(`${BASE_URL}${path}`);
|
||||
const options: https.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: 443,
|
||||
path: url.pathname + url.search,
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
resolve(json);
|
||||
} catch (e) {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== AUTHENTICATION =====
|
||||
async function authenticate(): Promise<Session> {
|
||||
const response = await makeRequest(
|
||||
'POST',
|
||||
'/Web/Services/index.php/Authentication/Authenticate',
|
||||
{ username: USERNAME, password: PASSWORD }
|
||||
);
|
||||
|
||||
if (!response.isAuthenticated) {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
|
||||
return {
|
||||
sessionToken: response.sessionToken,
|
||||
userId: response.userId,
|
||||
};
|
||||
}
|
||||
|
||||
async function signOut(session: Session): Promise<void> {
|
||||
await makeRequest(
|
||||
'POST',
|
||||
'/Web/Services/index.php/Authentication/SignOut',
|
||||
{ userId: session.userId, sessionToken: session.sessionToken }
|
||||
);
|
||||
}
|
||||
|
||||
// ===== API REQUESTS =====
|
||||
async function getReservations(
|
||||
session: Session,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): Promise<any[]> {
|
||||
const response = await makeRequest(
|
||||
'GET',
|
||||
`/Web/Services/index.php/Reservations/?startDateTime=${startDate}T00:00:00&endDateTime=${endDate}T23:59:59`,
|
||||
undefined,
|
||||
{
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
}
|
||||
);
|
||||
return response.reservations || [];
|
||||
}
|
||||
|
||||
async function createReservation(
|
||||
session: Session,
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
resourceId: number;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
const response = await makeRequest(
|
||||
'POST',
|
||||
'/Web/Services/index.php/Reservations/',
|
||||
{
|
||||
...data,
|
||||
userId: session.userId,
|
||||
termsAccepted: true,
|
||||
allowParticipation: false,
|
||||
},
|
||||
{
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
}
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function updateReservation(
|
||||
session: Session,
|
||||
referenceNumber: string,
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
resourceId: number;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
}
|
||||
): Promise<any> {
|
||||
const response = await makeRequest(
|
||||
'POST',
|
||||
`/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||
{
|
||||
...data,
|
||||
userId: session.userId,
|
||||
termsAccepted: true,
|
||||
allowParticipation: false,
|
||||
},
|
||||
{
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
}
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function deleteReservation(session: Session, referenceNumber: string): Promise<any> {
|
||||
const response = await makeRequest(
|
||||
'DELETE',
|
||||
`/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||
undefined,
|
||||
{
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
}
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getResources(session: Session): Promise<any[]> {
|
||||
const response = await makeRequest(
|
||||
'GET',
|
||||
'/Web/Services/index.php/Resources/',
|
||||
undefined,
|
||||
{
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
}
|
||||
);
|
||||
return response.resources || [];
|
||||
}
|
||||
|
||||
// ===== DATE RANGE TESTS =====
|
||||
function testDateRange() {
|
||||
log('\n📅 Testing Date Range Calculations...\n');
|
||||
|
||||
const now = new Date('2026-01-25'); // Simulate current date
|
||||
|
||||
// Test thisWeek
|
||||
const dayOfWeek = now.getDay();
|
||||
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diffToMonday);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
|
||||
logSuccess(
|
||||
'thisWeek',
|
||||
`Monday: ${monday.toISOString().split('T')[0]}, Sunday: ${sunday.toISOString().split('T')[0]}`
|
||||
);
|
||||
|
||||
// Test next2Weeks
|
||||
const next2Weeks = new Date(now);
|
||||
next2Weeks.setDate(now.getDate() + 14);
|
||||
logSuccess(
|
||||
'next2Weeks',
|
||||
`From: ${now.toISOString().split('T')[0]}, To: ${next2Weeks.toISOString().split('T')[0]}`
|
||||
);
|
||||
|
||||
// Test thisMonth
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
logSuccess(
|
||||
'thisMonth',
|
||||
`From: ${monthStart.toISOString().split('T')[0]}, To: ${monthEnd.toISOString().split('T')[0]}`
|
||||
);
|
||||
|
||||
// Test next2Months
|
||||
const next2Months = new Date(now);
|
||||
next2Months.setMonth(now.getMonth() + 2);
|
||||
logSuccess(
|
||||
'next2Months',
|
||||
`From: ${now.toISOString().split('T')[0]}, To: ${next2Months.toISOString().split('T')[0]}`
|
||||
);
|
||||
|
||||
// Test thisYear
|
||||
const yearStart = new Date(now.getFullYear(), 0, 1);
|
||||
const yearEnd = new Date(now.getFullYear(), 11, 31);
|
||||
logSuccess(
|
||||
'thisYear',
|
||||
`From: ${yearStart.toISOString().split('T')[0]}, To: ${yearEnd.toISOString().split('T')[0]}`
|
||||
);
|
||||
}
|
||||
|
||||
// ===== TIME FILTER TESTS =====
|
||||
function testTimeFilter() {
|
||||
log('\n⏰ Testing Time Filter Logic...\n');
|
||||
|
||||
const today = new Date('2026-02-07');
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const reservations = [
|
||||
{ title: 'Today', startDateTime: '2026-02-07T10:00:00' },
|
||||
{ title: 'Tomorrow', startDateTime: '2026-02-08T10:00:00' },
|
||||
{ title: 'In 3 days', startDateTime: '2026-02-10T10:00:00' },
|
||||
{ title: 'In 7 days', startDateTime: '2026-02-14T10:00:00' },
|
||||
{ title: 'In 10 days', startDateTime: '2026-02-17T10:00:00' },
|
||||
];
|
||||
|
||||
// Filter: today
|
||||
const todayOnly = reservations.filter((r) => {
|
||||
const startDate = new Date(r.startDateTime);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
return startDate.getTime() === today.getTime();
|
||||
});
|
||||
logSuccess('timeFilter=today', `Found ${todayOnly.length} reservation(s): ${todayOnly.map(r => r.title).join(', ')}`);
|
||||
|
||||
// Filter: next3Days
|
||||
const threeDays = new Date(today);
|
||||
threeDays.setDate(today.getDate() + 3);
|
||||
const next3Days = reservations.filter((r) => {
|
||||
const startDate = new Date(r.startDateTime);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
return startDate >= today && startDate <= threeDays;
|
||||
});
|
||||
logSuccess('timeFilter=next3Days', `Found ${next3Days.length} reservation(s): ${next3Days.map(r => r.title).join(', ')}`);
|
||||
|
||||
// Filter: next7Days
|
||||
const sevenDays = new Date(today);
|
||||
sevenDays.setDate(today.getDate() + 7);
|
||||
const next7Days = reservations.filter((r) => {
|
||||
const startDate = new Date(r.startDateTime);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
return startDate >= today && startDate <= sevenDays;
|
||||
});
|
||||
logSuccess('timeFilter=next7Days', `Found ${next7Days.length} reservation(s): ${next7Days.map(r => r.title).join(', ')}`);
|
||||
}
|
||||
|
||||
// ===== MAIN TEST =====
|
||||
async function runTests() {
|
||||
console.log('🧪 LibreBooking Trigger Test Suite\n');
|
||||
console.log('═'.repeat(60) + '\n');
|
||||
|
||||
// Test date calculations first (no API needed)
|
||||
testDateRange();
|
||||
testTimeFilter();
|
||||
|
||||
log('\n🔌 Testing API Operations...\n');
|
||||
|
||||
let session: Session | null = null;
|
||||
let createdRefNumber: string | null = null;
|
||||
|
||||
try {
|
||||
// 1. Authentication
|
||||
log('1️⃣ Authenticating...');
|
||||
session = await authenticate();
|
||||
logSuccess('Authentication', `Session: ${session.sessionToken.substring(0, 20)}...`);
|
||||
|
||||
// 2. Get Resources (to find available resource)
|
||||
log('\n2️⃣ Getting Resources...');
|
||||
const resources = await getResources(session);
|
||||
if (resources.length === 0) {
|
||||
logError('GetResources', 'No resources found');
|
||||
return;
|
||||
}
|
||||
logSuccess('GetResources', `Found ${resources.length} resources`);
|
||||
const testResourceId = resources[0].resourceId;
|
||||
log(` Using resource: ${resources[0].name} (ID: ${testResourceId})`);
|
||||
|
||||
// 3. Get Initial Reservations
|
||||
log('\n3️⃣ Getting initial reservations...');
|
||||
const initialReservations = await getReservations(session, TEST_DATE, '2026-02-14');
|
||||
logSuccess('GetReservations', `Found ${initialReservations.length} reservations`);
|
||||
|
||||
// Store initial IDs (simulating first poll)
|
||||
const seenIds = initialReservations.map((r: any) => r.referenceNumber);
|
||||
log(` Stored ${seenIds.length} IDs (simulating first poll)`);
|
||||
|
||||
// 4. Create New Reservation
|
||||
log('\n4️⃣ Creating new reservation...');
|
||||
const createResult = await createReservation(session, {
|
||||
title: 'TEST: Neue Reservierung für Trigger-Test',
|
||||
description: 'Diese Reservierung wird erstellt, um den "Neue Objekte" Trigger zu testen',
|
||||
resourceId: testResourceId,
|
||||
startDateTime: `${TEST_DATE}T14:00:00`,
|
||||
endDateTime: `${TEST_DATE}T15:00:00`,
|
||||
});
|
||||
|
||||
if (createResult.referenceNumber) {
|
||||
createdRefNumber = createResult.referenceNumber;
|
||||
logSuccess('CreateReservation', `Created: ${createdRefNumber}`);
|
||||
} else {
|
||||
logError('CreateReservation', `Failed: ${JSON.stringify(createResult)}`);
|
||||
}
|
||||
|
||||
// 5. Get Reservations Again (simulate second poll)
|
||||
log('\n5️⃣ Simulating second poll...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 seconds
|
||||
|
||||
const afterCreateReservations = await getReservations(session, TEST_DATE, '2026-02-14');
|
||||
const currentIds = afterCreateReservations.map((r: any) => r.referenceNumber);
|
||||
const newIds = currentIds.filter((id: string) => !seenIds.includes(id));
|
||||
|
||||
logSuccess(
|
||||
'NewReservationDetection',
|
||||
`Found ${newIds.length} new reservation(s): ${newIds.join(', ')}`
|
||||
);
|
||||
|
||||
// 6. Update Reservation
|
||||
if (createdRefNumber) {
|
||||
log('\n6️⃣ Updating reservation...');
|
||||
|
||||
// Store hash before update
|
||||
const reservationBefore = afterCreateReservations.find(
|
||||
(r: any) => r.referenceNumber === createdRefNumber
|
||||
);
|
||||
const hashBefore = JSON.stringify({
|
||||
title: reservationBefore?.title,
|
||||
description: reservationBefore?.description,
|
||||
});
|
||||
|
||||
// Update - must include all required fields
|
||||
const updateResult = await updateReservation(session, createdRefNumber, {
|
||||
title: 'TEST: GEÄNDERTE Reservierung',
|
||||
description: 'Diese Reservierung wurde geändert - Update-Trigger sollte feuern',
|
||||
resourceId: testResourceId,
|
||||
startDateTime: `${TEST_DATE}T14:00:00`,
|
||||
endDateTime: `${TEST_DATE}T15:00:00`,
|
||||
});
|
||||
|
||||
if (updateResult.referenceNumber) {
|
||||
logSuccess('UpdateReservation', `Updated: ${createdRefNumber}`);
|
||||
} else {
|
||||
logError('UpdateReservation', `Failed: ${JSON.stringify(updateResult)}`);
|
||||
}
|
||||
|
||||
// 7. Get Reservations Again (check for changes)
|
||||
log('\n7️⃣ Checking for changes...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
const afterUpdateReservations = await getReservations(session, TEST_DATE, '2026-02-14');
|
||||
const reservationAfter = afterUpdateReservations.find(
|
||||
(r: any) => r.referenceNumber === createdRefNumber
|
||||
);
|
||||
const hashAfter = JSON.stringify({
|
||||
title: reservationAfter?.title,
|
||||
description: reservationAfter?.description,
|
||||
});
|
||||
|
||||
if (hashBefore !== hashAfter) {
|
||||
logSuccess(
|
||||
'ChangeDetection',
|
||||
`Change detected! Title: "${reservationAfter?.title}"`
|
||||
);
|
||||
} else {
|
||||
logError('ChangeDetection', 'No change detected (hash unchanged)');
|
||||
}
|
||||
|
||||
// 8. Delete Reservation
|
||||
log('\n8️⃣ Deleting reservation...');
|
||||
await deleteReservation(session, createdRefNumber);
|
||||
logSuccess('DeleteReservation', `Deleted: ${createdRefNumber}`);
|
||||
|
||||
// Verify deletion
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const afterDeleteReservations = await getReservations(session, TEST_DATE, '2026-02-14');
|
||||
const stillExists = afterDeleteReservations.some(
|
||||
(r: any) => r.referenceNumber === createdRefNumber
|
||||
);
|
||||
|
||||
if (!stillExists) {
|
||||
logSuccess('DeletionVerified', 'Reservation successfully deleted');
|
||||
} else {
|
||||
logError('DeletionVerified', 'Reservation still exists!');
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Sign Out
|
||||
log('\n9️⃣ Signing out...');
|
||||
await signOut(session);
|
||||
logSuccess('SignOut', 'Successfully signed out');
|
||||
|
||||
} catch (error: any) {
|
||||
logError('TestSuite', `Error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log('📊 TEST SUMMARY\n');
|
||||
|
||||
const passed = results.filter((r) => r.success).length;
|
||||
const failed = results.filter((r) => !r.success).length;
|
||||
|
||||
console.log(` ✅ Passed: ${passed}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📝 Total: ${results.length}`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\n Failed Tests:');
|
||||
results
|
||||
.filter((r) => !r.success)
|
||||
.forEach((r) => console.log(` - ${r.name}: ${r.message}`));
|
||||
}
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
#!/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