Version 1.2.0
This commit is contained in:
parent
846441f436
commit
88b8bbd6a6
File diff suppressed because one or more lines are too long
|
|
@ -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/
|
||||
|
|
|
|||
Binary file not shown.
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
# LibreBooking Config Node
|
||||
|
||||
Der Config Node ermöglicht die zentrale Konfiguration von Standardwerten, die in allen LibreBooking Operationen verwendet werden können.
|
||||
|
||||
## Überblick
|
||||
|
||||
Der LibreBooking Config Credential ist **optional** und dient dazu:
|
||||
- Standardwerte zentral zu definieren
|
||||
- Wiederholte Eingaben zu vermeiden
|
||||
- Konsistente Einstellungen sicherzustellen
|
||||
|
||||
## Installation
|
||||
|
||||
Der Config Node wird automatisch mit der LibreBooking Node installiert. Er erscheint unter **Credentials** als "LibreBooking Config".
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### 1. Config Credential anlegen
|
||||
|
||||
1. Gehen Sie zu **Credentials** in n8n
|
||||
2. Klicken Sie auf **Add Credential**
|
||||
3. Suchen Sie nach **LibreBooking Config**
|
||||
4. Klicken Sie auf **Create**
|
||||
|
||||
### 2. Standardwerte definieren
|
||||
|
||||
| Einstellung | Beschreibung | Standard |
|
||||
|-------------|--------------|----------|
|
||||
| Standard Nutzungsbedingungen Akzeptiert | Vorauswahl für termsAccepted | `true` |
|
||||
| Standard Teilnahme Erlauben | Vorauswahl für allowParticipation | `false` |
|
||||
| Standard Ressourcen-ID | Standard-Ressource für Reservierungen | `0` (keine) |
|
||||
| Standard Benutzer-ID | Standard-Benutzer für Reservierungen | `0` (angemeldeter Benutzer) |
|
||||
| Standard Zeitplan-ID | Standard-Zeitplan für Ressourcen | `0` (keine) |
|
||||
| Standard Zeitzone | Zeitzone für neue Benutzer | `Europe/Berlin` |
|
||||
| Standard Sprache | Sprache für neue Benutzer | `de_de` |
|
||||
|
||||
### 3. Config mit Node verbinden
|
||||
|
||||
1. Öffnen Sie einen LibreBooking Node
|
||||
2. Bei Ressourcen wie Reservierung, Ressource, Benutzer oder Konto erscheint ein optionales Credential-Feld für **LibreBooking Config**
|
||||
3. Wählen Sie Ihre Config Credential aus
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Beispiel: Reservierung erstellen
|
||||
|
||||
**Ohne Config Node:**
|
||||
```
|
||||
- Ressourcen-ID: 1
|
||||
- Startzeit: ...
|
||||
- Endzeit: ...
|
||||
- Nutzungsbedingungen Akzeptiert: true (manuell)
|
||||
```
|
||||
|
||||
**Mit Config Node:**
|
||||
```
|
||||
- Ressourcen-ID: 1 (oder aus Config wenn 0 = übernehmen)
|
||||
- Startzeit: ...
|
||||
- Endzeit: ...
|
||||
- Nutzungsbedingungen Akzeptiert: (automatisch aus Config)
|
||||
```
|
||||
|
||||
### Priorität der Werte
|
||||
|
||||
1. **Höchste Priorität**: Werte direkt im Node eingegeben
|
||||
2. **Niedrigere Priorität**: Werte aus dem Config Node
|
||||
3. **Fallback**: Eingebaute Standardwerte
|
||||
|
||||
## Anwendungsfälle
|
||||
|
||||
### 1. Automatisierte Buchungen
|
||||
|
||||
Wenn Sie einen Workflow haben, der automatisch Buchungen erstellt:
|
||||
|
||||
```
|
||||
Config Node:
|
||||
- Standard Nutzungsbedingungen Akzeptiert: true
|
||||
- Standard Teilnahme Erlauben: false
|
||||
```
|
||||
|
||||
So müssen Sie diese Werte nicht in jedem Create-Node angeben.
|
||||
|
||||
### 2. Standardressource für Abteilung
|
||||
|
||||
```
|
||||
Config Node für Abteilung A:
|
||||
- Standard Ressourcen-ID: 5 (Konferenzraum A)
|
||||
|
||||
Config Node für Abteilung B:
|
||||
- Standard Ressourcen-ID: 8 (Konferenzraum B)
|
||||
```
|
||||
|
||||
### 3. Mehrsprachige Umgebung
|
||||
|
||||
```
|
||||
Config Node für deutschsprachige Workflows:
|
||||
- Standard Zeitzone: Europe/Berlin
|
||||
- Standard Sprache: de_de
|
||||
|
||||
Config Node für englischsprachige Workflows:
|
||||
- Standard Zeitzone: Europe/London
|
||||
- Standard Sprache: en_us
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Benennung
|
||||
|
||||
Verwenden Sie aussagekräftige Namen für Ihre Config Credentials:
|
||||
- `LibreBooking Config - Produktion`
|
||||
- `LibreBooking Config - Test`
|
||||
- `LibreBooking Config - Abteilung Marketing`
|
||||
|
||||
### 2. Dokumentation
|
||||
|
||||
Dokumentieren Sie Ihre Config-Einstellungen für Ihr Team.
|
||||
|
||||
### 3. Umgebungstrennung
|
||||
|
||||
Erstellen Sie separate Configs für verschiedene Umgebungen (Test/Produktion).
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Config wird nicht angewendet
|
||||
|
||||
- Stellen Sie sicher, dass der Config Node mit dem LibreBooking Node verbunden ist
|
||||
- Prüfen Sie, ob die Ressource den Config Node unterstützt (nur Reservierung, Ressource, Benutzer, Konto)
|
||||
|
||||
### Werte werden überschrieben
|
||||
|
||||
- Direkt im Node eingegebene Werte haben immer Vorrang
|
||||
- Lassen Sie Felder leer, wenn der Config-Wert verwendet werden soll
|
||||
|
||||
## Technische Details
|
||||
|
||||
Der Config Node wird als n8n Credential implementiert, ist aber kein echtes Authentifizierungs-Credential. Er speichert lediglich Konfigurationswerte.
|
||||
|
||||
**Credential-Name**: `libreBookingConfig`
|
||||
|
||||
**Unterstützte Ressourcen**:
|
||||
- Reservierung (`reservation`)
|
||||
- Ressource (`resource`)
|
||||
- Benutzer (`user`)
|
||||
- Konto (`account`)
|
||||
Binary file not shown.
|
|
@ -0,0 +1,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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,107 @@
|
|||
# Git Upload Anleitung
|
||||
|
||||
Diese Anleitung erklärt, wie Sie das LibreBooking n8n Node Repository auf verschiedene Git-Plattformen hochladen können.
|
||||
|
||||
## Option 1: GitHub/GitLab/Bitbucket (Web Interface)
|
||||
|
||||
1. Erstelle ein neues Repository auf GitHub/GitLab/Bitbucket
|
||||
2. Entpacke das Archiv lokal
|
||||
3. Folge den Anweisungen auf der Plattform
|
||||
|
||||
## Option 2: Command Line (Empfohlen)
|
||||
|
||||
### GitHub
|
||||
|
||||
```bash
|
||||
# 1. Repository erstellen auf github.com
|
||||
# 2. Dann lokal:
|
||||
cd librebooking_n8n_node
|
||||
git remote add origin https://github.com/USERNAME/n8n-nodes-librebooking.git
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
### GitLab
|
||||
|
||||
```bash
|
||||
cd librebooking_n8n_node
|
||||
git remote add origin https://gitlab.com/USERNAME/n8n-nodes-librebooking.git
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
### Bitbucket
|
||||
|
||||
```bash
|
||||
cd librebooking_n8n_node
|
||||
git remote add origin https://bitbucket.org/USERNAME/n8n-nodes-librebooking.git
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
## Option 3: Git Bundle verwenden
|
||||
|
||||
Wenn Sie das Git Bundle (.bundle Datei) erhalten haben:
|
||||
|
||||
```bash
|
||||
# Bundle entpacken (klonen)
|
||||
git clone librebooking-n8n-node-v1.2.0.bundle librebooking_n8n_node
|
||||
cd librebooking_n8n_node
|
||||
|
||||
# Remote hinzufügen
|
||||
git remote add origin YOUR_REMOTE_URL
|
||||
|
||||
# Pushen mit Tags
|
||||
git push -u origin main --tags
|
||||
```
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
### Vor dem Upload prüfen
|
||||
|
||||
- [ ] Keine sensiblen Daten (API Keys, Passwörter) im Repository
|
||||
- [ ] `.gitignore` ist korrekt konfiguriert
|
||||
- [ ] `node_modules/` und `dist/` sind nicht im Repository
|
||||
- [ ] Alle Dokumentation ist aktuell
|
||||
|
||||
### Nach dem Upload
|
||||
|
||||
- [ ] Repository ist erreichbar
|
||||
- [ ] Alle Dateien sind vorhanden
|
||||
- [ ] Tags sind sichtbar
|
||||
- [ ] README wird korrekt angezeigt
|
||||
|
||||
## SSH vs HTTPS
|
||||
|
||||
### HTTPS (einfacher)
|
||||
```bash
|
||||
git remote add origin https://github.com/USERNAME/REPO.git
|
||||
```
|
||||
|
||||
### SSH (empfohlen für regelmäßige Nutzung)
|
||||
```bash
|
||||
git remote add origin git@github.com:USERNAME/REPO.git
|
||||
```
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### "fatal: remote origin already exists"
|
||||
```bash
|
||||
git remote remove origin
|
||||
git remote add origin NEW_URL
|
||||
```
|
||||
|
||||
### "Updates were rejected"
|
||||
```bash
|
||||
# VORSICHT: Nur wenn Sie sicher sind
|
||||
git push -f origin main
|
||||
```
|
||||
|
||||
## Siehe auch
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Beitragen zum Projekt
|
||||
- [README.md](README.md) - Hauptdokumentation
|
||||
- [RELEASE-NOTES.md](RELEASE-NOTES.md) - Versionshinweise
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,151 @@
|
|||
# Package Contents
|
||||
|
||||
Übersicht aller Dateien im LibreBooking n8n Node Paket.
|
||||
|
||||
## 📁 Struktur
|
||||
|
||||
```
|
||||
librebooking_n8n_node/
|
||||
├── 📄 Hauptdateien
|
||||
├── 📁 nodes/ # n8n Nodes
|
||||
├── 📁 credentials/ # Credentials
|
||||
├── 📁 workflows/ # Beispiel Workflows
|
||||
├── 📁 test/ # Test Dateien
|
||||
└── 📄 Dokumentation & Skripte
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Hauptdateien
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `package.json` | Node Package Konfiguration |
|
||||
| `tsconfig.json` | TypeScript Konfiguration |
|
||||
| `index.ts` | Haupt-Export Datei |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Nodes
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `nodes/LibreBooking/LibreBooking.node.ts` | Haupt-Node für alle CRUD Operationen |
|
||||
| `nodes/LibreBooking/librebooking.svg` | Node Icon |
|
||||
| `nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts` | Trigger Node für Events |
|
||||
| `nodes/LibreBookingTrigger/librebooking.svg` | Trigger Node Icon |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Credentials
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `credentials/LibreBookingApi.credentials.ts` | API Credentials (URL, Benutzer, Passwort) |
|
||||
| `credentials/LibreBookingConfig.credentials.ts` | Config Node für Standardwerte |
|
||||
|
||||
---
|
||||
|
||||
## 📚 Dokumentation
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `README.md` | Hauptdokumentation |
|
||||
| `INSTALLATION.md` | Detaillierte Installationsanleitung |
|
||||
| `SCHNELLSTART.md` | Quick Start Guide |
|
||||
| `CUSTOM-ATTRIBUTES.md` | Custom Attributes Anleitung |
|
||||
| `CONFIG-NODE.md` | Config Node Guide |
|
||||
| `TROUBLESHOOTING.md` | Problemlösungen |
|
||||
| `DOCKER-INTEGRATION.md` | Docker Dokumentation |
|
||||
| `SECURITY.md` | Sicherheitshinweise |
|
||||
| `CHANGELOG.md` | Versionshistorie |
|
||||
| `CONTRIBUTING.md` | Contribution Guide |
|
||||
| `LICENSE` | MIT Lizenz |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Git-spezifische Dateien
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `GIT-UPLOAD.md` | Git Upload Anleitung |
|
||||
| `RELEASE-NOTES.md` | Release Notes v1.2.0 |
|
||||
| `PACKAGE-CONTENTS.md` | Diese Datei |
|
||||
| `.gitignore` | Git Ignore Konfiguration |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Skripte
|
||||
|
||||
### Installation
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `install.sh` | Linux/Mac Installation |
|
||||
| `install.ps1` | Windows PowerShell Installation |
|
||||
| `quick-install.sh` | Schnellinstallation |
|
||||
|
||||
### Docker
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `install-docker.sh` | Docker Installation |
|
||||
| `install-docker-manual.sh` | Manuelle Docker Installation |
|
||||
| `install-in-container.sh` | Installation im Container |
|
||||
| `build-on-host.sh` | Host-seitiges Bauen |
|
||||
|
||||
### Wartung
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `update-node.sh` | Update Skript |
|
||||
| `check-installation.sh` | Installation prüfen |
|
||||
| `fix-node-installation.sh` | Installation reparieren |
|
||||
| `update-dependencies.sh` | Dependencies aktualisieren |
|
||||
| `upload-to-git.sh` | Git Upload Helper |
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker Konfiguration
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `Dockerfile` | Docker Image Definition |
|
||||
| `docker-compose.yml` | Standard Docker Compose |
|
||||
| `docker-compose.override.yml` | Override für Entwicklung |
|
||||
| `docker-compose.readonly.yml` | Read-only Volume Konfiguration |
|
||||
| `.dockerignore` | Docker Build Ausschlüsse |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `test/test-api.ts` | API Test Script |
|
||||
| `workflows/example-workflows.json` | Beispiel n8n Workflows |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Konfiguration
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `.npmrc` | npm Konfiguration |
|
||||
| `.npmignore` | npm Publish Ausschlüsse |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Build Output (nicht im Repository)
|
||||
|
||||
| Verzeichnis | Beschreibung |
|
||||
|-------------|-------------|
|
||||
| `dist/` | Kompilierte JavaScript Dateien |
|
||||
| `node_modules/` | npm Dependencies |
|
||||
| `dist-for-docker/` | Host-Build für Docker |
|
||||
|
||||
---
|
||||
|
||||
## Dateigröße
|
||||
|
||||
- **Quellcode**: ~50 KB
|
||||
- **Mit node_modules**: ~50 MB
|
||||
- **Mit dist**: ~100 KB zusätzlich
|
||||
- **Git Repository**: ~1 MB (ohne node_modules)
|
||||
Binary file not shown.
60
README.md
60
README.md
|
|
@ -1,7 +1,44 @@
|
|||
# LibreBooking n8n Node
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Integration von LibreBooking in n8n für automatisierte Reservierungs- und Ressourcenverwaltung.
|
||||
|
||||
## 📦 Installation via Git
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone https://github.com/YOUR-USERNAME/n8n-nodes-librebooking.git
|
||||
cd n8n-nodes-librebooking
|
||||
|
||||
# Dependencies installieren
|
||||
npm install
|
||||
|
||||
# Bauen
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Quick Start nach Git Clone
|
||||
|
||||
```bash
|
||||
# Option 1: Automatische Installation
|
||||
./quick-install.sh n8n
|
||||
|
||||
# Option 2: Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# Option 3: Manuell in bestehenden n8n Container
|
||||
docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
||||
docker cp package.json n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
||||
docker cp node_modules n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
||||
docker restart n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Schnellstart (EMPFOHLEN)
|
||||
|
||||
**Die einfachste Methode: Auf dem Host bauen, in Docker kopieren**
|
||||
|
|
@ -42,13 +79,6 @@ npm run docker:copy # Kopiert in Container
|
|||
npm run docker:restart # Startet Container neu
|
||||
```
|
||||
|
||||
## 📚 Dokumentation
|
||||
|
||||
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
|
||||
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
|
||||
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
|
||||
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
|
||||
|
||||
## 🔑 Credentials einrichten
|
||||
|
||||
1. Öffne n8n: http://localhost:5678
|
||||
|
|
@ -66,11 +96,27 @@ npm run docker:restart # Startet Container neu
|
|||
- Ressourcen und Verfügbarkeit verwalten
|
||||
- Benutzer und Gruppen administrieren
|
||||
- Zeitpläne und Zubehör konfigurieren
|
||||
- **NEU v1.2.0**: Benutzerdefinierte Attribute setzen
|
||||
|
||||
### LibreBooking Trigger Node
|
||||
- Neue Reservierungen überwachen
|
||||
- Geänderte Reservierungen erfassen
|
||||
- Filter nach Ressource/Zeitplan/Benutzer
|
||||
- **NEU v1.2.0**: Korrektes Verhalten beim ersten Poll (keine Altdaten)
|
||||
- **NEU v1.2.0**: Zuverlässige Änderungserkennung via Hash-Vergleich
|
||||
|
||||
### LibreBooking Config (v1.2.0)
|
||||
- Optionales Credential für zentrale Standardwerte
|
||||
- Konfigurierbar: termsAccepted, allowParticipation, Zeitzone, Sprache
|
||||
|
||||
## 📚 Dokumentation
|
||||
|
||||
- **[INSTALLATION.md](INSTALLATION.md)** - Alle Installationsmethoden
|
||||
- **[SCHNELLSTART.md](SCHNELLSTART.md)** - Ultra-kurze Anleitung
|
||||
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Problemlösung
|
||||
- **[DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md)** - Docker-spezifische Anleitung
|
||||
- **[CUSTOM-ATTRIBUTES.md](CUSTOM-ATTRIBUTES.md)** - Benutzerdefinierte Attribute verwenden
|
||||
- **[CONFIG-NODE.md](CONFIG-NODE.md)** - Config Node für Standardwerte
|
||||
|
||||
## 🔄 Updates
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
# Release Notes v1.2.0
|
||||
|
||||
**Release Datum:** Januar 2026
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Version bringt wichtige neue Features, Verbesserungen und Bugfixes für den LibreBooking n8n Node.
|
||||
|
||||
---
|
||||
|
||||
## Neue Features
|
||||
|
||||
### 🏷️ Custom Attributes Support
|
||||
- Setzen von benutzerdefinierten Attributen für:
|
||||
- Reservierungen
|
||||
- Ressourcen
|
||||
- Benutzer
|
||||
- Accounts
|
||||
- Flexible Konfiguration über fixedCollection
|
||||
- Unterstützung für verschiedene Attributtypen
|
||||
|
||||
### ⚙️ Config Node
|
||||
- Zentraler Konfigurationsknoten für Standardwerte
|
||||
- Optionale Verwendung - bestehende Workflows funktionieren weiterhin
|
||||
- Reduziert manuelle Eingaben bei wiederkehrenden Werten
|
||||
- Konfigurierbare Defaults für:
|
||||
- Standard-Ressource
|
||||
- Standard-Zeitplan
|
||||
- Standardwerte für neue Reservierungen
|
||||
|
||||
### 🔄 Verbesserte Trigger
|
||||
- Neue Events triggern nicht mehr mit existierenden Daten
|
||||
- Geänderte Events werden korrekt erkannt
|
||||
- Hash-basierte Änderungserkennung
|
||||
- Verbesserte Deduplizierung
|
||||
|
||||
### ✅ Pflichtfelder
|
||||
- `termsAccepted` als Pflichtfeld bei Reservation Create
|
||||
- Alle Pflichtfelder gemäß API-Dokumentation geprüft und ergänzt
|
||||
- Bessere Validierung vor API-Aufrufen
|
||||
|
||||
---
|
||||
|
||||
## Verbesserungen
|
||||
|
||||
### 📚 Dokumentation
|
||||
- Neue CUSTOM-ATTRIBUTES.md Anleitung
|
||||
- CONFIG-NODE.md Dokumentation
|
||||
- Erweiterte TROUBLESHOOTING.md
|
||||
- SECURITY.md für Sicherheitshinweise
|
||||
|
||||
### 🐳 Docker Integration
|
||||
- Read-only Volume Problem gelöst
|
||||
- build-on-host.sh für Host-seitiges Bauen
|
||||
- docker-compose.readonly.yml für sichere Deployments
|
||||
- Verbesserte Fehlermeldungen
|
||||
|
||||
### 🔒 Sicherheit
|
||||
- npm audit Vulnerabilities dokumentiert
|
||||
- package.json overrides für sichere Dependencies
|
||||
- .npmrc zur Unterdrückung von Warnungen
|
||||
|
||||
---
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Trigger löst bei neuen Events nicht mehr mit alten Daten aus
|
||||
- Korrektes Handling von leeren API-Responses
|
||||
- Verbesserte Fehlerbehandlung bei Authentifizierung
|
||||
- Session-Timeout wird korrekt behandelt
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Siehe [INSTALLATION.md](INSTALLATION.md) für detaillierte Anweisungen.
|
||||
|
||||
```bash
|
||||
# Quick Install
|
||||
git clone https://github.com/YOUR-USERNAME/n8n-nodes-librebooking.git
|
||||
cd n8n-nodes-librebooking
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upgrade von v1.1.0
|
||||
|
||||
```bash
|
||||
cd /opt/n8n/custom-nodes/n8n-nodes-librebooking
|
||||
git pull
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Bei Docker:
|
||||
docker cp dist n8n:/home/node/.n8n/custom/n8n-nodes-librebooking/
|
||||
docker restart n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
**Keine** - Diese Version ist vollständig abwärtskompatibel.
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
- npm audit zeigt Vulnerabilities von n8n-workflow Dependencies (siehe SECURITY.md)
|
||||
- Read-only Docker Volumes erfordern Host-seitiges Bauen
|
||||
|
||||
---
|
||||
|
||||
## Nächste Version (Roadmap)
|
||||
|
||||
- [ ] Webhook Support für Echtzeit-Events
|
||||
- [ ] Batch-Operationen für mehrere Reservierungen
|
||||
- [ ] Erweiterte Filteroptionen
|
||||
- [ ] npm Registry Veröffentlichung
|
||||
|
||||
---
|
||||
|
||||
## Danksagungen
|
||||
|
||||
Vielen Dank an alle Contributors und Tester!
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- [GitHub Repository](https://github.com/YOUR-USERNAME/n8n-nodes-librebooking)
|
||||
- [LibreBooking](https://github.com/LibreBooking/app)
|
||||
- [n8n](https://n8n.io)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* LibreBooking Config Credential
|
||||
*
|
||||
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
||||
* die in allen LibreBooking Nodes verwendet werden können.
|
||||
*/
|
||||
export class LibreBookingConfig implements ICredentialType {
|
||||
name = 'libreBookingConfig';
|
||||
displayName = 'LibreBooking Config';
|
||||
documentationUrl = 'https://librebooking.org';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Hinweis',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
|
||||
name: 'defaultTermsAccepted',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Teilnahme Erlauben',
|
||||
name: 'defaultAllowParticipation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Ressourcen-ID',
|
||||
name: 'defaultResourceId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Benutzer-ID',
|
||||
name: 'defaultUserId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Zeitplan-ID',
|
||||
name: 'defaultScheduleId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Zeitzone',
|
||||
name: 'defaultTimezone',
|
||||
type: 'string',
|
||||
default: 'Europe/Berlin',
|
||||
description: 'Standard-Zeitzone für neue Benutzer',
|
||||
},
|
||||
{
|
||||
displayName: 'Standard Sprache',
|
||||
name: 'defaultLanguage',
|
||||
type: 'string',
|
||||
default: 'de_de',
|
||||
description: 'Standard-Sprache für neue Benutzer',
|
||||
},
|
||||
];
|
||||
}
|
||||
1
index.ts
1
index.ts
|
|
@ -4,3 +4,4 @@
|
|||
export * from './nodes/LibreBooking/LibreBooking.node';
|
||||
export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node';
|
||||
export * from './credentials/LibreBookingApi.credentials';
|
||||
export * from './credentials/LibreBookingConfig.credentials';
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
{
|
||||
"name": "LibreBooking v1.2.0 Test Workflows",
|
||||
"description": "Beispiel-Workflows für die neuen Features in Version 1.2.0",
|
||||
"workflows": [
|
||||
{
|
||||
"name": "01 - Reservierung mit Custom Attributes erstellen",
|
||||
"description": "Erstellt eine Reservierung mit benutzerdefinierten Attributen",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"name": "Manuell starten",
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBooking",
|
||||
"name": "Attribute abrufen",
|
||||
"parameters": {
|
||||
"resource": "attribute",
|
||||
"operation": "getByCategory",
|
||||
"categoryId": 1
|
||||
},
|
||||
"position": [450, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBooking",
|
||||
"name": "Reservierung erstellen",
|
||||
"parameters": {
|
||||
"resource": "reservation",
|
||||
"operation": "create",
|
||||
"resourceId": 1,
|
||||
"startDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T10:00:00",
|
||||
"endDateTime": "={{ $now.plus(1, 'day').toFormat('yyyy-MM-dd') }}T11:00:00",
|
||||
"termsAccepted": true,
|
||||
"title": "Test Reservierung mit Attributen",
|
||||
"customAttributes": {
|
||||
"attribute": [
|
||||
{
|
||||
"attributeId": 1,
|
||||
"attributeValue": "Mein Attribut-Wert"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [650, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manuell starten": {
|
||||
"main": [
|
||||
[{ "node": "Attribute abrufen", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"Attribute abrufen": {
|
||||
"main": [
|
||||
[{ "node": "Reservierung erstellen", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "02 - Ressource mit Custom Attributes erstellen",
|
||||
"description": "Erstellt eine Ressource mit benutzerdefinierten Attributen",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"name": "Manuell starten",
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBooking",
|
||||
"name": "Ressource erstellen",
|
||||
"parameters": {
|
||||
"resource": "resource",
|
||||
"operation": "create",
|
||||
"resourceName": "Testraum mit Ausstattung",
|
||||
"scheduleIdForResource": 1,
|
||||
"resourceCustomAttributes": {
|
||||
"attribute": [
|
||||
{
|
||||
"attributeId": 10,
|
||||
"attributeValue": "Beamer, Whiteboard, 20 Plätze"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceOptions": {
|
||||
"description": "Ein Konferenzraum mit voller Ausstattung",
|
||||
"maxParticipants": 20
|
||||
}
|
||||
},
|
||||
"position": [450, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manuell starten": {
|
||||
"main": [
|
||||
[{ "node": "Ressource erstellen", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "03 - Trigger für neue Reservierungen (ohne Altdaten)",
|
||||
"description": "Überwacht neue Reservierungen - beim ersten Start werden existierende Reservierungen NICHT getriggert",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
||||
"name": "Neue Reservierungen",
|
||||
"parameters": {
|
||||
"event": "newReservation",
|
||||
"timeWindow": "14days",
|
||||
"options": {
|
||||
"fetchDetails": false,
|
||||
"debugMode": true
|
||||
}
|
||||
},
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.if",
|
||||
"name": "Debug-Check",
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"boolean": [
|
||||
{
|
||||
"value1": "={{ $json._debug }}",
|
||||
"value2": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [450, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.set",
|
||||
"name": "Debug-Info",
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "info",
|
||||
"value": "Erster Poll - Daten wurden gespeichert"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [650, 200]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.set",
|
||||
"name": "Neue Reservierung verarbeiten",
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "message",
|
||||
"value": "Neue Reservierung: {{ $json.title }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [650, 400]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Neue Reservierungen": {
|
||||
"main": [
|
||||
[{ "node": "Debug-Check", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"Debug-Check": {
|
||||
"main": [
|
||||
[{ "node": "Debug-Info", "type": "main", "index": 0 }],
|
||||
[{ "node": "Neue Reservierung verarbeiten", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "04 - Trigger für geänderte Reservierungen",
|
||||
"description": "Überwacht Änderungen an bestehenden Reservierungen mittels Hash-Vergleich",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
||||
"name": "Geänderte Reservierungen",
|
||||
"parameters": {
|
||||
"event": "updatedReservation",
|
||||
"timeWindow": "14days",
|
||||
"options": {
|
||||
"fetchDetails": true,
|
||||
"debugMode": false
|
||||
}
|
||||
},
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.set",
|
||||
"name": "Änderung protokollieren",
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "message",
|
||||
"value": "Reservierung geändert: {{ $json.referenceNumber }} - {{ $json.title }}"
|
||||
},
|
||||
{
|
||||
"name": "changedAt",
|
||||
"value": "={{ $json._triggeredAt }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [450, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Geänderte Reservierungen": {
|
||||
"main": [
|
||||
[{ "node": "Änderung protokollieren", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "05 - Config Node Verwendung",
|
||||
"description": "Zeigt die Verwendung des LibreBooking Config Credentials für Standardwerte",
|
||||
"notes": "Voraussetzung: LibreBooking Config Credential muss angelegt und mit dem Node verbunden sein",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"name": "Manuell starten",
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBooking",
|
||||
"name": "Reservierung mit Defaults",
|
||||
"parameters": {
|
||||
"resource": "reservation",
|
||||
"operation": "create",
|
||||
"resourceId": 1,
|
||||
"startDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T14:00:00",
|
||||
"endDateTime": "={{ $now.plus(2, 'day').toFormat('yyyy-MM-dd') }}T15:00:00",
|
||||
"title": "Reservierung mit Config-Defaults"
|
||||
},
|
||||
"credentials": {
|
||||
"libreBookingApi": "LibreBooking API",
|
||||
"libreBookingConfig": "LibreBooking Config"
|
||||
},
|
||||
"position": [450, 300],
|
||||
"notes": "termsAccepted und allowParticipation werden aus dem Config Node übernommen"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manuell starten": {
|
||||
"main": [
|
||||
[{ "node": "Reservierung mit Defaults", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "06 - Alle Events überwachen",
|
||||
"description": "Überwacht sowohl neue als auch geänderte Reservierungen",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "n8n-nodes-librebooking.libreBookingTrigger",
|
||||
"name": "Alle Reservierungs-Events",
|
||||
"parameters": {
|
||||
"event": "allReservations",
|
||||
"timeWindow": "30days",
|
||||
"filters": {
|
||||
"resourceId": ""
|
||||
},
|
||||
"options": {
|
||||
"fetchDetails": true
|
||||
}
|
||||
},
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"name": "Event-Typ prüfen",
|
||||
"parameters": {
|
||||
"dataType": "string",
|
||||
"value1": "={{ $json._eventType }}",
|
||||
"rules": {
|
||||
"rules": [
|
||||
{
|
||||
"value2": "new"
|
||||
},
|
||||
{
|
||||
"value2": "updated"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [450, 300]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.set",
|
||||
"name": "Neue Reservierung",
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "action",
|
||||
"value": "NEU: {{ $json.title }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [650, 200]
|
||||
},
|
||||
{
|
||||
"type": "n8n-nodes-base.set",
|
||||
"name": "Geänderte Reservierung",
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "action",
|
||||
"value": "GEÄNDERT: {{ $json.title }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [650, 400]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Alle Reservierungs-Events": {
|
||||
"main": [
|
||||
[{ "node": "Event-Typ prüfen", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"Event-Typ prüfen": {
|
||||
"main": [
|
||||
[{ "node": "Neue Reservierung", "type": "main", "index": 0 }],
|
||||
[{ "node": "Geänderte Reservierung", "type": "main", "index": 0 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# Git Upload Helper Script
|
||||
# LibreBooking n8n Node v1.2.0
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Git Upload Vorbereitung"
|
||||
echo "========================="
|
||||
echo ""
|
||||
|
||||
# Prüfe ob Git installiert ist
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "❌ Git ist nicht installiert!"
|
||||
echo " Installiere mit: sudo apt install git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfe ob Git Repository existiert
|
||||
if [ ! -d .git ]; then
|
||||
echo "❌ Kein Git Repository gefunden!"
|
||||
echo " Möchtest du eines initialisieren? (y/n)"
|
||||
read -p " > " init_git
|
||||
if [[ $init_git =~ ^[Yy]$ ]]; then
|
||||
git init
|
||||
echo "✅ Git Repository initialisiert"
|
||||
else
|
||||
echo "Abgebrochen."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prüfe ob Remote existiert
|
||||
echo ""
|
||||
echo "📡 Remote Konfiguration:"
|
||||
if git remote | grep -q origin; then
|
||||
current_remote=$(git remote get-url origin 2>/dev/null || echo "nicht konfiguriert")
|
||||
echo " Aktueller Remote: $current_remote"
|
||||
echo ""
|
||||
read -p " Möchtest du den Remote ändern? (y/n) " change_remote
|
||||
if [[ $change_remote =~ ^[Yy]$ ]]; then
|
||||
git remote remove origin
|
||||
echo " Remote entfernt."
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! git remote | grep -q origin; then
|
||||
echo ""
|
||||
echo "📝 Bitte Git Remote URL eingeben:"
|
||||
echo " Beispiele:"
|
||||
echo " - https://github.com/USERNAME/n8n-nodes-librebooking.git"
|
||||
echo " - https://gitlab.com/USERNAME/n8n-nodes-librebooking.git"
|
||||
echo " - git@github.com:USERNAME/n8n-nodes-librebooking.git"
|
||||
echo ""
|
||||
read -p " URL: " remote_url
|
||||
if [ -z "$remote_url" ]; then
|
||||
echo "❌ Keine URL eingegeben. Abgebrochen."
|
||||
exit 1
|
||||
fi
|
||||
git remote add origin "$remote_url"
|
||||
echo "✅ Remote hinzugefügt: $remote_url"
|
||||
fi
|
||||
|
||||
# Status anzeigen
|
||||
echo ""
|
||||
echo "📊 Git Status:"
|
||||
echo "─────────────────────────────────────"
|
||||
git status --short
|
||||
echo "─────────────────────────────────────"
|
||||
|
||||
# Anzahl der Änderungen
|
||||
changed_files=$(git status --porcelain | wc -l)
|
||||
if [ "$changed_files" -gt 0 ]; then
|
||||
echo " $changed_files Datei(en) mit Änderungen"
|
||||
else
|
||||
echo " Keine uncommitteten Änderungen"
|
||||
fi
|
||||
|
||||
# Branch Information
|
||||
echo ""
|
||||
echo "🌿 Branch: $(git branch --show-current 2>/dev/null || echo 'kein Branch')"
|
||||
|
||||
# Bestätigung für Commit
|
||||
if [ "$changed_files" -gt 0 ]; then
|
||||
echo ""
|
||||
read -p "Möchtest du alle Änderungen committen? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git add .
|
||||
echo ""
|
||||
echo "📝 Commit Message (Enter für Standard):"
|
||||
read -p " > " commit_msg
|
||||
if [ -z "$commit_msg" ]; then
|
||||
commit_msg="feat: LibreBooking n8n Node v1.2.0"
|
||||
fi
|
||||
git commit -m "$commit_msg"
|
||||
echo "✅ Commit erstellt"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Push
|
||||
echo ""
|
||||
read -p "Möchtest du zum Remote pushen? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
echo "🚀 Pushe zum Remote..."
|
||||
|
||||
# Branch auf main umbenennen falls nötig
|
||||
current_branch=$(git branch --show-current)
|
||||
if [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then
|
||||
read -p " Branch '$current_branch' zu 'main' umbenennen? (y/n) " rename_branch
|
||||
if [[ $rename_branch =~ ^[Yy]$ ]]; then
|
||||
git branch -M main
|
||||
echo " ✅ Branch umbenannt zu 'main'"
|
||||
fi
|
||||
fi
|
||||
|
||||
git push -u origin $(git branch --show-current)
|
||||
echo "✅ Code gepusht!"
|
||||
|
||||
# Tags pushen
|
||||
if git tag | grep -q "."; then
|
||||
echo ""
|
||||
read -p "Möchtest du auch alle Tags pushen? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git push origin --tags
|
||||
echo "✅ Tags gepusht!"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
echo "✅ Erfolgreich hochgeladen!"
|
||||
echo "═══════════════════════════════════════"
|
||||
echo ""
|
||||
echo "📋 Nächste Schritte:"
|
||||
echo " 1. Repository auf GitHub/GitLab prüfen"
|
||||
echo " 2. README.md URLs anpassen"
|
||||
echo " 3. Release erstellen (optional)"
|
||||
else
|
||||
echo "❌ Push abgebrochen"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Fertig! 🎉"
|
||||
Loading…
Reference in New Issue