Version 1.2.0

This commit is contained in:
Sebastian Zell 2026-01-25 21:25:52 +01:00
parent 846441f436
commit 88b8bbd6a6
31 changed files with 2031 additions and 61 deletions

File diff suppressed because one or more lines are too long

45
.gitignore vendored
View File

@ -1,7 +1,46 @@
# Node modules
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
dist/
.env
*.tsbuildinfo
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
# Generated PDFs
*.pdf
# Environment
.env
.env.local
.env.*.local
# Test
coverage/
.nyc_output/
# Temporary files
*.tmp
*.temp
.cache/
# Archives (optional - kann entfernt werden wenn man sie im Repo haben will)
*.tar.gz
*.zip
*.bundle
# Docker build artifacts
dist-for-docker/

BIN
ARCHIV-INFO.pdf Normal file

Binary file not shown.

View File

@ -2,6 +2,30 @@
Alle wichtigen Änderungen werden hier dokumentiert.
## [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

144
CONFIG-NODE.md Normal file
View File

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

BIN
CONFIG-NODE.pdf Normal file

Binary file not shown.

159
CUSTOM-ATTRIBUTES.md Normal file
View File

@ -0,0 +1,159 @@
# 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)
## 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

BIN
CUSTOM-ATTRIBUTES.pdf Normal file

Binary file not shown.

BIN
DOCKER-INTEGRATION.pdf Normal file

Binary file not shown.

BIN
GIT-COMMANDS.pdf Normal file

Binary file not shown.

107
GIT-UPLOAD.md Normal file
View File

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

BIN
GIT-UPLOAD.pdf Normal file

Binary file not shown.

Binary file not shown.

151
PACKAGE-CONTENTS.md Normal file
View File

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

BIN
PACKAGE-CONTENTS.pdf Normal file

Binary file not shown.

View File

