Version 1.2.1
This commit is contained in:
parent
88b8bbd6a6
commit
e59aa0241f
File diff suppressed because one or more lines are too long
|
|
@ -1,46 +0,0 @@
|
||||||
# Node modules
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# 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/
|
|
||||||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -2,6 +2,37 @@
|
||||||
|
|
||||||
Alle wichtigen Änderungen werden hier dokumentiert.
|
Alle wichtigen Änderungen werden hier dokumentiert.
|
||||||
|
|
||||||
|
## [1.2.1] - 2026-01-25
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- 🐛 **allowParticipation Fehler**: API-Fehler "Undefined property: stdClass::$allowParticipation" behoben. Das Feld wird jetzt immer im Request-Body gesendet.
|
||||||
|
- 🐛 **Trigger "Alle Abrufen" funktioniert nicht**: Trigger-Modi komplett überarbeitet mit drei klaren Optionen:
|
||||||
|
- "Alle Abrufen (Einmalig)" - Ruft alle Reservierungen für einen Zeitraum ab
|
||||||
|
- "Neue Reservierungen (Polling)" - Erkennt neue Reservierungen
|
||||||
|
- "Geänderte Reservierungen (Polling)" - Erkennt Änderungen
|
||||||
|
- 🐛 **Custom Attributes bei GetAll**: Option fehlt
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
- ⭐ **Include Custom Attributes Option**: Neues "Custom Attributes Einschließen" Checkbox bei:
|
||||||
|
- Reservierungen → Alle Abrufen
|
||||||
|
- Ressourcen → Alle Abrufen
|
||||||
|
- Benutzer → Alle Abrufen
|
||||||
|
- 📋 **TEST-RESULTS.md**: Detaillierte Test-Dokumentation mit echten API-Tests
|
||||||
|
- 📋 **test-api.ts**: Verbessertes Test-Skript für alle API-Endpunkte
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- **Trigger Node**: Komplett überarbeitete UI mit klarerer Trennung der Modi
|
||||||
|
- **Trigger Zeitraum**: Optionale Start-/Enddatum-Felder für "Alle Abrufen" Mode
|
||||||
|
- **Reservierung erstellen/aktualisieren**: allowParticipation wird immer gesetzt (API-Pflichtfeld)
|
||||||
|
|
||||||
|
### Getestet
|
||||||
|
- ✅ 19 API-Tests erfolgreich bestanden
|
||||||
|
- ✅ Alle Trigger-Modi getestet
|
||||||
|
- ✅ Custom Attributes Integration getestet
|
||||||
|
- **Test-URL**: https://librebooking.zell-cloud.de
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.2.0] - 2026-01-25
|
## [1.2.0] - 2026-01-25
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,62 @@ Gleiche Vorgehensweise wie beim Erstellen.
|
||||||
- **Nur Admin**: Nur für Admins sichtbar?
|
- **Nur Admin**: Nur für Admins sichtbar?
|
||||||
- **Mögliche Werte**: Für Auswahllisten (komma-getrennt)
|
- **Mögliche Werte**: Für Auswahllisten (komma-getrennt)
|
||||||
|
|
||||||
|
## Elegante Lösung: Attribute automatisch abrufen (NEU in v1.2.1)
|
||||||
|
|
||||||
|
### Das Problem
|
||||||
|
Bisher musste man Attribut-IDs manuell eingeben, was umständlich war.
|
||||||
|
|
||||||
|
### Die Lösung: "Custom Attributes Einschließen"
|
||||||
|
Bei den GetAll-Operationen gibt es jetzt eine neue Option, die automatisch die Custom Attribute Values für jeden Eintrag abruft.
|
||||||
|
|
||||||
|
### Verwendung für Reservierungen
|
||||||
|
|
||||||
|
1. Wählen Sie **Ressource**: `Reservierung`
|
||||||
|
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||||
|
3. Unter **Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||||
|
|
||||||
|
**Ergebnis:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reservations": [
|
||||||
|
{
|
||||||
|
"referenceNumber": "abc123",
|
||||||
|
"title": "Meeting",
|
||||||
|
"startDate": "2026-02-07T10:00:00",
|
||||||
|
"customAttributes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"label": "Mietername",
|
||||||
|
"value": "Max Mustermann"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"label": "Adresse",
|
||||||
|
"value": "Hauptstraße 1, 12345 Stadt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verwendung für Ressourcen
|
||||||
|
|
||||||
|
1. Wählen Sie **Ressource**: `Ressource`
|
||||||
|
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||||
|
3. Unter **Ressourcen-Abruf-Optionen** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||||
|
|
||||||
|
### Verwendung für Benutzer
|
||||||
|
|
||||||
|
1. Wählen Sie **Ressource**: `Benutzer`
|
||||||
|
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||||
|
3. Unter **Benutzer-Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||||
|
|
||||||
|
### Wichtiger Hinweis
|
||||||
|
Diese Option führt für jeden Eintrag einen zusätzlichen API-Call durch. Bei vielen Einträgen kann dies länger dauern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tipps
|
## Tipps
|
||||||
|
|
||||||
### Attribut-IDs herausfinden
|
### Attribut-IDs herausfinden
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,143 @@
|
||||||
|
# LibreBooking n8n Node - Test Results
|
||||||
|
|
||||||
|
## Test Datum: 25.01.2026
|
||||||
|
|
||||||
|
### Test-Umgebung
|
||||||
|
- **URL**: https://librebooking.zell-cloud.de
|
||||||
|
- **Benutzer**: sebastian.zell@zell-aufmass.de
|
||||||
|
- **n8n Node Version**: 1.2.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test-Ergebnisse
|
||||||
|
|
||||||
|
### 1. Authentifizierung ✅
|
||||||
|
- Login erfolgreich
|
||||||
|
- Session Token wird korrekt generiert
|
||||||
|
- User ID wird zurückgegeben
|
||||||
|
|
||||||
|
### 2. Reservierungen ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 12 Reservierungen gefunden |
|
||||||
|
| Get All (mit Datumsfilter) | ✅ | Filtert korrekt nach Zeitraum |
|
||||||
|
| Get (Einzeln) | ✅ | Custom Attributes werden zurückgegeben |
|
||||||
|
| Create | ✅ | allowParticipation wird korrekt gesetzt |
|
||||||
|
| Update | ✅ | Änderungen werden übernommen |
|
||||||
|
| Delete | ✅ | Reservierung wird gelöscht |
|
||||||
|
|
||||||
|
**Custom Attributes für Reservierungen (9 gefunden):**
|
||||||
|
- Mietername (ID: 1, Typ: Text, Pflicht: ✅)
|
||||||
|
- Telefon (ID: 2, Typ: Text, Pflicht: ❌)
|
||||||
|
- Adresse (ID: 3, Typ: Text, Pflicht: ✅)
|
||||||
|
- Lage der Wohnung – Gebäudeart (ID: 11, Typ: Auswahl)
|
||||||
|
- Geschoss (ID: 9, Typ: Auswahl)
|
||||||
|
- Lage der Wohnung – Lage im Grundriss (ID: 10, Typ: Auswahl)
|
||||||
|
- Quadratmeter (ID: 12, Typ: Text)
|
||||||
|
- Clustername (ID: 4, Typ: Text, Pflicht: ✅)
|
||||||
|
- Status (ID: 8, Typ: Auswahl, Pflicht: ✅)
|
||||||
|
|
||||||
|
### 3. Ressourcen ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 4 Ressourcen gefunden |
|
||||||
|
| Get (Einzeln) | ✅ | Details werden abgerufen |
|
||||||
|
|
||||||
|
**Ressourcen:**
|
||||||
|
- Aufmass Team 1 (ID: 1)
|
||||||
|
- Aufmass Team 2 (ID: 2)
|
||||||
|
- Aufmass Team 3 (ID: 3)
|
||||||
|
- Aufmass Team 4 (ID: 4)
|
||||||
|
|
||||||
|
### 4. Benutzer ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 3 Benutzer gefunden |
|
||||||
|
| Get (Einzeln) | ✅ | Details werden abgerufen |
|
||||||
|
|
||||||
|
### 5. Zeitpläne ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 1 Zeitplan gefunden |
|
||||||
|
|
||||||
|
### 6. Attribute (nach Kategorie) ✅
|
||||||
|
|
||||||
|
| Kategorie | Anzahl |
|
||||||
|
|-----------|--------|
|
||||||
|
| Reservierung (1) | 9 |
|
||||||
|
| Benutzer (2) | 0 |
|
||||||
|
| Ressource (4) | 0 |
|
||||||
|
| Ressourcen-Typ (5) | 0 |
|
||||||
|
|
||||||
|
### 7. Gruppen ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 2 Gruppen gefunden |
|
||||||
|
|
||||||
|
### 8. Zubehör ✅
|
||||||
|
|
||||||
|
| Operation | Status | Details |
|
||||||
|
|-----------|--------|---------|
|
||||||
|
| Get All | ✅ | 0 Zubehörteile (keine konfiguriert) |
|
||||||
|
|
||||||
|
### 9. Sign Out ✅
|
||||||
|
- Session wird korrekt beendet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trigger Node Tests
|
||||||
|
|
||||||
|
### "Alle Abrufen" (Get All) Mode ✅
|
||||||
|
- Ruft alle Reservierungen für den angegebenen Zeitraum ab
|
||||||
|
- Optionale Start-/Enddatum-Filter funktionieren
|
||||||
|
- "Detaillierte Daten Abrufen" Option lädt Custom Attributes
|
||||||
|
|
||||||
|
### "Neue Reservierungen" (Poll) Mode ✅
|
||||||
|
- Erster Poll: Speichert IDs, triggert nicht
|
||||||
|
- Folgende Polls: Erkennt neue Reservierungen
|
||||||
|
- Debug-Modus zeigt gespeicherte IDs an
|
||||||
|
|
||||||
|
### "Geänderte Reservierungen" (Poll) Mode ✅
|
||||||
|
- Erster Poll: Speichert Hashes, triggert nicht
|
||||||
|
- Folgende Polls: Erkennt Änderungen durch Hash-Vergleich
|
||||||
|
- Änderungen an Titel, Beschreibung, Zeitraum werden erkannt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Behobene Probleme
|
||||||
|
|
||||||
|
### 1. allowParticipation Fehler ✅
|
||||||
|
**Problem**: API-Fehler "Undefined property: stdClass::$allowParticipation"
|
||||||
|
|
||||||
|
**Lösung**: `allowParticipation` wird jetzt immer im Request-Body gesendet (ist ein Pflichtfeld).
|
||||||
|
|
||||||
|
### 2. Trigger "Alle Abrufen" funktioniert nicht ✅
|
||||||
|
**Problem**: Mode war unklar, nutzte Polling-Logik
|
||||||
|
|
||||||
|
**Lösung**: Neuer "Alle Abrufen (Einmalig)" Mode mit optionalen Datum-Parametern.
|
||||||
|
|
||||||
|
### 3. Custom Attributes nicht elegant abrufbar ✅
|
||||||
|
**Problem**: Manuelles Eingeben von Attribut-IDs nötig
|
||||||
|
|
||||||
|
**Lösung**: "Custom Attributes Einschließen" Option bei GetAll-Operationen für:
|
||||||
|
- Reservierungen
|
||||||
|
- Ressourcen
|
||||||
|
- Benutzer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test-Zusammenfassung
|
||||||
|
|
||||||
|
| Kategorie | Tests | Bestanden | Fehlgeschlagen |
|
||||||
|
|-----------|-------|-----------|----------------|
|
||||||
|
| API-Endpunkte | 19 | 19 | 0 |
|
||||||
|
| Trigger Modes | 3 | 3 | 0 |
|
||||||
|
| Custom Attributes | 4 | 4 | 0 |
|
||||||
|
| **Gesamt** | **26** | **26** | **0** |
|
||||||
|
|
||||||
|
✅ **Alle Tests erfolgreich bestanden!**
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
||||||
|
/**
|
||||||
|
* LibreBooking API Credentials
|
||||||
|
*
|
||||||
|
* LibreBooking verwendet Session-basierte Authentifizierung.
|
||||||
|
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
|
||||||
|
*/
|
||||||
|
export declare class LibreBookingApi implements ICredentialType {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
properties: INodeProperties[];
|
||||||
|
test: ICredentialTestRequest;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=LibreBookingApi.credentials.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBookingApi.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,sBAAsB,EACtB,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACtD,IAAI,SAAqB;IACzB,WAAW,SAAsB;IACjC,gBAAgB,SAAuC;IAEvD,UAAU,EAAE,eAAe,EAAE,CA6B3B;IAGF,IAAI,EAAE,sBAAsB,CAa1B;CACF"}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.LibreBookingApi = void 0;
|
||||||
|
/**
|
||||||
|
* LibreBooking API Credentials
|
||||||
|
*
|
||||||
|
* LibreBooking verwendet Session-basierte Authentifizierung.
|
||||||
|
* Der Node holt bei jeder Ausführung einen neuen Session-Token.
|
||||||
|
*/
|
||||||
|
class LibreBookingApi {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'libreBookingApi';
|
||||||
|
this.displayName = 'LibreBooking API';
|
||||||
|
this.documentationUrl = 'https://librebooking.org/docs/api';
|
||||||
|
this.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'LibreBooking URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://booking.example.com',
|
||||||
|
required: true,
|
||||||
|
description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Benutzername',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Passwort',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Ihr LibreBooking-Passwort',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Test-Request um die Credentials zu validieren
|
||||||
|
this.test = {
|
||||||
|
request: {
|
||||||
|
baseURL: '={{$credentials.url}}',
|
||||||
|
url: '/Web/Services/index.php/Authentication/Authenticate',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
username: '={{$credentials.username}}',
|
||||||
|
password: '={{$credentials.password}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.LibreBookingApi = LibreBookingApi;
|
||||||
|
//# sourceMappingURL=LibreBookingApi.credentials.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBookingApi.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingApi.credentials.ts"],"names":[],"mappings":";;;AAOA;;;;;GAKG;AACH,MAAa,eAAe;IAA5B;QACC,SAAI,GAAG,iBAAiB,CAAC;QACzB,gBAAW,GAAG,kBAAkB,CAAC;QACjC,qBAAgB,GAAG,mCAAmC,CAAC;QAEvD,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,6BAA6B;gBAC1C,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,oEAAoE;aACjF;YACD;gBACC,WAAW,EAAE,cAAc;gBAC3B,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,mDAAmD;aAChE;YACD;gBACC,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE;oBACZ,QAAQ,EAAE,IAAI;iBACd;gBACD,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,2BAA2B;aACxC;SACD,CAAC;QAEF,gDAAgD;QAChD,SAAI,GAA2B;YAC9B,OAAO,EAAE;gBACR,OAAO,EAAE,uBAAuB;gBAChC,GAAG,EAAE,qDAAqD;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACR,cAAc,EAAE,kBAAkB;iBAClC;gBACD,IAAI,EAAE;oBACL,QAAQ,EAAE,4BAA4B;oBACtC,QAAQ,EAAE,4BAA4B;iBACtC;aACD;SACD,CAAC;IACH,CAAC;CAAA;AAnDD,0CAmDC"}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { ICredentialType, INodeProperties } from 'n8n-workflow';
|
||||||
|
/**
|
||||||
|
* LibreBooking Config Credential
|
||||||
|
*
|
||||||
|
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
||||||
|
* die in allen LibreBooking Nodes verwendet werden können.
|
||||||
|
*/
|
||||||
|
export declare class LibreBookingConfig implements ICredentialType {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
properties: INodeProperties[];
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=LibreBookingConfig.credentials.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBookingConfig.credentials.d.ts","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACzD,IAAI,SAAwB;IAC5B,WAAW,SAAyB;IACpC,gBAAgB,SAA8B;IAE9C,UAAU,EAAE,eAAe,EAAE,CAyD3B;CACF"}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.LibreBookingConfig = void 0;
|
||||||
|
/**
|
||||||
|
* LibreBooking Config Credential
|
||||||
|
*
|
||||||
|
* Ermöglicht die zentrale Konfiguration von Standardwerten,
|
||||||
|
* die in allen LibreBooking Nodes verwendet werden können.
|
||||||
|
*/
|
||||||
|
class LibreBookingConfig {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'libreBookingConfig';
|
||||||
|
this.displayName = 'LibreBooking Config';
|
||||||
|
this.documentationUrl = 'https://librebooking.org';
|
||||||
|
this.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Hinweis',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
description: 'Dieser Config-Node speichert Standardwerte für LibreBooking Operationen. Er ist optional und die Werte können in den einzelnen Nodes überschrieben werden.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Nutzungsbedingungen Akzeptiert',
|
||||||
|
name: 'defaultTermsAccepted',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Standardwert für die Akzeptanz der Nutzungsbedingungen bei Reservierungen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Teilnahme Erlauben',
|
||||||
|
name: 'defaultAllowParticipation',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Standardwert für die Teilnahme-Erlaubnis bei Reservierungen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Ressourcen-ID',
|
||||||
|
name: 'defaultResourceId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Standard-Ressourcen-ID für Reservierungen (0 = keine Standardressource)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Benutzer-ID',
|
||||||
|
name: 'defaultUserId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Standard-Benutzer-ID für Reservierungen (0 = angemeldeter Benutzer)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Zeitplan-ID',
|
||||||
|
name: 'defaultScheduleId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Standard-Zeitplan-ID für Ressourcen-Erstellung (0 = keine Standard-Zeitplan)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Zeitzone',
|
||||||
|
name: 'defaultTimezone',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Europe/Berlin',
|
||||||
|
description: 'Standard-Zeitzone für neue Benutzer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Standard Sprache',
|
||||||
|
name: 'defaultLanguage',
|
||||||
|
type: 'string',
|
||||||
|
default: 'de_de',
|
||||||
|
description: 'Standard-Sprache für neue Benutzer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.LibreBookingConfig = LibreBookingConfig;
|
||||||
|
//# sourceMappingURL=LibreBookingConfig.credentials.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBookingConfig.credentials.js","sourceRoot":"","sources":["../../credentials/LibreBookingConfig.credentials.ts"],"names":[],"mappings":";;;AAKA;;;;;GAKG;AACH,MAAa,kBAAkB;IAA/B;QACC,SAAI,GAAG,oBAAoB,CAAC;QAC5B,gBAAW,GAAG,qBAAqB,CAAC;QACpC,qBAAgB,GAAG,0BAA0B,CAAC;QAE9C,eAAU,GAAsB;YAC/B;gBACC,WAAW,EAAE,SAAS;gBACtB,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,4JAA4J;aACzK;YACD;gBACC,WAAW,EAAE,yCAAyC;gBACtD,IAAI,EAAE,sBAAsB;gBAC5B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,2EAA2E;aACxF;YACD;gBACC,WAAW,EAAE,6BAA6B;gBAC1C,IAAI,EAAE,2BAA2B;gBACjC,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,KAAK;gBACd,WAAW,EAAE,6DAA6D;aAC1E;YACD;gBACC,WAAW,EAAE,wBAAwB;gBACrC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,yEAAyE;aACtF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,eAAe;gBACrB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,qEAAqE;aAClF;YACD;gBACC,WAAW,EAAE,sBAAsB;gBACnC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,8EAA8E;aAC3F;YACD;gBACC,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,eAAe;gBACxB,WAAW,EAAE,qCAAqC;aAClD;YACD;gBACC,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,OAAO;gBAChB,WAAW,EAAE,oCAAoC;aACjD;SACD,CAAC;IACH,CAAC;CAAA;AA/DD,gDA+DC"}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
/**
|
||||||
|
* LibreBooking n8n Node
|
||||||
|
*
|
||||||
|
* Vollständige Integration für die LibreBooking API.
|
||||||
|
* Unterstützt alle wichtigen Ressourcen und Operationen.
|
||||||
|
*/
|
||||||
|
export declare class LibreBooking implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=LibreBooking.node.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBooking.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBooking/LibreBooking.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACC,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAI3B,MAAM,cAAc,CAAC;AA8LtB;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,SAAS;IACtC,WAAW,EAAE,oBAAoB,CAw2B/B;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAmjB9E"}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
|
||||||
|
|
||||||
|
<!-- Calendar base -->
|
||||||
|
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Calendar header -->
|
||||||
|
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
|
||||||
|
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
|
||||||
|
|
||||||
|
<!-- Calendar rings -->
|
||||||
|
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
||||||
|
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
||||||
|
|
||||||
|
<!-- Grid lines -->
|
||||||
|
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Booking indicator -->
|
||||||
|
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
|
||||||
|
|
||||||
|
<!-- Check mark -->
|
||||||
|
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { INodeType, INodeTypeDescription, IPollFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||||
|
/**
|
||||||
|
* LibreBooking Trigger Node
|
||||||
|
*
|
||||||
|
* Drei Modi:
|
||||||
|
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
||||||
|
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
||||||
|
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
||||||
|
*/
|
||||||
|
export declare class LibreBookingTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=LibreBookingTrigger.node.d.ts.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"LibreBookingTrigger.node.d.ts","sourceRoot":"","sources":["../../../nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAGlB,MAAM,cAAc,CAAC;AAyOtB;;;;;;;GAOG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IACpD,WAAW,EAAE,oBAAoB,CA4K/B;IAEI,IAAI,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,GAAG,IAAI,CAAC;CAkUxE"}
|
||||||
|
|
@ -0,0 +1,593 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.LibreBookingTrigger = void 0;
|
||||||
|
const n8n_workflow_1 = require("n8n-workflow");
|
||||||
|
/**
|
||||||
|
* Authentifizierung bei LibreBooking
|
||||||
|
*/
|
||||||
|
async function authenticateTrigger(pollFunctions, baseUrl, username, password) {
|
||||||
|
try {
|
||||||
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: { username, password },
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
if (!response.isAuthenticated) {
|
||||||
|
throw new n8n_workflow_1.NodeOperationError(pollFunctions.getNode(), 'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sessionToken: response.sessionToken,
|
||||||
|
userId: response.userId,
|
||||||
|
sessionExpires: response.sessionExpires,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
|
||||||
|
message: 'Authentifizierung fehlgeschlagen',
|
||||||
|
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Abmeldung
|
||||||
|
*/
|
||||||
|
async function signOutTrigger(pollFunctions, baseUrl, session) {
|
||||||
|
try {
|
||||||
|
await pollFunctions.helpers.httpRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: {
|
||||||
|
userId: session.userId,
|
||||||
|
sessionToken: session.sessionToken,
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Ignoriere SignOut-Fehler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reservierungen abrufen
|
||||||
|
*/
|
||||||
|
async function getReservations(pollFunctions, baseUrl, session, startDateTime, endDateTime, filters) {
|
||||||
|
const qs = {
|
||||||
|
startDateTime,
|
||||||
|
endDateTime,
|
||||||
|
};
|
||||||
|
if (filters.resourceId)
|
||||||
|
qs.resourceId = filters.resourceId;
|
||||||
|
if (filters.scheduleId)
|
||||||
|
qs.scheduleId = filters.scheduleId;
|
||||||
|
if (filters.userId)
|
||||||
|
qs.userId = filters.userId;
|
||||||
|
try {
|
||||||
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Booked-SessionToken': session.sessionToken,
|
||||||
|
'X-Booked-UserId': session.userId.toString(),
|
||||||
|
},
|
||||||
|
qs,
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
return response.reservations || [];
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new n8n_workflow_1.NodeApiError(pollFunctions.getNode(), error, {
|
||||||
|
message: 'Fehler beim Abrufen der Reservierungen',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
||||||
|
*/
|
||||||
|
async function getReservationDetails(pollFunctions, baseUrl, session, referenceNumber) {
|
||||||
|
try {
|
||||||
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Booked-SessionToken': session.sessionToken,
|
||||||
|
'X-Booked-UserId': session.userId.toString(),
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Zeitfenster berechnen für "Get All" Mode
|
||||||
|
*/
|
||||||
|
function getTimeWindowForGetAll(customStartDate, customEndDate, defaultDays = 14) {
|
||||||
|
if (customStartDate && customEndDate) {
|
||||||
|
return {
|
||||||
|
start: new Date(customStartDate).toISOString(),
|
||||||
|
end: new Date(customEndDate).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const endDate = new Date(now);
|
||||||
|
endDate.setDate(endDate.getDate() + defaultDays);
|
||||||
|
return {
|
||||||
|
start: now.toISOString(),
|
||||||
|
end: endDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Zeitfenster berechnen für Polling
|
||||||
|
*/
|
||||||
|
function getTimeWindowForPolling(timeWindow) {
|
||||||
|
const now = new Date();
|
||||||
|
const start = now.toISOString();
|
||||||
|
let endDate = new Date(now);
|
||||||
|
switch (timeWindow) {
|
||||||
|
case '7days':
|
||||||
|
endDate.setDate(endDate.getDate() + 7);
|
||||||
|
break;
|
||||||
|
case '14days':
|
||||||
|
endDate.setDate(endDate.getDate() + 14);
|
||||||
|
break;
|
||||||
|
case '30days':
|
||||||
|
endDate.setDate(endDate.getDate() + 30);
|
||||||
|
break;
|
||||||
|
case '90days':
|
||||||
|
endDate.setDate(endDate.getDate() + 90);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
endDate.setDate(endDate.getDate() + 14);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end: endDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Hash für Reservierung generieren (für Änderungserkennung)
|
||||||
|
*/
|
||||||
|
function getReservationHash(reservation) {
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* Drei Modi:
|
||||||
|
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
||||||
|
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
||||||
|
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
||||||
|
*/
|
||||||
|
class LibreBookingTrigger {
|
||||||
|
constructor() {
|
||||||
|
this.description = {
|
||||||
|
displayName: 'LibreBooking Trigger',
|
||||||
|
name: 'libreBookingTrigger',
|
||||||
|
icon: 'file:librebooking.svg',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
||||||
|
subtitle: '={{$parameter["triggerMode"]}}',
|
||||||
|
defaults: {
|
||||||
|
name: 'LibreBooking Trigger',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'libreBookingApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
polling: true,
|
||||||
|
properties: [
|
||||||
|
// =====================================================
|
||||||
|
// TRIGGER MODE SELECTOR
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Trigger-Modus',
|
||||||
|
name: 'triggerMode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Alle Abrufen (Einmalig)',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Neue Reservierungen (Polling)',
|
||||||
|
value: 'newReservations',
|
||||||
|
description: 'Nur bei neuen Reservierungen triggern',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Geänderte Reservierungen (Polling)',
|
||||||
|
value: 'updatedReservations',
|
||||||
|
description: 'Nur bei geänderten Reservierungen triggern',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
description: 'Wählen Sie den Trigger-Modus',
|
||||||
|
},
|
||||||
|
// =====================================================
|
||||||
|
// GET ALL MODE - DATE RANGE
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Startdatum',
|
||||||
|
name: 'startDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['getAll'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Startdatum für den Abruf (leer = heute)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Enddatum',
|
||||||
|
name: 'endDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['getAll'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
|
||||||
|
},
|
||||||
|
// =====================================================
|
||||||
|
// POLLING MODE - TIME WINDOW
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Zeitfenster',
|
||||||
|
name: 'timeWindow',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['newReservations', 'updatedReservations'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ name: 'Nächste 7 Tage', value: '7days' },
|
||||||
|
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||||
|
{ name: 'Nächste 30 Tage', value: '30days' },
|
||||||
|
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||||
|
],
|
||||||
|
default: '14days',
|
||||||
|
description: 'Zeitfenster für die Überwachung von Reservierungen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Hinweis',
|
||||||
|
name: 'pollingNotice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['newReservations', 'updatedReservations'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
|
||||||
|
},
|
||||||
|
// =====================================================
|
||||||
|
// FILTERS (ALL MODES)
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Filter',
|
||||||
|
name: 'filters',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Filter hinzufügen',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Ressourcen-ID',
|
||||||
|
name: 'resourceId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diese Ressource',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Zeitplan-ID',
|
||||||
|
name: 'scheduleId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diesen Zeitplan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Benutzer-ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diesen Benutzer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// =====================================================
|
||||||
|
// OPTIONS (ALL MODES)
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Optionen',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Option hinzufügen',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Detaillierte Daten Abrufen',
|
||||||
|
name: 'fetchDetails',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Debug-Modus',
|
||||||
|
name: 'debugMode',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Gibt zusätzliche Debug-Informationen aus',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async poll() {
|
||||||
|
const credentials = await this.getCredentials('libreBookingApi');
|
||||||
|
const baseUrl = credentials.url.replace(/\/$/, '');
|
||||||
|
const username = credentials.username;
|
||||||
|
const password = credentials.password;
|
||||||
|
const triggerMode = this.getNodeParameter('triggerMode');
|
||||||
|
const filters = this.getNodeParameter('filters', {});
|
||||||
|
const options = this.getNodeParameter('options', {});
|
||||||
|
// Debug-Modus
|
||||||
|
const debugMode = options.debugMode || false;
|
||||||
|
const fetchDetails = options.fetchDetails || false;
|
||||||
|
// Workflow Static Data für State-Management
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
let session;
|
||||||
|
try {
|
||||||
|
session = await authenticateTrigger(this, baseUrl, username, password);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const returnData = [];
|
||||||
|
// ==========================================
|
||||||
|
// MODE: Get All (One-Time / Every Poll)
|
||||||
|
// ==========================================
|
||||||
|
if (triggerMode === 'getAll') {
|
||||||
|
const startDate = this.getNodeParameter('startDate', '');
|
||||||
|
const endDate = this.getNodeParameter('endDate', '');
|
||||||
|
const { start, end } = getTimeWindowForGetAll(startDate || undefined, endDate || undefined, 14);
|
||||||
|
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`);
|
||||||
|
console.log(`[LibreBooking Trigger] Date Range: ${start} to ${end}`);
|
||||||
|
}
|
||||||
|
if (reservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
return [[{
|
||||||
|
json: {
|
||||||
|
_debug: true,
|
||||||
|
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
||||||
|
_startDate: start,
|
||||||
|
_endDate: end,
|
||||||
|
_count: 0,
|
||||||
|
},
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Alle Reservierungen zurückgeben
|
||||||
|
for (const reservation of reservations) {
|
||||||
|
let reservationData = reservation;
|
||||||
|
if (fetchDetails) {
|
||||||
|
try {
|
||||||
|
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Fallback auf Basisdaten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returnData.push({
|
||||||
|
json: {
|
||||||
|
...reservationData,
|
||||||
|
_eventType: 'getAll',
|
||||||
|
_triggeredAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==========================================
|
||||||
|
// MODE: New Reservations (Polling)
|
||||||
|
// ==========================================
|
||||||
|
else if (triggerMode === 'newReservations') {
|
||||||
|
const timeWindow = this.getNodeParameter('timeWindow', '14days');
|
||||||
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||||
|
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||||
|
// Initialisiere seenIds beim ersten Poll
|
||||||
|
if (!webhookData.seenIds) {
|
||||||
|
webhookData.seenIds = [];
|
||||||
|
webhookData.isFirstPoll = true;
|
||||||
|
}
|
||||||
|
const currentIds = reservations.map((r) => r.referenceNumber);
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] New Reservations Mode`);
|
||||||
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
||||||
|
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`);
|
||||||
|
}
|
||||||
|
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
||||||
|
if (webhookData.isFirstPoll) {
|
||||||
|
webhookData.seenIds = currentIds;
|
||||||
|
webhookData.isFirstPoll = false;
|
||||||
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
if (debugMode) {
|
||||||
|
return [[{
|
||||||
|
json: {
|
||||||
|
_debug: true,
|
||||||
|
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
||||||
|
_savedIds: currentIds.length,
|
||||||
|
_ids: currentIds,
|
||||||
|
_timestamp: webhookData.lastPollTime,
|
||||||
|
},
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
return null; // Nichts triggern beim ersten Poll
|
||||||
|
}
|
||||||
|
// Nur NEUE Reservierungen (die wir noch nicht gesehen haben)
|
||||||
|
const newReservations = reservations.filter((r) => !webhookData.seenIds.includes(r.referenceNumber));
|
||||||
|
// Update seenIds mit allen aktuellen IDs
|
||||||
|
webhookData.seenIds = currentIds;
|
||||||
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
if (newReservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] No new reservations found`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Neue Reservierungen verarbeiten
|
||||||
|
for (const reservation of newReservations) {
|
||||||
|
let reservationData = reservation;
|
||||||
|
if (fetchDetails) {
|
||||||
|
try {
|
||||||
|
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Fallback auf Basisdaten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returnData.push({
|
||||||
|
json: {
|
||||||
|
...reservationData,
|
||||||
|
_eventType: 'new',
|
||||||
|
_triggeredAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (debugMode && returnData.length > 0) {
|
||||||
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==========================================
|
||||||
|
// MODE: Updated Reservations (Polling)
|
||||||
|
// ==========================================
|
||||||
|
else if (triggerMode === 'updatedReservations') {
|
||||||
|
const timeWindow = this.getNodeParameter('timeWindow', '14days');
|
||||||
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||||
|
const reservations = await getReservations(this, baseUrl, session, start, end, filters);
|
||||||
|
// Initialisiere reservationHashes beim ersten Poll
|
||||||
|
if (!webhookData.reservationHashes) {
|
||||||
|
webhookData.reservationHashes = {};
|
||||||
|
webhookData.isFirstPoll = true;
|
||||||
|
}
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
||||||
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
||||||
|
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`);
|
||||||
|
}
|
||||||
|
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
|
||||||
|
if (webhookData.isFirstPoll) {
|
||||||
|
for (const reservation of reservations) {
|
||||||
|
webhookData.reservationHashes[reservation.referenceNumber] = getReservationHash(reservation);
|
||||||
|
}
|
||||||
|
webhookData.isFirstPoll = false;
|
||||||
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
if (debugMode) {
|
||||||
|
return [[{
|
||||||
|
json: {
|
||||||
|
_debug: true,
|
||||||
|
_message: 'Erster Poll - Hashes wurden gespeichert, keine Events getriggert',
|
||||||
|
_savedHashes: Object.keys(webhookData.reservationHashes).length,
|
||||||
|
_timestamp: webhookData.lastPollTime,
|
||||||
|
},
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
return null; // Nichts triggern beim ersten Poll
|
||||||
|
}
|
||||||
|
// Geänderte Reservierungen finden
|
||||||
|
const updatedReservations = [];
|
||||||
|
const newHashes = {};
|
||||||
|
for (const reservation of reservations) {
|
||||||
|
const currentHash = getReservationHash(reservation);
|
||||||
|
const oldHash = webhookData.reservationHashes[reservation.referenceNumber];
|
||||||
|
newHashes[reservation.referenceNumber] = currentHash;
|
||||||
|
// Nur als "geändert" markieren, wenn:
|
||||||
|
// 1. Wir die Reservierung schon kennen (nicht neu)
|
||||||
|
// 2. Der Hash sich geändert hat
|
||||||
|
if (oldHash && currentHash !== oldHash) {
|
||||||
|
updatedReservations.push(reservation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update Hashes mit allen aktuellen Reservierungen
|
||||||
|
webhookData.reservationHashes = newHashes;
|
||||||
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
if (updatedReservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Geänderte Reservierungen verarbeiten
|
||||||
|
for (const reservation of updatedReservations) {
|
||||||
|
let reservationData = reservation;
|
||||||
|
if (fetchDetails) {
|
||||||
|
try {
|
||||||
|
const details = await getReservationDetails(this, baseUrl, session, reservation.referenceNumber);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Fallback auf Basisdaten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returnData.push({
|
||||||
|
json: {
|
||||||
|
...reservationData,
|
||||||
|
_eventType: 'updated',
|
||||||
|
_triggeredAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (debugMode && returnData.length > 0) {
|
||||||
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (returnData.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [returnData];
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await signOutTrigger(this, baseUrl, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.LibreBookingTrigger = LibreBookingTrigger;
|
||||||
|
//# sourceMappingURL=LibreBookingTrigger.node.js.map
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="60" height="60" rx="8" fill="#2C3E50"/>
|
||||||
|
|
||||||
|
<!-- Calendar base -->
|
||||||
|
<rect x="10" y="15" width="40" height="35" rx="3" fill="#ECF0F1" stroke="#3498DB" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Calendar header -->
|
||||||
|
<rect x="10" y="15" width="40" height="10" rx="3" fill="#3498DB"/>
|
||||||
|
<rect x="10" y="22" width="40" height="3" fill="#3498DB"/>
|
||||||
|
|
||||||
|
<!-- Calendar rings -->
|
||||||
|
<rect x="18" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
||||||
|
<rect x="38" y="12" width="4" height="8" rx="1" fill="#2C3E50"/>
|
||||||
|
|
||||||
|
<!-- Grid lines -->
|
||||||
|
<line x1="10" y1="33" x2="50" y2="33" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="10" y1="41" x2="50" y2="41" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="23" y1="25" x2="23" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
<line x1="37" y1="25" x2="37" y2="50" stroke="#BDC3C7" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Booking indicator -->
|
||||||
|
<rect x="25" y="35" width="10" height="4" rx="1" fill="#27AE60"/>
|
||||||
|
|
||||||
|
<!-- Check mark -->
|
||||||
|
<path d="M39 27 L42 30 L48 22" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -580,6 +580,13 @@ export class LibreBooking implements INodeType {
|
||||||
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
||||||
{ displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' },
|
{ displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' },
|
||||||
{ displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' },
|
{ displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' },
|
||||||
|
{
|
||||||
|
displayName: 'Custom Attributes Einschließen',
|
||||||
|
name: 'includeCustomAttributes',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Für jede Reservierung die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -681,6 +688,23 @@ export class LibreBooking implements INodeType {
|
||||||
{ displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 },
|
{ displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Ressourcen-Abruf-Optionen',
|
||||||
|
name: 'resourceGetAllOptions',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Option hinzufügen',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { resource: ['resource'], operation: ['getAll'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Custom Attributes Einschließen',
|
||||||
|
name: 'includeCustomAttributes',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Für jede Ressource die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// SCHEDULE PARAMETERS
|
// SCHEDULE PARAMETERS
|
||||||
|
|
@ -812,6 +836,13 @@ export class LibreBooking implements INodeType {
|
||||||
{ displayName: 'Vorname', name: 'firstName', type: 'string', default: '' },
|
{ displayName: 'Vorname', name: 'firstName', type: 'string', default: '' },
|
||||||
{ displayName: 'Nachname', name: 'lastName', type: 'string', default: '' },
|
{ displayName: 'Nachname', name: 'lastName', type: 'string', default: '' },
|
||||||
{ displayName: 'Organisation', name: 'organization', type: 'string', default: '' },
|
{ displayName: 'Organisation', name: 'organization', type: 'string', default: '' },
|
||||||
|
{
|
||||||
|
displayName: 'Custom Attributes Einschließen',
|
||||||
|
name: 'includeCustomAttributes',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Für jeden Benutzer die vollständigen Custom Attributes abrufen (zusätzliche API-Aufrufe)',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1077,7 +1108,31 @@ export class LibreBooking implements INodeType {
|
||||||
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
||||||
if (filters.startDateTime) qs.startDateTime = filters.startDateTime;
|
if (filters.startDateTime) qs.startDateTime = filters.startDateTime;
|
||||||
if (filters.endDateTime) qs.endDateTime = filters.endDateTime;
|
if (filters.endDateTime) qs.endDateTime = filters.endDateTime;
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs);
|
|
||||||
|
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs);
|
||||||
|
|
||||||
|
// If includeCustomAttributes is enabled, fetch details for each reservation
|
||||||
|
if (filters.includeCustomAttributes && response.reservations && response.reservations.length > 0) {
|
||||||
|
const enrichedReservations = [];
|
||||||
|
for (const reservation of response.reservations) {
|
||||||
|
try {
|
||||||
|
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${reservation.referenceNumber}`);
|
||||||
|
enrichedReservations.push({
|
||||||
|
...reservation,
|
||||||
|
customAttributes: details.customAttributes || [],
|
||||||
|
owner: details.owner,
|
||||||
|
participants: details.participants || [],
|
||||||
|
invitees: details.invitees || [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to original reservation data
|
||||||
|
enrichedReservations.push(reservation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = { ...response, reservations: enrichedReservations };
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = response;
|
||||||
} else if (operation === 'get') {
|
} else if (operation === 'get') {
|
||||||
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
|
const referenceNumber = this.getNodeParameter('referenceNumber', i) as string;
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`);
|
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`);
|
||||||
|
|
@ -1103,11 +1158,10 @@ export class LibreBooking implements INodeType {
|
||||||
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
|
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
|
||||||
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
|
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
|
||||||
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
|
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
|
||||||
if (additionalFields.allowParticipation !== undefined) {
|
// allowParticipation is REQUIRED by the API
|
||||||
body.allowParticipation = additionalFields.allowParticipation;
|
body.allowParticipation = additionalFields.allowParticipation !== undefined
|
||||||
} else {
|
? additionalFields.allowParticipation
|
||||||
body.allowParticipation = configDefaults.defaultAllowParticipation;
|
: configDefaults.defaultAllowParticipation;
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Attributes verarbeiten
|
// Custom Attributes verarbeiten
|
||||||
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
|
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
|
||||||
|
|
@ -1140,7 +1194,10 @@ export class LibreBooking implements INodeType {
|
||||||
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
|
if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources);
|
||||||
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
|
if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants);
|
||||||
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
|
if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees);
|
||||||
if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation;
|
// allowParticipation is REQUIRED by the API
|
||||||
|
body.allowParticipation = additionalFields.allowParticipation !== undefined
|
||||||
|
? additionalFields.allowParticipation
|
||||||
|
: false;
|
||||||
|
|
||||||
// Custom Attributes verarbeiten
|
// Custom Attributes verarbeiten
|
||||||
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
|
if (customAttributes?.attribute && customAttributes.attribute.length > 0) {
|
||||||
|
|
@ -1170,7 +1227,29 @@ export class LibreBooking implements INodeType {
|
||||||
// RESOURCE
|
// RESOURCE
|
||||||
else if (resource === 'resource') {
|
else if (resource === 'resource') {
|
||||||
if (operation === 'getAll') {
|
if (operation === 'getAll') {
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/');
|
const resourceGetAllOptions = this.getNodeParameter('resourceGetAllOptions', i, {}) as any;
|
||||||
|
|
||||||
|
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/');
|
||||||
|
|
||||||
|
// If includeCustomAttributes is enabled, fetch details for each resource
|
||||||
|
if (resourceGetAllOptions.includeCustomAttributes && response.resources && response.resources.length > 0) {
|
||||||
|
const enrichedResources = [];
|
||||||
|
for (const res of response.resources) {
|
||||||
|
try {
|
||||||
|
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${res.resourceId}`);
|
||||||
|
enrichedResources.push({
|
||||||
|
...res,
|
||||||
|
customAttributes: details.customAttributes || [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to original resource data
|
||||||
|
enrichedResources.push(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = { ...response, resources: enrichedResources };
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = response;
|
||||||
} else if (operation === 'get') {
|
} else if (operation === 'get') {
|
||||||
const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number;
|
const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number;
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`);
|
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`);
|
||||||
|
|
@ -1280,7 +1359,28 @@ export class LibreBooking implements INodeType {
|
||||||
if (userFilters.firstName) qs.firstName = userFilters.firstName;
|
if (userFilters.firstName) qs.firstName = userFilters.firstName;
|
||||||
if (userFilters.lastName) qs.lastName = userFilters.lastName;
|
if (userFilters.lastName) qs.lastName = userFilters.lastName;
|
||||||
if (userFilters.organization) qs.organization = userFilters.organization;
|
if (userFilters.organization) qs.organization = userFilters.organization;
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs);
|
|
||||||
|
let response = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs);
|
||||||
|
|
||||||
|
// If includeCustomAttributes is enabled, fetch details for each user
|
||||||
|
if (userFilters.includeCustomAttributes && response.users && response.users.length > 0) {
|
||||||
|
const enrichedUsers = [];
|
||||||
|
for (const user of response.users) {
|
||||||
|
try {
|
||||||
|
const details = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${user.id}`);
|
||||||
|
enrichedUsers.push({
|
||||||
|
...user,
|
||||||
|
customAttributes: details.customAttributes || [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to original user data
|
||||||
|
enrichedUsers.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = { ...response, users: enrichedUsers };
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = response;
|
||||||
} else if (operation === 'get') {
|
} else if (operation === 'get') {
|
||||||
const userId = this.getNodeParameter('userId', i) as number;
|
const userId = this.getNodeParameter('userId', i) as number;
|
||||||
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`);
|
responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ async function authenticateTrigger(
|
||||||
if (!response.isAuthenticated) {
|
if (!response.isAuthenticated) {
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
pollFunctions.getNode(),
|
pollFunctions.getNode(),
|
||||||
'Authentifizierung fehlgeschlagen',
|
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +65,7 @@ async function authenticateTrigger(
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||||
message: 'Authentifizierung fehlgeschlagen',
|
message: 'Authentifizierung fehlgeschlagen',
|
||||||
|
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +114,7 @@ async function getReservations(
|
||||||
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
||||||
if (filters.userId) qs.userId = filters.userId;
|
if (filters.userId) qs.userId = filters.userId;
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||||
|
|
@ -126,10 +128,15 @@ async function getReservations(
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.reservations || [];
|
return response.reservations || [];
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||||
|
message: 'Fehler beim Abrufen der Reservierungen',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detaillierte Reservierungsdaten abrufen
|
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
||||||
*/
|
*/
|
||||||
async function getReservationDetails(
|
async function getReservationDetails(
|
||||||
pollFunctions: IPollFunctions,
|
pollFunctions: IPollFunctions,
|
||||||
|
|
@ -137,6 +144,7 @@ async function getReservationDetails(
|
||||||
session: LibreBookingSession,
|
session: LibreBookingSession,
|
||||||
referenceNumber: string,
|
referenceNumber: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
try {
|
||||||
const response = await pollFunctions.helpers.httpRequest({
|
const response = await pollFunctions.helpers.httpRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||||
|
|
@ -149,12 +157,40 @@ async function getReservationDetails(
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeitfenster berechnen
|
* Zeitfenster berechnen für "Get All" Mode
|
||||||
*/
|
*/
|
||||||
function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
function getTimeWindowForGetAll(
|
||||||
|
customStartDate?: string,
|
||||||
|
customEndDate?: string,
|
||||||
|
defaultDays: number = 14
|
||||||
|
): { start: string; end: string } {
|
||||||
|
if (customStartDate && customEndDate) {
|
||||||
|
return {
|
||||||
|
start: new Date(customStartDate).toISOString(),
|
||||||
|
end: new Date(customEndDate).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const endDate = new Date(now);
|
||||||
|
endDate.setDate(endDate.getDate() + defaultDays);
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: now.toISOString(),
|
||||||
|
end: endDate.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeitfenster berechnen für Polling
|
||||||
|
*/
|
||||||
|
function getTimeWindowForPolling(timeWindow: string): { start: string; end: string } {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const start = now.toISOString();
|
const start = now.toISOString();
|
||||||
|
|
||||||
|
|
@ -184,7 +220,6 @@ function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash für Reservierung generieren (für Änderungserkennung)
|
* Hash für Reservierung generieren (für Änderungserkennung)
|
||||||
* Nur relevante Felder berücksichtigen, die Änderungen anzeigen
|
|
||||||
*/
|
*/
|
||||||
function getReservationHash(reservation: ReservationData): string {
|
function getReservationHash(reservation: ReservationData): string {
|
||||||
const relevantData = {
|
const relevantData = {
|
||||||
|
|
@ -206,11 +241,10 @@ function getReservationHash(reservation: ReservationData): string {
|
||||||
/**
|
/**
|
||||||
* LibreBooking Trigger Node
|
* LibreBooking Trigger Node
|
||||||
*
|
*
|
||||||
* Überwacht neue und geänderte Reservierungen in LibreBooking.
|
* Drei Modi:
|
||||||
*
|
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
||||||
* WICHTIG: Beim ersten Poll werden nur die IDs/Hashes gespeichert,
|
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
||||||
* aber keine Events getriggert. Dies verhindert, dass alle
|
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
||||||
* existierenden Reservierungen als "neu" getriggert werden.
|
|
||||||
*/
|
*/
|
||||||
export class LibreBookingTrigger implements INodeType {
|
export class LibreBookingTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
@ -220,7 +254,7 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
||||||
subtitle: '={{$parameter["event"]}}',
|
subtitle: '={{$parameter["triggerMode"]}}',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'LibreBooking Trigger',
|
name: 'LibreBooking Trigger',
|
||||||
},
|
},
|
||||||
|
|
@ -234,57 +268,74 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
],
|
],
|
||||||
polling: true,
|
polling: true,
|
||||||
properties: [
|
properties: [
|
||||||
|
// =====================================================
|
||||||
|
// TRIGGER MODE SELECTOR
|
||||||
|
// =====================================================
|
||||||
{
|
{
|
||||||
displayName: 'Event',
|
displayName: 'Trigger-Modus',
|
||||||
name: 'event',
|
name: 'triggerMode',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'Neue Reservierung',
|
name: 'Alle Abrufen (Einmalig)',
|
||||||
value: 'newReservation',
|
value: 'getAll',
|
||||||
description: 'Wird bei neuen Reservierungen ausgelöst (nicht beim ersten Poll)'
|
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Geänderte Reservierung',
|
name: 'Neue Reservierungen (Polling)',
|
||||||
value: 'updatedReservation',
|
value: 'newReservations',
|
||||||
description: 'Wird bei geänderten Reservierungen ausgelöst'
|
description: 'Nur bei neuen Reservierungen triggern',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Alle Reservierungen',
|
name: 'Geänderte Reservierungen (Polling)',
|
||||||
value: 'allReservations',
|
value: 'updatedReservations',
|
||||||
description: 'Wird bei neuen und geänderten Reservierungen ausgelöst'
|
description: 'Nur bei geänderten Reservierungen triggern',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'newReservation',
|
default: 'getAll',
|
||||||
|
description: 'Wählen Sie den Trigger-Modus',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// GET ALL MODE - DATE RANGE
|
||||||
|
// =====================================================
|
||||||
{
|
{
|
||||||
displayName: 'Hinweis',
|
displayName: 'Startdatum',
|
||||||
name: 'notice',
|
name: 'startDate',
|
||||||
type: 'notice',
|
type: 'dateTime',
|
||||||
default: '',
|
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
event: ['newReservation', 'allReservations'],
|
triggerMode: ['getAll'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende neue Reservierungen lösen den Trigger aus.',
|
default: '',
|
||||||
|
description: 'Startdatum für den Abruf (leer = heute)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Filter',
|
displayName: 'Enddatum',
|
||||||
name: 'filters',
|
name: 'endDate',
|
||||||
type: 'collection',
|
type: 'dateTime',
|
||||||
placeholder: 'Filter hinzufügen',
|
displayOptions: {
|
||||||
default: {},
|
show: {
|
||||||
options: [
|
triggerMode: ['getAll'],
|
||||||
{ displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' },
|
|
||||||
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
|
||||||
{ displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Enddatum für den Abruf (leer = 14 Tage in der Zukunft)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// POLLING MODE - TIME WINDOW
|
||||||
|
// =====================================================
|
||||||
{
|
{
|
||||||
displayName: 'Zeitfenster',
|
displayName: 'Zeitfenster',
|
||||||
name: 'timeWindow',
|
name: 'timeWindow',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['newReservations', 'updatedReservations'],
|
||||||
|
},
|
||||||
|
},
|
||||||
options: [
|
options: [
|
||||||
{ name: 'Nächste 7 Tage', value: '7days' },
|
{ name: 'Nächste 7 Tage', value: '7days' },
|
||||||
{ name: 'Nächste 14 Tage', value: '14days' },
|
{ name: 'Nächste 14 Tage', value: '14days' },
|
||||||
|
|
@ -292,7 +343,58 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
{ name: 'Nächste 90 Tage', value: '90days' },
|
{ name: 'Nächste 90 Tage', value: '90days' },
|
||||||
],
|
],
|
||||||
default: '14days',
|
default: '14days',
|
||||||
|
description: 'Zeitfenster für die Überwachung von Reservierungen',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Hinweis',
|
||||||
|
name: 'pollingNotice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
triggerMode: ['newReservations', 'updatedReservations'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'Beim ersten Poll werden existierende Reservierungen gespeichert, aber nicht getriggert. Nur nachfolgende Änderungen lösen den Trigger aus.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// FILTERS (ALL MODES)
|
||||||
|
// =====================================================
|
||||||
|
{
|
||||||
|
displayName: 'Filter',
|
||||||
|
name: 'filters',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Filter hinzufügen',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Ressourcen-ID',
|
||||||
|
name: 'resourceId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diese Ressource',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Zeitplan-ID',
|
||||||
|
name: 'scheduleId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diesen Zeitplan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Benutzer-ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'number',
|
||||||
|
default: '',
|
||||||
|
description: 'Nur Reservierungen für diesen Benutzer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// OPTIONS (ALL MODES)
|
||||||
|
// =====================================================
|
||||||
{
|
{
|
||||||
displayName: 'Optionen',
|
displayName: 'Optionen',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
|
@ -305,7 +407,7 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
name: 'fetchDetails',
|
name: 'fetchDetails',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Ruft vollständige Reservierungsdaten ab (zusätzliche API-Aufrufe)',
|
description: 'Ruft vollständige Reservierungsdaten inkl. Custom Attributes ab (zusätzliche API-Aufrufe)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Debug-Modus',
|
displayName: 'Debug-Modus',
|
||||||
|
|
@ -325,16 +427,16 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
const username = credentials.username as string;
|
const username = credentials.username as string;
|
||||||
const password = credentials.password as string;
|
const password = credentials.password as string;
|
||||||
|
|
||||||
const event = this.getNodeParameter('event') as string;
|
const triggerMode = this.getNodeParameter('triggerMode') as string;
|
||||||
const filters = this.getNodeParameter('filters', {}) as any;
|
const filters = this.getNodeParameter('filters', {}) as any;
|
||||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
|
||||||
const options = this.getNodeParameter('options', {}) as any;
|
const options = this.getNodeParameter('options', {}) as any;
|
||||||
|
|
||||||
// Workflow Static Data für State-Management
|
|
||||||
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
|
||||||
|
|
||||||
// Debug-Modus
|
// Debug-Modus
|
||||||
const debugMode = options.debugMode || false;
|
const debugMode = options.debugMode || false;
|
||||||
|
const fetchDetails = options.fetchDetails || false;
|
||||||
|
|
||||||
|
// Workflow Static Data für State-Management
|
||||||
|
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
||||||
|
|
||||||
let session: LibreBookingSession;
|
let session: LibreBookingSession;
|
||||||
try {
|
try {
|
||||||
|
|
@ -344,7 +446,20 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { start, end } = getTimeWindow(timeWindow);
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MODE: Get All (One-Time / Every Poll)
|
||||||
|
// ==========================================
|
||||||
|
if (triggerMode === 'getAll') {
|
||||||
|
const startDate = this.getNodeParameter('startDate', '') as string;
|
||||||
|
const endDate = this.getNodeParameter('endDate', '') as string;
|
||||||
|
|
||||||
|
const { start, end } = getTimeWindowForGetAll(
|
||||||
|
startDate || undefined,
|
||||||
|
endDate || undefined,
|
||||||
|
14
|
||||||
|
);
|
||||||
|
|
||||||
const reservations = await getReservations(
|
const reservations = await getReservations(
|
||||||
this,
|
this,
|
||||||
|
|
@ -355,12 +470,72 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
filters,
|
filters,
|
||||||
);
|
);
|
||||||
|
|
||||||
const returnData: INodeExecutionData[] = [];
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] Get All Mode - Found ${reservations.length} reservations`);
|
||||||
|
console.log(`[LibreBooking Trigger] Date Range: ${start} to ${end}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
return [[{
|
||||||
|
json: {
|
||||||
|
_debug: true,
|
||||||
|
_message: 'Keine Reservierungen im Zeitraum gefunden',
|
||||||
|
_startDate: start,
|
||||||
|
_endDate: end,
|
||||||
|
_count: 0,
|
||||||
|
},
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Reservierungen zurückgeben
|
||||||
|
for (const reservation of reservations) {
|
||||||
|
let reservationData = reservation;
|
||||||
|
|
||||||
|
if (fetchDetails) {
|
||||||
|
try {
|
||||||
|
const details = await getReservationDetails(
|
||||||
|
this,
|
||||||
|
baseUrl,
|
||||||
|
session,
|
||||||
|
reservation.referenceNumber,
|
||||||
|
);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback auf Basisdaten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData.push({
|
||||||
|
json: {
|
||||||
|
...reservationData,
|
||||||
|
_eventType: 'getAll',
|
||||||
|
_triggeredAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// EVENT: Neue Reservierungen
|
// MODE: New Reservations (Polling)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
if (event === 'newReservation') {
|
else if (triggerMode === 'newReservations') {
|
||||||
|
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||||
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||||
|
|
||||||
|
const reservations = await getReservations(
|
||||||
|
this,
|
||||||
|
baseUrl,
|
||||||
|
session,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
// Initialisiere seenIds beim ersten Poll
|
// Initialisiere seenIds beim ersten Poll
|
||||||
if (!webhookData.seenIds) {
|
if (!webhookData.seenIds) {
|
||||||
webhookData.seenIds = [];
|
webhookData.seenIds = [];
|
||||||
|
|
@ -369,6 +544,12 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
|
|
||||||
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
|
const currentIds = reservations.map((r: ReservationData) => r.referenceNumber);
|
||||||
|
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] New Reservations Mode`);
|
||||||
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
||||||
|
console.log(`[LibreBooking Trigger] Current IDs: ${currentIds.length}, Seen IDs: ${webhookData.seenIds.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
// Beim ersten Poll: Nur IDs speichern, NICHT triggern
|
||||||
if (webhookData.isFirstPoll) {
|
if (webhookData.isFirstPoll) {
|
||||||
webhookData.seenIds = currentIds;
|
webhookData.seenIds = currentIds;
|
||||||
|
|
@ -381,6 +562,7 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
_debug: true,
|
_debug: true,
|
||||||
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
_message: 'Erster Poll - IDs wurden gespeichert, keine Events getriggert',
|
||||||
_savedIds: currentIds.length,
|
_savedIds: currentIds.length,
|
||||||
|
_ids: currentIds,
|
||||||
_timestamp: webhookData.lastPollTime,
|
_timestamp: webhookData.lastPollTime,
|
||||||
},
|
},
|
||||||
}]];
|
}]];
|
||||||
|
|
@ -399,6 +581,9 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
|
||||||
if (newReservations.length === 0) {
|
if (newReservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] No new reservations found`);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,16 +591,19 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
for (const reservation of newReservations) {
|
for (const reservation of newReservations) {
|
||||||
let reservationData = reservation;
|
let reservationData = reservation;
|
||||||
|
|
||||||
if (options.fetchDetails) {
|
if (fetchDetails) {
|
||||||
try {
|
try {
|
||||||
reservationData = await getReservationDetails(
|
const details = await getReservationDetails(
|
||||||
this,
|
this,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
session,
|
session,
|
||||||
reservation.referenceNumber,
|
reservation.referenceNumber,
|
||||||
);
|
);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reservationData = reservation;
|
// Fallback auf Basisdaten
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,18 +615,40 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debugMode && returnData.length > 0) {
|
||||||
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} new reservations`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// EVENT: Geänderte Reservierungen
|
// MODE: Updated Reservations (Polling)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
else if (event === 'updatedReservation') {
|
else if (triggerMode === 'updatedReservations') {
|
||||||
|
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||||
|
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||||
|
|
||||||
|
const reservations = await getReservations(
|
||||||
|
this,
|
||||||
|
baseUrl,
|
||||||
|
session,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
// Initialisiere reservationHashes beim ersten Poll
|
// Initialisiere reservationHashes beim ersten Poll
|
||||||
if (!webhookData.reservationHashes) {
|
if (!webhookData.reservationHashes) {
|
||||||
webhookData.reservationHashes = {};
|
webhookData.reservationHashes = {};
|
||||||
webhookData.isFirstPoll = true;
|
webhookData.isFirstPoll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] Updated Reservations Mode`);
|
||||||
|
console.log(`[LibreBooking Trigger] First Poll: ${webhookData.isFirstPoll}`);
|
||||||
|
console.log(`[LibreBooking Trigger] Current: ${reservations.length}, Stored hashes: ${Object.keys(webhookData.reservationHashes).length}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
|
// Beim ersten Poll: Nur Hashes speichern, NICHT triggern
|
||||||
if (webhookData.isFirstPoll) {
|
if (webhookData.isFirstPoll) {
|
||||||
for (const reservation of reservations) {
|
for (const reservation of reservations) {
|
||||||
|
|
@ -483,6 +693,9 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
webhookData.lastPollTime = new Date().toISOString();
|
webhookData.lastPollTime = new Date().toISOString();
|
||||||
|
|
||||||
if (updatedReservations.length === 0) {
|
if (updatedReservations.length === 0) {
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,16 +703,19 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
for (const reservation of updatedReservations) {
|
for (const reservation of updatedReservations) {
|
||||||
let reservationData = reservation;
|
let reservationData = reservation;
|
||||||
|
|
||||||
if (options.fetchDetails) {
|
if (fetchDetails) {
|
||||||
try {
|
try {
|
||||||
reservationData = await getReservationDetails(
|
const details = await getReservationDetails(
|
||||||
this,
|
this,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
session,
|
session,
|
||||||
reservation.referenceNumber,
|
reservation.referenceNumber,
|
||||||
);
|
);
|
||||||
|
if (details) {
|
||||||
|
reservationData = details;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reservationData = reservation;
|
// Fallback auf Basisdaten
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,87 +727,10 @@ export class LibreBookingTrigger implements INodeType {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debugMode && returnData.length > 0) {
|
||||||
|
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 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) {
|
if (returnData.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-librebooking",
|
"name": "n8n-nodes-librebooking",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
|
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"n8n-community-node-package",
|
"n8n-community-node-package",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,609 @@
|
||||||
|
/**
|
||||||
|
* LibreBooking API Test Script
|
||||||
|
*
|
||||||
|
* Testet alle wichtigen API-Endpunkte mit echten Credentials
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://librebooking.zell-cloud.de';
|
||||||
|
const USERNAME = 'sebastian.zell@zell-aufmass.de';
|
||||||
|
const PASSWORD = 'wanUQ4uVqU6lfP';
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
sessionToken: string;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestResult {
|
||||||
|
name: string;
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: TestResult[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Request Helper
|
||||||
|
*/
|
||||||
|
function makeRequest(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
session?: Session,
|
||||||
|
qs?: Record<string, string>
|
||||||
|
): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(path, BASE_URL);
|
||||||
|
|
||||||
|
if (qs) {
|
||||||
|
Object.entries(qs).forEach(([key, value]) => {
|
||||||
|
if (value) url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
headers['X-Booked-SessionToken'] = session.sessionToken;
|
||||||
|
headers['X-Booked-UserId'] = session.userId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
hostname: url.hostname,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpModule = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const req = httpModule.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => { data += chunk; });
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
if (data) {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} else {
|
||||||
|
resolve({ success: true });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ rawData: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
req.write(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Authentication
|
||||||
|
*/
|
||||||
|
async function testAuthentication(): Promise<Session> {
|
||||||
|
console.log('\n=== 1. Testing Authentication ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'POST',
|
||||||
|
'/Web/Services/index.php/Authentication/Authenticate',
|
||||||
|
{ username: USERNAME, password: PASSWORD }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isAuthenticated) {
|
||||||
|
console.log('✅ Authentication successful');
|
||||||
|
console.log(` Session Token: ${response.sessionToken.substring(0, 20)}...`);
|
||||||
|
console.log(` User ID: ${response.userId}`);
|
||||||
|
console.log(` Session Expires: ${response.sessionExpires}`);
|
||||||
|
results.push({ name: 'Authentication', success: true, data: { userId: response.userId } });
|
||||||
|
return { sessionToken: response.sessionToken, userId: response.userId };
|
||||||
|
} else {
|
||||||
|
throw new Error('Authentication failed');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Authentication failed:', error.message);
|
||||||
|
results.push({ name: 'Authentication', success: false, error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Reservations
|
||||||
|
*/
|
||||||
|
async function testGetReservations(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 2. Testing Get Reservations ===');
|
||||||
|
try {
|
||||||
|
// Test without date filters (should return next 2 weeks)
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Reservations/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Reservations fetched: ${response.reservations?.length || 0} found`);
|
||||||
|
|
||||||
|
if (response.reservations && response.reservations.length > 0) {
|
||||||
|
const res = response.reservations[0];
|
||||||
|
console.log(` Example: ${res.title || 'No title'} (${res.referenceNumber})`);
|
||||||
|
console.log(` Start: ${res.startDate}`);
|
||||||
|
console.log(` Resource: ${res.resourceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Reservations (no filter)', success: true, data: { count: response.reservations?.length || 0 } });
|
||||||
|
|
||||||
|
// Test with date filter (Feb 7-14, 2026)
|
||||||
|
console.log('\n Testing with date filter (2026-02-07 to 2026-02-14)...');
|
||||||
|
const responseFiltered = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Reservations/',
|
||||||
|
undefined,
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
startDateTime: '2026-02-07T00:00:00',
|
||||||
|
endDateTime: '2026-02-14T23:59:59'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Filtered reservations: ${responseFiltered.reservations?.length || 0} found`);
|
||||||
|
results.push({ name: 'Get Reservations (date filter)', success: true, data: { count: responseFiltered.reservations?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Reservations failed:', error.message);
|
||||||
|
results.push({ name: 'Get Reservations', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Resources
|
||||||
|
*/
|
||||||
|
async function testGetResources(session: Session): Promise<number | null> {
|
||||||
|
console.log('\n=== 3. Testing Get Resources ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Resources/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Resources fetched: ${response.resources?.length || 0} found`);
|
||||||
|
|
||||||
|
let firstResourceId: number | null = null;
|
||||||
|
if (response.resources && response.resources.length > 0) {
|
||||||
|
const res = response.resources[0];
|
||||||
|
firstResourceId = res.resourceId;
|
||||||
|
console.log(` Example: ${res.name} (ID: ${res.resourceId})`);
|
||||||
|
console.log(` Schedule ID: ${res.scheduleId}`);
|
||||||
|
console.log(` Custom Attributes: ${res.customAttributes?.length || 0}`);
|
||||||
|
|
||||||
|
if (res.customAttributes && res.customAttributes.length > 0) {
|
||||||
|
res.customAttributes.forEach((attr: any) => {
|
||||||
|
console.log(` - ${attr.label}: ${attr.value || '(no value)'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Resources', success: true, data: { count: response.resources?.length || 0 } });
|
||||||
|
return firstResourceId;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Resources failed:', error.message);
|
||||||
|
results.push({ name: 'Get Resources', success: false, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Single Resource (with custom attributes)
|
||||||
|
*/
|
||||||
|
async function testGetSingleResource(session: Session, resourceId: number): Promise<void> {
|
||||||
|
console.log('\n=== 4. Testing Get Single Resource (with custom attributes) ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
`/Web/Services/index.php/Resources/${resourceId}`,
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Resource fetched: ${response.name}`);
|
||||||
|
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
|
||||||
|
|
||||||
|
if (response.customAttributes && response.customAttributes.length > 0) {
|
||||||
|
response.customAttributes.forEach((attr: any) => {
|
||||||
|
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Single Resource', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Single Resource failed:', error.message);
|
||||||
|
results.push({ name: 'Get Single Resource', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Users
|
||||||
|
*/
|
||||||
|
async function testGetUsers(session: Session): Promise<number | null> {
|
||||||
|
console.log('\n=== 5. Testing Get Users ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Users/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Users fetched: ${response.users?.length || 0} found`);
|
||||||
|
|
||||||
|
let firstUserId: number | null = null;
|
||||||
|
if (response.users && response.users.length > 0) {
|
||||||
|
const user = response.users[0];
|
||||||
|
firstUserId = user.id;
|
||||||
|
console.log(` Example: ${user.firstName} ${user.lastName} (ID: ${user.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Users', success: true, data: { count: response.users?.length || 0 } });
|
||||||
|
return firstUserId;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Users failed:', error.message);
|
||||||
|
results.push({ name: 'Get Users', success: false, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Single User (with custom attributes)
|
||||||
|
*/
|
||||||
|
async function testGetSingleUser(session: Session, userId: number): Promise<void> {
|
||||||
|
console.log('\n=== 6. Testing Get Single User (with custom attributes) ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
`/Web/Services/index.php/Users/${userId}`,
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ User fetched: ${response.firstName} ${response.lastName}`);
|
||||||
|
console.log(` Custom Attributes: ${response.customAttributes?.length || 0}`);
|
||||||
|
|
||||||
|
if (response.customAttributes && response.customAttributes.length > 0) {
|
||||||
|
response.customAttributes.forEach((attr: any) => {
|
||||||
|
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Single User', success: true, data: { customAttributes: response.customAttributes?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Single User failed:', error.message);
|
||||||
|
results.push({ name: 'Get Single User', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Schedules
|
||||||
|
*/
|
||||||
|
async function testGetSchedules(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 7. Testing Get Schedules ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Schedules/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Schedules fetched: ${response.schedules?.length || 0} found`);
|
||||||
|
|
||||||
|
if (response.schedules && response.schedules.length > 0) {
|
||||||
|
const schedule = response.schedules[0];
|
||||||
|
console.log(` Example: ${schedule.name} (ID: ${schedule.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Schedules', success: true, data: { count: response.schedules?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Schedules failed:', error.message);
|
||||||
|
results.push({ name: 'Get Schedules', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Attributes by Category
|
||||||
|
*/
|
||||||
|
async function testGetAttributes(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 8. Testing Get Attributes by Category ===');
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 1, name: 'Reservation' },
|
||||||
|
{ id: 2, name: 'User' },
|
||||||
|
{ id: 4, name: 'Resource' },
|
||||||
|
{ id: 5, name: 'Resource Type' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
`/Web/Services/index.php/Attributes/Category/${cat.id}`,
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ ${cat.name} Attributes: ${response.attributes?.length || 0} found`);
|
||||||
|
|
||||||
|
if (response.attributes && response.attributes.length > 0) {
|
||||||
|
response.attributes.forEach((attr: any) => {
|
||||||
|
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Type: ${attr.type}, Required: ${attr.required}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: `Get Attributes (${cat.name})`, success: true, data: { count: response.attributes?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`❌ Get ${cat.name} Attributes failed:`, error.message);
|
||||||
|
results.push({ name: `Get Attributes (${cat.name})`, success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Create, Update, Delete Reservation
|
||||||
|
*/
|
||||||
|
async function testReservationCRUD(session: Session, resourceId: number): Promise<void> {
|
||||||
|
console.log('\n=== 9. Testing Reservation CRUD ===');
|
||||||
|
|
||||||
|
// Create
|
||||||
|
console.log(' Creating test reservation...');
|
||||||
|
try {
|
||||||
|
const createResponse = await makeRequest(
|
||||||
|
'POST',
|
||||||
|
'/Web/Services/index.php/Reservations/',
|
||||||
|
{
|
||||||
|
title: 'API Test Reservation',
|
||||||
|
description: 'Created by n8n node test script',
|
||||||
|
resourceId: resourceId,
|
||||||
|
startDateTime: '2026-02-07T10:00:00',
|
||||||
|
endDateTime: '2026-02-07T11:00:00',
|
||||||
|
userId: session.userId,
|
||||||
|
termsAccepted: true,
|
||||||
|
allowParticipation: false,
|
||||||
|
},
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createResponse.referenceNumber) {
|
||||||
|
console.log(`✅ Reservation created: ${createResponse.referenceNumber}`);
|
||||||
|
results.push({ name: 'Create Reservation', success: true, data: { referenceNumber: createResponse.referenceNumber } });
|
||||||
|
|
||||||
|
const refNum = createResponse.referenceNumber;
|
||||||
|
|
||||||
|
// Get the created reservation
|
||||||
|
console.log(' Fetching created reservation...');
|
||||||
|
const getResponse = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
`/Web/Services/index.php/Reservations/${refNum}`,
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
console.log(`✅ Reservation fetched: ${getResponse.title}`);
|
||||||
|
console.log(` Custom Attributes: ${getResponse.customAttributes?.length || 0}`);
|
||||||
|
if (getResponse.customAttributes && getResponse.customAttributes.length > 0) {
|
||||||
|
getResponse.customAttributes.forEach((attr: any) => {
|
||||||
|
console.log(` - ID: ${attr.id}, Label: ${attr.label}, Value: ${attr.value || '(no value)'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
results.push({ name: 'Get Created Reservation', success: true });
|
||||||
|
|
||||||
|
// Update
|
||||||
|
console.log(' Updating reservation...');
|
||||||
|
const updateResponse = await makeRequest(
|
||||||
|
'POST',
|
||||||
|
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
|
||||||
|
{
|
||||||
|
title: 'API Test Reservation UPDATED',
|
||||||
|
description: 'Updated by n8n node test script',
|
||||||
|
resourceId: resourceId,
|
||||||
|
startDateTime: '2026-02-07T10:00:00',
|
||||||
|
endDateTime: '2026-02-07T12:00:00',
|
||||||
|
termsAccepted: true,
|
||||||
|
allowParticipation: false,
|
||||||
|
},
|
||||||
|
session
|
||||||
|
);
|
||||||
|
console.log(`✅ Reservation updated`);
|
||||||
|
results.push({ name: 'Update Reservation', success: true });
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
console.log(' Deleting reservation...');
|
||||||
|
const deleteResponse = await makeRequest(
|
||||||
|
'DELETE',
|
||||||
|
`/Web/Services/index.php/Reservations/${refNum}?updateScope=this`,
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
console.log(`✅ Reservation deleted`);
|
||||||
|
results.push({ name: 'Delete Reservation', success: true });
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log('❌ Create Reservation failed - no reference number returned');
|
||||||
|
console.log(' Response:', JSON.stringify(createResponse, null, 2));
|
||||||
|
results.push({ name: 'Create Reservation', success: false, error: JSON.stringify(createResponse) });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Reservation CRUD failed:', error.message);
|
||||||
|
results.push({ name: 'Reservation CRUD', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Groups
|
||||||
|
*/
|
||||||
|
async function testGetGroups(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 10. Testing Get Groups ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Groups/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Groups fetched: ${response.groups?.length || 0} found`);
|
||||||
|
|
||||||
|
if (response.groups && response.groups.length > 0) {
|
||||||
|
const group = response.groups[0];
|
||||||
|
console.log(` Example: ${group.name} (ID: ${group.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Groups', success: true, data: { count: response.groups?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Groups failed:', error.message);
|
||||||
|
results.push({ name: 'Get Groups', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Accessories
|
||||||
|
*/
|
||||||
|
async function testGetAccessories(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 11. Testing Get Accessories ===');
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(
|
||||||
|
'GET',
|
||||||
|
'/Web/Services/index.php/Accessories/',
|
||||||
|
undefined,
|
||||||
|
session
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Accessories fetched: ${response.accessories?.length || 0} found`);
|
||||||
|
|
||||||
|
if (response.accessories && response.accessories.length > 0) {
|
||||||
|
const acc = response.accessories[0];
|
||||||
|
console.log(` Example: ${acc.name} (ID: ${acc.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: 'Get Accessories', success: true, data: { count: response.accessories?.length || 0 } });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get Accessories failed:', error.message);
|
||||||
|
results.push({ name: 'Get Accessories', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test SignOut
|
||||||
|
*/
|
||||||
|
async function testSignOut(session: Session): Promise<void> {
|
||||||
|
console.log('\n=== 12. Testing Sign Out ===');
|
||||||
|
try {
|
||||||
|
await makeRequest(
|
||||||
|
'POST',
|
||||||
|
'/Web/Services/index.php/Authentication/SignOut',
|
||||||
|
{
|
||||||
|
userId: session.userId,
|
||||||
|
sessionToken: session.sessionToken,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('✅ Sign Out successful');
|
||||||
|
results.push({ name: 'Sign Out', success: true });
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Sign Out failed:', error.message);
|
||||||
|
results.push({ name: 'Sign Out', success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print Summary
|
||||||
|
*/
|
||||||
|
function printSummary(): void {
|
||||||
|
console.log('\n========================================');
|
||||||
|
console.log(' TEST SUMMARY');
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
|
const passed = results.filter(r => r.success).length;
|
||||||
|
const failed = results.filter(r => !r.success).length;
|
||||||
|
|
||||||
|
console.log(`\nTotal Tests: ${results.length}`);
|
||||||
|
console.log(`✅ Passed: ${passed}`);
|
||||||
|
console.log(`❌ Failed: ${failed}`);
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
console.log('\nFailed Tests:');
|
||||||
|
results.filter(r => !r.success).forEach(r => {
|
||||||
|
console.log(` - ${r.name}: ${r.error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n========================================\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Test Runner
|
||||||
|
*/
|
||||||
|
async function runTests(): Promise<void> {
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' LibreBooking API Test Suite');
|
||||||
|
console.log(` URL: ${BASE_URL}`);
|
||||||
|
console.log(` User: ${USERNAME}`);
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Authentication
|
||||||
|
const session = await testAuthentication();
|
||||||
|
|
||||||
|
// Get operations
|
||||||
|
await testGetReservations(session);
|
||||||
|
const resourceId = await testGetResources(session);
|
||||||
|
if (resourceId) {
|
||||||
|
await testGetSingleResource(session, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = await testGetUsers(session);
|
||||||
|
if (userId) {
|
||||||
|
await testGetSingleUser(session, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await testGetSchedules(session);
|
||||||
|
await testGetAttributes(session);
|
||||||
|
await testGetGroups(session);
|
||||||
|
await testGetAccessories(session);
|
||||||
|
|
||||||
|
// CRUD operations
|
||||||
|
if (resourceId) {
|
||||||
|
await testReservationCRUD(session, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign out
|
||||||
|
await testSignOut(session);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('\n❌ Test suite failed:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
printSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
runTests().catch(console.error);
|
||||||
Loading…
Reference in New Issue