Merge pull request 'Agrarmonitor' (#2) from Agrarmonitor into main
Build and Push Multi-Platform Images / build-and-push (push) Successful in 11s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 11s
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -50,3 +50,14 @@ BELEGNUMMER_SET_URL=https://beispiel-api.de/set-number/{Jahr}/{Nummer}
|
|||||||
# Leer lassen für lokale Entwicklung (erlaubt alle Origins).
|
# Leer lassen für lokale Entwicklung (erlaubt alle Origins).
|
||||||
# NODE_ENV=production ohne CORS_ORIGIN blockiert alle Cross-Origin-Anfragen.
|
# NODE_ENV=production ohne CORS_ORIGIN blockiert alle Cross-Origin-Anfragen.
|
||||||
CORS_ORIGIN=
|
CORS_ORIGIN=
|
||||||
|
|
||||||
|
# --- Agrarmonitor ---
|
||||||
|
AGRARMONITOR_BASE_URL=https://admin7.agrarmonitor.de
|
||||||
|
AGRARMONITOR_API_BASE_URL=https://api.agrarmonitor.de
|
||||||
|
AGRARMONITOR_USERNAME=
|
||||||
|
AGRARMONITOR_PASSWORD=
|
||||||
|
AGRARMONITOR_API_TOKEN=
|
||||||
|
AGRARMONITOR_COOKIE_PATH=./data/agrarmonitor-cookies.json
|
||||||
|
AGRARMONITOR_ENCRYPTION_KEY= # optional, 16+ Zeichen für Cookie-Verschlüsselung
|
||||||
|
AGRARMONITOR_POLLING_CRON=0 */30 * * * * # Polling-Intervall (Standard: alle 30 Minuten); leer lassen zum Deaktivieren
|
||||||
|
AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard: einmal pro Minute); leer lassen zum Deaktivieren
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ services:
|
|||||||
- IMAP_PASSWORD=${IMAP_PASSWORD:-}
|
- IMAP_PASSWORD=${IMAP_PASSWORD:-}
|
||||||
- BELEGNUMMER_GET_URL=${BELEGNUMMER_GET_URL:-}
|
- BELEGNUMMER_GET_URL=${BELEGNUMMER_GET_URL:-}
|
||||||
- BELEGNUMMER_SET_URL=${BELEGNUMMER_SET_URL:-}
|
- BELEGNUMMER_SET_URL=${BELEGNUMMER_SET_URL:-}
|
||||||
|
- AGRARMONITOR_BASE_URL=${AGRARMONITOR_BASE_URL:-https://admin7.agrarmonitor.de}
|
||||||
|
- AGRARMONITOR_API_BASE_URL=${AGRARMONITOR_API_BASE_URL:-https://api.agrarmonitor.de}
|
||||||
|
- AGRARMONITOR_USERNAME=${AGRARMONITOR_USERNAME:-}
|
||||||
|
- AGRARMONITOR_PASSWORD=${AGRARMONITOR_PASSWORD:-}
|
||||||
|
- AGRARMONITOR_API_TOKEN=${AGRARMONITOR_API_TOKEN:-}
|
||||||
|
- AGRARMONITOR_COOKIE_PATH=${AGRARMONITOR_COOKIE_PATH:-./data/agrarmonitor-cookies.json}
|
||||||
|
- AGRARMONITOR_ENCRYPTION_KEY=${AGRARMONITOR_ENCRYPTION_KEY:-}
|
||||||
|
- AGRARMONITOR_POLLING_CRON=${AGRARMONITOR_POLLING_CRON:-}
|
||||||
|
- AGRARMONITOR_UPLOAD_CHECK_CRON=${AGRARMONITOR_UPLOAD_CHECK_CRON:-}
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/scans:/mnt/scans
|
- /mnt/scans:/mnt/scans
|
||||||
- /mnt/paperlessmanager:/mnt/data
|
- /mnt/paperlessmanager:/mnt/data
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
|||||||
|
# Agrarmonitor Polling Service — Design
|
||||||
|
|
||||||
|
**Datum:** 2026-05-23
|
||||||
|
**Branch:** Agrarmonitor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der Polling-Service prüft regelmäßig Paperless-Dokumente, die als "fertig für Agrarmonitor" markiert sind (Tag-ID konfigurierbar), gleicht sie mit Agrarmonitor-Eingangsrechnungen ab und aktualisiert sowohl Agrarmonitor (Eingangsdatum, Lieferscheinnummer) als auch Paperless (Korrespondent, Betrieb, Tags) sobald ein Buchungsdatum vorliegt.
|
||||||
|
|
||||||
|
Logik basiert auf `ProcessEingaenge.cs` aus dem C#-Paperlessworker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenbankänderungen
|
||||||
|
|
||||||
|
### `Client`-Entity
|
||||||
|
Neue nullable Spalte:
|
||||||
|
```typescript
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
AgrarmonitorBetriebId: number | null;
|
||||||
|
```
|
||||||
|
Verknüpft einen Paperless-Betrieb (Client) mit seiner Agrarmonitor-BetriebId. Wird vom Polling-Service für die Dokumentzuordnung genutzt.
|
||||||
|
|
||||||
|
### `Setting`-Entity (neue Einträge, per Seed/upsert)
|
||||||
|
Zwei neue Einträge identifiziert über das `Tag`-Feld:
|
||||||
|
- `Tag = 'agrarmonitor_tag_fertig'`, Wert default `'4'` — Tag-ID für "fertig in Agrarmonitor"
|
||||||
|
- `Tag = 'agrarmonitor_tag_verbucht'`, Wert default `'9'` — Tag-ID für "verbucht"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### Neue Datei: `agrarmonitor-polling.service.ts`
|
||||||
|
|
||||||
|
**Methoden:**
|
||||||
|
- `runPolling(): Promise<PollingResult>` — Haupt-Logik, synchron ausführbar
|
||||||
|
- Automatischer Start via `@Cron(env.AGRARMONITOR_POLLING_CRON)`
|
||||||
|
|
||||||
|
**Polling-Logik (sequenziell):**
|
||||||
|
1. Tag-IDs aus `Setting`-Tabelle lesen (Tag-Fertig, Tag-Verbucht)
|
||||||
|
2. `agrarmonitorService.getClient()` → Connector holen
|
||||||
|
3. `client.fetchCustomers()` → aktive Lieferanten (`ist_lieferant=1`, `ist_aktiv=1`) als Paperless-Korrespondenten synchronisieren
|
||||||
|
4. Paperless-Dokumente mit Tag-Fertig laden
|
||||||
|
5. Pro Dokument mit `interneBelegnummer`:
|
||||||
|
- `client.eingangsrechnungenLivesearch(belegnummer)` aufrufen
|
||||||
|
- Kein Treffer → überspringen
|
||||||
|
- Mehrere Treffer → Fehler loggen, überspringen
|
||||||
|
- Kein `eingangsDatum` + Paperless hat `Eingangsdatum` → `client.setEingangsdatum()` aufrufen
|
||||||
|
- `buchungsDatum` vorhanden → Paperless-Dokument aktualisieren:
|
||||||
|
- Korrespondenten über `AgrarmonitorBetriebId`-Mapping setzen
|
||||||
|
- Betrieb/Owner/Gruppe aus Client-Tabelle übernehmen
|
||||||
|
- Tag-Fertig entfernen, Tag-Verbucht hinzufügen
|
||||||
|
- `paperlessService.updateDocument()` aufrufen
|
||||||
|
- 500ms Pause zwischen Dokumenten (API-Schonung)
|
||||||
|
6. `PollingResult` zurückgeben: `{processed, updated, skipped, errors}`
|
||||||
|
|
||||||
|
**Abhängigkeiten:** `AgrarmonitorService`, `PaperlessService`, `Repository<Setting>`, `Repository<Client>`
|
||||||
|
|
||||||
|
### `AgrarmonitorModule` — Erweiterungen
|
||||||
|
```typescript
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Setting, Client]),
|
||||||
|
PaperlessModule,
|
||||||
|
ScheduleModule, // bereits global registriert
|
||||||
|
]
|
||||||
|
providers: [AgrarmonitorService, AgrarmonitorPollingService]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neue Endpoints (in `AgrarmonitorController`)
|
||||||
|
|
||||||
|
| Method | Route | Beschreibung |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `GET` | `/api/agrarmonitor/polling-config` | Tag-IDs lesen |
|
||||||
|
| `PUT` | `/api/agrarmonitor/polling-config` | Tag-IDs speichern |
|
||||||
|
| `POST` | `/api/agrarmonitor/run-polling` | Polling manuell auslösen |
|
||||||
|
|
||||||
|
Alle Routen: `@RequirePermissions(Permission.MANAGE_SETTINGS)`
|
||||||
|
|
||||||
|
### `.env.example` — neuer Eintrag
|
||||||
|
```
|
||||||
|
AGRARMONITOR_POLLING_CRON=0 */30 * * * * # alle 30 Minuten
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Agrarmonitor-Tab (bestehend erweitern)
|
||||||
|
|
||||||
|
**Neuer Abschnitt "Polling-Konfiguration":**
|
||||||
|
- Input "Tag: Fertig in Agrarmonitor" (Zahl, lädt aus `/api/agrarmonitor/polling-config`)
|
||||||
|
- Input "Tag: Verbucht" (Zahl)
|
||||||
|
- Speichern-Button → `PUT /api/agrarmonitor/polling-config`
|
||||||
|
|
||||||
|
**Neuer Abschnitt "Polling ausführen":**
|
||||||
|
- Button "Jetzt ausführen" → `POST /api/agrarmonitor/run-polling`
|
||||||
|
- Ergebnisanzeige: "X verarbeitet, X aktualisiert, X Fehler"
|
||||||
|
|
||||||
|
### "Benutzer & Betriebe"-Tab (bestehend erweitern)
|
||||||
|
|
||||||
|
In der Client-Tabelle: neue Spalte "Agrarmonitor-BetriebId" (inline editierbar, Zahl oder leer).
|
||||||
|
Speichern via `PUT /api/settings/clients/:id` (neuer Endpoint oder bestehenden erweitern).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
Cron / manueller Trigger
|
||||||
|
→ AgrarmonitorPollingService.runPolling()
|
||||||
|
→ AgrarmonitorService.getClient() (Connector)
|
||||||
|
→ PaperlessService.getDocuments() (Tag-Fertig-Filter)
|
||||||
|
→ pro Dokument:
|
||||||
|
→ client.eingangsrechnungenLivesearch()
|
||||||
|
→ client.setEingangsdatum() (falls nötig)
|
||||||
|
→ client.fetchCustomers() + PaperlessService.updateDocument()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
- Einzelne Dokument-Fehler werden geloggt, überspringen den Eintrag, brechen den gesamten Lauf nicht ab
|
||||||
|
- Connector-Fehler (Verbindung zu Agrarmonitor) brechen den Lauf ab, geben `{error}` zurück
|
||||||
|
- Ergebnis wird immer als strukturiertes Objekt zurückgegeben (nie 500)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht im Scope
|
||||||
|
|
||||||
|
- E-Mail-Benachrichtigung bei Fehlern (kommt ggf. später via Postprocessing)
|
||||||
|
- Rückgängig machen von Buchungen
|
||||||
|
- Polling-Log in der Datenbank (Ergebnis nur in Backend-Logs + API-Response)
|
||||||
Generated
+597
-3
@@ -20,6 +20,7 @@
|
|||||||
"@types/form-data": "^2.2.1",
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"agrarmonitor-connector": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#cd89a30",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"basic-ftp": "^5.2.1",
|
"basic-ftp": "^5.2.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -215,6 +216,53 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "5.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||||
|
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||||
|
"@csstools/css-calc": "^3.2.0",
|
||||||
|
"@csstools/css-color-parser": "^4.1.0",
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/dom-selector": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||||
|
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||||
|
"bidi-js": "^1.0.3",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/generational-cache": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/nwsapi": {
|
||||||
|
"version": "2.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||||
|
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
@@ -741,6 +789,18 @@
|
|||||||
"url": "https://github.com/sponsors/Borewit"
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@bramus/specificity": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-tree": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"specificity": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@buttercup/fetch": {
|
"node_modules/@buttercup/fetch": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
|
||||||
@@ -785,6 +845,140 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^6.0.2",
|
||||||
|
"@csstools/css-calc": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"css-tree": "^3.2.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"css-tree": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||||
@@ -975,6 +1169,23 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@exodus/bytes": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@noble/hashes": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -3040,7 +3251,7 @@
|
|||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.21.3 || >=16"
|
"node": "^14.21.3 || >=16"
|
||||||
@@ -4640,6 +4851,26 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agrarmonitor-connector": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#cd89a3063144a4f7746c9946d1e4f888f8f0f8b4",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"axios-cookiejar-support": "^5.0.5",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"tough-cookie": "^4.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
@@ -4888,6 +5119,25 @@
|
|||||||
"proxy-from-env": "^2.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-cookiejar-support": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-jJG+p7JnOYxkVrYkCDKBrLqUmcpwHZTNQrEcIEKr5qe7YVTyPAD9nCsi1cO5LDmQpQApfS430czO+oceI3g/3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"http-cookie-agent": "^6.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/3846masa"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">=0.20.0",
|
||||||
|
"tough-cookie": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.3.0",
|
"version": "30.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
||||||
@@ -5041,6 +5291,15 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@@ -5758,6 +6017,19 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.27.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
@@ -5767,6 +6039,19 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^16.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.20",
|
"version": "1.11.20",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
|
||||||
@@ -5799,6 +6084,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dedent": {
|
"node_modules/dedent": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
|
||||||
@@ -7331,6 +7622,18 @@
|
|||||||
"integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==",
|
"integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -7385,6 +7688,30 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-cookie-agent": {
|
||||||
|
"version": "6.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-6.0.8.tgz",
|
||||||
|
"integrity": "sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/3846masa"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tough-cookie": "^4.0.0 || ^5.0.0",
|
||||||
|
"undici": "^5.11.0 || ^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"undici": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@@ -7648,6 +7975,12 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-promise": {
|
"node_modules/is-promise": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
@@ -8620,6 +8953,76 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "29.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||||
|
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^5.1.11",
|
||||||
|
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||||
|
"@bramus/specificity": "^2.4.2",
|
||||||
|
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||||
|
"@exodus/bytes": "^1.15.0",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"data-urls": "^7.0.0",
|
||||||
|
"decimal.js": "^10.6.0",
|
||||||
|
"html-encoding-sniffer": "^6.0.0",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"lru-cache": "^11.3.5",
|
||||||
|
"parse5": "^8.0.1",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^6.0.1",
|
||||||
|
"undici": "^7.25.0",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1",
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^16.0.1",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/lru-cache": {
|
||||||
|
"version": "11.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
|
||||||
|
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/tough-cookie": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^7.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/undici": {
|
||||||
|
"version": "7.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||||
|
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -9162,6 +9565,12 @@
|
|||||||
"is-buffer": "~1.1.6"
|
"is-buffer": "~1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||||
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
@@ -9772,6 +10181,30 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5/node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseley": {
|
"node_modules/parseley": {
|
||||||
"version": "0.12.1",
|
"version": "0.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||||
@@ -10227,11 +10660,22 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/psl": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/lupomontero"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -10497,7 +10941,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -10629,6 +11072,18 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||||
@@ -10968,6 +11423,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-support": {
|
"node_modules/source-map-support": {
|
||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
@@ -11273,6 +11737,12 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/synckit": {
|
"node_modules/synckit": {
|
||||||
"version": "0.11.12",
|
"version": "0.11.12",
|
||||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
|
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
|
||||||
@@ -11557,6 +12027,24 @@
|
|||||||
"tlds": "bin.js"
|
"tlds": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "7.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz",
|
||||||
|
"integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^7.0.30"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "7.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz",
|
||||||
|
"integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
@@ -11618,6 +12106,42 @@
|
|||||||
"url": "https://github.com/sponsors/Borewit"
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"psl": "^1.1.33",
|
||||||
|
"punycode": "^2.1.1",
|
||||||
|
"universalify": "^0.2.0",
|
||||||
|
"url-parse": "^1.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie/node_modules/universalify": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
@@ -12179,6 +12703,17 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||||
|
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -12358,6 +12893,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
@@ -12450,6 +12997,15 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.105.4",
|
"version": "5.105.4",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
|
||||||
@@ -12651,6 +13207,29 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "16.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||||
|
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.11.0",
|
||||||
|
"tr46": "^6.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -12762,6 +13341,21 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@types/form-data": "^2.2.1",
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"agrarmonitor-connector": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
"basic-ftp": "^5.2.1",
|
"basic-ftp": "^5.2.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AgrarmonitorService } from './agrarmonitor.service';
|
||||||
|
import { PaperlessService } from '../paperless/paperless.service';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
import { Client } from '../database/entities/client.entity';
|
||||||
|
|
||||||
|
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||||
|
const EINGANGSDATUM_FIELD_ID = 9;
|
||||||
|
const EXTERN_BELEGNUMMER_FIELD_ID = 3;
|
||||||
|
const DOCS_PAGE_SIZE = 500;
|
||||||
|
const AGRARMONITOR_BASE_URL = 'https://admin7.agrarmonitor.de';
|
||||||
|
|
||||||
|
export interface PollingResult {
|
||||||
|
processed: number;
|
||||||
|
updated: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgrarmonitorPollingService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AgrarmonitorPollingService.name);
|
||||||
|
private pollingRunning = false;
|
||||||
|
private uploadCheckRunning = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly agrarmonitorService: AgrarmonitorService,
|
||||||
|
private readonly paperlessService: PaperlessService,
|
||||||
|
@InjectRepository(Setting) private readonly settingRepo: Repository<Setting>,
|
||||||
|
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.upsertSetting('agrarmonitor_tag_fertig', '4');
|
||||||
|
await this.upsertSetting('agrarmonitor_tag_verbucht', '9');
|
||||||
|
await this.upsertSetting('agrarmonitor_tag_hochgeladen', '');
|
||||||
|
await this.upsertSetting('agrarmonitor_link_field', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *')
|
||||||
|
async scheduledPolling() {
|
||||||
|
if (!process.env['AGRARMONITOR_POLLING_CRON']) return;
|
||||||
|
this.runPolling().catch((err) => this.logger.error('Cron-Polling-Fehler:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cron(process.env['AGRARMONITOR_UPLOAD_CHECK_CRON'] || '0 * * * * *')
|
||||||
|
async scheduledUploadCheck() {
|
||||||
|
if (!process.env['AGRARMONITOR_UPLOAD_CHECK_CRON']) return;
|
||||||
|
this.processVerarbeiteteDocuments().catch((err) => this.logger.error('Cron-Upload-Check-Fehler:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPollingConfig(): Promise<{ tagFertig: string; tagVerbucht: string; tagHochgeladen: string; linkField: string }> {
|
||||||
|
const [fertig, verbucht, hochgeladen, linkField] = await Promise.all([
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
tagFertig: fertig?.Wert ?? '4',
|
||||||
|
tagVerbucht: verbucht?.Wert ?? '9',
|
||||||
|
tagHochgeladen: hochgeladen?.Wert ?? '',
|
||||||
|
linkField: linkField?.Wert ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePollingConfig(
|
||||||
|
tagFertig: string,
|
||||||
|
tagVerbucht: string,
|
||||||
|
tagHochgeladen: string,
|
||||||
|
linkField: string,
|
||||||
|
): Promise<{ tagFertig: string; tagVerbucht: string; tagHochgeladen: string; linkField: string }> {
|
||||||
|
await Promise.all([
|
||||||
|
this.settingRepo.update({ Tag: 'agrarmonitor_tag_fertig' }, { Wert: tagFertig }),
|
||||||
|
this.settingRepo.update({ Tag: 'agrarmonitor_tag_verbucht' }, { Wert: tagVerbucht }),
|
||||||
|
this.settingRepo.update({ Tag: 'agrarmonitor_tag_hochgeladen' }, { Wert: tagHochgeladen }),
|
||||||
|
this.settingRepo.update({ Tag: 'agrarmonitor_link_field' }, { Wert: linkField }),
|
||||||
|
]);
|
||||||
|
return { tagFertig, tagVerbucht, tagHochgeladen, linkField };
|
||||||
|
}
|
||||||
|
|
||||||
|
async runPolling(): Promise<PollingResult> {
|
||||||
|
if (this.pollingRunning) {
|
||||||
|
this.logger.warn('Polling läuft bereits, überspringe');
|
||||||
|
return { processed: 0, updated: 0, skipped: 0, errors: ['Polling bereits aktiv'] };
|
||||||
|
}
|
||||||
|
this.pollingRunning = true;
|
||||||
|
|
||||||
|
const result: PollingResult = { processed: 0, updated: 0, skipped: 0, errors: [] };
|
||||||
|
this.logger.log('Starte Agrarmonitor-Polling');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [tagFertigSetting, tagVerbuchtSetting] = await Promise.all([
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }),
|
||||||
|
]);
|
||||||
|
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
||||||
|
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
||||||
|
|
||||||
|
if (isNaN(tagFertigId) || isNaN(tagVerbuchtId)) {
|
||||||
|
const msg = 'Tag-IDs ungültig (keine Zahlen)';
|
||||||
|
this.logger.error(msg);
|
||||||
|
return { ...result, errors: [msg] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let amClient: Awaited<ReturnType<typeof this.agrarmonitorService.getClient>>;
|
||||||
|
try {
|
||||||
|
amClient = await this.agrarmonitorService.getClient();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `Connector-Fehler: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
return { ...result, errors: [msg] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let customers: Awaited<ReturnType<typeof amClient.fetchCustomers>>;
|
||||||
|
try {
|
||||||
|
customers = await amClient.fetchCustomers();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `Kunden-Abruf fehlgeschlagen: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
return { ...result, errors: [msg] };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const customer of customers.filter(
|
||||||
|
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||||
|
)) {
|
||||||
|
try {
|
||||||
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
|
const existing = await this.paperlessService.getCorrespondentByName(displayName);
|
||||||
|
if (!existing) {
|
||||||
|
await this.paperlessService.addCorrespondent({
|
||||||
|
name: displayName,
|
||||||
|
match: '',
|
||||||
|
matching_algorithm: 0,
|
||||||
|
is_insensitive: true,
|
||||||
|
owner: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.logger.warn(`Korrespondenten-Sync fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsResponse = await this.paperlessService.getDocuments({
|
||||||
|
page: 1,
|
||||||
|
page_size: DOCS_PAGE_SIZE,
|
||||||
|
truncate_content: true,
|
||||||
|
tags__id__all: tagFertigId,
|
||||||
|
});
|
||||||
|
const docs: any[] = docsResponse?.results ?? [];
|
||||||
|
if ((docsResponse?.count ?? 0) > DOCS_PAGE_SIZE) {
|
||||||
|
this.logger.warn(`Mehr als ${DOCS_PAGE_SIZE} Dokumente bereit — nur erste ${DOCS_PAGE_SIZE} werden verarbeitet`);
|
||||||
|
}
|
||||||
|
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
result.processed++;
|
||||||
|
|
||||||
|
const interneBelegnummer =
|
||||||
|
((doc.custom_fields as any[]) ?? []).find(
|
||||||
|
(cf: any) => cf.field === INTERN_BELEGNUMMER_FIELD_ID,
|
||||||
|
)?.value as string ?? '';
|
||||||
|
|
||||||
|
if (!interneBelegnummer) {
|
||||||
|
this.logger.log(`Dokument ${doc.id as number} hat keine interne Belegnummer`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amResults: Awaited<ReturnType<typeof amClient.eingangsrechnungenLivesearch>>;
|
||||||
|
try {
|
||||||
|
amResults = await amClient.eingangsrechnungenLivesearch(interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status = (err as any)?.response?.status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
this.agrarmonitorService.clearClient();
|
||||||
|
const msg = `Session abgelaufen (${status}) — Polling abgebrochen, nächster Lauf meldet sich neu an`;
|
||||||
|
this.logger.warn(msg);
|
||||||
|
result.errors.push(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amResults.length === 0) {
|
||||||
|
this.logger.log(`${interneBelegnummer} nicht in Agrarmonitor gefunden`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amResults.length > 1) {
|
||||||
|
const msg = `${interneBelegnummer}: Mehrfach gefunden`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amDoc = amResults[0];
|
||||||
|
|
||||||
|
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
||||||
|
try {
|
||||||
|
await amClient.setLieferscheinNummer(amDoc.eingangId, interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.logger.warn(`${interneBelegnummer}: Lieferscheinnummer setzen fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amDoc.eingangsDatum) {
|
||||||
|
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
||||||
|
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
||||||
|
);
|
||||||
|
if (eingangsdatumField?.value) {
|
||||||
|
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
||||||
|
if (!isNaN(eingangsdatum.getTime())) {
|
||||||
|
await amClient.setEingangsdatum(amDoc.eingangId, eingangsdatum);
|
||||||
|
this.logger.log(`Eingangsdatum für ${interneBelegnummer} gesetzt`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.skipped++;
|
||||||
|
} else if (amDoc.buchungsDatum) {
|
||||||
|
try {
|
||||||
|
let correspondentId: number | undefined;
|
||||||
|
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
||||||
|
if (customer) {
|
||||||
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
|
let corr = await this.paperlessService.getCorrespondentByName(displayName);
|
||||||
|
if (!corr) {
|
||||||
|
corr = await this.paperlessService.addCorrespondent({
|
||||||
|
name: displayName,
|
||||||
|
match: '',
|
||||||
|
matching_algorithm: 0,
|
||||||
|
is_insensitive: true,
|
||||||
|
owner: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (corr) correspondentId = corr.id as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ownerId: number | undefined;
|
||||||
|
const matchedClient = await this.clientRepo.findOneBy({
|
||||||
|
AgrarmonitorBetriebId: amDoc.betriebId,
|
||||||
|
});
|
||||||
|
if (matchedClient) ownerId = matchedClient.PaperlessUserId;
|
||||||
|
|
||||||
|
const currentTags: number[] = (doc.tags as number[]) ?? [];
|
||||||
|
const newTags = [...new Set(currentTags.filter((t) => t !== tagFertigId).concat([tagVerbuchtId]))];
|
||||||
|
|
||||||
|
const updateData: Record<string, any> = { tags: newTags };
|
||||||
|
if (correspondentId !== undefined) updateData.correspondent = correspondentId;
|
||||||
|
if (ownerId !== undefined) updateData.owner = ownerId;
|
||||||
|
|
||||||
|
await this.paperlessService.updateDocument(doc.id as number, updateData);
|
||||||
|
this.logger.log(`Beleg ${interneBelegnummer} gebucht`);
|
||||||
|
result.updated++;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.skipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
||||||
|
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.pollingRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processVerarbeiteteDocuments(): Promise<PollingResult> {
|
||||||
|
if (this.uploadCheckRunning) {
|
||||||
|
this.logger.warn('Upload-Check läuft bereits, überspringe');
|
||||||
|
return { processed: 0, updated: 0, skipped: 0, errors: ['Upload-Check bereits aktiv'] };
|
||||||
|
}
|
||||||
|
this.uploadCheckRunning = true;
|
||||||
|
|
||||||
|
const result: PollingResult = { processed: 0, updated: 0, skipped: 0, errors: [] };
|
||||||
|
this.logger.log('Starte Upload-Check');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [hochgeladenSetting, fertigSetting, linkFieldSetting] = await Promise.all([
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
|
||||||
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tagHochgeladenId = parseInt(hochgeladenSetting?.Wert ?? '', 10);
|
||||||
|
const tagFertigId = parseInt(fertigSetting?.Wert ?? '4', 10);
|
||||||
|
const linkFieldId = parseInt(linkFieldSetting?.Wert ?? '', 10);
|
||||||
|
|
||||||
|
if (isNaN(tagHochgeladenId)) {
|
||||||
|
this.logger.warn('Tag "hochgeladen" nicht konfiguriert — Upload-Check übersprungen');
|
||||||
|
return { ...result, errors: ['Tag "hochgeladen" nicht konfiguriert'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let amClient: Awaited<ReturnType<typeof this.agrarmonitorService.getClient>>;
|
||||||
|
try {
|
||||||
|
amClient = await this.agrarmonitorService.getClient();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `Connector-Fehler: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
return { ...result, errors: [msg] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsResponse = await this.paperlessService.getDocuments({
|
||||||
|
page: 1,
|
||||||
|
page_size: DOCS_PAGE_SIZE,
|
||||||
|
truncate_content: true,
|
||||||
|
tags__id__all: tagHochgeladenId,
|
||||||
|
});
|
||||||
|
const docs: any[] = docsResponse?.results ?? [];
|
||||||
|
if ((docsResponse?.count ?? 0) > DOCS_PAGE_SIZE) {
|
||||||
|
this.logger.warn(`Mehr als ${DOCS_PAGE_SIZE} Dokumente hochgeladen — nur erste ${DOCS_PAGE_SIZE} werden geprüft`);
|
||||||
|
}
|
||||||
|
this.logger.log(`${docs.length} Dokumente laut Paperless im Dateieingang`);
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
result.processed++;
|
||||||
|
|
||||||
|
const interneBelegnummer =
|
||||||
|
((doc.custom_fields as any[]) ?? []).find(
|
||||||
|
(cf: any) => cf.field === INTERN_BELEGNUMMER_FIELD_ID,
|
||||||
|
)?.value as string ?? '';
|
||||||
|
|
||||||
|
if (!interneBelegnummer) {
|
||||||
|
this.logger.log(`Dokument ${doc.id as number} hat keine interne Belegnummer`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vorhanden: boolean;
|
||||||
|
try {
|
||||||
|
vorhanden = await amClient.eingangsrechnungVorhanden(interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status = (err as any)?.response?.status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
this.agrarmonitorService.clearClient();
|
||||||
|
const msg = `Session abgelaufen (${status}) — Upload-Check abgebrochen`;
|
||||||
|
this.logger.warn(msg);
|
||||||
|
result.errors.push(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const msg = `${interneBelegnummer}: Vorhanden-Check fehlgeschlagen`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vorhanden) {
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Dokument ${interneBelegnummer} ist bereits verarbeitet, aktualisiere Paperless`);
|
||||||
|
|
||||||
|
let amResults: Awaited<ReturnType<typeof amClient.eingangsrechnungenLivesearch>>;
|
||||||
|
try {
|
||||||
|
amResults = await amClient.eingangsrechnungenLivesearch(interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status = (err as any)?.response?.status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
this.agrarmonitorService.clearClient();
|
||||||
|
const msg = `Session abgelaufen (${status}) — Upload-Check abgebrochen`;
|
||||||
|
this.logger.warn(msg);
|
||||||
|
result.errors.push(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amResults.length > 1) {
|
||||||
|
this.logger.log(`Dokument ${interneBelegnummer} ist doppelt vorhanden`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amDoc = amResults[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Kundendaten abrufen
|
||||||
|
const customer = await amClient.getCustomerById(amDoc.kundenId);
|
||||||
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
|
if (!lieferantennummer) {
|
||||||
|
this.logger.log(`Kunde ${amDoc.kundenId} hat keine Lieferantennummer — Dokument wird übersprungen`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Korrespondent ermitteln oder anlegen
|
||||||
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
|
let corr = await this.paperlessService.getCorrespondentByName(displayName);
|
||||||
|
if (!corr) {
|
||||||
|
corr = await this.paperlessService.addCorrespondent({
|
||||||
|
name: displayName,
|
||||||
|
match: '',
|
||||||
|
matching_algorithm: 0,
|
||||||
|
is_insensitive: true,
|
||||||
|
owner: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner aus Client-Tabelle
|
||||||
|
let ownerId: number | undefined;
|
||||||
|
const matchedClient = await this.clientRepo.findOneBy({ AgrarmonitorBetriebId: amDoc.betriebId });
|
||||||
|
if (matchedClient) ownerId = matchedClient.PaperlessUserId;
|
||||||
|
|
||||||
|
// Tags: hochgeladen entfernen, fertig hinzufügen
|
||||||
|
const currentTags: number[] = (doc.tags as number[]) ?? [];
|
||||||
|
const newTags = [...new Set(currentTags.filter((t) => t !== tagHochgeladenId).concat([tagFertigId]))];
|
||||||
|
|
||||||
|
// Custom fields aufbauen: bestehende behalten, extern + link setzen
|
||||||
|
const existingFields: any[] = ((doc.custom_fields as any[]) ?? []).map((f: any) => ({ ...f }));
|
||||||
|
this.setCustomField(existingFields, EXTERN_BELEGNUMMER_FIELD_ID, amDoc.belegNummer);
|
||||||
|
if (!isNaN(linkFieldId)) {
|
||||||
|
this.setCustomField(
|
||||||
|
existingFields,
|
||||||
|
linkFieldId,
|
||||||
|
`${AGRARMONITOR_BASE_URL}/rechnungen/detail/${amDoc.eingangId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: Record<string, any> = {
|
||||||
|
title: (amDoc.dokumentTyp === 0 ? 'ERG ' : 'EGU ') + amDoc.belegNummer,
|
||||||
|
document_type: amDoc.dokumentTyp === 0 ? 1 : 2,
|
||||||
|
tags: newTags,
|
||||||
|
custom_fields: existingFields,
|
||||||
|
};
|
||||||
|
if (amDoc.belegDatum) updateData.created = amDoc.belegDatum.toISOString().slice(0, 10);
|
||||||
|
if (corr) updateData.correspondent = corr.id as number;
|
||||||
|
if (ownerId !== undefined) updateData.owner = ownerId;
|
||||||
|
|
||||||
|
await this.paperlessService.updateDocument(doc.id as number, updateData);
|
||||||
|
await this.paperlessService.addNote(
|
||||||
|
doc.id as number,
|
||||||
|
`Beleg in Agrarmonitor verarbeitet: ${new Date().toLocaleString('de-DE')}`,
|
||||||
|
);
|
||||||
|
this.logger.log(`Beleg ${interneBelegnummer} auf AMfertig gesetzt`);
|
||||||
|
result.updated++;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Upload-Check abgeschlossen: ${result.processed} geprüft, ${result.updated} aktualisiert, ` +
|
||||||
|
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.uploadCheckRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCustomField(fields: any[], fieldId: number, value: any): void {
|
||||||
|
const existing = fields.find((f) => f.field === fieldId);
|
||||||
|
if (existing) {
|
||||||
|
existing.value = value;
|
||||||
|
} else {
|
||||||
|
fields.push({ field: fieldId, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCustomerName(customer: Record<string, unknown>, nummer: string): string {
|
||||||
|
const firma = (customer['firma'] as string) ?? '';
|
||||||
|
const nachname = (customer['nachname'] as string) ?? '';
|
||||||
|
const vorname = (customer['vorname'] as string) ?? '';
|
||||||
|
const name = firma || (nachname + (vorname ? ', ' + vorname : ''));
|
||||||
|
return `${name} (${nummer})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertSetting(tag: string, defaultValue: string): Promise<void> {
|
||||||
|
const existing = await this.settingRepo.findOneBy({ Tag: tag });
|
||||||
|
if (!existing) {
|
||||||
|
await this.settingRepo.save(
|
||||||
|
this.settingRepo.create({ Typ: 1, Wert: defaultValue, Tag: tag }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, Post, Put } from '@nestjs/common';
|
||||||
|
import { AgrarmonitorService } from './agrarmonitor.service';
|
||||||
|
import { AgrarmonitorPollingService } from './agrarmonitor-polling.service';
|
||||||
|
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||||
|
import { Permission } from '../auth/permissions.enum';
|
||||||
|
|
||||||
|
@Controller('api/agrarmonitor')
|
||||||
|
export class AgrarmonitorController {
|
||||||
|
constructor(
|
||||||
|
private readonly service: AgrarmonitorService,
|
||||||
|
private readonly pollingService: AgrarmonitorPollingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('status')
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async getStatus() {
|
||||||
|
return this.service.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
@HttpCode(200)
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async registerDevice(@Body() body: { pcName: string; agrarmonitorId: string }) {
|
||||||
|
return this.service.registerDevice(body.pcName, body.agrarmonitorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('polling-config')
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async getPollingConfig() {
|
||||||
|
return this.pollingService.getPollingConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('polling-config')
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async updatePollingConfig(@Body() body: { tagFertig: string; tagVerbucht: string; tagHochgeladen: string; linkField: string }) {
|
||||||
|
return this.pollingService.updatePollingConfig(body.tagFertig, body.tagVerbucht, body.tagHochgeladen, body.linkField);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('run-polling')
|
||||||
|
@HttpCode(200)
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async runPolling() {
|
||||||
|
return this.pollingService.runPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('process-uploads')
|
||||||
|
@HttpCode(200)
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async processUploads() {
|
||||||
|
return this.pollingService.processVerarbeiteteDocuments();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AgrarmonitorService } from './agrarmonitor.service';
|
||||||
|
import { AgrarmonitorPollingService } from './agrarmonitor-polling.service';
|
||||||
|
import { AgrarmonitorController } from './agrarmonitor.controller';
|
||||||
|
import { PaperlessModule } from '../paperless/paperless.module';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
import { Client } from '../database/entities/client.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Setting, Client]),
|
||||||
|
PaperlessModule,
|
||||||
|
],
|
||||||
|
providers: [AgrarmonitorService, AgrarmonitorPollingService],
|
||||||
|
controllers: [AgrarmonitorController],
|
||||||
|
exports: [AgrarmonitorService],
|
||||||
|
})
|
||||||
|
export class AgrarmonitorModule {}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import {
|
||||||
|
createAgrarmonitorClient,
|
||||||
|
FileCookieStore,
|
||||||
|
AesGcmCookieEncryptor,
|
||||||
|
type AgrarmonitorConnectorResult,
|
||||||
|
} from 'agrarmonitor-connector';
|
||||||
|
|
||||||
|
export interface AgrarmonitorStatusDto {
|
||||||
|
connected: boolean;
|
||||||
|
registriert: boolean | null;
|
||||||
|
freigeschaltet: boolean | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgrarmonitorRegisterResultDto {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgrarmonitorService {
|
||||||
|
private readonly logger = new Logger(AgrarmonitorService.name);
|
||||||
|
private client: AgrarmonitorConnectorResult | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
async getClient(): Promise<AgrarmonitorConnectorResult> {
|
||||||
|
if (this.client) return this.client;
|
||||||
|
|
||||||
|
const username = this.configService.get<string>('AGRARMONITOR_USERNAME', '');
|
||||||
|
const password = this.configService.get<string>('AGRARMONITOR_PASSWORD', '');
|
||||||
|
const baseUrl = this.configService.get<string>('AGRARMONITOR_BASE_URL', 'https://admin7.agrarmonitor.de');
|
||||||
|
const apiBaseUrl = this.configService.get<string>('AGRARMONITOR_API_BASE_URL', 'https://api.agrarmonitor.de');
|
||||||
|
const apiToken = this.configService.get<string>('AGRARMONITOR_API_TOKEN');
|
||||||
|
const cookiePath = this.configService.get<string>('AGRARMONITOR_COOKIE_PATH', './data/agrarmonitor-cookies.json');
|
||||||
|
const encryptionKey = this.configService.get<string>('AGRARMONITOR_ENCRYPTION_KEY');
|
||||||
|
|
||||||
|
const encryptor = encryptionKey ? new AesGcmCookieEncryptor(encryptionKey) : undefined;
|
||||||
|
const cookieStore = new FileCookieStore(cookiePath, { encryptor, logger: this.logger });
|
||||||
|
|
||||||
|
this.client = await createAgrarmonitorClient({
|
||||||
|
baseUrl,
|
||||||
|
apiBaseUrl,
|
||||||
|
apiToken,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
cookieStore,
|
||||||
|
autoLogin: true,
|
||||||
|
autoRetry: false,
|
||||||
|
timeoutMs: 10000,
|
||||||
|
loginStrategy: 'redirect',
|
||||||
|
logger: this.logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearClient(): void {
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(): Promise<AgrarmonitorStatusDto> {
|
||||||
|
try {
|
||||||
|
const client = await this.getClient();
|
||||||
|
const [registrierungStatus, freigeschaltetStatus] = await Promise.all([
|
||||||
|
client.checkRegistriert(),
|
||||||
|
client.checkFreigeschaltet(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
connected: true,
|
||||||
|
registriert: registrierungStatus.registriert,
|
||||||
|
freigeschaltet: freigeschaltetStatus.freigeschaltet,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
this.client = null;
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
registriert: null,
|
||||||
|
freigeschaltet: null,
|
||||||
|
error: err?.message ?? 'Verbindung fehlgeschlagen',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerDevice(pcName: string, agrarmonitorId: string): Promise<AgrarmonitorRegisterResultDto> {
|
||||||
|
const client = await this.getClient();
|
||||||
|
const result = await client.registerDevice({ agrarmonitorId, pcName });
|
||||||
|
return { success: result.success, message: result.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { BarcodeModule } from './barcode/barcode.module';
|
|||||||
import { InboxPostprocessorModule } from './inbox-postprocessor/inbox-postprocessor.module';
|
import { InboxPostprocessorModule } from './inbox-postprocessor/inbox-postprocessor.module';
|
||||||
import { UserSettingsModule } from './user-settings/user-settings.module';
|
import { UserSettingsModule } from './user-settings/user-settings.module';
|
||||||
import { LabelPrintAgentModule } from './label-print-agent/label-print-agent.module';
|
import { LabelPrintAgentModule } from './label-print-agent/label-print-agent.module';
|
||||||
|
import { AgrarmonitorModule } from './agrarmonitor/agrarmonitor.module';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -47,6 +48,7 @@ import * as path from 'path';
|
|||||||
InboxPostprocessorModule,
|
InboxPostprocessorModule,
|
||||||
UserSettingsModule,
|
UserSettingsModule,
|
||||||
LabelPrintAgentModule,
|
LabelPrintAgentModule,
|
||||||
|
AgrarmonitorModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -10,4 +10,7 @@ export class Client {
|
|||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
PaperlessUserId!: number;
|
PaperlessUserId!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
AgrarmonitorBetriebId!: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,13 +376,26 @@ export class SettingsController {
|
|||||||
async updateCorrespondentSetting(@Param('id') id: string, @Body() body: { agrarmonitorId: number | null }) {
|
async updateCorrespondentSetting(@Param('id') id: string, @Body() body: { agrarmonitorId: number | null }) {
|
||||||
const corrId = parseInt(id, 10);
|
const corrId = parseInt(id, 10);
|
||||||
let setting = await this.corrSettingRepo.findOneBy({ CorrespondentId: corrId });
|
let setting = await this.corrSettingRepo.findOneBy({ CorrespondentId: corrId });
|
||||||
|
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
setting = this.corrSettingRepo.create({ CorrespondentId: corrId, AgrarmonitorId: body.agrarmonitorId });
|
setting = this.corrSettingRepo.create({ CorrespondentId: corrId, AgrarmonitorId: body.agrarmonitorId });
|
||||||
} else {
|
} else {
|
||||||
setting.AgrarmonitorId = body.agrarmonitorId;
|
setting.AgrarmonitorId = body.agrarmonitorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.corrSettingRepo.save(setting);
|
return this.corrSettingRepo.save(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Betriebe ===
|
||||||
|
@Get('clients')
|
||||||
|
async getClients() {
|
||||||
|
return this.clientRepo.find({ order: { Name: 'ASC' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('clients/:id')
|
||||||
|
async updateClient(@Param('id') id: string, @Body() body: { AgrarmonitorBetriebId: number | null }) {
|
||||||
|
const clientId = parseInt(id, 10);
|
||||||
|
await this.clientRepo.update(clientId, { AgrarmonitorBetriebId: body.AgrarmonitorBetriebId ?? null });
|
||||||
|
return this.clientRepo.findOneByOrFail({ Id: clientId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ export interface SettingUserClient {
|
|||||||
Role: 'viewer' | 'editor' | 'admin';
|
Role: 'viewer' | 'editor' | 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SettingClient {
|
||||||
|
Id: number;
|
||||||
|
Name: string;
|
||||||
|
PaperlessUserId: number;
|
||||||
|
AgrarmonitorBetriebId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const settingsApi = {
|
export const settingsApi = {
|
||||||
// Dokumenttypen
|
// Dokumenttypen
|
||||||
getDocTypes: () => api.get<SettingDocType[]>('/api/settings/document-types').then(r => r.data),
|
getDocTypes: () => api.get<SettingDocType[]>('/api/settings/document-types').then(r => r.data),
|
||||||
@@ -144,6 +151,11 @@ export const settingsApi = {
|
|||||||
updateCorrespondentSetting: (id: number, agrarmonitorId: number | null) =>
|
updateCorrespondentSetting: (id: number, agrarmonitorId: number | null) =>
|
||||||
api.put<any>(`/api/settings/correspondents/${id}`, { agrarmonitorId }).then(r => r.data),
|
api.put<any>(`/api/settings/correspondents/${id}`, { agrarmonitorId }).then(r => r.data),
|
||||||
|
|
||||||
|
// Betriebe
|
||||||
|
getClients: () => api.get<SettingClient[]>('/api/settings/clients').then(r => r.data),
|
||||||
|
updateClient: (id: number, AgrarmonitorBetriebId: number | null) =>
|
||||||
|
api.put<SettingClient>(`/api/settings/clients/${id}`, { AgrarmonitorBetriebId }).then(r => r.data),
|
||||||
|
|
||||||
// Inbox-Postprozessor (global, deprecated)
|
// Inbox-Postprozessor (global, deprecated)
|
||||||
listInboxActions: () =>
|
listInboxActions: () =>
|
||||||
api.get<InboxAction[]>('/api/settings/inbox-actions').then((r) => r.data),
|
api.get<InboxAction[]>('/api/settings/inbox-actions').then((r) => r.data),
|
||||||
@@ -176,3 +188,41 @@ export const INBOX_ACTION_LABELS: Record<InboxActionType, string> = {
|
|||||||
EXPORT: 'Export (FTP/WebDAV)',
|
EXPORT: 'Export (FTP/WebDAV)',
|
||||||
PAPERLESS: 'In Paperless importieren',
|
PAPERLESS: 'In Paperless importieren',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AgrarmonitorStatusData {
|
||||||
|
connected: boolean;
|
||||||
|
registriert: boolean | null;
|
||||||
|
freigeschaltet: boolean | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgrarmonitorPollingConfig {
|
||||||
|
tagFertig: string;
|
||||||
|
tagVerbucht: string;
|
||||||
|
tagHochgeladen: string;
|
||||||
|
linkField: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgrarmonitorPollingResult {
|
||||||
|
processed: number;
|
||||||
|
updated: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agrarmonitorApi = {
|
||||||
|
getStatus: () =>
|
||||||
|
api.get<AgrarmonitorStatusData>('/api/agrarmonitor/status').then((r) => r.data),
|
||||||
|
registerDevice: (pcName: string, agrarmonitorId: string) =>
|
||||||
|
api
|
||||||
|
.post<{ success: boolean; message: string }>('/api/agrarmonitor/register', { pcName, agrarmonitorId })
|
||||||
|
.then((r) => r.data),
|
||||||
|
getPollingConfig: () =>
|
||||||
|
api.get<AgrarmonitorPollingConfig>('/api/agrarmonitor/polling-config').then((r) => r.data),
|
||||||
|
updatePollingConfig: (config: AgrarmonitorPollingConfig) =>
|
||||||
|
api.put<AgrarmonitorPollingConfig>('/api/agrarmonitor/polling-config', config).then((r) => r.data),
|
||||||
|
runPolling: () =>
|
||||||
|
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/run-polling').then((r) => r.data),
|
||||||
|
processUploads: () =>
|
||||||
|
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/process-uploads').then((r) => r.data),
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
UserOutlined, FileTextOutlined, ThunderboltOutlined,
|
UserOutlined, FileTextOutlined, ThunderboltOutlined,
|
||||||
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
||||||
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
||||||
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined,
|
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, GlobalOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
type SettingPostprocessingLog, type FilterGroup, type FilterCondition,
|
type SettingPostprocessingLog, type FilterGroup, type FilterCondition,
|
||||||
INBOX_ACTION_LABELS,
|
INBOX_ACTION_LABELS,
|
||||||
type InboxAction, type InboxActionType,
|
type InboxAction, type InboxActionType,
|
||||||
|
agrarmonitorApi, type AgrarmonitorStatusData,
|
||||||
|
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
|
||||||
} from '../api/settings';
|
} from '../api/settings';
|
||||||
import { clientsApi, type Client } from '../api/inbox';
|
import { clientsApi, type Client } from '../api/inbox';
|
||||||
import { apiKeysApi, type ApiKey } from '../api/api-keys';
|
import { apiKeysApi, type ApiKey } from '../api/api-keys';
|
||||||
@@ -212,7 +214,9 @@ function FilterBuilder({ value, onChange, tags, docTypes, correspondents, custom
|
|||||||
function UserClientsTab() {
|
function UserClientsTab() {
|
||||||
const [data, setData] = useState<SettingUserClient[]>([]);
|
const [data, setData] = useState<SettingUserClient[]>([]);
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
|
const [allClients, setAllClients] = useState<SettingClient[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [clientsLoading, setClientsLoading] = useState(false);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
@@ -228,7 +232,15 @@ function UserClientsTab() {
|
|||||||
} finally { setLoading(false); }
|
} finally { setLoading(false); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
const loadAllClients = useCallback(async () => {
|
||||||
|
setClientsLoading(true);
|
||||||
|
try {
|
||||||
|
const cls = await settingsApi.getClients();
|
||||||
|
setAllClients(cls);
|
||||||
|
} finally { setClientsLoading(false); }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { load(); loadAllClients(); }, [load, loadAllClients]);
|
||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
@@ -245,6 +257,37 @@ function UserClientsTab() {
|
|||||||
load();
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateBetriebId = async (id: number, val: number | null) => {
|
||||||
|
try {
|
||||||
|
const updated = await settingsApi.updateClient(id, val);
|
||||||
|
setAllClients(prev => prev.map(c => c.Id === id ? updated : c));
|
||||||
|
} catch {
|
||||||
|
message.error('Speichern fehlgeschlagen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allClientColumns: ColumnsType<SettingClient> = [
|
||||||
|
{ title: 'Name', dataIndex: 'Name', key: 'name' },
|
||||||
|
{
|
||||||
|
title: 'Agrarmonitor-BetriebId',
|
||||||
|
dataIndex: 'AgrarmonitorBetriebId',
|
||||||
|
key: 'betriebId',
|
||||||
|
render: (val: number | null, record) => (
|
||||||
|
<InputNumber
|
||||||
|
value={val ?? undefined}
|
||||||
|
placeholder="–"
|
||||||
|
min={1}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const parsed = e.target.value ? parseInt(e.target.value, 10) : null;
|
||||||
|
const current = val ?? null;
|
||||||
|
if (parsed !== current) handleUpdateBetriebId(record.Id, isNaN(parsed as number) ? null : parsed);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const columns: ColumnsType<SettingUserClient> = [
|
const columns: ColumnsType<SettingUserClient> = [
|
||||||
{ title: 'User ID', dataIndex: 'UserId', key: 'userId' },
|
{ title: 'User ID', dataIndex: 'UserId', key: 'userId' },
|
||||||
{
|
{
|
||||||
@@ -276,6 +319,21 @@ function UserClientsTab() {
|
|||||||
Zuordnung hinzufügen
|
Zuordnung hinzufügen
|
||||||
</Button>
|
</Button>
|
||||||
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
|
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title level={5} style={{ marginBottom: 8 }}>Betriebe — Agrarmonitor-Zuordnung</Typography.Title>
|
||||||
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||||
|
Ordne jedem Betrieb die zugehörige Agrarmonitor-BetriebId zu. Wird beim Polling verwendet.
|
||||||
|
</Typography.Text>
|
||||||
|
<Table
|
||||||
|
dataSource={allClients}
|
||||||
|
columns={allClientColumns}
|
||||||
|
loading={clientsLoading}
|
||||||
|
rowKey="Id"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal title="Neue Zuordnung" open={modalOpen} onOk={handleAdd} onCancel={() => setModalOpen(false)}>
|
<Modal title="Neue Zuordnung" open={modalOpen} onOk={handleAdd} onCancel={() => setModalOpen(false)}>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item name="UserId" label="User ID (Authentik)" rules={[{ required: true }]}>
|
<Form.Item name="UserId" label="User ID (Authentik)" rules={[{ required: true }]}>
|
||||||
@@ -2221,6 +2279,283 @@ function BarcodeTemplatesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Agrarmonitor Tab
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function AgrarmonitorTab() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [pollingForm] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [registering, setRegistering] = useState(false);
|
||||||
|
const [pollingConfigLoading, setPollingConfigLoading] = useState(false);
|
||||||
|
const [pollingSaving, setPollingSaving] = useState(false);
|
||||||
|
const [pollingRunning, setPollingRunning] = useState(false);
|
||||||
|
const [uploadCheckRunning, setUploadCheckRunning] = useState(false);
|
||||||
|
const [status, setStatus] = useState<AgrarmonitorStatusData | null>(null);
|
||||||
|
const [registerResult, setRegisterResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
const [pollingResult, setPollingResult] = useState<AgrarmonitorPollingResult | null>(null);
|
||||||
|
const [uploadCheckResult, setUploadCheckResult] = useState<AgrarmonitorPollingResult | null>(null);
|
||||||
|
const [customFields, setCustomFields] = useState<PaperlessCustomField[]>([]);
|
||||||
|
|
||||||
|
const handleLoadStatus = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setRegisterResult(null);
|
||||||
|
try {
|
||||||
|
const data = await agrarmonitorApi.getStatus();
|
||||||
|
setStatus(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err?.code === 'ECONNABORTED'
|
||||||
|
? 'Timeout – Backend antwortet nicht rechtzeitig'
|
||||||
|
: (err?.response?.data?.message ?? err?.message ?? 'Netzwerkfehler');
|
||||||
|
setStatus({ connected: false, registriert: null, freigeschaltet: null, error: msg });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setRegistering(true);
|
||||||
|
setRegisterResult(null);
|
||||||
|
try {
|
||||||
|
const result = await agrarmonitorApi.registerDevice(values.pcName, values.agrarmonitorId);
|
||||||
|
setRegisterResult(result);
|
||||||
|
if (result.success) {
|
||||||
|
message.success('Gerät erfolgreich registriert');
|
||||||
|
await handleLoadStatus();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setRegisterResult({ success: false, message: 'Registrierung fehlgeschlagen' });
|
||||||
|
} finally {
|
||||||
|
setRegistering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadPollingConfig = useCallback(async () => {
|
||||||
|
setPollingConfigLoading(true);
|
||||||
|
try {
|
||||||
|
const cfg = await agrarmonitorApi.getPollingConfig();
|
||||||
|
pollingForm.setFieldsValue(cfg);
|
||||||
|
} catch {
|
||||||
|
message.error('Polling-Konfiguration konnte nicht geladen werden');
|
||||||
|
} finally {
|
||||||
|
setPollingConfigLoading(false);
|
||||||
|
}
|
||||||
|
}, [pollingForm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleLoadPollingConfig();
|
||||||
|
paperlessApi.getCustomFields().then(setCustomFields).catch(() => {});
|
||||||
|
}, [handleLoadPollingConfig]);
|
||||||
|
|
||||||
|
const handleSavePollingConfig = async () => {
|
||||||
|
const values = await pollingForm.validateFields() as AgrarmonitorPollingConfig;
|
||||||
|
setPollingSaving(true);
|
||||||
|
try {
|
||||||
|
await agrarmonitorApi.updatePollingConfig(values);
|
||||||
|
message.success('Konfiguration gespeichert');
|
||||||
|
} catch {
|
||||||
|
message.error('Speichern fehlgeschlagen');
|
||||||
|
} finally {
|
||||||
|
setPollingSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRunPolling = async () => {
|
||||||
|
setPollingRunning(true);
|
||||||
|
setPollingResult(null);
|
||||||
|
try {
|
||||||
|
const result = await agrarmonitorApi.runPolling();
|
||||||
|
setPollingResult(result);
|
||||||
|
} catch {
|
||||||
|
message.error('Polling fehlgeschlagen');
|
||||||
|
} finally {
|
||||||
|
setPollingRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProcessUploads = async () => {
|
||||||
|
setUploadCheckRunning(true);
|
||||||
|
setUploadCheckResult(null);
|
||||||
|
try {
|
||||||
|
const result = await agrarmonitorApi.processUploads();
|
||||||
|
setUploadCheckResult(result);
|
||||||
|
} catch {
|
||||||
|
message.error('Upload-Check fehlgeschlagen');
|
||||||
|
} finally {
|
||||||
|
setUploadCheckRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStatusTag = (value: boolean | null, labelTrue: string, labelFalse: string) => {
|
||||||
|
if (value === null) return <Tag>–</Tag>;
|
||||||
|
return value
|
||||||
|
? <Tag color="success">{labelTrue}</Tag>
|
||||||
|
: <Tag color="error">{labelFalse}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 600 }}>
|
||||||
|
<Typography.Title level={4}>Agrarmonitor</Typography.Title>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
Verbindungsstatus und Geräte-Registrierung für die Agrarmonitor-Schnittstelle.
|
||||||
|
Zugangsdaten werden in der <code>.env</code> konfiguriert.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Button loading={loading} onClick={handleLoadStatus}>
|
||||||
|
Status abrufen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status && (
|
||||||
|
<Card size="small" title="Status">
|
||||||
|
<Space direction="vertical">
|
||||||
|
<div>
|
||||||
|
<strong>Verbindung: </strong>
|
||||||
|
{status.connected
|
||||||
|
? <Tag color="success">Verbunden</Tag>
|
||||||
|
: <Tag color="error">Nicht verbunden</Tag>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Registriert: </strong>
|
||||||
|
{renderStatusTag(status.registriert, 'Ja', 'Nein')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Freigeschaltet: </strong>
|
||||||
|
{renderStatusTag(status.freigeschaltet, 'Ja', 'Nein')}
|
||||||
|
</div>
|
||||||
|
{status.error && (
|
||||||
|
<div style={{ color: '#ff4d4f' }}>{status.error}</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status?.registriert === false && (
|
||||||
|
<Card size="small" title="Gerät registrieren">
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="pcName"
|
||||||
|
label="PC-Name"
|
||||||
|
rules={[{ required: true, message: 'Bitte PC-Name eingeben' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="BUERO-PC-01" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="agrarmonitorId"
|
||||||
|
label="Agrarmonitor-ID / Firma"
|
||||||
|
rules={[{ required: true, message: 'Bitte Agrarmonitor-ID eingeben' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Agrarmonitor-ID" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" loading={registering} onClick={handleRegister}>
|
||||||
|
Gerät registrieren
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
{registerResult && (
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<Tag color={registerResult.success ? 'success' : 'error'}>
|
||||||
|
{registerResult.message}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card size="small" title="Polling-Konfiguration" loading={pollingConfigLoading}>
|
||||||
|
<Form form={pollingForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="tagFertig"
|
||||||
|
label="Tag-ID: Fertig in Agrarmonitor"
|
||||||
|
rules={[{ required: true, message: 'Pflichtfeld' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="4" style={{ width: 120 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="tagVerbucht"
|
||||||
|
label="Tag-ID: Verbucht"
|
||||||
|
rules={[{ required: true, message: 'Pflichtfeld' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="9" style={{ width: 120 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="tagHochgeladen" label="Tag-ID: Hochgeladen in Agrarmonitor">
|
||||||
|
<Input placeholder="3" style={{ width: 120 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="linkField" label="Custom Field: Agrarmonitor-Link">
|
||||||
|
<Select allowClear placeholder="Kein Feld ausgewählt" style={{ width: 280 }}>
|
||||||
|
{customFields.map(f => (
|
||||||
|
<Select.Option key={f.id} value={String(f.id)}>{f.id}: {f.name}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" loading={pollingSaving} onClick={handleSavePollingConfig}>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="Polling ausführen">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Button loading={pollingRunning} onClick={handleRunPolling}>
|
||||||
|
Jetzt ausführen
|
||||||
|
</Button>
|
||||||
|
{pollingResult && (
|
||||||
|
<div>
|
||||||
|
<Tag color="blue">{pollingResult.processed} verarbeitet</Tag>
|
||||||
|
<Tag color="success">{pollingResult.updated} aktualisiert</Tag>
|
||||||
|
<Tag>{pollingResult.skipped} übersprungen</Tag>
|
||||||
|
{pollingResult.errors.length > 0 && (
|
||||||
|
<Tag color="error">{pollingResult.errors.length} Fehler</Tag>
|
||||||
|
)}
|
||||||
|
{pollingResult.errors.length > 0 && (
|
||||||
|
<ul style={{ marginTop: 8, paddingLeft: 20 }}>
|
||||||
|
{pollingResult.errors.map((e, i) => (
|
||||||
|
<li key={i} style={{ color: '#ff4d4f' }}>{e}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="Dokumenten-Verarbeitung">
|
||||||
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||||
|
Prüft Belege mit Tag "Hochgeladen in Agrarmonitor" und setzt den Tag auf "AMfertig",
|
||||||
|
sobald sie im Agrarmonitor-Buchungssystem erscheinen.
|
||||||
|
</Typography.Text>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Button loading={uploadCheckRunning} onClick={handleProcessUploads}>
|
||||||
|
Jetzt prüfen
|
||||||
|
</Button>
|
||||||
|
{uploadCheckResult && (
|
||||||
|
<div>
|
||||||
|
<Tag color="blue">{uploadCheckResult.processed} geprüft</Tag>
|
||||||
|
<Tag color="success">{uploadCheckResult.updated} aktualisiert</Tag>
|
||||||
|
<Tag>{uploadCheckResult.skipped} übersprungen</Tag>
|
||||||
|
{uploadCheckResult.errors.length > 0 && (
|
||||||
|
<Tag color="error">{uploadCheckResult.errors.length} Fehler</Tag>
|
||||||
|
)}
|
||||||
|
{uploadCheckResult.errors.length > 0 && (
|
||||||
|
<ul style={{ marginTop: 8, paddingLeft: 20 }}>
|
||||||
|
{uploadCheckResult.errors.map((e, i) => (
|
||||||
|
<li key={i} style={{ color: '#ff4d4f' }}>{e}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// Settings Page
|
// Settings Page
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -2277,6 +2612,11 @@ export default function SettingsPage() {
|
|||||||
label: <span><KeyOutlined /> API-Keys</span>,
|
label: <span><KeyOutlined /> API-Keys</span>,
|
||||||
children: <ApiKeysTab />,
|
children: <ApiKeysTab />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'agrarmonitor',
|
||||||
|
label: <span><GlobalOutlined /> Agrarmonitor</span>,
|
||||||
|
children: <AgrarmonitorTab />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user