@ -1,7 +1,44 @@
# 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**
@ -42,13 +79,6 @@ npm run docker:copy # Kopiert in Container
npm run docker:restart # Startet Container neu
```
## 📚 Dokumentation
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
## 🔑 Credentials einrichten
1. Öffne n8n: http://localhost:5678
@ -66,11 +96,27 @@ npm run docker:restart # Startet Container neu
- Ressourcen und Verfügbarkeit verwalten
- Benutzer und Gruppen administrieren
- Zeitpläne und Zubehör konfigurieren
- **NEU v1.2.0**: Benutzerdefinierte Attribute setzen
### LibreBooking Trigger Node
- Neue Reservierungen überwachen
- Geänderte Reservierungen erfassen
- Filter nach Ressource/Zeitplan/Benutzer
- **NEU v1.2.0**: Korrektes Verhalten beim ersten Poll (keine Altdaten)
- **NEU v1.2.0**: Zuverlässige Änderungserkennung via Hash-Vergleich
### LibreBooking Config (v1.2.0)
- Optionales Credential für zentrale Standardwerte
- Konfigurierbar: termsAccepted, allowParticipation, Zeitzone, Sprache
## 📚 Dokumentation
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
- **[CUSTOM-ATTRIBUTES.md](CUSTOM-ATTRIBUTES.md)** - Benutzerdefinierte Attribute verwenden
- **[CONFIG-NODE.md](CONFIG-NODE.md)** - Config Node für Standardwerte
## 🔄 Updates

135
RELEASE-NOTES.md Normal file
View File

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

BIN
RELEASE-NOTES.pdf Normal file

Binary file not shown.

BIN
SCHNELLSTART-DOCKER.pdf Normal file

Binary file not shown.

BIN
SCHNELLSTART.pdf Normal file

Binary file not shown.

BIN
SECURITY.pdf Normal file

Binary file not shown.

BIN
TROUBLESHOOTING.pdf Normal file

Binary file not shown.

View File

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

View File

@ -4,3 +4,4 @@
export * from './nodes/LibreBooking/LibreBooking.node';
export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node';
export * from './credentials/LibreBookingApi.credentials';
export * from './credentials/LibreBookingConfig.credentials';

Binary file not shown.

Binary file not shown.

View File

@ -14,6 +14,16 @@ interface LibreBookingSession {
sessionExpires: string;
}
interface ConfigDefaults {
defaultTermsAccepted: boolean;
defaultAllowParticipation: boolean;
defaultResourceId: number;
defaultUserId: number;
defaultScheduleId: number;
defaultTimezone: string;
defaultLanguage: string;
}
/**
* Authentifizierung bei LibreBooking
*/
@ -140,6 +150,52 @@ 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
*
@ -165,6 +221,15 @@ export class LibreBooking implements INodeType {
name: 'libreBookingApi',
required: true,
},
{
name: 'libreBookingConfig',
required: false,
displayOptions: {
show: {
resource: ['reservation', 'resource', 'user', 'account'],
},
},
},
],
properties: [
// =====================================================
@ -420,6 +485,16 @@ 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',
@ -440,6 +515,41 @@ 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',
@ -454,7 +564,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: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true },
{ displayName: 'Ressourcen-ID (Update)', name: 'resourceId', type: 'number', default: '', description: 'Ressourcen-ID für Updates' },
],
},
{
@ -513,6 +623,42 @@ 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',
@ -579,6 +725,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create'] } },
default: '',
description: 'E-Mail-Adresse des Benutzers (Pflichtfeld)',
},
{
displayName: 'Benutzername',
@ -587,6 +734,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create'] } },
default: '',
description: 'Benutzername für die Anmeldung (Pflichtfeld)',
},
{
displayName: 'Passwort',
@ -596,6 +744,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } },
default: '',
description: 'Passwort des Benutzers (Pflichtfeld)',
},
{
displayName: 'Vorname',
@ -604,6 +753,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } },
default: '',
description: 'Vorname des Benutzers (Pflichtfeld)',
},
{
displayName: 'Nachname',
@ -612,6 +762,42 @@ 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',
@ -656,6 +842,41 @@ 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',
@ -713,6 +934,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } },
default: '',
description: 'Name der Gruppe (Pflichtfeld)',
},
{
displayName: 'Standard-Gruppe',
@ -788,6 +1010,7 @@ export class LibreBooking implements INodeType {
required: true,
displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } },
default: '',
description: 'Anzeigename des Attributs (Pflichtfeld)',
},
{
displayName: 'Attribut-Typ',
@ -832,6 +1055,9 @@ 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 {
@ -859,17 +1085,38 @@ 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() };
const body: any = {
resourceId,
startDateTime: new Date(startDateTime).toISOString(),
endDateTime: new Date(endDateTime).toISOString(),
termsAccepted,
};
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);
if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation;
if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted;
if (additionalFields.allowParticipation !== undefined) {
body.allowParticipation = additionalFields.allowParticipation;
} else {
body.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,
}));
}
responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body);
} else if (operation === 'update') {
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
@ -877,8 +1124,15 @@ 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() };
const body: any = {
startDateTime: new Date(startDateTime).toISOString(),
endDateTime: new Date(endDateTime).toISOString(),
termsAccepted: true, // termsAccepted wird auch bei Updates benötigt
};
if (title) body.title = title;
if (additionalFields.description) body.description = additionalFields.description;
if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId;
@ -886,6 +1140,16 @@ 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);
if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation;
// 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;
@ -927,8 +1191,11 @@ 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;
@ -940,12 +1207,24 @@ 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;
@ -957,6 +1236,15 @@ 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;
@ -999,35 +1287,59 @@ export class LibreBooking implements INodeType {
} else if (operation === 'create') {
const emailAddress = this.getNodeParameter('emailAddress', i) as string;
const userName = this.getNodeParameter('userName', i) as string;
const pw = this.getNodeParameter('password', i) as string;
const userPw = 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: pw, firstName, lastName };
if (userOptions.timezone) body.timezone = userOptions.timezone;
if (userOptions.language) body.language = userOptions.language;
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 };
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 pw = this.getNodeParameter('password', i) as string;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw });
const userPw = this.getNodeParameter('password', i) as string;
responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: userPw });
} else if (operation === 'delete') {
const userId = this.getNodeParameter('userId', i) as number;
responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`);
@ -1041,23 +1353,42 @@ 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;
if (accountData.timezone) body.timezone = accountData.timezone;
if (accountData.language) body.language = accountData.language;
body.timezone = accountData.timezone || configDefaults.defaultTimezone;
body.language = accountData.language || configDefaults.defaultLanguage;
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;
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,
}));
}
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;
@ -1067,6 +1398,15 @@ 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,9 +20,18 @@ interface ReservationData {
title: string;
resourceId: number;
userId: number;
description?: string;
resourceName?: string;
[key: string]: any;
}
interface WorkflowStaticData {
seenIds?: string[];
reservationHashes?: Record<string, string>;
isFirstPoll?: boolean;
lastPollTime?: string;
}
/**
* Authentifizierung bei LibreBooking
*/
@ -174,14 +183,34 @@ function getTimeWindow(timeWindow: string): { start: string; end: string } {
}
/**
* Eindeutigen Schlüssel für Reservierung generieren
* Hash für Reservierung generieren (für Änderungserkennung)
* Nur relevante Felder berücksichtigen, die Änderungen anzeigen
*/
function getReservationKey(reservation: ReservationData): string {
return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`;
function getReservationHash(reservation: ReservationData): string {
const relevantData = {
referenceNumber: reservation.referenceNumber,
startDate: reservation.startDate,
endDate: reservation.endDate,
title: reservation.title || '',
description: reservation.description || '',
resourceId: reservation.resourceId,
resourceName: reservation.resourceName || '',
userId: reservation.userId,
requiresApproval: reservation.requiresApproval,
participants: reservation.participants || [],
invitees: reservation.invitees || [],
};
return JSON.stringify(relevantData);
}
/**
* LibreBooking Trigger Node
*
* Überwacht neue und geänderte Reservierungen in LibreBooking.
*
* WICHTIG: Beim ersten Poll werden nur die IDs/Hashes gespeichert,
* aber keine Events getriggert. Dies verhindert, dass alle
* existierenden Reservierungen als "neu" getriggert werden.
*/
export class LibreBookingTrigger implements INodeType {
description: INodeTypeDescription = {
@ -210,12 +239,36 @@ export class LibreBookingTrigger implements INodeType {
name: 'event',
type: 'options',
options: [
{ name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' },
{ name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' },
{ name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' },
{
name: 'Neue Reservierung',
value: 'newReservation',
description: 'Wird bei neuen Reservierungen ausgelöst (nicht beim ersten Poll)'
},
{
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: 'newReservation',
},
{
displayName: 'Hinweis',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
event: ['newReservation', 'allReservations'],
},
},
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende neue Reservierungen lösen den Trigger aus.',
},
{
displayName: 'Filter',
name: 'filters',
@ -247,7 +300,20 @@ export class LibreBookingTrigger implements INodeType {
placeholder: 'Option hinzufügen',
default: {},
options: [
{ displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false },
{
displayName: 'Detaillierte Daten Abrufen',
name: 'fetchDetails',
type: 'boolean',
default: false,
description: 'Ruft vollständige Reservierungsdaten ab (zusätzliche API-Aufrufe)',
},
{
displayName: 'Debug-Modus',
name: 'debugMode',
type: 'boolean',
default: false,
description: 'Gibt zusätzliche Debug-Informationen aus',
},
],
},
],
@ -264,8 +330,11 @@ export class LibreBookingTrigger implements INodeType {
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
const options = this.getNodeParameter('options', {}) as any;
const workflowStaticData = this.getWorkflowStaticData('node');
const previousReservations = (workflowStaticData.reservations as Record<string, string>) || {};
// Workflow Static Data für State-Management
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
// Debug-Modus
const debugMode = options.debugMode || false;
let session: LibreBookingSession;
try {
@ -287,31 +356,54 @@ export class LibreBookingTrigger implements INodeType {
);
const returnData: INodeExecutionData[] = [];
const currentReservations: Record<string, string> = {};
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';
// ==========================================
// EVENT: Neue Reservierungen
// ==========================================
if (event === 'newReservation') {
// Initialisiere seenIds beim ersten Poll
if (!webhookData.seenIds) {
webhookData.seenIds = [];
webhookData.isFirstPoll = true;
}
if (shouldTrigger) {
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
// 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,
_timestamp: webhookData.lastPollTime,
},
}]];
}
return null; // Nichts triggern beim ersten Poll
}
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
const newReservations = reservations.filter((r: ReservationData) =>
!webhookData.seenIds!.includes(r.referenceNumber)
);
// Update seenIds mit allen aktuellen IDs
webhookData.seenIds = currentIds;
webhookData.lastPollTime = new Date().toISOString();
if (newReservations.length === 0) {
return null;
}
// Neue Reservierungen verarbeiten
for (const reservation of newReservations) {
let reservationData = reservation;
if (options.fetchDetails) {
@ -320,7 +412,7 @@ export class LibreBookingTrigger implements INodeType {
this,
baseUrl,
session,
refNumber,
reservation.referenceNumber,
);
} catch (error) {
reservationData = reservation;
@ -330,14 +422,177 @@ export class LibreBookingTrigger implements INodeType {
returnData.push({
json: {
...reservationData,
_eventType: eventType,
_eventType: 'new',
_triggeredAt: new Date().toISOString(),
},
});
}
}
workflowStaticData.reservations = currentReservations;
// ==========================================
// EVENT: Geänderte Reservierungen
// ==========================================
else if (event === 'updatedReservation') {
// Initialisiere reservationHashes beim ersten Poll
if (!webhookData.reservationHashes) {
webhookData.reservationHashes = {};
webhookData.isFirstPoll = true;
}
// 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
const 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 (updatedReservations.length === 0) {
return null;
}
// Geänderte Reservierungen verarbeiten
for (const reservation of updatedReservations) {
let reservationData = reservation;
if (options.fetchDetails) {
try {
reservationData = await getReservationDetails(
this,
baseUrl,
session,
reservation.referenceNumber,
);
} catch (error) {
reservationData = reservation;
}
}
returnData.push({
json: {
...reservationData,
_eventType: 'updated',
_triggeredAt: new Date().toISOString(),
},
});
}
}
// ==========================================
// EVENT: Alle Reservierungen (Neu + Geändert)
// ==========================================
else if (event === 'allReservations') {
// Initialisiere beide Tracking-Strukturen beim ersten Poll
if (!webhookData.seenIds || !webhookData.reservationHashes) {
webhookData.seenIds = [];
webhookData.reservationHashes = {};
webhookData.isFirstPoll = true;
}
// Beim ersten Poll: IDs und Hashes speichern, NICHT triggern
if (webhookData.isFirstPoll) {
webhookData.seenIds = reservations.map((r: ReservationData) => r.referenceNumber);
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 - IDs und Hashes wurden gespeichert, keine Events getriggert',
_savedIds: webhookData.seenIds.length,
_savedHashes: Object.keys(webhookData.reservationHashes).length,
_timestamp: webhookData.lastPollTime,
},
}]];
}
return null;
}
const newHashes: Record<string, string> = {};
const currentIds: string[] = [];
for (const reservation of reservations) {
const refNumber = reservation.referenceNumber;
const currentHash = getReservationHash(reservation);
currentIds.push(refNumber);
newHashes[refNumber] = currentHash;
const isNew = !webhookData.seenIds!.includes(refNumber);
const oldHash = webhookData.reservationHashes![refNumber];
const isUpdated = oldHash && currentHash !== oldHash;
if (isNew || isUpdated) {
let reservationData = reservation;
if (options.fetchDetails) {
try {
reservationData = await getReservationDetails(
this,
baseUrl,
session,
refNumber,
);
} catch (error) {
reservationData = reservation;
}
}
returnData.push({
json: {
...reservationData,
_eventType: isNew ? 'new' : 'updated',
_triggeredAt: new Date().toISOString(),
},
});
}
}
// Update State
webhookData.seenIds = currentIds;
webhookData.reservationHashes = newHashes;
webhookData.lastPollTime = new Date().toISOString();
}
if (returnData.length === 0) {
return null;

View File

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

View File

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

149
upload-to-git.sh Executable file
View File

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