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.
|
||||
|
||||
## [1.2.1] - 2026-01-25
|
||||
|
||||
### Behoben
|
||||
- 🐛 **allowParticipation Fehler**: API-Fehler "Undefined property: stdClass::$allowParticipation" behoben. Das Feld wird jetzt immer im Request-Body gesendet.
|
||||
- 🐛 **Trigger "Alle Abrufen" funktioniert nicht**: Trigger-Modi komplett überarbeitet mit drei klaren Optionen:
|
||||
- "Alle Abrufen (Einmalig)" - Ruft alle Reservierungen für einen Zeitraum ab
|
||||
- "Neue Reservierungen (Polling)" - Erkennt neue Reservierungen
|
||||
- "Geänderte Reservierungen (Polling)" - Erkennt Änderungen
|
||||
- 🐛 **Custom Attributes bei GetAll**: Option fehlt
|
||||
|
||||
### Hinzugefügt
|
||||
- ⭐ **Include Custom Attributes Option**: Neues "Custom Attributes Einschließen" Checkbox bei:
|
||||
- Reservierungen → Alle Abrufen
|
||||
- Ressourcen → Alle Abrufen
|
||||
- Benutzer → Alle Abrufen
|
||||
- 📋 **TEST-RESULTS.md**: Detaillierte Test-Dokumentation mit echten API-Tests
|
||||
- 📋 **test-api.ts**: Verbessertes Test-Skript für alle API-Endpunkte
|
||||
|
||||
### Geändert
|
||||
- **Trigger Node**: Komplett überarbeitete UI mit klarerer Trennung der Modi
|
||||
- **Trigger Zeitraum**: Optionale Start-/Enddatum-Felder für "Alle Abrufen" Mode
|
||||
- **Reservierung erstellen/aktualisieren**: allowParticipation wird immer gesetzt (API-Pflichtfeld)
|
||||
|
||||
### Getestet
|
||||
- ✅ 19 API-Tests erfolgreich bestanden
|
||||
- ✅ Alle Trigger-Modi getestet
|
||||
- ✅ Custom Attributes Integration getestet
|
||||
- **Test-URL**: https://librebooking.zell-cloud.de
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-01-25
|
||||
|
||||
### Hinzugefügt
|
||||
|
|
|
|||
|
|
@ -128,6 +128,62 @@ Gleiche Vorgehensweise wie beim Erstellen.
|
|||
- **Nur Admin**: Nur für Admins sichtbar?
|
||||
- **Mögliche Werte**: Für Auswahllisten (komma-getrennt)
|
||||
|
||||
## Elegante Lösung: Attribute automatisch abrufen (NEU in v1.2.1)
|
||||
|
||||
### Das Problem
|
||||
Bisher musste man Attribut-IDs manuell eingeben, was umständlich war.
|
||||
|
||||
### Die Lösung: "Custom Attributes Einschließen"
|
||||
Bei den GetAll-Operationen gibt es jetzt eine neue Option, die automatisch die Custom Attribute Values für jeden Eintrag abruft.
|
||||
|
||||
### Verwendung für Reservierungen
|
||||
|
||||
1. Wählen Sie **Ressource**: `Reservierung`
|
||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||
3. Unter **Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||
|
||||
**Ergebnis:**
|
||||
```json
|
||||
{
|
||||
"reservations": [
|
||||
{
|
||||
"referenceNumber": "abc123",
|
||||
"title": "Meeting",
|
||||
"startDate": "2026-02-07T10:00:00",
|
||||
"customAttributes": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "Mietername",
|
||||
"value": "Max Mustermann"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "Adresse",
|
||||
"value": "Hauptstraße 1, 12345 Stadt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Verwendung für Ressourcen
|
||||
|
||||
1. Wählen Sie **Ressource**: `Ressource`
|
||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||
3. Unter **Ressourcen-Abruf-Optionen** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||
|
||||
### Verwendung für Benutzer
|
||||
|
||||
1. Wählen Sie **Ressource**: `Benutzer`
|
||||
2. Wählen Sie **Operation**: `Alle Abrufen`
|
||||
3. Unter **Benutzer-Filter** aktivieren Sie **Custom Attributes Einschließen** ✅
|
||||
|
||||
### Wichtiger Hinweis
|
||||
Diese Option führt für jeden Eintrag einen zusätzlichen API-Call durch. Bei vielen Einträgen kann dies länger dauern.
|
||||
|
||||
---
|
||||
|
||||
## Tipps
|
||||
|
||||
### Attribut-IDs herausfinden
|
||||
|
|
|
|||
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 |
File diff suppressed because it is too large
Load Diff
|
|
@ -53,7 +53,7 @@ async function authenticateTrigger(
|
|||
if (!response.isAuthenticated) {
|
||||
throw new NodeOperationError(
|
||||
pollFunctions.getNode(),
|
||||
'Authentifizierung fehlgeschlagen',
|
||||
'Authentifizierung fehlgeschlagen. Überprüfen Sie Ihre Zugangsdaten.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +65,7 @@ async function authenticateTrigger(
|
|||
} catch (error: any) {
|
||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||
message: 'Authentifizierung fehlgeschlagen',
|
||||
description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -113,23 +114,29 @@ async function getReservations(
|
|||
if (filters.scheduleId) qs.scheduleId = filters.scheduleId;
|
||||
if (filters.userId) qs.userId = filters.userId;
|
||||
|
||||
const response = await pollFunctions.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
},
|
||||
qs,
|
||||
json: true,
|
||||
});
|
||||
try {
|
||||
const response = await pollFunctions.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/Web/Services/index.php/Reservations/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
},
|
||||
qs,
|
||||
json: true,
|
||||
});
|
||||
|
||||
return response.reservations || [];
|
||||
return response.reservations || [];
|
||||
} catch (error: any) {
|
||||
throw new NodeApiError(pollFunctions.getNode(), error, {
|
||||
message: 'Fehler beim Abrufen der Reservierungen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaillierte Reservierungsdaten abrufen
|
||||
* Detaillierte Reservierungsdaten abrufen (inkl. Custom Attributes)
|
||||
*/
|
||||
async function getReservationDetails(
|
||||
pollFunctions: IPollFunctions,
|
||||
|
|
@ -137,24 +144,53 @@ async function getReservationDetails(
|
|||
session: LibreBookingSession,
|
||||
referenceNumber: string,
|
||||
): Promise<any> {
|
||||
const response = await pollFunctions.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
try {
|
||||
const response = await pollFunctions.helpers.httpRequest({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Booked-SessionToken': session.sessionToken,
|
||||
'X-Booked-UserId': session.userId.toString(),
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
|
||||
return response;
|
||||
return response;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeitfenster berechnen
|
||||
* 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 start = now.toISOString();
|
||||
|
||||
|
|
@ -184,7 +220,6 @@ function getTimeWindow(timeWindow: string): { start: string; end: string } {
|
|||
|
||||
/**
|
||||
* Hash für Reservierung generieren (für Änderungserkennung)
|
||||
* Nur relevante Felder berücksichtigen, die Änderungen anzeigen
|
||||
*/
|
||||
function getReservationHash(reservation: ReservationData): string {
|
||||
const relevantData = {
|
||||
|
|
@ -206,11 +241,10 @@ function getReservationHash(reservation: ReservationData): string {
|
|||
/**
|
||||
* LibreBooking Trigger Node
|
||||
*
|
||||
* Überwacht neue und geänderte Reservierungen in LibreBooking.
|
||||
*
|
||||
* WICHTIG: Beim ersten Poll werden nur die IDs/Hashes gespeichert,
|
||||
* aber keine Events getriggert. Dies verhindert, dass alle
|
||||
* existierenden Reservierungen als "neu" getriggert werden.
|
||||
* Drei Modi:
|
||||
* 1. Get All (One-Time): Alle Reservierungen für einen Zeitraum abrufen
|
||||
* 2. New Reservations (Poll): Bei neuen Reservierungen triggern
|
||||
* 3. Updated Reservations (Poll): Bei geänderten Reservierungen triggern
|
||||
*/
|
||||
export class LibreBookingTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
@ -220,7 +254,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst',
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
subtitle: '={{$parameter["triggerMode"]}}',
|
||||
defaults: {
|
||||
name: 'LibreBooking Trigger',
|
||||
},
|
||||
|
|
@ -234,57 +268,74 @@ export class LibreBookingTrigger implements INodeType {
|
|||
],
|
||||
polling: true,
|
||||
properties: [
|
||||
// =====================================================
|
||||
// TRIGGER MODE SELECTOR
|
||||
// =====================================================
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
displayName: 'Trigger-Modus',
|
||||
name: 'triggerMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Neue Reservierung',
|
||||
value: 'newReservation',
|
||||
description: 'Wird bei neuen Reservierungen ausgelöst (nicht beim ersten Poll)'
|
||||
{
|
||||
name: 'Alle Abrufen (Einmalig)',
|
||||
value: 'getAll',
|
||||
description: 'Alle Reservierungen für einen Zeitraum abrufen (bei jedem Poll)',
|
||||
},
|
||||
{
|
||||
name: 'Geänderte Reservierung',
|
||||
value: 'updatedReservation',
|
||||
description: 'Wird bei geänderten Reservierungen ausgelöst'
|
||||
{
|
||||
name: 'Neue Reservierungen (Polling)',
|
||||
value: 'newReservations',
|
||||
description: 'Nur bei neuen Reservierungen triggern',
|
||||
},
|
||||
{
|
||||
name: 'Alle Reservierungen',
|
||||
value: 'allReservations',
|
||||
description: 'Wird bei neuen und geänderten Reservierungen ausgelöst'
|
||||
{
|
||||
name: 'Geänderte Reservierungen (Polling)',
|
||||
value: 'updatedReservations',
|
||||
description: 'Nur bei geänderten Reservierungen triggern',
|
||||
},
|
||||
],
|
||||
default: 'newReservation',
|
||||
default: 'getAll',
|
||||
description: 'Wählen Sie den Trigger-Modus',
|
||||
},
|
||||
|
||||
// =====================================================
|
||||
// GET ALL MODE - DATE RANGE
|
||||
// =====================================================
|
||||
{
|
||||
displayName: 'Hinweis',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayName: 'Startdatum',
|
||||
name: 'startDate',
|
||||
type: 'dateTime',
|
||||
displayOptions: {
|
||||
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',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Filter hinzufügen',
|
||||
default: {},
|
||||
options: [
|
||||
{ displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' },
|
||||
{ displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' },
|
||||
{ displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' },
|
||||
],
|
||||
displayName: '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' },
|
||||
|
|
@ -292,7 +343,58 @@ export class LibreBookingTrigger implements INodeType {
|
|||
{ 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',
|
||||
|
|
@ -300,12 +402,12 @@ export class LibreBookingTrigger implements INodeType {
|
|||
placeholder: 'Option hinzufügen',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Detaillierte Daten Abrufen',
|
||||
name: 'fetchDetails',
|
||||
type: 'boolean',
|
||||
{
|
||||
displayName: 'Detaillierte Daten Abrufen',
|
||||
name: 'fetchDetails',
|
||||
type: 'boolean',
|
||||
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',
|
||||
|
|
@ -325,16 +427,16 @@ export class LibreBookingTrigger implements INodeType {
|
|||
const username = credentials.username as string;
|
||||
const password = credentials.password as string;
|
||||
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const triggerMode = this.getNodeParameter('triggerMode') as string;
|
||||
const filters = this.getNodeParameter('filters', {}) as any;
|
||||
const timeWindow = this.getNodeParameter('timeWindow', '14days') as string;
|
||||
const options = this.getNodeParameter('options', {}) as any;
|
||||
|
||||
// Debug-Modus
|
||||
const debugMode = options.debugMode || false;
|
||||
const fetchDetails = options.fetchDetails || false;
|
||||
|
||||
// Workflow Static Data für State-Management
|
||||
const webhookData = this.getWorkflowStaticData('node') as WorkflowStaticData;
|
||||
|
||||
// Debug-Modus
|
||||
const debugMode = options.debugMode || false;
|
||||
|
||||
let session: LibreBookingSession;
|
||||
try {
|
||||
|
|
@ -344,23 +446,96 @@ export class LibreBookingTrigger implements INodeType {
|
|||
}
|
||||
|
||||
try {
|
||||
const { start, end } = getTimeWindow(timeWindow);
|
||||
|
||||
const reservations = await getReservations(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
start,
|
||||
end,
|
||||
filters,
|
||||
);
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
// ==========================================
|
||||
// EVENT: Neue Reservierungen
|
||||
// MODE: Get All (One-Time / Every Poll)
|
||||
// ==========================================
|
||||
if (event === 'newReservation') {
|
||||
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(
|
||||
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') as string;
|
||||
const { start, end } = getTimeWindowForPolling(timeWindow);
|
||||
|
||||
const reservations = await getReservations(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
start,
|
||||
end,
|
||||
filters,
|
||||
);
|
||||
|
||||
// Initialisiere seenIds beim ersten Poll
|
||||
if (!webhookData.seenIds) {
|
||||
webhookData.seenIds = [];
|
||||
|
|
@ -369,28 +544,35 @@ export class LibreBookingTrigger implements INodeType {
|
|||
|
||||
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
|
||||
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: ReservationData) =>
|
||||
const newReservations = reservations.filter((r: ReservationData) =>
|
||||
!webhookData.seenIds!.includes(r.referenceNumber)
|
||||
);
|
||||
|
||||
|
|
@ -399,6 +581,9 @@ export class LibreBookingTrigger implements INodeType {
|
|||
webhookData.lastPollTime = new Date().toISOString();
|
||||
|
||||
if (newReservations.length === 0) {
|
||||
if (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No new reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -406,16 +591,19 @@ export class LibreBookingTrigger implements INodeType {
|
|||
for (const reservation of newReservations) {
|
||||
let reservationData = reservation;
|
||||
|
||||
if (options.fetchDetails) {
|
||||
if (fetchDetails) {
|
||||
try {
|
||||
reservationData = await getReservationDetails(
|
||||
const details = await getReservationDetails(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
reservation.referenceNumber,
|
||||
);
|
||||
if (details) {
|
||||
reservationData = details;
|
||||
}
|
||||
} 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
|
||||
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) {
|
||||
|
|
@ -446,7 +656,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
}
|
||||
webhookData.isFirstPoll = false;
|
||||
webhookData.lastPollTime = new Date().toISOString();
|
||||
|
||||
|
||||
if (debugMode) {
|
||||
return [[{
|
||||
json: {
|
||||
|
|
@ -457,7 +667,7 @@ export class LibreBookingTrigger implements INodeType {
|
|||
},
|
||||
}]];
|
||||
}
|
||||
|
||||
|
||||
return null; // Nichts triggern beim ersten Poll
|
||||
}
|
||||
|
||||
|
|
@ -483,6 +693,9 @@ export class LibreBookingTrigger implements INodeType {
|
|||
webhookData.lastPollTime = new Date().toISOString();
|
||||
|
||||
if (updatedReservations.length === 0) {
|
||||
if (debugMode) {
|
||||
console.log(`[LibreBooking Trigger] No updated reservations found`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -490,16 +703,19 @@ export class LibreBookingTrigger implements INodeType {
|
|||
for (const reservation of updatedReservations) {
|
||||
let reservationData = reservation;
|
||||
|
||||
if (options.fetchDetails) {
|
||||
if (fetchDetails) {
|
||||
try {
|
||||
reservationData = await getReservationDetails(
|
||||
const details = await getReservationDetails(
|
||||
this,
|
||||
baseUrl,
|
||||
session,
|
||||
reservation.referenceNumber,
|
||||
);
|
||||
if (details) {
|
||||
reservationData = details;
|
||||
}
|
||||
} catch (error) {
|
||||
reservationData = reservation;
|
||||
// Fallback auf Basisdaten
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -511,87 +727,10 @@ export class LibreBookingTrigger implements INodeType {
|
|||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 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;
|
||||
if (debugMode && returnData.length > 0) {
|
||||
console.log(`[LibreBooking Trigger] Triggering ${returnData.length} updated reservations`);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-librebooking",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung",
|
||||
"keywords": [
|
||||
"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