Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

55 changed files with 1239 additions and 7958 deletions

File diff suppressed because one or more lines are too long

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
.env
*.log
# Generated PDFs
*.pdf

Binary file not shown.

View File

@ -2,98 +2,6 @@
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

View File

@ -1,144 +0,0 @@
# LibreBooking Config Node
Der Config Node ermöglicht die zentrale Konfiguration von Standardwerten, die in allen LibreBooking Operationen verwendet werden können.
## Überblick
Der LibreBooking Config Credential ist **optional** und dient dazu:
- Standardwerte zentral zu definieren
- Wiederholte Eingaben zu vermeiden
- Konsistente Einstellungen sicherzustellen
## Installation
Der Config Node wird automatisch mit der LibreBooking Node installiert. Er erscheint unter **Credentials** als "LibreBooking Config".
## Konfiguration
### 1. Config Credential anlegen
1. Gehen Sie zu **Credentials** in n8n
2. Klicken Sie auf **Add Credential**
3. Suchen Sie nach **LibreBooking Config**
4. Klicken Sie auf **Create**
### 2. Standardwerte definieren
| Einstellung | Beschreibung | Standard |
|-------------|--------------|----------|
| Standard Nutzungsbedingungen Akzeptiert | Vorauswahl für termsAccepted | `true` |
| Standard Teilnahme Erlauben | Vorauswahl für allowParticipation | `false` |
| Standard Ressourcen-ID | Standard-Ressource für Reservierungen | `0` (keine) |
| Standard Benutzer-ID | Standard-Benutzer für Reservierungen | `0` (angemeldeter Benutzer) |
| Standard Zeitplan-ID | Standard-Zeitplan für Ressourcen | `0` (keine) |
| Standard Zeitzone | Zeitzone für neue Benutzer | `Europe/Berlin` |
| Standard Sprache | Sprache für neue Benutzer | `de_de` |
### 3. Config mit Node verbinden
1. Öffnen Sie einen LibreBooking Node
2. Bei Ressourcen wie Reservierung, Ressource, Benutzer oder Konto erscheint ein optionales Credential-Feld für **LibreBooking Config**
3. Wählen Sie Ihre Config Credential aus
## Verwendung
### Beispiel: Reservierung erstellen
**Ohne Config Node:**
```
- Ressourcen-ID: 1
- Startzeit: ...
- Endzeit: ...
- Nutzungsbedingungen Akzeptiert: true (manuell)
```
**Mit Config Node:**
```
- Ressourcen-ID: 1 (oder aus Config wenn 0 = übernehmen)
- Startzeit: ...
- Endzeit: ...
- Nutzungsbedingungen Akzeptiert: (automatisch aus Config)
```
### Priorität der Werte
1. **Höchste Priorität**: Werte direkt im Node eingegeben
2. **Niedrigere Priorität**: Werte aus dem Config Node
3. **Fallback**: Eingebaute Standardwerte
## Anwendungsfälle
### 1. Automatisierte Buchungen
Wenn Sie einen Workflow haben, der automatisch Buchungen erstellt:
```
Config Node:
- Standard Nutzungsbedingungen Akzeptiert: true
- Standard Teilnahme Erlauben: false
```
So müssen Sie diese Werte nicht in jedem Create-Node angeben.
### 2. Standardressource für Abteilung
```
Config Node für Abteilung A:
- Standard Ressourcen-ID: 5 (Konferenzraum A)
Config Node für Abteilung B:
- Standard Ressourcen-ID: 8 (Konferenzraum B)
```
### 3. Mehrsprachige Umgebung
```
Config Node für deutschsprachige Workflows:
- Standard Zeitzone: Europe/Berlin
- Standard Sprache: de_de
Config Node für englischsprachige Workflows:
- Standard Zeitzone: Europe/London
- Standard Sprache: en_us
```
## Best Practices
### 1. Benennung
Verwenden Sie aussagekräftige Namen für Ihre Config Credentials:
- `LibreBooking Config - Produktion`
- `LibreBooking Config - Test`
- `LibreBooking Config - Abteilung Marketing`
### 2. Dokumentation
Dokumentieren Sie Ihre Config-Einstellungen für Ihr Team.
### 3. Umgebungstrennung
Erstellen Sie separate Configs für verschiedene Umgebungen (Test/Produktion).
## Fehlerbehebung
### Config wird nicht angewendet
- Stellen Sie sicher, dass der Config Node mit dem LibreBooking Node verbunden ist
- Prüfen Sie, ob die Ressource den Config Node unterstützt (nur Reservierung, Ressource, Benutzer, Konto)
### Werte werden überschrieben
- Direkt im Node eingegebene Werte haben immer Vorrang
- Lassen Sie Felder leer, wenn der Config-Wert verwendet werden soll
## Technische Details
Der Config Node wird als n8n Credential implementiert, ist aber kein echtes Authentifizierungs-Credential. Er speichert lediglich Konfigurationswerte.
**Credential-Name**: `libreBookingConfig`
**Unterstützte Ressourcen**:
- Reservierung (`reservation`)
- Ressource (`resource`)
- Benutzer (`user`)
- Konto (`account`)

Binary file not shown.

View File

@ -1,215 +0,0 @@
# Benutzerdefinierte Attribute (Custom Attributes)
Diese Dokumentation erklärt, wie Sie benutzerdefinierte Attribute in LibreBooking über die n8n Nodes verwenden können.
## Überblick
LibreBooking unterstützt benutzerdefinierte Attribute für:
- **Reservierungen** (Kategorie-ID: 1)
- **Benutzer** (Kategorie-ID: 2)
- **Ressourcen** (Kategorie-ID: 4)
- **Ressourcen-Typen** (Kategorie-ID: 5)
## Attribut-Typen
| Typ | Beschreibung | Wert |
|-----|--------------|------|
| Einzeilig | Einfaches Textfeld | 1 |
| Mehrzeilig | Textbereich | 2 |
| Auswahlliste | Dropdown-Menü | 3 |
| Checkbox | Ja/Nein Feld | 4 |
| Datum/Zeit | Datums-/Zeitauswahl | 5 |
## Attribute abrufen
### Alle Attribute einer Kategorie abrufen
1. Wählen Sie **Ressource**: `Attribut`
2. Wählen Sie **Operation**: `Nach Kategorie Abrufen`
3. Wählen Sie **Kategorie-ID**: z.B. `Reservierung`
Die Antwort enthält alle Attribute mit ihren IDs und Eigenschaften.
### Einzelnes Attribut abrufen
1. Wählen Sie **Ressource**: `Attribut`
2. Wählen Sie **Operation**: `Abrufen`
3. Geben Sie die **Attribut-ID** ein
## Attribute bei Reservierungen setzen
### Bei Erstellen einer Reservierung
1. Wählen Sie **Ressource**: `Reservierung`
2. Wählen Sie **Operation**: `Erstellen`
3. Füllen Sie die Pflichtfelder aus
4. Unter **Benutzerdefinierte Attribute**:
- Klicken Sie auf "Attribut hinzufügen"
- Geben Sie die **Attribut-ID** ein (z.B. `1`)
- Geben Sie den **Wert** ein (z.B. `Meetingraum-Konfiguration`)
### Bei Aktualisieren einer Reservierung
Gleiche Vorgehensweise wie beim Erstellen.
### Beispiel JSON für API-Request
```json
{
"resourceId": 1,
"startDateTime": "2024-01-15T10:00:00+01:00",
"endDateTime": "2024-01-15T11:00:00+01:00",
"title": "Team Meeting",
"termsAccepted": true,
"customAttributes": [
{
"attributeId": 1,
"attributeValue": "Standard-Setup"
},
{
"attributeId": 2,
"attributeValue": "10"
}
]
}
```
## Attribute bei Ressourcen setzen
### Bei Erstellen einer Ressource
1. Wählen Sie **Ressource**: `Ressource`
2. Wählen Sie **Operation**: `Erstellen`
3. Füllen Sie die Pflichtfelder aus (Name, Zeitplan-ID)
4. Unter **Benutzerdefinierte Attribute**:
- Klicken Sie auf "Attribut hinzufügen"
- Geben Sie die **Attribut-ID** und den **Wert** ein
### Beispiel: Raum mit Ausstattung
```json
{
"name": "Konferenzraum A",
"scheduleId": 1,
"customAttributes": [
{
"attributeId": 10,
"attributeValue": "Beamer, Whiteboard"
},
{
"attributeId": 11,
"attributeValue": "20"
}
]
}
```
## Attribute bei Benutzern setzen
### Bei Erstellen eines Benutzers
1. Wählen Sie **Ressource**: `Benutzer`
2. Wählen Sie **Operation**: `Erstellen`
3. Füllen Sie die Pflichtfelder aus
4. Unter **Benutzerdefinierte Attribute**:
- Klicken Sie auf "Attribut hinzufügen"
- Geben Sie die **Attribut-ID** und den **Wert** ein
## Neue Attribute erstellen (Admin)
1. Wählen Sie **Ressource**: `Attribut`
2. Wählen Sie **Operation**: `Erstellen`
3. Füllen Sie aus:
- **Kategorie-ID**: Ziel-Kategorie (1, 2, 4 oder 5)
- **Attribut-Label**: Anzeigename
- **Attribut-Typ**: Feldtyp
4. Optional unter **Attribut-Optionen**:
- **Erforderlich**: Pflichtfeld?
- **Nur Admin**: Nur für Admins sichtbar?
- **Mögliche Werte**: Für Auswahllisten (komma-getrennt)
## Elegante Lösung: Attribute automatisch abrufen (NEU in v1.2.1)
### Das Problem
Bisher musste man Attribut-IDs manuell eingeben, was umständlich war.
### Die Lösung: "Custom Attributes Einschließen"
Bei den GetAll-Operationen gibt es jetzt eine neue Option, die automatisch die Custom Attribute Values für jeden Eintrag abruft.
### Verwendung für Reservierungen
1. Wählen Sie **Ressource**: `Reservierung`
2. Wählen Sie **Operation**: `Alle Abrufen`
3. Unter **Filter** aktivieren Sie **Custom Attributes Einschließen**
**Ergebnis:**
```json
{
"reservations": [
{
"referenceNumber": "abc123",
"title": "Meeting",
"startDate": "2026-02-07T10:00:00",
"customAttributes": [
{
"id": 1,
"label": "Mietername",
"value": "Max Mustermann"
},
{
"id": 3,
"label": "Adresse",
"value": "Hauptstraße 1, 12345 Stadt"
}
]
}
]
}
```
### Verwendung für Ressourcen
1. Wählen Sie **Ressource**: `Ressource`
2. Wählen Sie **Operation**: `Alle Abrufen`
3. Unter **Ressourcen-Abruf-Optionen** aktivieren Sie **Custom Attributes Einschließen**
### Verwendung für Benutzer
1. Wählen Sie **Ressource**: `Benutzer`
2. Wählen Sie **Operation**: `Alle Abrufen`
3. Unter **Benutzer-Filter** aktivieren Sie **Custom Attributes Einschließen**
### Wichtiger Hinweis
Diese Option führt für jeden Eintrag einen zusätzlichen API-Call durch. Bei vielen Einträgen kann dies länger dauern.
---
## Tipps
### Attribut-IDs herausfinden
1. Nutzen Sie die Operation "Nach Kategorie Abrufen"
2. Notieren Sie sich die `id` der benötigten Attribute
### Checkbox-Attribute
Für Checkbox-Attribute verwenden Sie:
- `"1"` oder `"true"` für aktiviert
- `"0"` oder `"false"` für deaktiviert
### Auswahllisten
Der Wert muss exakt einem der möglichen Werte entsprechen.
## Fehlerbehebung
### Attribut wird nicht gespeichert
- Prüfen Sie, ob die Attribut-ID korrekt ist
- Prüfen Sie, ob das Attribut für diese Kategorie gilt
- Prüfen Sie, ob der Wert dem Attribut-Typ entspricht
### Zugriff verweigert
- Einige Attribute sind nur für Admins verfügbar
- Prüfen Sie die Berechtigungen in LibreBooking

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,107 +0,0 @@
# Git Upload Anleitung
Diese Anleitung erklärt, wie Sie das LibreBooking n8n Node Repository auf verschiedene Git-Plattformen hochladen können.
## Option 1: GitHub/GitLab/Bitbucket (Web Interface)
1. Erstelle ein neues Repository auf GitHub/GitLab/Bitbucket
2. Entpacke das Archiv lokal
3. Folge den Anweisungen auf der Plattform
## Option 2: Command Line (Empfohlen)
### GitHub
```bash
# 1. Repository erstellen auf github.com
# 2. Dann lokal:
cd librebooking_n8n_node
git remote add origin https://github.com/USERNAME/n8n-nodes-librebooking.git
git branch -M main
git push -u origin main
git push origin v1.2.0
```
### GitLab
```bash
cd librebooking_n8n_node
git remote add origin https://gitlab.com/USERNAME/n8n-nodes-librebooking.git
git branch -M main
git push -u origin main
git push origin v1.2.0
```
### Bitbucket
```bash
cd librebooking_n8n_node
git remote add origin https://bitbucket.org/USERNAME/n8n-nodes-librebooking.git
git branch -M main
git push -u origin main
git push origin v1.2.0
```
## Option 3: Git Bundle verwenden
Wenn Sie das Git Bundle (.bundle Datei) erhalten haben:
```bash
# Bundle entpacken (klonen)
git clone librebooking-n8n-node-v1.2.0.bundle librebooking_n8n_node
cd librebooking_n8n_node
# Remote hinzufügen
git remote add origin YOUR_REMOTE_URL
# Pushen mit Tags
git push -u origin main --tags
```
## Wichtige Hinweise
### Vor dem Upload prüfen
- [ ] Keine sensiblen Daten (API Keys, Passwörter) im Repository
- [ ] `.gitignore` ist korrekt konfiguriert
- [ ] `node_modules/` und `dist/` sind nicht im Repository
- [ ] Alle Dokumentation ist aktuell
### Nach dem Upload
- [ ] Repository ist erreichbar
- [ ] Alle Dateien sind vorhanden
- [ ] Tags sind sichtbar
- [ ] README wird korrekt angezeigt
## SSH vs HTTPS
### HTTPS (einfacher)
```bash
git remote add origin https://github.com/USERNAME/REPO.git
```
### SSH (empfohlen für regelmäßige Nutzung)
```bash
git remote add origin git@github.com:USERNAME/REPO.git
```
## Fehlerbehandlung
### "fatal: remote origin already exists"
```bash
git remote remove origin
git remote add origin NEW_URL
```
### "Updates were rejected"
```bash
# VORSICHT: Nur wenn Sie sicher sind
git push -f origin main
```
## Siehe auch
- [CONTRIBUTING.md](CONTRIBUTING.md) - Beitragen zum Projekt
- [README.md](README.md) - Hauptdokumentation
- [RELEASE-NOTES.md](RELEASE-NOTES.md) - Versionshinweise

Binary file not shown.

Binary file not shown.

View File

@ -1,151 +0,0 @@
# Package Contents
Übersicht aller Dateien im LibreBooking n8n Node Paket.
## 📁 Struktur
```
librebooking_n8n_node/
├── 📄 Hauptdateien
├── 📁 nodes/ # n8n Nodes
├── 📁 credentials/ # Credentials
├── 📁 workflows/ # Beispiel Workflows
├── 📁 test/ # Test Dateien
└── 📄 Dokumentation & Skripte
```
---
## 📄 Hauptdateien
| Datei | Beschreibung |
|-------|-------------|
| `package.json` | Node Package Konfiguration |
| `tsconfig.json` | TypeScript Konfiguration |
| `index.ts` | Haupt-Export Datei |
---
## 📁 Nodes
| Datei | Beschreibung |
|-------|-------------|
| `nodes/LibreBooking/LibreBooking.node.ts` | Haupt-Node für alle CRUD Operationen |
| `nodes/LibreBooking/librebooking.svg` | Node Icon |
| `nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts` | Trigger Node für Events |
| `nodes/LibreBookingTrigger/librebooking.svg` | Trigger Node Icon |
---
## 🔑 Credentials
| Datei | Beschreibung |
|-------|-------------|
| `credentials/LibreBookingApi.credentials.ts` | API Credentials (URL, Benutzer, Passwort) |
| `credentials/LibreBookingConfig.credentials.ts` | Config Node für Standardwerte |
---
## 📚 Dokumentation
| Datei | Beschreibung |
|-------|-------------|
| `README.md` | Hauptdokumentation |
| `INSTALLATION.md` | Detaillierte Installationsanleitung |
| `SCHNELLSTART.md` | Quick Start Guide |
| `CUSTOM-ATTRIBUTES.md` | Custom Attributes Anleitung |
| `CONFIG-NODE.md` | Config Node Guide |
| `TROUBLESHOOTING.md` | Problemlösungen |
| `DOCKER-INTEGRATION.md` | Docker Dokumentation |
| `SECURITY.md` | Sicherheitshinweise |
| `CHANGELOG.md` | Versionshistorie |
| `CONTRIBUTING.md` | Contribution Guide |
| `LICENSE` | MIT Lizenz |
---
## 📦 Git-spezifische Dateien
| Datei | Beschreibung |
|-------|-------------|
| `GIT-UPLOAD.md` | Git Upload Anleitung |
| `RELEASE-NOTES.md` | Release Notes v1.2.0 |
| `PACKAGE-CONTENTS.md` | Diese Datei |
| `.gitignore` | Git Ignore Konfiguration |
---
## 🛠️ Skripte
### Installation
| Datei | Beschreibung |
|-------|-------------|
| `install.sh` | Linux/Mac Installation |
| `install.ps1` | Windows PowerShell Installation |
| `quick-install.sh` | Schnellinstallation |
### Docker
| Datei | Beschreibung |
|-------|-------------|
| `install-docker.sh` | Docker Installation |
| `install-docker-manual.sh` | Manuelle Docker Installation |
| `install-in-container.sh` | Installation im Container |
| `build-on-host.sh` | Host-seitiges Bauen |
### Wartung
| Datei | Beschreibung |
|-------|-------------|
| `update-node.sh` | Update Skript |
| `check-installation.sh` | Installation prüfen |
| `fix-node-installation.sh` | Installation reparieren |
| `update-dependencies.sh` | Dependencies aktualisieren |
| `upload-to-git.sh` | Git Upload Helper |
---
## 🐳 Docker Konfiguration
| Datei | Beschreibung |
|-------|-------------|
| `Dockerfile` | Docker Image Definition |
| `docker-compose.yml` | Standard Docker Compose |
| `docker-compose.override.yml` | Override für Entwicklung |
| `docker-compose.readonly.yml` | Read-only Volume Konfiguration |
| `.dockerignore` | Docker Build Ausschlüsse |
---
## 🧪 Test
| Datei | Beschreibung |
|-------|-------------|
| `test/test-api.ts` | API Test Script |
| `workflows/example-workflows.json` | Beispiel n8n Workflows |
---
## ⚙️ Konfiguration
| Datei | Beschreibung |
|-------|-------------|
| `.npmrc` | npm Konfiguration |
| `.npmignore` | npm Publish Ausschlüsse |
---
## 📊 Build Output (nicht im Repository)
| Verzeichnis | Beschreibung |
|-------------|-------------|
| `dist/` | Kompilierte JavaScript Dateien |
| `node_modules/` | npm Dependencies |
| `dist-for-docker/` | Host-Build für Docker |
---
## Dateigröße
- **Quellcode**: ~50 KB
- **Mit node_modules**: ~50 MB
- **Mit dist**: ~100 KB zusätzlich
- **Git Repository**: ~1 MB (ohne node_modules)

Binary file not shown.

View File

@ -1,44 +1,7 @@
# LibreBooking n8n Node
![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![n8n](https://img.shields.io/badge/n8n-compatible-orange.svg)
![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)
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**
@ -79,6 +42,13 @@ 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
@ -96,27 +66,11 @@ 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

View File

@ -1,135 +0,0 @@
# Release Notes v1.2.0
**Release Datum:** Januar 2026
## Übersicht
Diese Version bringt wichtige neue Features, Verbesserungen und Bugfixes für den LibreBooking n8n Node.
---
## Neue Features
### 🏷️ Custom Attributes Support
- Setzen von benutzerdefinierten Attributen für:
- Reservierungen
- Ressourcen
- Benutzer
- Accounts
- Flexible Konfiguration über fixedCollection
- Unterstützung für verschiedene Attributtypen
### ⚙️ Config Node
- Zentraler Konfigurationsknoten für Standardwerte
- Optionale Verwendung - bestehende Workflows funktionieren weiterhin
- Reduziert manuelle Eingaben bei wiederkehrenden Werten
- Konfigurierbare Defaults für:
- Standard-Ressource
- Standard-Zeitplan
- Standardwerte für neue Reservierungen
### 🔄 Verbesserte Trigger
- Neue Events triggern nicht mehr mit existierenden Daten
- Geänderte Events werden korrekt erkannt
- Hash-basierte Änderungserkennung
- Verbesserte Deduplizierung
### ✅ Pflichtfelder
- `termsAccepted` als Pflichtfeld bei Reservation Create
- Alle Pflichtfelder gemäß API-Dokumentation geprüft und ergänzt
- Bessere Validierung vor API-Aufrufen
---
## Verbesserungen
### 📚 Dokumentation
- Neue CUSTOM-ATTRIBUTES.md Anleitung
- CONFIG-NODE.md Dokumentation
- Erweiterte TROUBLESHOOTING.md
- SECURITY.md für Sicherheitshinweise
### 🐳 Docker Integration
- Read-only Volume Problem gelöst
- build-on-host.sh für Host-seitiges Bauen
- docker-compose.readonly.yml für sichere Deployments
- Verbesserte Fehlermeldungen
### 🔒 Sicherheit
- npm audit Vulnerabilities dokumentiert
- package.json overrides für sichere Dependencies
- .npmrc zur Unterdrückung von Warnungen
---
## Bugfixes
- Trigger löst bei neuen Events nicht mehr mit alten Daten aus
- Korrektes Handling von leeren API-Responses
- Verbesserte Fehlerbehandlung bei Authentifizierung
- Session-Timeout wird korrekt behandelt
---
## Installation
Siehe [INSTALLATION.md](INSTALLATION.md) für detaillierte Anweisungen.
```bash
# Quick Install
git clone https://github.com/YOUR-USERNAME/n8n-nodes-librebooking.git
cd n8n-nodes-librebooking
npm install
npm run build
```
---
## Upgrade von v1.1.0
```bash
cd /opt/n8n/custom-nodes/n8n-nodes-librebooking
git pull
npm install
npm run build
# Bei Docker:
docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
docker restart n8n
```
---
## Breaking Changes
**Keine** - Diese Version ist vollständig abwärtskompatibel.
---
## Bekannte Einschränkungen
- npm audit zeigt Vulnerabilities von n8n-workflow Dependencies (siehe SECURITY.md)
- Read-only Docker Volumes erfordern Host-seitiges Bauen
---
## Nächste Version (Roadmap)
- [ ] Webhook Support für Echtzeit-Events
- [ ] Batch-Operationen für mehrere Reservierungen
- [ ] Erweiterte Filteroptionen
- [ ] npm Registry Veröffentlichung
---
## Danksagungen
Vielen Dank an alle Contributors und Tester!
---
## Links
- [GitHub Repository](https://github.com/YOUR-USERNAME/n8n-nodes-librebooking)
- [LibreBooking](https://github.com/LibreBooking/app)
- [n8n](https://n8n.io)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,143 +0,0 @@
# LibreBooking n8n Node - Test Results
## Test Datum: 25.01.2026
### Test-Umgebung
- **URL**: https://librebooking.zell-cloud.de
- **Benutzer**: sebastian.zell@zell-aufmass.de
- **n8n Node Version**: 1.2.1
---
## Test-Ergebnisse
### 1. Authentifizierung ✅
- Login erfolgreich
- Session Token wird korrekt generiert
- User ID wird zurückgegeben
### 2. Reservierungen ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 12 Reservierungen gefunden |
| Get All (mit Datumsfilter) | ✅ | Filtert korrekt nach Zeitraum |
| Get (Einzeln) | ✅ | Custom Attributes werden zurückgegeben |
| Create | ✅ | allowParticipation wird korrekt gesetzt |
| Update | ✅ | Änderungen werden übernommen |
| Delete | ✅ | Reservierung wird gelöscht |
**Custom Attributes für Reservierungen (9 gefunden):**
- Mietername (ID: 1, Typ: Text, Pflicht: ✅)
- Telefon (ID: 2, Typ: Text, Pflicht: ❌)
- Adresse (ID: 3, Typ: Text, Pflicht: ✅)
- Lage der Wohnung Gebäudeart (ID: 11, Typ: Auswahl)
- Geschoss (ID: 9, Typ: Auswahl)
- Lage der Wohnung Lage im Grundriss (ID: 10, Typ: Auswahl)
- Quadratmeter (ID: 12, Typ: Text)
- Clustername (ID: 4, Typ: Text, Pflicht: ✅)
- Status (ID: 8, Typ: Auswahl, Pflicht: ✅)
### 3. Ressourcen ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 4 Ressourcen gefunden |
| Get (Einzeln) | ✅ | Details werden abgerufen |
**Ressourcen:**
- Aufmass Team 1 (ID: 1)
- Aufmass Team 2 (ID: 2)
- Aufmass Team 3 (ID: 3)
- Aufmass Team 4 (ID: 4)
### 4. Benutzer ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 3 Benutzer gefunden |
| Get (Einzeln) | ✅ | Details werden abgerufen |
### 5. Zeitpläne ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 1 Zeitplan gefunden |
### 6. Attribute (nach Kategorie) ✅
| Kategorie | Anzahl |
|-----------|--------|
| Reservierung (1) | 9 |
| Benutzer (2) | 0 |
| Ressource (4) | 0 |
| Ressourcen-Typ (5) | 0 |
### 7. Gruppen ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 2 Gruppen gefunden |
### 8. Zubehör ✅
| Operation | Status | Details |
|-----------|--------|---------|
| Get All | ✅ | 0 Zubehörteile (keine konfiguriert) |
### 9. Sign Out ✅
- Session wird korrekt beendet
---
## Trigger Node Tests
### "Alle Abrufen" (Get All) Mode ✅
- Ruft alle Reservierungen für den angegebenen Zeitraum ab
- Optionale Start-/Enddatum-Filter funktionieren
- "Detaillierte Daten Abrufen" Option lädt Custom Attributes
### "Neue Reservierungen" (Poll) Mode ✅
- Erster Poll: Speichert IDs, triggert nicht
- Folgende Polls: Erkennt neue Reservierungen
- Debug-Modus zeigt gespeicherte IDs an
### "Geänderte Reservierungen" (Poll) Mode ✅
- Erster Poll: Speichert Hashes, triggert nicht
- Folgende Polls: Erkennt Änderungen durch Hash-Vergleich
- Änderungen an Titel, Beschreibung, Zeitraum werden erkannt
---
## Behobene Probleme
### 1. allowParticipation Fehler ✅
**Problem**: API-Fehler "Undefined property: stdClass::$allowParticipation"
**Lösung**: `allowParticipation` wird jetzt immer im Request-Body gesendet (ist ein Pflichtfeld).
### 2. Trigger "Alle Abrufen" funktioniert nicht ✅
**Problem**: Mode war unklar, nutzte Polling-Logik
**Lösung**: Neuer "Alle Abrufen (Einmalig)" Mode mit optionalen Datum-Parametern.
### 3. Custom Attributes nicht elegant abrufbar ✅
**Problem**: Manuelles Eingeben von Attribut-IDs nötig
**Lösung**: "Custom Attributes Einschließen" Option bei GetAll-Operationen für:
- Reservierungen
- Ressourcen
- Benutzer
---
## Test-Zusammenfassung
| Kategorie | Tests | Bestanden | Fehlgeschlagen |
|-----------|-------|-----------|----------------|
| API-Endpunkte | 19 | 19 | 0 |
| Trigger Modes | 3 | 3 | 0 |
| Custom Attributes | 4 | 4 | 0 |
| **Gesamt** | **26** | **26** | **0** |
✅ **Alle Tests erfolgreich bestanden!**

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -1,75 +0,0 @@
import {
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
/**
* LibreBooking Config Credential
*
* Ermöglicht die zentrale Konfiguration von Standardwerten,
* die in allen LibreBooking Nodes verwendet werden können.
*/
export class LibreBookingConfig implements ICredentialType {
name = 'libreBookingConfig';
displayName = 'LibreBooking Config';
documentationUrl = 'https://librebooking.org';
properties: INodeProperties[] = [
{
displayName: 'Hinweis',
name: 'notice',
type: 'notice',
default: '',
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
},
{
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
name: 'defaultTermsAccepted',
type: 'boolean',
default: true,
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
},
{
displayName: 'Standard Teilnahme Erlauben',
name: 'defaultAllowParticipation',
type: 'boolean',
default: false,
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
},
{
displayName: 'Standard Ressourcen-ID',
name: 'defaultResourceId',
type: 'number',
default: 0,
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
},
{
displayName: 'Standard Benutzer-ID',
name: 'defaultUserId',
type: 'number',
default: 0,
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
},
{
displayName: 'Standard Zeitplan-ID',
name: 'defaultScheduleId',
type: 'number',
default: 0,
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
},
{
displayName: 'Standard Zeitzone',
name: 'defaultTimezone',
type: 'string',
default: 'Europe/Berlin',
description: 'Standard-Zeitzone für neue Benutzer',
},
{
displayName: 'Standard Sprache',
name: 'defaultLanguage',
type: 'string',
default: 'de_de',
description: 'Standard-Sprache für neue Benutzer',
},
];
}

View File

@ -1,15 +0,0 @@
import { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
/**
* LibreBooking API Credentials
*
* LibreBooking verwendet Session-basierte Authentifizierung.
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
*/
export declare class LibreBookingApi implements ICredentialType {
name: string;
displayName: string;
documentationUrl: string;
properties: INodeProperties[];
test: ICredentialTestRequest;
}
//# sourceMappingURL=LibreBookingApi.credentials.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"LibreBookingApi.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,sBAAsB,EACtB,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACtD,IAAI,SAAqB;IACzB,WAAW,SAAsB;IACjC,gBAAgB,SAAuC;IAEvD,UAAU,EAAE,eAAe,EAAE,CA6B3B;IAGF,IAAI,EAAE,sBAAsB,CAa1B;CACF"}

View File

@ -1,63 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LibreBookingApi = void 0;
/**
* LibreBooking API Credentials
*
* LibreBooking verwendet Session-basierte Authentifizierung.
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
*/
class LibreBookingApi {
constructor() {
this.name = 'libreBookingApi';
this.displayName = 'LibreBooking API';
this.documentationUrl = 'https://librebooking.org/docs/api';
this.properties = [
{
displayName: 'LibreBooking URL',
name: 'url',
type: 'string',
default: '',
placeholder: 'https://booking.example.com',
required: true,
description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)',
},
{
displayName: 'Benutzername',
name: 'username',
type: 'string',
default: '',
required: true,
description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse',
},
{
displayName: 'Passwort',
name: 'password',
type: 'string',
typeOptions: {
password: true,
},
default: '',
required: true,
description: 'Ihr LibreBooking-Passwort',
},
];
// Test-Request um die Credentials zu validieren
this.test = {
request: {
baseURL: '={{$credentials.url}}',
url: '/Web/Services/index.php/Authentication/Authenticate',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: {
username: '={{$credentials.username}}',
password: '={{$credentials.password}}',
},
},
};
}
}
exports.LibreBookingApi = LibreBookingApi;
//# sourceMappingURL=LibreBookingApi.credentials.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"LibreBookingApi.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":";;;AAOA;;;;;GAKG;AACH,MAAa,eAAe;IAA5B;QACC,SAAI,GAAG,iBAAiB,CAAC;QACzB,gBAAW,GAAG,kBAAkB,CAAC;QACjC,qBAAgB,GAAG,mCAAmC,CAAC;QAEvD,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,6BAA6B;gBAC1C,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,oEAAoE;aACjF;YACD;gBACC,WAAW,EAAE,cAAc;gBAC3B,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,mDAAmD;aAChE;YACD;gBACC,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE;oBACZ,QAAQ,EAAE,IAAI;iBACd;gBACD,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,2BAA2B;aACxC;SACD,CAAC;QAEF,gDAAgD;QAChD,SAAI,GAA2B;YAC9B,OAAO,EAAE;gBACR,OAAO,EAAE,uBAAuB;gBAChC,GAAG,EAAE,qDAAqD;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACR,cAAc,EAAE,kBAAkB;iBAClC;gBACD,IAAI,EAAE;oBACL,QAAQ,EAAE,4BAA4B;oBACtC,QAAQ,EAAE,4BAA4B;iBACtC;aACD;SACD,CAAC;IACH,CAAC;CAAA;AAnDD,0CAmDC"}

View File

@ -1,14 +0,0 @@
import { ICredentialType, INodeProperties } from 'n8n-workflow';
/**
* LibreBooking Config Credential
*
* Ermöglicht die zentrale Konfiguration von Standardwerten,
* die in allen LibreBooking Nodes verwendet werden können.
*/
export declare class LibreBookingConfig implements ICredentialType {
name: string;
displayName: string;
documentationUrl: string;
properties: INodeProperties[];
}
//# sourceMappingURL=LibreBookingConfig.credentials.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"LibreBookingConfig.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACzD,IAAI,SAAwB;IAC5B,WAAW,SAAyB;IACpC,gBAAgB,SAA8B;IAE9C,UAAU,EAAE,eAAe,EAAE,CAyD3B;CACF"}

View File

@ -1,76 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LibreBookingConfig = void 0;
/**
* LibreBooking Config Credential
*
* Ermöglicht die zentrale Konfiguration von Standardwerten,
* die in allen LibreBooking Nodes verwendet werden können.
*/
class LibreBookingConfig {
constructor() {
this.name = 'libreBookingConfig';
this.displayName = 'LibreBooking Config';
this.documentationUrl = 'https://librebooking.org';
this.properties = [
{
displayName: 'Hinweis',
name: 'notice',
type: 'notice',
default: '',
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
},
{
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
name: 'defaultTermsAccepted',
type: 'boolean',
default: true,
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
},
{
displayName: 'Standard Teilnahme Erlauben',
name: 'defaultAllowParticipation',
type: 'boolean',
default: false,
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
},
{
displayName: 'Standard Ressourcen-ID',
name: 'defaultResourceId',
type: 'number',
default: 0,
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
},
{
displayName: 'Standard Benutzer-ID',
name: 'defaultUserId',
type: 'number',
default: 0,
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
},
{
displayName: 'Standard Zeitplan-ID',
name: 'defaultScheduleId',
type: 'number',
default: 0,
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
},
{
displayName: 'Standard Zeitzone',
name: 'defaultTimezone',
type: 'string',
default: 'Europe/Berlin',
description: 'Standard-Zeitzone für neue Benutzer',
},
{
displayName: 'Standard Sprache',
name: 'defaultLanguage',
type: 'string',
default: 'de_de',
description: 'Standard-Sprache für neue Benutzer',
},
];
}
}
exports.LibreBookingConfig = LibreBookingConfig;
//# sourceMappingURL=LibreBookingConfig.credentials.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"LibreBookingConfig.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":";;;AAKA;;;;;GAKG;AACH,MAAa,kBAAkB;IAA/B;QACC,SAAI,GAAG,oBAAoB,CAAC;QAC5B,gBAAW,GAAG,qBAAqB,CAAC;QACpC,qBAAgB,GAAG,0BAA0B,CAAC;QAE9C,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,SAAS;gBACtB,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,4JAA4J;aACzK;YACD;gBACC,WAAW,EAAE,yCAAyC;gBACtD,IAAI,EAAE,sBAAsB;gBAC5B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,2EAA2E;aACxF;YACD;gBACC,WAAW,EAAE,6BAA6B;gBAC1C,IAAI,EAAE,2BAA2B;gBACjC,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,KAAK;gBACd,WAAW,EAAE,6DAA6D;aAC1E;YACD;gBACC,WAAW,EAAE,wBAAwB;gBACrC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,yEAAyE;aACtF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,eAAe;gBACrB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,qEAAqE;aAClF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,8EAA8E;aAC3F;YACD;gBACC,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,eAAe;gBACxB,WAAW,EAAE,qCAAqC;aAClD;YACD;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,OAAO;gBAChB,WAAW,EAAE,oCAAoC;aACjD;SACD,CAAC;IACH,CAAC;CAAA;AA/DD,gDA+DC"}

View File

@ -1,12 +0,0 @@
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
/**
* LibreBooking n8n Node
*
* Vollständige Integration für die LibreBooking API.
* Unterstützt alle wichtigen Ressourcen und Operationen.
*/
export declare class LibreBooking implements INodeType {
description: INodeTypeDescription;
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
}
//# sourceMappingURL=LibreBooking.node.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"LibreBooking.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBooking/LibreBooking.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACC,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAI3B,MAAM,cAAc,CAAC;AA8LtB;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,SAAS;IACtC,WAAW,EAAE,oBAAoB,CAw2B/B;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAmjB9E"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
<!-- Background -->
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
<!-- Calendar base -->
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
<!-- Calendar header -->
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
<!-- Calendar rings -->
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
<!-- Grid lines -->
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
<!-- Booking indicator -->
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
<!-- Check mark -->
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,14 +0,0 @@
import { INodeType, INodeTypeDescription, IPollFunctions, INodeExecutionData } from 'n8n-workflow';
/**
* LibreBooking Trigger Node
*
* Drei Modi:
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
*/
export declare class LibreBookingTrigger implements INodeType {
description: INodeTypeDescription;
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
}
//# sourceMappingURL=LibreBookingTrigger.node.d.ts.map

View File

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

View File

@ -1,813 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LibreBookingTrigger = void 0;
const n8n_workflow_1 = require("n8n-workflow");
/**
* Authentifizierung bei LibreBooking
*/
async function authenticateTrigger(pollFunctions, baseUrl, username, password) {
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'POST',
url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`,
headers: { 'Content-Type': 'application/json' },
body: { username, password },
json: true,
});
if (!response.isAuthenticated) {
throw new n8n_workflow_1.NodeOperationError(pollFunctions.getNode(), 'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.');
}
return {
sessionToken: response.sessionToken,
userId: response.userId,
sessionExpires: response.sessionExpires,
};
}
catch (error) {
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
message: 'Authentifizierung fehlgeschlagen',
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
});
}
}
/**
* Abmeldung
*/
async function signOutTrigger(pollFunctions, baseUrl, session) {
try {
await pollFunctions.helpers.httpRequest({
method: 'POST',
url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`,
headers: { 'Content-Type': 'application/json' },
body: {
userId: session.userId,
sessionToken: session.sessionToken,
},
json: true,
});
}
catch (error) {
// Ignoriere SignOut-Fehler
}
}
/**
* Reservierungen abrufen
*/
async function getReservations(pollFunctions, baseUrl, session, startDateTime, endDateTime, filters) {
const qs = {
startDateTime,
endDateTime,
};
if (filters.resourceId)
qs.resourceId = filters.resourceId;
if (filters.scheduleId)
qs.scheduleId = filters.scheduleId;
if (filters.userId)
qs.userId = filters.userId;
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'GET',
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
headers: {
'Content-Type': 'application/json',
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
},
qs,
json: true,
});
return response.reservations || [];
}
catch (error) {
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
message: 'Fehler beim Abrufen der Reservierungen',
});
}
}
/**
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
*/
async function getReservationDetails(pollFunctions, baseUrl, session, referenceNumber) {
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'GET',
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
headers: {
'Content-Type': 'application/json',
'X-Booked-SessionToken': session.sessionToken,
'X-Booked-UserId': session.userId.toString(),
},
json: true,
});
return response;
}
catch (error) {
return null;
}
}
/**
* Berechnet Zeitraum basierend auf der gewählten Option
*/
function getDateRange(dateRange) {
const now = new Date();
let startDate;
let endDate;
switch (dateRange) {
case 'thisWeek':
// Montag dieser Woche (ISO: Montag = 1, Sonntag = 0)
startDate = new Date(now);
const dayOfWeek = now.getDay();
const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
startDate.setDate(now.getDate() + diffToMonday);
startDate.setHours(0, 0, 0, 0);
// Sonntag dieser Woche
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Weeks':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setDate(now.getDate() + 14);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisMonth':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'next2Months':
startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now);
endDate.setMonth(now.getMonth() + 2);
endDate.setHours(23, 59, 59, 999);
break;
case 'thisYear':
startDate = new Date(now.getFullYear(), 0, 1);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(now.getFullYear(), 11, 31);
endDate.setHours(23, 59, 59, 999);
break;
default: // custom
return { startDate: '', endDate: '' };
}
return {
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
};
}
/**
* Zeitfenster berechnen für "Get All" Mode
*/
function getTimeWindowForGetAll(dateRange, customStartDate, customEndDate, defaultDays = 14) {
// Wenn ein vordefinierter Zeitraum gewählt wurde
if (dateRange && dateRange !== 'custom') {
const { startDate, endDate } = getDateRange(dateRange);
return { start: startDate, end: endDate };
}
// Custom Modus mit manuellen Daten
if (customStartDate && customEndDate) {
return {
start: new Date(customStartDate).toISOString(),
end: new Date(customEndDate).toISOString(),
};
}
// Fallback: Ab heute für defaultDays
const now = new Date();
const endDate = new Date(now);
endDate.setDate(endDate.getDate() + defaultDays);
return {
start: now.toISOString(),
end: endDate.toISOString(),
};
}
/**
* Zeitfenster berechnen für Polling
*/
function getTimeWindowForPolling(timeWindow) {
const now = new Date();
const start = now.toISOString();
let endDate = new Date(now);
switch (timeWindow) {
case '7days':
endDate.setDate(endDate.getDate() + 7);
break;
case '14days':
endDate.setDate(endDate.getDate() + 14);
break;
case '30days':
endDate.setDate(endDate.getDate() + 30);
break;
case '90days':
endDate.setDate(endDate.getDate() + 90);
break;
case '180days':
endDate.setDate(endDate.getDate() + 180);
break;
default:
endDate.setDate(endDate.getDate() + 14);
}
return {
start,
end: endDate.toISOString(),
};
}
/**
* Filter Reservierungen nach Zeitpunkt
*/
function filterByTime(reservations, timeFilter) {
if (timeFilter === 'all') {
return reservations;
}
const now = new Date();
now.setHours(0, 0, 0, 0);
return reservations.filter((reservation) => {
// Verwende startDate oder startDateTime
const dateStr = reservation.startDateTime || reservation.startDate;
if (!dateStr)
return true;
const startDate = new Date(dateStr);
startDate.setHours(0, 0, 0, 0);
if (timeFilter === 'today') {
return startDate.getTime() === now.getTime();
}
if (timeFilter === 'next3Days') {
const threeDaysFromNow = new Date(now);
threeDaysFromNow.setDate(now.getDate() + 3);
return startDate >= now && startDate <= threeDaysFromNow;
}
if (timeFilter === 'next7Days') {
const sevenDaysFromNow = new Date(now);
sevenDaysFromNow.setDate(now.getDate() + 7);
return startDate >= now && startDate <= sevenDaysFromNow;
}
return true;
});
}
/**
* Hash für Reservierung generieren (für Änderungserkennung)
*/
function getReservationHash(reservation) {
const relevantData = {
referenceNumber: reservation.referenceNumber,
startDate: reservation.startDate || reservation.startDateTime,
endDate: reservation.endDate || reservation.endDateTime,
title: reservation.title || '',
description: reservation.description || '',
resourceId: reservation.resourceId,
resourceName: reservation.resourceName || '',
userId: reservation.userId,
requiresApproval: reservation.requiresApproval,
participants: reservation.participants || [],
invitees: reservation.invitees || [],
statusId: reservation.statusId,
};
return JSON.stringify(relevantData);
}
/**
* LibreBooking Trigger Node
*
* Drei Modi:
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
*/
class LibreBookingTrigger {
constructor() {
this.description = {
displayName: 'LibreBooking Trigger',
name: 'libreBookingTrigger',
icon: 'file:librebooking.svg',
group: ['trigger'],
version: 1,
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
subtitle: '={{$parameter["triggerMode"]}}',
defaults: {
name: 'LibreBooking Trigger',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'libreBookingApi',
required: true,
},
],
polling: true,
properties: [
// =====================================================
// TRIGGER MODE SELECTOR
// =====================================================
{
displayName: 'Trigger-Modus',
name: 'triggerMode',
type: 'options',
options: [
{
name: 'Alle Abrufen (Einmalig)',
value: 'getAll',
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
},
{
name: 'Neue Reservierungen (Polling)',
value: 'newReservations',
description: 'Nur bei neuen Reservierungen triggern',
},
{
name: 'Geänderte Reservierungen (Polling)',
value: 'updatedReservations',
description: 'Nur bei geänderten Reservierungen triggern',
},
],
default: 'getAll',
description: 'Wählen Sie den Trigger-Modus',
},
// =====================================================
// GET ALL MODE - DATE RANGE SELECTOR
// =====================================================
{
displayName: 'Zeitraum',
name: 'dateRange',
type: 'options',
displayOptions: {
show: {
triggerMode: ['getAll'],
},
},
options: [
{
name: 'Benutzerdefiniert',
value: 'custom',
description: 'Start- und Enddatum manuell angeben',
},
{
name: 'Diese Woche',
value: 'thisWeek',
description: 'Von Montag bis Sonntag der aktuellen Woche',
},
{
name: 'Nächste 2 Wochen',
value: 'next2Weeks',
description: 'Ab heute bis 14 Tage in die Zukunft',
},
{
name: 'Dieser Monat',
value: 'thisMonth',
description: 'Vom 1. bis zum letzten Tag des aktuellen Monats',
},
{
name: 'Nächste 2 Monate',
value: 'next2Months',
description: 'Ab heute bis 2 Monate in die Zukunft',
},
{
name: 'Dieses Jahr',
value: 'thisYear',
description: 'Vom 1. Januar bis 31. Dezember des aktuellen Jahres',
},
],
default: 'custom',
description: 'Vordefinierter Zeitraum für den Abruf',
},
{
displayName: 'Startdatum',
name: 'startDate',
type: 'dateTime',
displayOptions: {
show: {
triggerMode: ['getAll'],
dateRange: ['custom'],
},
},
default: '',
description: 'Startdatum für den Abruf (leer = heute)',
},
{
displayName: 'Enddatum',
name: 'endDate',
type: 'dateTime',
displayOptions: {
show: {
triggerMode: ['getAll'],
dateRange: ['custom'],
},
},
default: '',
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
},
// =====================================================
// POLLING MODE - TIME WINDOW
// =====================================================
{
displayName: 'Zeitfenster',
name: 'timeWindow',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{ name: 'Nächste 7 Tage', value: '7days' },
{ name: 'Nächste 14 Tage', value: '14days' },
{ name: 'Nächste 30 Tage', value: '30days' },
{ name: 'Nächste 90 Tage', value: '90days' },
{ name: 'Nächste 180 Tage (6 Monate)', value: '180days' },
],
default: '14days',
description: 'Zeitfenster für die Überwachung von Reservierungen',
},
// =====================================================
// TIME FILTER FOR NEW/UPDATED MODES
// =====================================================
{
displayName: 'Zeit-Filter',
name: 'timeFilter',
type: 'options',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
options: [
{
name: 'Alle (Kein Filter)',
value: 'all',
description: 'Alle neuen/geänderten Reservierungen, unabhängig vom Datum',
},
{
name: 'Nur Heute',
value: 'today',
description: 'Nur Reservierungen, die heute stattfinden',
},
{
name: 'Nächste 3 Tage',
value: 'next3Days',
description: 'Nur Reservierungen, die in den nächsten 3 Tagen stattfinden',
},
{
name: 'Nächste 7 Tage',
value: 'next7Days',
description: 'Nur Reservierungen, die in den nächsten 7 Tagen stattfinden',
},
],
default: 'all',
description: 'Filtert Reservierungen nach ihrem Startdatum',
},
{
displayName: 'Hinweis',
name: 'pollingNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
triggerMode: ['newReservations', 'updatedReservations'],
},
},
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
},
// =====================================================
// FILTERS (ALL MODES)
// =====================================================
{
displayName: 'Filter',
name: 'filters',
type: 'collection',
placeholder: 'Filter hinzufügen',
default: {},
options: [
{
displayName: 'Ressourcen-ID',
name: 'resourceId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diese Ressource',
},
{
displayName: 'Zeitplan-ID',
name: 'scheduleId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diesen Zeitplan',
},
{
displayName: 'Benutzer-ID',
name: 'userId',
type: 'number',
default: '',
description: 'Nur Reservierungen für diesen Benutzer',
},
],
},
// =====================================================
// OPTIONS (ALL MODES)
// =====================================================
{
displayName: 'Optionen',
name: 'options',
type: 'collection',
placeholder: 'Option hinzufügen',
default: {},
options: [
{
displayName: 'Detaillierte Daten Abrufen',
name: 'fetchDetails',
type: 'boolean',
default: false,
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
},
{
displayName: 'Debug-Modus',
name: 'debugMode',
type: 'boolean',
default: false,
description: 'Gibt zusätzliche Debug-Informationen aus',
},
],
},
],
};
}
async poll() {
const credentials = await this.getCredentials('libreBookingApi');
const baseUrl = credentials.url.replace(/\/$/, '');
const username = credentials.username;
const password = credentials.password;
const triggerMode = this.getNodeParameter('triggerMode');
const filters = this.getNodeParameter('filters', {});
const options = this.getNodeParameter('options', {});
// Debug-Modus
const debugMode = options.debugMode || false;
const fetchDetails = options.fetchDetails || false;
// Workflow Static Data für State-Management
const webhookData = this.getWorkflowStaticData('node');
let session;
try {
session = await authenticateTrigger(this, baseUrl, username, password);
}
catch (error) {
throw error;
}
try {
const returnData = [];
// ==========================================
// MODE: Get All (One-Time / Every Poll)
// ==========================================
if (triggerMode === 'getAll') {
const dateRange = this.getNodeParameter('dateRange', 'custom');
const startDate = this.getNodeParameter('startDate', '');
const endDate = this.getNodeParameter('endDate', '');
const { start, end } = getTimeWindowForGetAll(dateRange, startDate || undefined, endDate || undefined, 14);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
if (debugMode) {
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`);
console.log(`[LibreBooking Trigger] Date Range: ${dateRange}`);
console.log(`[LibreBooking Trigger] Period: ${start} to ${end}`);
}
if (reservations.length === 0) {
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Keine Reservierungen im Zeitraum gefunden',
_dateRange: dateRange,
_startDate: start,
_endDate: end,
_count: 0,
},
},
],
];
}
return null;
}
// Alle Reservierungen zurückgeben
for (const reservation of reservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
if (details) {
reservationData = details;
}
}
catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'getAll',
_dateRange: dateRange,
_triggeredAt: new Date().toISOString(),
},
});
}
}
// ==========================================
// MODE: New Reservations (Polling)
// ==========================================
else if (triggerMode === 'newReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days');
const timeFilter = this.getNodeParameter('timeFilter', 'all');
const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere seenIds beim ersten Poll
if (!webhookData.seenIds) {
webhookData.seenIds = [];
webhookData.isFirstPoll = true;
}
const currentIds = reservations.map((r) => r.referenceNumber);
if (debugMode) {
console.log(`[LibreBooking Trigger] New Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`);
}
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
if (webhookData.isFirstPoll) {
webhookData.seenIds = currentIds;
webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
_savedIds: currentIds.length,
_ids: currentIds,
_timestamp: webhookData.lastPollTime,
},
},
],
];
}
return null; // Nichts triggern beim ersten Poll
}
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
let newReservations = reservations.filter((r) => !webhookData.seenIds.includes(r.referenceNumber));
// Update seenIds mit allen aktuellen IDs
webhookData.seenIds = currentIds;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(`[LibreBooking Trigger] Found ${newReservations.length} new reservations before filter`);
}
if (newReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
newReservations = filterByTime(newReservations, timeFilter);
if (debugMode) {
console.log(`[LibreBooking Trigger] ${newReservations.length} reservations after time filter (${timeFilter})`);
}
if (newReservations.length === 0) {
return null;
}
// Neue Reservierungen verarbeiten
for (const reservation of newReservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
if (details) {
reservationData = details;
}
}
catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'new',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(),
},
});
}
if (debugMode && returnData.length > 0) {
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
}
}
// ==========================================
// MODE: Updated Reservations (Polling)
// ==========================================
else if (triggerMode === 'updatedReservations') {
const timeWindow = this.getNodeParameter('timeWindow', '14days');
const timeFilter = this.getNodeParameter('timeFilter', 'all');
const { start, end } = getTimeWindowForPolling(timeWindow);
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
// Initialisiere reservationHashes beim ersten Poll
if (!webhookData.reservationHashes) {
webhookData.reservationHashes = {};
webhookData.isFirstPoll = true;
}
if (debugMode) {
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
console.log(`[LibreBooking Trigger] Time Filter: ${timeFilter}`);
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`);
}
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
if (webhookData.isFirstPoll) {
for (const reservation of reservations) {
webhookData.reservationHashes[reservation.referenceNumber] =
getReservationHash(reservation);
}
webhookData.isFirstPoll = false;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
return [
[
{
json: {
_debug: true,
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
_savedHashes: Object.keys(webhookData.reservationHashes).length,
_timestamp: webhookData.lastPollTime,
},
},
],
];
}
return null; // Nichts triggern beim ersten Poll
}
// Geänderte Reservierungen finden
let updatedReservations = [];
const newHashes = {};
for (const reservation of reservations) {
const currentHash = getReservationHash(reservation);
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
newHashes[reservation.referenceNumber] = currentHash;
// Nur als "geändert" markieren, wenn:
// 1. Wir die Reservierung schon kennen (nicht neu)
// 2. Der Hash sich geändert hat
if (oldHash && currentHash !== oldHash) {
updatedReservations.push(reservation);
}
}
// Update Hashes mit allen aktuellen Reservierungen
webhookData.reservationHashes = newHashes;
webhookData.lastPollTime = new Date().toISOString();
if (debugMode) {
console.log(`[LibreBooking Trigger] Found ${updatedReservations.length} updated reservations before filter`);
}
if (updatedReservations.length === 0) {
return null;
}
// Zeit-Filter anwenden
updatedReservations = filterByTime(updatedReservations, timeFilter);
if (debugMode) {
console.log(`[LibreBooking Trigger] ${updatedReservations.length} reservations after time filter (${timeFilter})`);
}
if (updatedReservations.length === 0) {
return null;
}
// Geänderte Reservierungen verarbeiten
for (const reservation of updatedReservations) {
let reservationData = reservation;
if (fetchDetails) {
try {
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
if (details) {
reservationData = details;
}
}
catch (error) {
// Fallback auf Basisdaten
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'updated',
_timeFilter: timeFilter,
_triggeredAt: new Date().toISOString(),
},
});
}
if (debugMode && returnData.length > 0) {
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
}
}
if (returnData.length === 0) {
return null;
}
return [returnData];
}
finally {
await signOutTrigger(this, baseUrl, session);
}
}
}
exports.LibreBookingTrigger = LibreBookingTrigger;
//# sourceMappingURL=LibreBookingTrigger.node.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
<!-- Background -->
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
<!-- Calendar base -->
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
<!-- Calendar header -->
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
<!-- Calendar rings -->
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
<!-- Grid lines -->
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
<!-- Booking indicator -->
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
<!-- Check mark -->
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -4,4 +4,3 @@
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.

View File

@ -14,16 +14,6 @@ interface LibreBookingSession {
sessionExpires: string;
}
interface ConfigDefaults {
defaultTermsAccepted: boolean;
defaultAllowParticipation: boolean;
defaultResourceId: number;
defaultUserId: number;
defaultScheduleId: number;
defaultTimezone: string;
defaultLanguage: string;
}
/**
* Authentifizierung bei LibreBooking
*/
@ -150,52 +140,6 @@ function parseIdList(value: string | undefined): number[] {
return value.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id));
}
/**
* Config-Defaults laden
*/
async function getConfigDefaults(executeFunctions: IExecuteFunctions): Promise<ConfigDefaults> {
const defaults: ConfigDefaults = {
defaultTermsAccepted: true,
defaultAllowParticipation: false,
defaultResourceId: 0,
defaultUserId: 0,
defaultScheduleId: 0,
defaultTimezone: 'Europe/Berlin',
defaultLanguage: 'de_de',
};
try {
const configCredentials = await executeFunctions.getCredentials('libreBookingConfig');
if (configCredentials) {
if (configCredentials.defaultTermsAccepted !== undefined) {
defaults.defaultTermsAccepted = configCredentials.defaultTermsAccepted as boolean;
}
if (configCredentials.defaultAllowParticipation !== undefined) {
defaults.defaultAllowParticipation = configCredentials.defaultAllowParticipation as boolean;
}
if (configCredentials.defaultResourceId !== undefined && configCredentials.defaultResourceId !== 0) {
defaults.defaultResourceId = configCredentials.defaultResourceId as number;
}
if (configCredentials.defaultUserId !== undefined && configCredentials.defaultUserId !== 0) {
defaults.defaultUserId = configCredentials.defaultUserId as number;
}
if (configCredentials.defaultScheduleId !== undefined && configCredentials.defaultScheduleId !== 0) {
defaults.defaultScheduleId = configCredentials.defaultScheduleId as number;
}
if (configCredentials.defaultTimezone) {
defaults.defaultTimezone = configCredentials.defaultTimezone as string;
}
if (configCredentials.defaultLanguage) {
defaults.defaultLanguage = configCredentials.defaultLanguage as string;
}
}
} catch (error) {
// Config-Credential ist optional, ignoriere Fehler
}
return defaults;
}
/**
* LibreBooking n8n Node
*
@ -221,15 +165,6 @@ export class LibreBooking implements INodeType {
name: 'libreBookingApi',
required: true,
},
{
name: 'libreBookingConfig',
required: false,
displayOptions: {
show: {
resource: ['reservation', 'resource', 'user', 'account'],
},
},
},
],
properties: [
// =====================================================
@ -485,16 +420,6 @@ export class LibreBooking implements INodeType {
default: '',
description: 'Endzeitpunkt der Reservierung (ISO 8601 Format)',
},
// PFLICHTFELD: termsAccepted für Reservierung erstellen
{
displayName: 'Nutzungsbedingungen Akzeptiert',
name: 'termsAccepted',
type: 'boolean',
required: true,
displayOptions: { show: { resource: ['reservation'], operation: ['create'] } },
default: true,
description: 'Ob der Benutzer die Nutzungsbedingungen akzeptiert (Pflichtfeld laut API)',
},
{
displayName: 'Titel',
name: 'title',
@ -515,41 +440,6 @@ export class LibreBooking implements INodeType {
],
default: 'this',
},
// CUSTOM ATTRIBUTES für Reservierungen
{
displayName: 'Benutzerdefinierte Attribute',
name: 'customAttributes',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Attribut hinzufügen',
displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } },
options: [
{
name: 'attribute',
displayName: 'Attribut',
values: [
{
displayName: 'Attribut-ID',
name: 'attributeId',
type: 'number',
default: 0,
description: 'Die ID des benutzerdefinierten Attributs',
},
{
displayName: 'Wert',
name: 'attributeValue',
type: 'string',
default: '',
description: 'Der Wert für dieses Attribut',
},
],
},
],
description: 'Benutzerdefinierte Attribute für diese Reservierung setzen',
},
{
displayName: 'Zusätzliche Felder',
name: 'additionalFields',
@ -564,7 +454,7 @@ export class LibreBooking implements INodeType {
{ displayName: 'Teilnehmer', name: 'participants', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' },
{ displayName: 'Eingeladene', name: 'invitees', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' },
{ displayName: 'Teilnahme Erlauben', name: 'allowParticipation', type: 'boolean', default: true },
{ displayName: 'Ressourcen-ID (Update)', name: 'resourceId', type: 'number', default: '', description: 'Ressourcen-ID für Updates' },
{ displayName: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true },
],
},
{
@ -580,13 +470,6 @@ export class LibreBooking implements INodeType {
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
{ displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' },
{ displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' },
{
displayName: 'Custom Attributes Einschließen',
name: 'includeCustomAttributes',
type: 'boolean',
default: false,
description: 'Für jede Reservierung die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
},
],
},
@ -630,42 +513,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['resource'], operation: ['create'] } },
default: 1,
description: 'Die ID des Zeitplans für diese Ressource (Pflichtfeld)',
},
// CUSTOM ATTRIBUTES für Ressourcen
{
displayName: 'Benutzerdefinierte Attribute',
name: 'resourceCustomAttributes',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Attribut hinzufügen',
displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } },
options: [
{
name: 'attribute',
displayName: 'Attribut',
values: [
{
displayName: 'Attribut-ID',
name: 'attributeId',
type: 'number',
default: 0,
description: 'Die ID des benutzerdefinierten Attributs',
},
{
displayName: 'Wert',
name: 'attributeValue',
type: 'string',
default: '',
description: 'Der Wert für dieses Attribut',
},
],
},
],
description: 'Benutzerdefinierte Attribute für diese Ressource setzen',
},
{
displayName: 'Ressourcen-Optionen',
@ -688,23 +535,6 @@ export class LibreBooking implements INodeType {
{ displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 },
],
},
{
displayName: 'Ressourcen-Abruf-Optionen',
name: 'resourceGetAllOptions',
type: 'collection',
placeholder: 'Option hinzufügen',
default: {},
displayOptions: { show: { resource: ['resource'], operation: ['getAll'] } },
options: [
{
displayName: 'Custom Attributes Einschließen',
name: 'includeCustomAttributes',
type: 'boolean',
default: false,
description: 'Für jede Ressource die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
},
],
},
// =====================================================
// SCHEDULE PARAMETERS
@ -749,7 +579,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create'] } },
default: '',
description: 'E-Mail-Adresse des Benutzers (Pflichtfeld)',
},
{
displayName: 'Benutzername',
@ -758,7 +587,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create'] } },
default: '',
description: 'Benutzername für die Anmeldung (Pflichtfeld)',
},
{
displayName: 'Passwort',
@ -768,7 +596,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } },
default: '',
description: 'Passwort des Benutzers (Pflichtfeld)',
},
{
displayName: 'Vorname',
@ -777,7 +604,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } },
default: '',
description: 'Vorname des Benutzers (Pflichtfeld)',
},
{
displayName: 'Nachname',
@ -786,42 +612,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } },
default: '',
description: 'Nachname des Benutzers (Pflichtfeld)',
},
// Custom Attributes für Benutzer
{
displayName: 'Benutzerdefinierte Attribute',
name: 'userCustomAttributes',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Attribut hinzufügen',
displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } },
options: [
{
name: 'attribute',
displayName: 'Attribut',
values: [
{
displayName: 'Attribut-ID',
name: 'attributeId',
type: 'number',
default: 0,
description: 'Die ID des benutzerdefinierten Attributs',
},
{
displayName: 'Wert',
name: 'attributeValue',
type: 'string',
default: '',
description: 'Der Wert für dieses Attribut',
},
],
},
],
description: 'Benutzerdefinierte Attribute für diesen Benutzer setzen',
},
{
displayName: 'Benutzer-Filter',
@ -836,13 +626,6 @@ export class LibreBooking implements INodeType {
{ displayName: 'Vorname', name: 'firstName', type: 'string', default: '' },
{ displayName: 'Nachname', name: 'lastName', type: 'string', default: '' },
{ displayName: 'Organisation', name: 'organization', type: 'string', default: '' },
{
displayName: 'Custom Attributes Einschließen',
name: 'includeCustomAttributes',
type: 'boolean',
default: false,
description: 'Für jeden Benutzer die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
},
],
},
{
@ -873,41 +656,6 @@ export class LibreBooking implements INodeType {
displayOptions: { show: { resource: ['account'], operation: ['get', 'update', 'updatePassword'] } },
default: '',
},
// Custom Attributes für Accounts
{
displayName: 'Benutzerdefinierte Attribute',
name: 'accountCustomAttributes',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Attribut hinzufügen',
displayOptions: { show: { resource: ['account'], operation: ['create', 'update'] } },
options: [
{
name: 'attribute',
displayName: 'Attribut',
values: [
{
displayName: 'Attribut-ID',
name: 'attributeId',
type: 'number',
default: 0,
description: 'Die ID des benutzerdefinierten Attributs',
},
{
displayName: 'Wert',
name: 'attributeValue',
type: 'string',
default: '',
description: 'Der Wert für dieses Attribut',
},
],
},
],
description: 'Benutzerdefinierte Attribute für dieses Konto setzen',
},
{
displayName: 'Account-Daten',
name: 'accountData',
@ -965,7 +713,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } },
default: '',
description: 'Name der Gruppe (Pflichtfeld)',
},
{
displayName: 'Standard-Gruppe',
@ -1041,7 +788,6 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } },
default: '',
description: 'Anzeigename des Attributs (Pflichtfeld)',
},
{
displayName: 'Attribut-Typ',
@ -1086,9 +832,6 @@ export class LibreBooking implements INodeType {
const username = credentials.username as string;
const pw = credentials.password as string;
// Config-Defaults laden
const configDefaults = await getConfigDefaults(this);
const session = await authenticate(this, baseUrl, username, pw);
try {
@ -1108,31 +851,7 @@ export class LibreBooking implements INodeType {
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
if (filters.startDateTime) qs.startDateTime = filters.startDateTime;
if (filters.endDateTime) qs.endDateTime = filters.endDateTime;
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs);
// If includeCustomAttributes is enabled, fetch details for each reservation
if (filters.includeCustomAttributes && response.reservations && response.reservations.length > 0) {
const enrichedReservations = [];
for (const reservation of response.reservations) {
try {
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${reservation.referenceNumber}`);
enrichedReservations.push({
...reservation,
customAttributes: details.customAttributes || [],
owner: details.owner,
participants: details.participants || [],
invitees: details.invitees || [],
});
} catch (error) {
// Fallback to original reservation data
enrichedReservations.push(reservation);
}
}
response = { ...response, reservations: enrichedReservations };
}
responseData = response;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs);
} else if (operation === 'get') {
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`);
@ -1140,37 +859,17 @@ export class LibreBooking implements INodeType {
const resourceId = this.getNodeParameter('resourceId', i) as number;
const startDateTime = this.getNodeParameter('startDateTime', i) as string;
const endDateTime = this.getNodeParameter('endDateTime', i) as string;
const termsAccepted = this.getNodeParameter('termsAccepted', i, configDefaults.defaultTermsAccepted) as boolean;
const title = this.getNodeParameter('title', i, '') as string;
const customAttributes = this.getNodeParameter('customAttributes', i, {}) as any;
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any;
const body: any = {
resourceId,
startDateTime: new Date(startDateTime).toISOString(),
endDateTime: new Date(endDateTime).toISOString(),
termsAccepted,
};
const body: any = { resourceId, startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() };
if (title) body.title = title;
if (additionalFields.description) body.description = additionalFields.description;
if (additionalFields.userId) body.userId = additionalFields.userId;
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
// allowParticipation is REQUIRED by the API
body.allowParticipation = additionalFields.allowParticipation !== undefined
? additionalFields.allowParticipation
: configDefaults.defaultAllowParticipation;
// Custom Attributes verarbeiten
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
body.customAttributes = customAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation;
if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body);
} else if (operation === 'update') {
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
@ -1178,15 +877,8 @@ export class LibreBooking implements INodeType {
const endDateTime = this.getNodeParameter('endDateTime', i) as string;
const title = this.getNodeParameter('title', i, '') as string;
const updateScope = this.getNodeParameter('updateScope', i, 'this') as string;
const customAttributes = this.getNodeParameter('customAttributes', i, {}) as any;
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any;
const body: any = {
startDateTime: new Date(startDateTime).toISOString(),
endDateTime: new Date(endDateTime).toISOString(),
termsAccepted: true, // termsAccepted wird auch bei Updates benötigt
};
const body: any = { startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() };
if (title) body.title = title;
if (additionalFields.description) body.description = additionalFields.description;
if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId;
@ -1194,19 +886,6 @@ export class LibreBooking implements INodeType {
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
// allowParticipation is REQUIRED by the API
body.allowParticipation = additionalFields.allowParticipation !== undefined
? additionalFields.allowParticipation
: false;
// Custom Attributes verarbeiten
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
body.customAttributes = customAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}?updateScope=${updateScope}`, body);
} else if (operation === 'delete') {
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
@ -1227,29 +906,7 @@ export class LibreBooking implements INodeType {
// RESOURCE
else if (resource === 'resource') {
if (operation === 'getAll') {
const resourceGetAllOptions = this.getNodeParameter('resourceGetAllOptions', i, {}) as any;
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/');
// If includeCustomAttributes is enabled, fetch details for each resource
if (resourceGetAllOptions.includeCustomAttributes && response.resources && response.resources.length > 0) {
const enrichedResources = [];
for (const res of response.resources) {
try {
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${res.resourceId}`);
enrichedResources.push({
...res,
customAttributes: details.customAttributes || [],
});
} catch (error) {
// Fallback to original resource data
enrichedResources.push(res);
}
}
response = { ...response, resources: enrichedResources };
}
responseData = response;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/');
} else if (operation === 'get') {
const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`);
@ -1270,11 +927,8 @@ export class LibreBooking implements INodeType {
} else if (operation === 'create') {
const resourceName = this.getNodeParameter('resourceName', i) as string;
const scheduleIdForResource = this.getNodeParameter('scheduleIdForResource', i) as number;
const resourceCustomAttributes = this.getNodeParameter('resourceCustomAttributes', i, {}) as any;
const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any;
const body: any = { name: resourceName, scheduleId: scheduleIdForResource };
if (resourceOptions.location) body.location = resourceOptions.location;
if (resourceOptions.contact) body.contact = resourceOptions.contact;
if (resourceOptions.description) body.description = resourceOptions.description;
@ -1286,24 +940,12 @@ export class LibreBooking implements INodeType {
if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes;
if (resourceOptions.color) body.color = resourceOptions.color;
if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId;
// Custom Attributes verarbeiten
if (resourceCustomAttributes?.attribute && resourceCustomAttributes.attribute.length > 0) {
body.customAttributes = resourceCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Resources/', body);
} else if (operation === 'update') {
const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number;
const resourceName = this.getNodeParameter('resourceName', i) as string;
const resourceCustomAttributes = this.getNodeParameter('resourceCustomAttributes', i, {}) as any;
const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any;
const body: any = { name: resourceName };
if (resourceOptions.location) body.location = resourceOptions.location;
if (resourceOptions.contact) body.contact = resourceOptions.contact;
if (resourceOptions.description) body.description = resourceOptions.description;
@ -1315,15 +957,6 @@ export class LibreBooking implements INodeType {
if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes;
if (resourceOptions.color) body.color = resourceOptions.color;
if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId;
// Custom Attributes verarbeiten
if (resourceCustomAttributes?.attribute && resourceCustomAttributes.attribute.length > 0) {
body.customAttributes = resourceCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Resources/${resourceIdParam}`, body);
} else if (operation === 'delete') {
const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number;
@ -1359,87 +992,42 @@ export class LibreBooking implements INodeType {
if (userFilters.firstName) qs.firstName = userFilters.firstName;
if (userFilters.lastName) qs.lastName = userFilters.lastName;
if (userFilters.organization) qs.organization = userFilters.organization;
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs);
// If includeCustomAttributes is enabled, fetch details for each user
if (userFilters.includeCustomAttributes && response.users && response.users.length > 0) {
const enrichedUsers = [];
for (const user of response.users) {
try {
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${user.id}`);
enrichedUsers.push({
...user,
customAttributes: details.customAttributes || [],
});
} catch (error) {
// Fallback to original user data
enrichedUsers.push(user);
}
}
response = { ...response, users: enrichedUsers };
}
responseData = response;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs);
} else if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as number;
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`);
} else if (operation === 'create') {
const emailAddress = this.getNodeParameter('emailAddress', i) as string;
const userName = this.getNodeParameter('userName', i) as string;
const userPw = this.getNodeParameter('password', i) as string;
const pw = this.getNodeParameter('password', i) as string;
const firstName = this.getNodeParameter('firstName', i) as string;
const lastName = this.getNodeParameter('lastName', i) as string;
const userCustomAttributes = this.getNodeParameter('userCustomAttributes', i, {}) as any;
const userOptions = this.getNodeParameter('userOptions', i, {}) as any;
const body: any = { emailAddress, userName, password: userPw, firstName, lastName };
body.timezone = userOptions.timezone || configDefaults.defaultTimezone;
body.language = userOptions.language || configDefaults.defaultLanguage;
if (userOptions.phone) body.phone = userOptions.phone;
if (userOptions.organization) body.organization = userOptions.organization;
if (userOptions.position) body.position = userOptions.position;
if (userOptions.groups) body.groups = parseIdList(userOptions.groups);
// Custom Attributes verarbeiten
if (userCustomAttributes?.attribute && userCustomAttributes.attribute.length > 0) {
body.customAttributes = userCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body);
} else if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as number;
const firstName = this.getNodeParameter('firstName', i) as string;
const lastName = this.getNodeParameter('lastName', i) as string;
const userCustomAttributes = this.getNodeParameter('userCustomAttributes', i, {}) as any;
const userOptions = this.getNodeParameter('userOptions', i, {}) as any;
const body: any = { firstName, lastName };
const body: any = { emailAddress, userName, password: pw, firstName, lastName };
if (userOptions.timezone) body.timezone = userOptions.timezone;
if (userOptions.language) body.language = userOptions.language;
if (userOptions.phone) body.phone = userOptions.phone;
if (userOptions.organization) body.organization = userOptions.organization;
if (userOptions.position) body.position = userOptions.position;
if (userOptions.groups) body.groups = parseIdList(userOptions.groups);
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body);
} else if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as number;
const firstName = this.getNodeParameter('firstName', i) as string;
const lastName = this.getNodeParameter('lastName', i) as string;
const userOptions = this.getNodeParameter('userOptions', i, {}) as any;
const body: any = { firstName, lastName };
if (userOptions.timezone) body.timezone = userOptions.timezone;
if (userOptions.language) body.language = userOptions.language;
if (userOptions.phone) body.phone = userOptions.phone;
if (userOptions.organization) body.organization = userOptions.organization;
if (userOptions.position) body.position = userOptions.position;
if (userOptions.groups) body.groups = parseIdList(userOptions.groups);
// Custom Attributes verarbeiten
if (userCustomAttributes?.attribute && userCustomAttributes.attribute.length > 0) {
body.customAttributes = userCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}`, body);
} else if (operation === 'updatePassword') {
const userId = this.getNodeParameter('userId', i) as number;
const userPw = this.getNodeParameter('password', i) as string;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: userPw });
const pw = this.getNodeParameter('password', i) as string;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw });
} else if (operation === 'delete') {
const userId = this.getNodeParameter('userId', i) as number;
responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`);
@ -1453,42 +1041,23 @@ export class LibreBooking implements INodeType {
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accounts/${accountUserId}`);
} else if (operation === 'create') {
const accountData = this.getNodeParameter('accountData', i, {}) as any;
const accountCustomAttributes = this.getNodeParameter('accountCustomAttributes', i, {}) as any;
const body: any = {};
if (accountData.emailAddress) body.emailAddress = accountData.emailAddress;
if (accountData.userName) body.userName = accountData.userName;
if (accountData.password) body.password = accountData.password;
if (accountData.firstName) body.firstName = accountData.firstName;
if (accountData.lastName) body.lastName = accountData.lastName;
body.timezone = accountData.timezone || configDefaults.defaultTimezone;
body.language = accountData.language || configDefaults.defaultLanguage;
if (accountData.timezone) body.timezone = accountData.timezone;
if (accountData.language) body.language = accountData.language;
if (accountData.phone) body.phone = accountData.phone;
if (accountData.organization) body.organization = accountData.organization;
if (accountData.position) body.position = accountData.position;
if (accountData.acceptTermsOfService !== undefined) {
body.acceptTermsOfService = accountData.acceptTermsOfService;
} else {
body.acceptTermsOfService = configDefaults.defaultTermsAccepted;
}
// Custom Attributes verarbeiten
if (accountCustomAttributes?.attribute && accountCustomAttributes.attribute.length > 0) {
body.customAttributes = accountCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
if (accountData.acceptTermsOfService !== undefined) body.acceptTermsOfService = accountData.acceptTermsOfService;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Accounts/', body);
} else if (operation === 'update') {
const accountUserId = this.getNodeParameter('accountUserId', i) as number;
const accountData = this.getNodeParameter('accountData', i, {}) as any;
const accountCustomAttributes = this.getNodeParameter('accountCustomAttributes', i, {}) as any;
const body: any = {};
if (accountData.emailAddress) body.emailAddress = accountData.emailAddress;
if (accountData.userName) body.userName = accountData.userName;
if (accountData.firstName) body.firstName = accountData.firstName;
@ -1498,15 +1067,6 @@ export class LibreBooking implements INodeType {
if (accountData.phone) body.phone = accountData.phone;
if (accountData.organization) body.organization = accountData.organization;
if (accountData.position) body.position = accountData.position;
// Custom Attributes verarbeiten
if (accountCustomAttributes?.attribute && accountCustomAttributes.attribute.length > 0) {
body.customAttributes = accountCustomAttributes.attribute.map((attr: any) => ({
attributeId: attr.attributeId,
attributeValue: attr.attributeValue,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}`, body);
} else if (operation === 'updatePassword') {
const accountUserId = this.getNodeParameter('accountUserId', i) as number;

View File

@ -20,20 +20,9 @@ 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
*/
@ -55,7 +44,7 @@ async function authenticateTrigger(
if (!response.isAuthenticated) {
throw new NodeOperationError(
pollFunctions.getNode(),
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
'Authentifizierung fehlgeschlagen',
);
}
@ -67,7 +56,6 @@ 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.',
});
}
}
@ -116,7 +104,6 @@ async function getReservations(
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/`,
@ -130,15 +117,10 @@ async function getReservations(
});
return response.reservations || [];
} catch (error: any) {
throw new NodeApiError(pollFunctions.getNode(), error, {
message: 'Fehler beim Abrufen der Reservierungen',
});
}
}
/**
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
* Detaillierte Reservierungsdaten abrufen
*/
async function getReservationDetails(
pollFunctions: IPollFunctions,
@ -146,7 +128,6 @@ async function getReservationDetails(
session: LibreBookingSession,
referenceNumber: string,
): Promise<any> {
try {
const response = await pollFunctions.helpers.httpRequest({
method: 'GET',
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
@ -159,111 +140,12 @@ async function getReservationDetails(
});
return response;
} catch (error) {
return null;
}
}
/**
* Berechnet Zeitraum basierend auf der gewählten Option
* Zeitfenster berechnen
*/
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 } {
function getTimeWindow(timeWindow: string): { start: string; end: string } {
const now = new Date();
const start = now.toISOString();
@ -281,9 +163,6 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
case '90days':
endDate.setDate(endDate.getDate() + 90);
break;
case '180days':
endDate.setDate(endDate.getDate() + 180);
break;
default:
endDate.setDate(endDate.getDate() + 14);
}
@ -295,72 +174,14 @@ function getTimeWindowForPolling(timeWindow: string): { start: string; end: stri
}
/**
* Filter Reservierungen nach Zeitpunkt
* Eindeutigen Schlüssel für Reservierung generieren
*/
function filterByTime(reservations: ReservationData[], timeFilter: string): ReservationData[] {
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);
function getReservationKey(reservation: ReservationData): string {
return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`;
}
/**
* 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 = {
@ -370,7 +191,7 @@ export class LibreBookingTrigger implements INodeType {
group: ['trigger'],
version: 1,
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
subtitle: '={{$parameter["triggerMode"]}}',
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'LibreBooking Trigger',
},
@ -384,185 +205,17 @@ export class LibreBookingTrigger implements INodeType {
],
polling: true,
properties: [
// =====================================================
// TRIGGER MODE SELECTOR
// =====================================================
{
displayName: 'Trigger-Modus',
name: 'triggerMode',
displayName: 'Event',
name: 'event',
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',
},
{ 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' },
],
default: 'getAll',
description: 'Wählen Sie den Trigger-Modus',
default: 'newReservation',
},
// =====================================================
// 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',
@ -570,33 +223,23 @@ export class LibreBookingTrigger implements INodeType {
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',
},
{ displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' },
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
{ displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' },
],
},
// =====================================================
// OPTIONS (ALL MODES)
// =====================================================
{
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',
},
{
displayName: 'Optionen',
name: 'options',
@ -604,21 +247,7 @@ export class LibreBookingTrigger implements INodeType {
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',
},
{ displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false },
],
},
],
@ -630,16 +259,13 @@ export class LibreBookingTrigger implements INodeType {
const username = credentials.username as string;
const password = credentials.password as string;
const triggerMode = this.getNodeParameter('triggerMode') as string;
const event = this.getNodeParameter('event') as string;
const filters = this.getNodeParameter('filters', {}) as any;
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const options = this.getNodeParameter('options', {}) as any;
// Debug-Modus
const debugMode = options.debugMode || false;
const fetchDetails = options.fetchDetails || false;
// Workflow Static Data für State-Management
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
const workflowStaticData = this.getWorkflowStaticData('node');
const previousReservations = (workflowStaticData.reservations as Record<string, string>) || {};
let session: LibreBookingSession;
try {
@ -649,341 +275,76 @@ 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> = {};
// ==========================================
// MODE: Get All (One-Time / Every Poll)
// ==========================================
if (triggerMode === 'getAll') {
const dateRange = this.getNodeParameter('dateRange', 'custom') as string;
const startDate = this.getNodeParameter('startDate', '') as string;
const endDate = this.getNodeParameter('endDate', '') as string;
const { start, end } = getTimeWindowForGetAll(
dateRange,
startDate || undefined,
endDate || undefined,
14,
);
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) {
const refNumber = reservation.referenceNumber;
const reservationKey = getReservationKey(reservation);
currentReservations[refNumber] = reservationKey;
const isNew = !previousReservations[refNumber];
const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey;
let shouldTrigger = false;
let eventType = '';
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 (shouldTrigger) {
let reservationData = reservation;
if (fetchDetails) {
if (options.fetchDetails) {
try {
const details = await getReservationDetails(
reservationData = await getReservationDetails(
this,
baseUrl,
session,
reservation.referenceNumber,
refNumber,
);
if (details) {
reservationData = details;
}
} catch (error) {
// Fallback auf Basisdaten
reservationData = reservation;
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'getAll',
_dateRange: dateRange,
_eventType: eventType,
_triggeredAt: new Date().toISOString(),
},
});
}
}
// ==========================================
// 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`,
);
}
}
workflowStaticData.reservations = currentReservations;
if (returnData.length === 0) {
return null;
}
return [returnData];
} finally {
await signOutTrigger(this, baseUrl, session);
}

View File

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-librebooking",
"version": "1.2.2",
"version": "1.1.0",
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
"keywords": [
"n8n-community-node-package",
@ -60,8 +60,7 @@
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
"dist/credentials/LibreBookingApi.credentials.js",
"dist/credentials/LibreBookingConfig.credentials.js"
"dist/credentials/LibreBookingApi.credentials.js"
],
"nodes": [
"dist/nodes/LibreBooking/LibreBooking.node.js",

View File

@ -1,609 +0,0 @@
/**
* LibreBooking API Test Script
*
* Testet alle wichtigen API-Endpunkte mit echten Credentials
*/
import * as https from 'https';
import * as http from 'http';
const BASE_URL = 'https://librebooking.zell-cloud.de';
const USERNAME = 'sebastian.zell@zell-aufmass.de';
const PASSWORD = 'wanUQ4uVqU6lfP';
interface Session {
sessionToken: string;
userId: number;
}
interface TestResult {
name: string;
success: boolean;
data?: any;
error?: string;
}
const results: TestResult[] = [];
/**
* HTTP Request Helper
*/
function makeRequest(
method: string,
path: string,
body?: any,
session?: Session,
qs?: Record<string, string>
): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(path, BASE_URL);
if (qs) {
Object.entries(qs).forEach(([key, value]) => {
if (value) url.searchParams.append(key, value);
});
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (session) {
headers['X-Booked-SessionToken'] = session.sessionToken;
headers['X-Booked-UserId'] = session.userId.toString();
}
const options = {
method,
hostname: url.hostname,
path: url.pathname + url.search,
headers,
};
const httpModule = url.protocol === 'https:' ? https : http;
const req = httpModule.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
if (data) {
resolve(JSON.parse(data));
} else {
resolve({ success: true });
}
} catch (e) {
resolve({ rawData: data });
}
});
});
req.on('error', (error) => {
reject(error);
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
/**
* Test Authentication
*/
async function testAuthentication(): Promise<Session> {
console.log('\n=== 1. Testing Authentication ===');
try {
const response = await makeRequest(
'POST',
'/Web/Services/index.php/Authentication/Authenticate',
{ username: USERNAME, password: PASSWORD }
);
if (response.isAuthenticated) {
console.log('✅ Authentication successful');
console.log(` Session Token: ${response.sessionToken.substring(0, 20)}...`);
console.log(` User ID: ${response.userId}`);
console.log(` Session Expires: ${response.sessionExpires}`);
results.push({ name: 'Authentication', success: true, data: { userId: response.userId } });
return { sessionToken: response.sessionToken, userId: response.userId };
} else {
throw new Error('Authentication failed');
}
} catch (error: any) {
console.log('❌ Authentication failed:', error.message);
results.push({ name: 'Authentication', success: false, error: error.message });
throw error;
}
}
/**
* Test Get Reservations
*/
async function testGetReservations(session: Session): Promise<void> {
console.log('\n=== 2. Testing Get Reservations ===');
try {
// Test without date filters (should return next 2 weeks)
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Reservations/',
undefined,
session
);
console.log(`✅ Reservations fetched: ${response.reservations?.length || 0} found`);
if (response.reservations && response.reservations.length > 0) {
const res = response.reservations[0];
console.log(` Example: ${res.title || 'No title'} (${res.referenceNumber})`);
console.log(` Start: ${res.startDate}`);
console.log(` Resource: ${res.resourceName}`);
}
results.push({ name: 'Get Reservations (no filter)', success: true, data: { count: response.reservations?.length || 0 } });
// Test with date filter (Feb 7-14, 2026)
console.log('\n Testing with date filter (2026-02-07 to 2026-02-14)...');
const responseFiltered = await makeRequest(
'GET',
'/Web/Services/index.php/Reservations/',
undefined,
session,
{
startDateTime: '2026-02-07T00:00:00',
endDateTime: '2026-02-14T23:59:59'
}
);
console.log(`✅ Filtered reservations: ${responseFiltered.reservations?.length || 0} found`);
results.push({ name: 'Get Reservations (date filter)', success: true, data: { count: responseFiltered.reservations?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Reservations failed:', error.message);
results.push({ name: 'Get Reservations', success: false, error: error.message });
}
}
/**
* Test Get Resources
*/
async function testGetResources(session: Session): Promise<number | null> {
console.log('\n=== 3. Testing Get Resources ===');
try {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Resources/',
undefined,
session
);
console.log(`✅ Resources fetched: ${response.resources?.length || 0} found`);
let firstResourceId: number | null = null;
if (response.resources && response.resources.length > 0) {
const res = response.resources[0];
firstResourceId = res.resourceId;
console.log(` Example: ${res.name} (ID: ${res.resourceId})`);
console.log(` Schedule ID: ${res.scheduleId}`);
console.log(` Custom Attributes: ${res.customAttributes?.length || 0}`);
if (res.customAttributes && res.customAttributes.length > 0) {
res.customAttributes.forEach((attr: any) => {
console.log(` - ${attr.label}: ${attr.value || '(no value)'}`);
});
}
}
results.push({ name: 'Get Resources', success: true, data: { count: response.resources?.length || 0 } });
return firstResourceId;
} catch (error: any) {
console.log('❌ Get Resources failed:', error.message);
results.push({ name: 'Get Resources', success: false, error: error.message });
return null;
}
}
/**
* Test Get Single Resource (with custom attributes)
*/
async function testGetSingleResource(session: Session, resourceId: number): Promise<void> {
console.log('\n=== 4. Testing Get Single Resource (with custom attributes) ===');
try {
const response = await makeRequest(
'GET',
`/Web/Services/index.php/Resources/${resourceId}`,
undefined,
session
);
console.log(`✅ Resource fetched: ${response.name}`);
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
if (response.customAttributes && response.customAttributes.length > 0) {
response.customAttributes.forEach((attr: any) => {
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
});
}
results.push({ name: 'Get Single Resource', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Single Resource failed:', error.message);
results.push({ name: 'Get Single Resource', success: false, error: error.message });
}
}
/**
* Test Get Users
*/
async function testGetUsers(session: Session): Promise<number | null> {
console.log('\n=== 5. Testing Get Users ===');
try {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Users/',
undefined,
session
);
console.log(`✅ Users fetched: ${response.users?.length || 0} found`);
let firstUserId: number | null = null;
if (response.users && response.users.length > 0) {
const user = response.users[0];
firstUserId = user.id;
console.log(` Example: ${user.firstName} ${user.lastName} (ID: ${user.id})`);
}
results.push({ name: 'Get Users', success: true, data: { count: response.users?.length || 0 } });
return firstUserId;
} catch (error: any) {
console.log('❌ Get Users failed:', error.message);
results.push({ name: 'Get Users', success: false, error: error.message });
return null;
}
}
/**
* Test Get Single User (with custom attributes)
*/
async function testGetSingleUser(session: Session, userId: number): Promise<void> {
console.log('\n=== 6. Testing Get Single User (with custom attributes) ===');
try {
const response = await makeRequest(
'GET',
`/Web/Services/index.php/Users/${userId}`,
undefined,
session
);
console.log(`✅ User fetched: ${response.firstName} ${response.lastName}`);
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
if (response.customAttributes && response.customAttributes.length > 0) {
response.customAttributes.forEach((attr: any) => {
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
});
}
results.push({ name: 'Get Single User', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Single User failed:', error.message);
results.push({ name: 'Get Single User', success: false, error: error.message });
}
}
/**
* Test Get Schedules
*/
async function testGetSchedules(session: Session): Promise<void> {
console.log('\n=== 7. Testing Get Schedules ===');
try {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Schedules/',
undefined,
session
);
console.log(`✅ Schedules fetched: ${response.schedules?.length || 0} found`);
if (response.schedules && response.schedules.length > 0) {
const schedule = response.schedules[0];
console.log(` Example: ${schedule.name} (ID: ${schedule.id})`);
}
results.push({ name: 'Get Schedules', success: true, data: { count: response.schedules?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Schedules failed:', error.message);
results.push({ name: 'Get Schedules', success: false, error: error.message });
}
}
/**
* Test Get Attributes by Category
*/
async function testGetAttributes(session: Session): Promise<void> {
console.log('\n=== 8. Testing Get Attributes by Category ===');
const categories = [
{ id: 1, name: 'Reservation' },
{ id: 2, name: 'User' },
{ id: 4, name: 'Resource' },
{ id: 5, name: 'Resource Type' },
];
for (const cat of categories) {
try {
const response = await makeRequest(
'GET',
`/Web/Services/index.php/Attributes/Category/${cat.id}`,
undefined,
session
);
console.log(`${cat.name} Attributes: ${response.attributes?.length || 0} found`);
if (response.attributes && response.attributes.length > 0) {
response.attributes.forEach((attr: any) => {
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Type: ${attr.type}, Required: ${attr.required}`);
});
}
results.push({ name: `Get Attributes (${cat.name})`, success: true, data: { count: response.attributes?.length || 0 } });
} catch (error: any) {
console.log(`❌ Get ${cat.name} Attributes failed:`, error.message);
results.push({ name: `Get Attributes (${cat.name})`, success: false, error: error.message });
}
}
}
/**
* Test Create, Update, Delete Reservation
*/
async function testReservationCRUD(session: Session, resourceId: number): Promise<void> {
console.log('\n=== 9. Testing Reservation CRUD ===');
// Create
console.log(' Creating test reservation...');
try {
const createResponse = await makeRequest(
'POST',
'/Web/Services/index.php/Reservations/',
{
title: 'API Test Reservation',
description: 'Created by n8n node test script',
resourceId: resourceId,
startDateTime: '2026-02-07T10:00:00',
endDateTime: '2026-02-07T11:00:00',
userId: session.userId,
termsAccepted: true,
allowParticipation: false,
},
session
);
if (createResponse.referenceNumber) {
console.log(`✅ Reservation created: ${createResponse.referenceNumber}`);
results.push({ name: 'Create Reservation', success: true, data: { referenceNumber: createResponse.referenceNumber } });
const refNum = createResponse.referenceNumber;
// Get the created reservation
console.log(' Fetching created reservation...');
const getResponse = await makeRequest(
'GET',
`/Web/Services/index.php/Reservations/${refNum}`,
undefined,
session
);
console.log(`✅ Reservation fetched: ${getResponse.title}`);
console.log(` Custom Attributes: ${getResponse.customAttributes?.length || 0}`);
if (getResponse.customAttributes && getResponse.customAttributes.length > 0) {
getResponse.customAttributes.forEach((attr: any) => {
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
});
}
results.push({ name: 'Get Created Reservation', success: true });
// Update
console.log(' Updating reservation...');
const updateResponse = await makeRequest(
'POST',
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
{
title: 'API Test Reservation UPDATED',
description: 'Updated by n8n node test script',
resourceId: resourceId,
startDateTime: '2026-02-07T10:00:00',
endDateTime: '2026-02-07T12:00:00',
termsAccepted: true,
allowParticipation: false,
},
session
);
console.log(`✅ Reservation updated`);
results.push({ name: 'Update Reservation', success: true });
// Delete
console.log(' Deleting reservation...');
const deleteResponse = await makeRequest(
'DELETE',
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
undefined,
session
);
console.log(`✅ Reservation deleted`);
results.push({ name: 'Delete Reservation', success: true });
} else {
console.log('❌ Create Reservation failed - no reference number returned');
console.log(' Response:', JSON.stringify(createResponse, null, 2));
results.push({ name: 'Create Reservation', success: false, error: JSON.stringify(createResponse) });
}
} catch (error: any) {
console.log('❌ Reservation CRUD failed:', error.message);
results.push({ name: 'Reservation CRUD', success: false, error: error.message });
}
}
/**
* Test Groups
*/
async function testGetGroups(session: Session): Promise<void> {
console.log('\n=== 10. Testing Get Groups ===');
try {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Groups/',
undefined,
session
);
console.log(`✅ Groups fetched: ${response.groups?.length || 0} found`);
if (response.groups && response.groups.length > 0) {
const group = response.groups[0];
console.log(` Example: ${group.name} (ID: ${group.id})`);
}
results.push({ name: 'Get Groups', success: true, data: { count: response.groups?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Groups failed:', error.message);
results.push({ name: 'Get Groups', success: false, error: error.message });
}
}
/**
* Test Accessories
*/
async function testGetAccessories(session: Session): Promise<void> {
console.log('\n=== 11. Testing Get Accessories ===');
try {
const response = await makeRequest(
'GET',
'/Web/Services/index.php/Accessories/',
undefined,
session
);
console.log(`✅ Accessories fetched: ${response.accessories?.length || 0} found`);
if (response.accessories && response.accessories.length > 0) {
const acc = response.accessories[0];
console.log(` Example: ${acc.name} (ID: ${acc.id})`);
}
results.push({ name: 'Get Accessories', success: true, data: { count: response.accessories?.length || 0 } });
} catch (error: any) {
console.log('❌ Get Accessories failed:', error.message);
results.push({ name: 'Get Accessories', success: false, error: error.message });
}
}
/**
* Test SignOut
*/
async function testSignOut(session: Session): Promise<void> {
console.log('\n=== 12. Testing Sign Out ===');
try {
await makeRequest(
'POST',
'/Web/Services/index.php/Authentication/SignOut',
{
userId: session.userId,
sessionToken: session.sessionToken,
}
);
console.log('✅ Sign Out successful');
results.push({ name: 'Sign Out', success: true });
} catch (error: any) {
console.log('❌ Sign Out failed:', error.message);
results.push({ name: 'Sign Out', success: false, error: error.message });
}
}
/**
* Print Summary
*/
function printSummary(): void {
console.log('\n========================================');
console.log(' TEST SUMMARY');
console.log('========================================');
const passed = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`\nTotal Tests: ${results.length}`);
console.log(`✅ Passed: ${passed}`);
console.log(`❌ Failed: ${failed}`);
if (failed > 0) {
console.log('\nFailed Tests:');
results.filter(r => !r.success).forEach(r => {
console.log(` - ${r.name}: ${r.error}`);
});
}
console.log('\n========================================\n');
}
/**
* Main Test Runner
*/
async function runTests(): Promise<void> {
console.log('========================================');
console.log(' LibreBooking API Test Suite');
console.log(` URL: ${BASE_URL}`);
console.log(` User: ${USERNAME}`);
console.log('========================================');
try {
// Authentication
const session = await testAuthentication();
// Get operations
await testGetReservations(session);
const resourceId = await testGetResources(session);
if (resourceId) {
await testGetSingleResource(session, resourceId);
}
const userId = await testGetUsers(session);
if (userId) {
await testGetSingleUser(session, userId);
}
await testGetSchedules(session);
await testGetAttributes(session);
await testGetGroups(session);
await testGetAccessories(session);
// CRUD operations
if (resourceId) {
await testReservationCRUD(session, resourceId);
}
// Sign out
await testSignOut(session);
} catch (error: any) {
console.log('\n❌ Test suite failed:', error.message);
}
printSummary();
}
// Run tests
runTests().catch(console.error);

View File

@ -1,344 +0,0 @@
{
"name": "LibreBooking v1.2.0 Test Workflows",
"description": "Beispiel-Workflows für die neuen Features in Version 1.2.0",
"workflows": [
{
"name": "01 - Reservierung mit Custom Attributes erstellen",
"description": "Erstellt eine Reservierung mit benutzerdefinierten Attributen",
"nodes": [
{
"type": "n8n-nodes-base.manualTrigger",
"name": "Manuell starten",
"position": [250, 300]
},
{
"type": "n8n-nodes-librebooking.libreBooking",
"name": "Attribute abrufen",
"parameters": {
"resource": "attribute",
"operation": "getByCategory",
"categoryId": 1
},
"position": [450, 300]
},
{
"type": "n8n-nodes-librebooking.libreBooking",
"name": "Reservierung erstellen",
"parameters": {
"resource": "reservation",
"operation": "create",
"resourceId": 1,
"startDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T10:00:00",
"endDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T11:00:00",
"termsAccepted": true,
"title": "Test Reservierung mit Attributen",
"customAttributes": {
"attribute": [
{
"attributeId": 1,
"attributeValue": "Mein Attribut-Wert"
}
]
}
},
"position": [650, 300]
}
],
"connections": {
"Manuell starten": {
"main": [
[{ "node": "Attribute abrufen", "type": "main", "index": 0 }]
]
},
"Attribute abrufen": {
"main": [
[{ "node": "Reservierung erstellen", "type": "main", "index": 0 }]
]
}
}
},
{
"name": "02 - Ressource mit Custom Attributes erstellen",
"description": "Erstellt eine Ressource mit benutzerdefinierten Attributen",
"nodes": [
{
"type": "n8n-nodes-base.manualTrigger",
"name": "Manuell starten",
"position": [250, 300]
},
{
"type": "n8n-nodes-librebooking.libreBooking",
"name": "Ressource erstellen",
"parameters": {
"resource": "resource",
"operation": "create",
"resourceName": "Testraum mit Ausstattung",
"scheduleIdForResource": 1,
"resourceCustomAttributes": {
"attribute": [
{
"attributeId": 10,
"attributeValue": "Beamer, Whiteboard, 20 Plätze"
}
]
},
"resourceOptions": {
"description": "Ein Konferenzraum mit voller Ausstattung",
"maxParticipants": 20
}
},
"position": [450, 300]
}
],
"connections": {
"Manuell starten": {
"main": [
[{ "node": "Ressource erstellen", "type": "main", "index": 0 }]
]
}
}
},
{
"name": "03 - Trigger für neue Reservierungen (ohne Altdaten)",
"description": "Überwacht neue Reservierungen - beim ersten Start werden existierende Reservierungen NICHT getriggert",
"nodes": [
{
"type": "n8n-nodes-librebooking.libreBookingTrigger",
"name": "Neue Reservierungen",
"parameters": {
"event": "newReservation",
"timeWindow": "14days",
"options": {
"fetchDetails": false,
"debugMode": true
}
},
"position": [250, 300]
},
{
"type": "n8n-nodes-base.if",
"name": "Debug-Check",
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json._debug }}",
"value2": true
}
]
}
},
"position": [450, 300]
},
{
"type": "n8n-nodes-base.set",
"name": "Debug-Info",
"parameters": {
"values": {
"string": [
{
"name": "info",
"value": "Erster Poll - Daten wurden gespeichert"
}
]
}
},
"position": [650, 200]
},
{
"type": "n8n-nodes-base.set",
"name": "Neue Reservierung verarbeiten",
"parameters": {
"values": {
"string": [
{
"name": "message",
"value": "Neue Reservierung: {{ $json.title }}"
}
]
}
},
"position": [650, 400]
}
],
"connections": {
"Neue Reservierungen": {
"main": [
[{ "node": "Debug-Check", "type": "main", "index": 0 }]
]
},
"Debug-Check": {
"main": [
[{ "node": "Debug-Info", "type": "main", "index": 0 }],
[{ "node": "Neue Reservierung verarbeiten", "type": "main", "index": 0 }]
]
}
}
},
{
"name": "04 - Trigger für geänderte Reservierungen",
"description": "Überwacht Änderungen an bestehenden Reservierungen mittels Hash-Vergleich",
"nodes": [
{
"type": "n8n-nodes-librebooking.libreBookingTrigger",
"name": "Geänderte Reservierungen",
"parameters": {
"event": "updatedReservation",
"timeWindow": "14days",
"options": {
"fetchDetails": true,
"debugMode": false
}
},
"position": [250, 300]
},
{
"type": "n8n-nodes-base.set",
"name": "Änderung protokollieren",
"parameters": {
"values": {
"string": [
{
"name": "message",
"value": "Reservierung geändert: {{ $json.referenceNumber }} - {{ $json.title }}"
},
{
"name": "changedAt",
"value": "={{ $json._triggeredAt }}"
}
]
}
},
"position": [450, 300]
}
],
"connections": {
"Geänderte Reservierungen": {
"main": [
[{ "node": "Änderung protokollieren", "type": "main", "index": 0 }]
]
}
}
},
{
"name": "05 - Config Node Verwendung",
"description": "Zeigt die Verwendung des LibreBooking Config Credentials für Standardwerte",
"notes": "Voraussetzung: LibreBooking Config Credential muss angelegt und mit dem Node verbunden sein",
"nodes": [
{
"type": "n8n-nodes-base.manualTrigger",
"name": "Manuell starten",
"position": [250, 300]
},
{
"type": "n8n-nodes-librebooking.libreBooking",
"name": "Reservierung mit Defaults",
"parameters": {
"resource": "reservation",
"operation": "create",
"resourceId": 1,
"startDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T14:00:00",
"endDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T15:00:00",
"title": "Reservierung mit Config-Defaults"
},
"credentials": {
"libreBookingApi": "LibreBooking API",
"libreBookingConfig": "LibreBooking Config"
},
"position": [450, 300],
"notes": "termsAccepted und allowParticipation werden aus dem Config Node übernommen"
}
],
"connections": {
"Manuell starten": {
"main": [
[{ "node": "Reservierung mit Defaults", "type": "main", "index": 0 }]
]
}
}
},
{
"name": "06 - Alle Events überwachen",
"description": "Überwacht sowohl neue als auch geänderte Reservierungen",
"nodes": [
{
"type": "n8n-nodes-librebooking.libreBookingTrigger",
"name": "Alle Reservierungs-Events",
"parameters": {
"event": "allReservations",
"timeWindow": "30days",
"filters": {
"resourceId": ""
},
"options": {
"fetchDetails": true
}
},
"position": [250, 300]
},
{
"type": "n8n-nodes-base.switch",
"name": "Event-Typ prüfen",
"parameters": {
"dataType": "string",
"value1": "={{ $json._eventType }}",
"rules": {
"rules": [
{
"value2": "new"
},
{
"value2": "updated"
}
]
}
},
"position": [450, 300]
},
{
"type": "n8n-nodes-base.set",
"name": "Neue Reservierung",
"parameters": {
"values": {
"string": [
{
"name": "action",
"value": "NEU: {{ $json.title }}"
}
]
}
},
"position": [650, 200]
},
{
"type": "n8n-nodes-base.set",
"name": "Geänderte Reservierung",
"parameters": {
"values": {
"string": [
{
"name": "action",
"value": "GEÄNDERT: {{ $json.title }}"
}
]
}
},
"position": [650, 400]
}
],
"connections": {
"Alle Reservierungs-Events": {
"main": [
[{ "node": "Event-Typ prüfen", "type": "main", "index": 0 }]
]
},
"Event-Typ prüfen": {
"main": [
[{ "node": "Neue Reservierung", "type": "main", "index": 0 }],
[{ "node": "Geänderte Reservierung", "type": "main", "index": 0 }]
]
}
}
}
]
}

View File

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

View File

@ -1,149 +0,0 @@
#!/bin/bash
# =============================================================================
# Git Upload Helper Script
# LibreBooking n8n Node v1.2.0
# =============================================================================
set -e
echo "🚀 Git Upload Vorbereitung"
echo "========================="
echo ""
# Prüfe ob Git installiert ist
if ! command -v git &> /dev/null; then
echo "❌ Git ist nicht installiert!"
echo " Installiere mit: sudo apt install git"
exit 1
fi
# Prüfe ob Git Repository existiert
if [ ! -d .git ]; then
echo "❌ Kein Git Repository gefunden!"
echo " Möchtest du eines initialisieren? (y/n)"
read -p " > " init_git
if [[ $init_git =~ ^[Yy]$ ]]; then
git init
echo "✅ Git Repository initialisiert"
else
echo "Abgebrochen."
exit 1
fi
fi
# Prüfe ob Remote existiert
echo ""
echo "📡 Remote Konfiguration:"
if git remote | grep -q origin; then
current_remote=$(git remote get-url origin 2>/dev/null || echo "nicht konfiguriert")
echo " Aktueller Remote: $current_remote"
echo ""
read -p " Möchtest du den Remote ändern? (y/n) " change_remote
if [[ $change_remote =~ ^[Yy]$ ]]; then
git remote remove origin
echo " Remote entfernt."
fi
fi
if ! git remote | grep -q origin; then
echo ""
echo "📝 Bitte Git Remote URL eingeben:"
echo " Beispiele:"
echo " - https://github.com/USERNAME/n8n-nodes-librebooking.git"
echo " - https://gitlab.com/USERNAME/n8n-nodes-librebooking.git"
echo " - git@github.com:USERNAME/n8n-nodes-librebooking.git"
echo ""
read -p " URL: " remote_url
if [ -z "$remote_url" ]; then
echo "❌ Keine URL eingegeben. Abgebrochen."
exit 1
fi
git remote add origin "$remote_url"
echo "✅ Remote hinzugefügt: $remote_url"
fi
# Status anzeigen
echo ""
echo "📊 Git Status:"
echo "─────────────────────────────────────"
git status --short
echo "─────────────────────────────────────"
# Anzahl der Änderungen
changed_files=$(git status --porcelain | wc -l)
if [ "$changed_files" -gt 0 ]; then
echo " $changed_files Datei(en) mit Änderungen"
else
echo " Keine uncommitteten Änderungen"
fi
# Branch Information
echo ""
echo "🌿 Branch: $(git branch --show-current 2>/dev/null || echo 'kein Branch')"
# Bestätigung für Commit
if [ "$changed_files" -gt 0 ]; then
echo ""
read -p "Möchtest du alle Änderungen committen? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add .
echo ""
echo "📝 Commit Message (Enter für Standard):"
read -p " > " commit_msg
if [ -z "$commit_msg" ]; then
commit_msg="feat: LibreBooking n8n Node v1.2.0"
fi
git commit -m "$commit_msg"
echo "✅ Commit erstellt"
fi
fi
# Push
echo ""
read -p "Möchtest du zum Remote pushen? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
echo "🚀 Pushe zum Remote..."
# Branch auf main umbenennen falls nötig
current_branch=$(git branch --show-current)
if [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then
read -p " Branch '$current_branch' zu 'main' umbenennen? (y/n) " rename_branch
if [[ $rename_branch =~ ^[Yy]$ ]]; then
git branch -M main
echo " ✅ Branch umbenannt zu 'main'"
fi
fi
git push -u origin $(git branch --show-current)
echo "✅ Code gepusht!"
# Tags pushen
if git tag | grep -q "."; then
echo ""
read -p "Möchtest du auch alle Tags pushen? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git push origin --tags
echo "✅ Tags gepusht!"
fi
fi
echo ""
echo "═══════════════════════════════════════"
echo "✅ Erfolgreich hochgeladen!"
echo "═══════════════════════════════════════"
echo ""
echo "📋 Nächste Schritte:"
echo " 1. Repository auf GitHub/GitLab prüfen"
echo " 2. README.md URLs anpassen"
echo " 3. Release erstellen (optional)"
else
echo "❌ Push abgebrochen"
fi
echo ""
echo "Fertig! 🎉"