From 9707b6700d50bb8d99c3a55fc32d09c6fc931e83 Mon Sep 17 00:00:00 2001
From: ah-admin
Date: Sun, 4 Oct 2026 14:38:51 +0200
Subject: [PATCH] v1.23: Nebenkostenabrechnung als PDF aus Kosten- und
Zaehlermodul
---
UAT-Featureliste Mieterportal.xlsx | Bin 11548 -> 11970 bytes
kimi.md | 17 +
make-uat-liste.py | 29 +-
server/package-lock.json | 822 +++++++++++++++++++++++++++++
server/package.json | 1 +
server/src/abrechnung.js | 245 +++++++++
server/src/index.js | 19 +
src/pages/Kosten.tsx | 118 ++++-
src/store/store.tsx | 23 +
9 files changed, 1258 insertions(+), 16 deletions(-)
create mode 100644 server/src/abrechnung.js
diff --git a/UAT-Featureliste Mieterportal.xlsx b/UAT-Featureliste Mieterportal.xlsx
index c3a7849313cf65e5aeeede68b098b26b0cd331ea..2bd0e455048639856704d32c564be6c78aeb40b1 100644
GIT binary patch
delta 7635
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
PD0*H-j9F5Ke)oR>lNiy6
delta 7220
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.pdf`, idempotentes
+documents-Row (Titel `Nebenkostenabrechnung – , Whg. `,
+Kategorie „Nebenkostenabrechnung“, alte Datei wird ersetzt). Endpoint
+`POST /api/abrechnungen` (auth, adminOnly) → `{dokument, zusammenfassung}`.
+Frontend: `AbrechnungDialog` in src/pages/Kosten.tsx (Button im Kosten-Header
+neben KostenDialog), Ergebnisbox mit Download-Link. E2E live: Testkosten
+(hausweit + wohnungsbezogen) → Abrechnung we1/2026 → PDF geprüft (Umlaute,
+€, ² ok) → Testdaten bereinigt. UAT-Liste: neue Spalte „Ziel-Version" (Spalte
+K), COUNTIF-Bugfix (zählte fälschlich Spalte I statt J).
+
**Erledigt 04.10. (v1.22):** Direkt-E-Mail der Verwaltung an Mieter mit
automatischem CC an die Verwaltung. Mail-API (mail-server/api/server.py,
auf dem Server manuell ausgerollt, Container mp-mail-api per docker restart –
diff --git a/make-uat-liste.py b/make-uat-liste.py
index c4a28e2..ca0a2d2 100644
--- a/make-uat-liste.py
+++ b/make-uat-liste.py
@@ -20,11 +20,11 @@ ws = wb.active
ws.title = 'Featureliste'
ws.sheet_view.showGridLines = False
-ws.merge_cells('B2:K2')
+ws.merge_cells('B2:L2')
c = ws['B2']
c.value = 'UAT & Feature-Wunschliste – Hinterstein Mieterportal'
c.font = Font(bold=True, size=16, color=DUNKEL)
-ws.merge_cells('B3:K3')
+ws.merge_cells('B3:L3')
c = ws['B3']
c.value = ('Gewünschte Features erfassen: Zeile ergänzen, Modul wählen, Priorität setzen. '
'Die Entwicklung aktualisiert den Status. Beispielzeilen 001–003 gern überschreiben.')
@@ -36,15 +36,15 @@ for i, (label, wert) in enumerate(kennz):
col = 2 + i * 2
lc = ws.cell(row=5, column=col, value=label)
lc.font = Font(size=9, bold=True, color='64748B')
- vc = ws.cell(row=6, column=col, value=f'=COUNTIF($I$10:$I$109,"{wert}")')
+ vc = ws.cell(row=6, column=col, value=f'=COUNTIF($J$10:$J$109,"{wert}")')
vc.font = Font(size=14, bold=True, color=AKZENT)
vc.alignment = Alignment(horizontal='left')
KOPF = ['ID', 'Modul', 'Feature / Anforderung', 'Beschreibung / Akzeptanzkriterium',
'Priorität', 'Wunschtermin', 'Eingereicht von', 'Eingereicht am', 'Status',
- 'Ergebnis im Test / Bemerkung']
-BREITEN = [7, 20, 30, 48, 11, 13, 15, 12, 12, 34]
-SPALTEN = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
+ 'Ziel-Version', 'Ergebnis im Test / Bemerkung']
+BREITEN = [7, 20, 30, 48, 11, 13, 15, 12, 12, 12, 34]
+SPALTEN = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
HEADER_ZEILE = 9
for j, (name, breite, buchstabe) in enumerate(zip(KOPF, BREITEN, SPALTEN)):
@@ -64,23 +64,23 @@ STATUS = ['Offen', 'In Prüfung', 'Umgesetzt', 'Abgelehnt']
beispiele = [
['001', 'Steuer & Kosten', 'Nebenkostenabrechnung als PDF',
'Pro Wohnung und Abrechnungsjahr eine PDF-Abrechnung aus Kosten + Zählerständen erzeugen und im Dokumentenmodul ablegen.',
- 'Mittel', '', '', '', 'Offen', ''],
+ 'Mittel', '', '', '', 'Umgesetzt', 'v1.23', ''],
['002', 'Benutzerverwaltung', 'E-Mail-Adressen importierter Mieter erfassen',
'Fehlende E-Mail-Adressen der importierten Mieter erfassen, damit Benachrichtigungen versendet werden können.',
- 'Hoch', '', '', '', 'Offen', ''],
+ 'Hoch', '', '', '', 'Offen', '', ''],
['003', 'Mietkonto & Zahlungen', 'Zahlungseingang manuell als erledigt markieren',
'Verwaltung kann eine offene Buchung als „bezahlt“ markieren; Saldo und Fälligkeiten aktualisieren sich.',
- 'Niedrig', '', '', '', 'Offen', ''],
+ 'Niedrig', '', '', '', 'Offen', '', ''],
]
duenn = Side(style='thin', color='E2E8F0')
for i in range(100):
zeile = 10 + i
- daten = beispiele[i] if i < len(beispiele) else [f'{i+1:03d}', '', '', '', '', '', '', '', 'Offen', '']
+ daten = beispiele[i] if i < len(beispiele) else [f'{i+1:03d}', '', '', '', '', '', '', '', 'Offen', '', '']
for j, wert in enumerate(daten):
cell = ws.cell(row=zeile, column=2 + j, value=wert)
cell.font = Font(size=10)
- cell.alignment = Alignment(vertical='top', wrap_text=(j in (2, 3, 9)))
+ cell.alignment = Alignment(vertical='top', wrap_text=(j in (2, 3, 10)))
cell.border = Border(bottom=duenn)
if i % 2 == 1:
cell.fill = PatternFill('solid', fgColor=GRAU)
@@ -134,8 +134,9 @@ bloecke = [
'notieren, wann das Feature als fertig gilt. Je konkreter das Akzeptanzkriterium, desto schneller die Umsetzung.'),
('Priorität',
'Hoch = dringend für den Betrieb · Mittel = wichtig, aber zeitunkritisch · Niedrig = Nice-to-have.'),
- ('Status (wird von der Entwicklung gepflegt)',
- 'Offen → In Prüfung (Aufwand wird bewertet) → Umgesetzt (nach Auslieferung) bzw. Abgelehnt (mit Begründung in der letzten Spalte).'),
+ ('Status & Ziel-Version (wird von der Entwicklung gepflegt)',
+ 'Offen → In Prüfung (Aufwand wird bewertet) → Umgesetzt (nach Auslieferung) bzw. Abgelehnt (mit Begründung in der letzten Spalte). '
+ 'Sobald ein Punkt eingeplant ist, trägt die Entwicklung die geplante Auslieferung in der Spalte „Ziel-Version“ ein (z. B. v1.24).'),
('UAT – Abnahme testen',
'Nach Umsetzung im Portal gegen das Akzeptanzkriterium testen. Ergebnis in der Spalte '
'„Ergebnis im Test / Bemerkung“ festhalten: „OK“ oder Fehlerbeschreibung mit Schritten zum Nachvollziehen. '
@@ -156,7 +157,7 @@ for titel, text in bloecke:
zeile += 2
wa.merge_cells(f'B{zeile}:C{zeile}')
-c = wa.cell(row=zeile, column=2, value='Aktueller Portal-Stand: v1.21 (Live-Stammdaten importiert)')
+c = wa.cell(row=zeile, column=2, value='Aktueller Portal-Stand: v1.23 (Nebenkostenabrechnung als PDF)')
c.font = Font(size=9, italic=True, color='94A3B8')
out = 'UAT-Featureliste Mieterportal.xlsx'
diff --git a/server/package-lock.json b/server/package-lock.json
index ddfc2ab..37c5c20 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -10,12 +10,22 @@
"dependencies": {
"express": "^4.21.2",
"multer": "^2.0.2",
+ "pdfkit": "^0.15.2",
"pg": "^8.13.1"
},
"engines": {
"node": ">=20"
}
},
+ "node_modules/@swc/helpers": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
+ "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -35,12 +45,63 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/body-parser": {
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
@@ -65,6 +126,24 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -85,6 +164,24 @@
"node": ">= 0.8"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -114,6 +211,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -150,6 +256,13 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -159,6 +272,72 @@
"ms": "2.0.0"
}
},
+ "node_modules/deep-equal": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.5",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.2",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -178,6 +357,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
+ "license": "MIT"
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -225,6 +410,26 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
@@ -316,6 +521,38 @@
"node": ">= 0.8"
}
},
+ "node_modules/fontkit": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz",
+ "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.3.13",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "deep-equal": "^2.0.5",
+ "dfa": "^1.2.0",
+ "restructure": "^2.0.1",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.3.1",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -343,6 +580,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -392,6 +638,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -404,6 +674,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -454,6 +739,20 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -463,6 +762,264 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/jpeg-exif": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
+ "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT"
+ },
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -577,6 +1134,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -589,6 +1191,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -604,6 +1212,19 @@
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
+ "node_modules/pdfkit": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.2.tgz",
+ "integrity": "sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-js": "^4.2.0",
+ "fontkit": "^1.8.1",
+ "jpeg-exif": "^1.1.4",
+ "linebreak": "^1.0.2",
+ "png-js": "^1.0.0"
+ }
+ },
"node_modules/pg": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
@@ -693,6 +1314,23 @@
"split2": "^4.1.0"
}
},
+ "node_modules/png-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
+ "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
+ "dependencies": {
+ "browserify-zlib": "^0.2.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -789,6 +1427,32 @@
"node": ">= 0.8"
}
},
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/restructure": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
+ "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==",
+ "license": "MIT"
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -809,6 +1473,23 @@
],
"license": "MIT"
},
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -860,6 +1541,38 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -956,6 +1669,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -964,6 +1690,12 @@
"node": ">=10.0.0"
}
},
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -973,6 +1705,12 @@
"node": ">=0.6"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -986,6 +1724,32 @@
"node": ">= 0.6"
}
},
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1013,6 +1777,64 @@
"node": ">= 0.8"
}
},
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.24",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz",
+ "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/server/package.json b/server/package.json
index 1e05b5e..8c0ba32 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"express": "^4.21.2",
"multer": "^2.0.2",
+ "pdfkit": "^0.15.2",
"pg": "^8.13.1"
}
}
diff --git a/server/src/abrechnung.js b/server/src/abrechnung.js
new file mode 100644
index 0000000..eb8a4d6
--- /dev/null
+++ b/server/src/abrechnung.js
@@ -0,0 +1,245 @@
+/** Nebenkostenabrechnung als PDF (v1.23).
+ *
+ * Erzeugt pro Wohnung und Abrechnungsjahr eine PDF-Abrechnung direkt aus
+ * dem Kosten- und Zählerstandsmodul:
+ * - umlagefähige, hausweite Kosten → Verteilung nach Wohnflächenanteil
+ * - umlagefähige, wohnungsbezogene Kosten → voll der Wohnung
+ * - Abzug der Nebenkosten-Vorauszahlung (Warmmiete − Kaltmiete) × 12
+ * - Anlage: Zählerstände der Wohnung (Stand + Datum)
+ * Das PDF landet im Upload-Verzeichnis und wird als Dokument
+ * („Nebenkostenabrechnung") der Wohnung zugeordnet – erneutes Erzeugen
+ * für dieselbe Wohnung + Jahr ersetzt Datei und Dokument.
+ */
+
+const fs = require('fs')
+const path = require('path')
+const PDFDocument = require('pdfkit')
+
+const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads')
+const FARBE_DUNKEL = '#22333b'
+const FARBE_AKZENT = '#f59e0b'
+const FARBE_SOFT = '#64748b'
+
+const eur = (n) =>
+ Number(n || 0).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'
+const zahl = (n) =>
+ Number(n || 0).toLocaleString('de-DE', { maximumFractionDigits: 2 })
+const dDatum = () => new Date().toLocaleDateString('de-DE')
+
+/** Sammelt alle Positionen und Zahlen für Wohnung + Jahr. */
+async function abrechnungsDaten(pool, unitId, jahr) {
+ const { rows: einheiten } = await pool.query('SELECT * FROM units WHERE id = $1', [unitId])
+ if (!einheiten.length) throw Object.assign(new Error('Wohnung nicht gefunden.'), { status: 404 })
+ const einheit = einheiten[0]
+
+ const { rows: hausEinheiten } = await pool.query(
+ 'SELECT * FROM units WHERE building = $1', [einheit.building],
+ )
+ const hausFlaeche = hausEinheiten.reduce((s, u) => s + Number(u.size || 0), 0)
+ const anteil = hausFlaeche > 0 ? Number(einheit.size || 0) / hausFlaeche : 0
+
+ const { rows: kosten } = await pool.query(
+ `SELECT * FROM costs WHERE building = $1 AND jahr = $2 AND umlegbar = TRUE
+ AND (unit_id = $3 OR unit_id IS NULL) ORDER BY kostenart`,
+ [einheit.building, jahr, einheit.id],
+ )
+ const positionen = []
+ for (const k of kosten) {
+ const wohnungsAnteil = k.unit_id
+ ? Number(k.betrag) // wohnungsbezogen: voller Betrag
+ : Number(k.betrag) * anteil // hausweit: nach Wohnflächenanteil
+ positionen.push({
+ kostenart: k.kostenart,
+ notiz: k.notiz,
+ hausGesamt: k.unit_id ? null : Number(k.betrag),
+ verteilung: k.unit_id ? 'wohnungsbezogen' : `${zahl(einheit.size)} von ${zahl(hausFlaeche)} m²`,
+ betrag: wohnungsAnteil,
+ })
+ }
+ // Zusammenfassen nach Kostenart
+ const jeArt = new Map()
+ for (const p of positionen) {
+ const alt = jeArt.get(p.kostenart) || { ...p, betrag: 0, hausGesamt: p.hausGesamt }
+ alt.betrag += p.betrag
+ if (p.hausGesamt !== null) alt.hausGesamt = (alt.hausGesamt || 0) + p.hausGesamt
+ jeArt.set(p.kostenart, alt)
+ }
+
+ const summe = [...jeArt.values()].reduce((s, p) => s + p.betrag, 0)
+ const vorauszahlung = (Number(einheit.warm_rent) - Number(einheit.cold_rent)) * 12
+ const differenz = summe - vorauszahlung
+
+ const { rows: zaehler } = await pool.query(
+ `SELECT nummer, art, stand, stand_am FROM meters WHERE unit_id = $1 ORDER BY art, nummer`,
+ [einheit.id],
+ )
+ return { einheit, jahr, positionen: [...jeArt.values()], summe, vorauszahlung, differenz, zaehler }
+}
+
+/** Zeichnet das PDF und liefert den Dateinamen (im UPLOAD_DIR). */
+async function bauePdf(daten) {
+ const { einheit, jahr, positionen, summe, vorauszahlung, differenz, zaehler } = daten
+ const dateiName = `ab-${einheit.id}-${jahr}.pdf`
+ const vollerPfad = path.join(UPLOAD_DIR, dateiName)
+
+ await new Promise((resolve, reject) => {
+ const doc = new PDFDocument({ size: 'A4', margin: 50, info: {
+ Title: `Nebenkostenabrechnung ${jahr} – ${einheit.building}, Whg. ${einheit.number}`,
+ Author: 'Hinterstein Immobilien',
+ } })
+ const strom = fs.createWriteStream(vollerPfad)
+ strom.on('finish', resolve)
+ strom.on('error', reject)
+ doc.pipe(strom)
+
+ const breite = doc.page.width - 100 // nutzbare Breite
+ let y = doc.y
+
+ // ── Kopf ──
+ doc.font('Helvetica-Bold').fontSize(16).fillColor(FARBE_DUNKEL)
+ doc.text('Hinterstein Immobilien', 50, y)
+ doc.font('Helvetica').fontSize(9).fillColor(FARBE_SOFT)
+ doc.text('Nebenkostenabrechnung', 50, y + 20, { align: 'right', width: breite })
+ doc.moveTo(50, y + 36).lineTo(50 + breite, y + 36).lineWidth(1.5).strokeColor(FARBE_AKZENT).stroke()
+ y += 50
+
+ doc.font('Helvetica-Bold').fontSize(13).fillColor(FARBE_DUNKEL)
+ doc.text(`Nebenkostenabrechnung ${jahr}`, 50, y)
+ y += 22
+ doc.font('Helvetica').fontSize(10).fillColor('#0f172a')
+ doc.text(`Abrechnungszeitraum: 01.01.${jahr} – 31.12.${jahr}`, 50, y)
+ y += 16
+ doc.text(`Objekt: ${einheit.building} · Wohnung ${einheit.number} · ${zahl(einheit.size)} m²`, 50, y)
+ y += 16
+ doc.text(`Mieter: ${einheit.tenant || '–'}`, 50, y)
+ y += 16
+ doc.text(`Erstellt am ${dDatum()}`, 50, y)
+ y += 26
+
+ // ── Kostenpositionen ──
+ doc.font('Helvetica-Bold').fontSize(11).fillColor(FARBE_DUNKEL)
+ doc.text('Umlagefähige Betriebskosten', 50, y)
+ y += 18
+
+ const spalten = { art: 50, gesamt: 300, schluessel: 390, betrag: 480 }
+ doc.font('Helvetica-Bold').fontSize(8.5).fillColor(FARBE_SOFT)
+ doc.text('KOSTENART', spalten.art, y)
+ doc.text('HAUS GESAMT', spalten.gesamt, y, { width: 85, align: 'right' })
+ doc.text('VERTEILUNG', spalten.schluessel, y)
+ doc.text('IHRE KOSTEN', spalten.betrag, y, { width: 65, align: 'right' })
+ y += 6
+ doc.moveTo(50, y).lineTo(50 + breite, y).lineWidth(0.5).strokeColor('#cbd5e1').stroke()
+ y += 12
+
+ doc.font('Helvetica').fontSize(9.5).fillColor('#0f172a')
+ for (const p of positionen) {
+ if (y > 730) { doc.addPage(); y = 60 }
+ doc.text(p.kostenart, spalten.art, y, { width: 240 })
+ doc.text(p.hausGesamt !== null ? eur(p.hausGesamt) : '–', spalten.gesamt, y, { width: 85, align: 'right' })
+ doc.fillColor(FARBE_SOFT).text(p.verteilung, spalten.schluessel, y, { width: 85 })
+ doc.fillColor('#0f172a').text(eur(p.betrag), spalten.betrag, y, { width: 65, align: 'right' })
+ y += 18
+ }
+ y += 2
+ doc.moveTo(50, y).lineTo(50 + breite, y).lineWidth(0.5).strokeColor('#cbd5e1').stroke()
+ y += 14
+
+ // ── Summenblock ──
+ const zeile = (label, wert, fett, farbe) => {
+ doc.font(fett ? 'Helvetica-Bold' : 'Helvetica').fontSize(10).fillColor(farbe || '#0f172a')
+ doc.text(label, 50, y, { width: 400 })
+ doc.text(wert, 430, y, { width: 115, align: 'right' })
+ y += 17
+ }
+ zeile('Summe umlagefähige Kosten', eur(summe), true)
+ zeile(`Nebenkostenvorauszahlung (${eur(Number(einheit.warm_rent) - Number(einheit.cold_rent))}/Monat × 12)`, '- ' + eur(vorauszahlung))
+ y += 4
+ doc.moveTo(320, y - 4).lineTo(545, y - 4).lineWidth(0.5).strokeColor('#cbd5e1').stroke()
+ const nachzahlung = differenz > 0
+ zeile(
+ nachzahlung ? 'Nachzahlung' : 'Erstattung',
+ (nachzahlung ? '+ ' : '- ') + eur(Math.abs(differenz)),
+ true,
+ nachzahlung ? '#b91c1c' : '#047857',
+ )
+ y += 14
+
+ // ── Anlage Zählerstände ──
+ if (zaehler.length) {
+ if (y > 640) { doc.addPage(); y = 60 }
+ doc.font('Helvetica-Bold').fontSize(11).fillColor(FARBE_DUNKEL)
+ doc.text('Anlage: Zählerstände der Wohnung', 50, y)
+ y += 16
+ doc.font('Helvetica-Bold').fontSize(8.5).fillColor(FARBE_SOFT)
+ doc.text('ZÄHLERART', 50, y)
+ doc.text('NUMMER', 160, y)
+ doc.text('STAND', 330, y, { width: 90, align: 'right' })
+ doc.text('STAND VOM', 440, y, { width: 105, align: 'right' })
+ y += 6
+ doc.moveTo(50, y).lineTo(50 + breite, y).lineWidth(0.5).strokeColor('#cbd5e1').stroke()
+ y += 12
+ doc.font('Helvetica').fontSize(9.5).fillColor('#0f172a')
+ for (const z of zaehler) {
+ if (y > 730) { doc.addPage(); y = 60 }
+ doc.text(z.art, 50, y)
+ doc.text(z.nummer, 160, y, { width: 150 })
+ doc.text(zahl(z.stand), 330, y, { width: 90, align: 'right' })
+ doc.text(z.stand_am || '–', 440, y, { width: 105, align: 'right' })
+ y += 18
+ }
+ y += 10
+ doc.font('Helvetica').fontSize(8).fillColor(FARBE_SOFT)
+ doc.text('Hinweis: Verbrauchskosten (z. B. Wasser) sind im Abrechnungszeitraum in den o. g. Kostenarten enthalten.', 50, y, { width: breite })
+ y += 12
+ }
+
+ // ── Fußzeile ──
+ doc.font('Helvetica').fontSize(8).fillColor(FARBE_SOFT)
+ doc.text(
+ 'Hinterstein Immobilien · Diese Abrechnung wurde automatisch aus dem Mieterportal erzeugt (Kosten- und Zählerstandsmodul).',
+ 50, Math.max(y + 8, 770), { width: breite },
+ )
+
+ doc.end()
+ })
+ return dateiName
+}
+
+/** Erzeugt die Abrechnung: PDF + Dokumenten-Eintrag (ersetzt vorhandene). */
+async function erzeugeAbrechnung(pool, unitId, jahr) {
+ const daten = await abrechnungsDaten(pool, unitId, jahr)
+ const { einheit } = daten
+ const dateiName = await bauePdf(daten)
+ const groesseBytes = fs.statSync(path.join(UPLOAD_DIR, dateiName)).size
+ const groesse = groesseBytes > 1024
+ ? `${zahl(groesseBytes / 1024)} KB`
+ : `${groesseBytes} B`
+ const titel = `Nebenkostenabrechnung ${jahr} – ${einheit.building}, Whg. ${einheit.number}`
+
+ const { rows: vorhanden } = await pool.query(
+ 'SELECT * FROM documents WHERE title = $1 AND unit_id = $2', [titel, einheit.id],
+ )
+ let dokument
+ if (vorhanden.length) {
+ const alt = vorhanden[0].file_path
+ if (alt && alt !== dateiName) {
+ await fs.promises.unlink(path.join(UPLOAD_DIR, alt)).catch(() => {})
+ }
+ const { rows } = await pool.query(
+ `UPDATE documents SET file_name = $1, file_path = $2, size = $3, date = $4, category = 'Nebenkostenabrechnung'
+ WHERE id = $5 RETURNING *`,
+ [titel, dateiName, groesse, dDatum(), vorhanden[0].id],
+ )
+ dokument = rows[0]
+ } else {
+ const { rows } = await pool.query(
+ `INSERT INTO documents (id, unit_id, title, category, size, date, file_name, file_path)
+ VALUES ($1,$2,$3,'Nebenkostenabrechnung',$4,$5,$6,$7) RETURNING *`,
+ [`d-ab-${einheit.id}-${jahr}`, einheit.id, titel, groesse, dDatum(), titel, dateiName],
+ )
+ dokument = rows[0]
+ }
+ return { dokument, daten }
+}
+
+module.exports = { erzeugeAbrechnung }
diff --git a/server/src/index.js b/server/src/index.js
index 6eb822a..1e7dd53 100644
--- a/server/src/index.js
+++ b/server/src/index.js
@@ -14,6 +14,7 @@ const multer = require('multer')
const { pool, init, sessionsAufraeumen } = require('./db')
const { seed, seedeZaehler, seedeHaeuser, seedeKosten } = require('./seed')
const mail = require('./mail')
+const { erzeugeAbrechnung } = require('./abrechnung')
const PORT = Number(process.env.PORT || 3000)
const SESSION_DAUER_MS = 8 * 60 * 60 * 1000
@@ -1364,6 +1365,24 @@ app.post('/api/ki-antwort', async (req, res, next) => {
} catch (e) { next(e) }
})
+// ── Nebenkostenabrechnung als PDF (v1.23) ─────────────────────
+app.post('/api/abrechnungen', auth, adminOnly, async (req, res, next) => {
+ try {
+ const unitId = String((req.body || {}).unitId || '')
+ const jahr = Number((req.body || {}).jahr)
+ if (!unitId || !Number.isInteger(jahr) || jahr < 2000 || jahr > 2100)
+ return res.status(400).json({ fehler: 'Wohnung und gültiges Jahr erforderlich.' })
+ const { dokument, daten } = await erzeugeAbrechnung(pool, unitId, jahr)
+ console.log(`[abrechnung] ${dokument.title} erzeugt (${dokument.size})`)
+ res.json({ dokument: mapDocument(dokument), zusammenfassung: {
+ summe: daten.summe, vorauszahlung: daten.vorauszahlung, differenz: daten.differenz,
+ } })
+ } catch (e) {
+ if (e.status) return res.status(e.status).json({ fehler: e.message })
+ next(e)
+ }
+})
+
// ── Fehlerbehandlung ─────────────────────────────────────────
app.use((err, _req, res, _next) => {
console.error('[api]', err)
diff --git a/src/pages/Kosten.tsx b/src/pages/Kosten.tsx
index 8eb1c07..403fc0a 100644
--- a/src/pages/Kosten.tsx
+++ b/src/pages/Kosten.tsx
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react'
-import { Calculator, Receipt, ReceiptText, PiggyBank, Plus, Pencil, Trash2, Coins, FileScan, Check, ExternalLink, ScanSearch } from 'lucide-react'
+import { Calculator, Receipt, ReceiptText, PiggyBank, Plus, Pencil, Trash2, Coins, FileScan, Check, ExternalLink, ScanSearch, FileDown, Landmark } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
@@ -193,6 +193,115 @@ function KostenDialog({ position, vorbefuellung, onGespeichert, triggerLabel }:
)
}
+/** Nebenkostenabrechnung als PDF erzeugen (nur Verwaltung, v1.23). */
+function AbrechnungDialog() {
+ const { units, erzeugeAbrechnung } = useStore()
+ const [open, setOpen] = useState(false)
+ const [einheit, setEinheit] = useState(units[0]?.id ?? '')
+ const [jahr, setJahr] = useState(String(new Date().getFullYear()))
+ const [sende, setSende] = useState(false)
+ const [fehler, setFehler] = useState(null)
+ const [ergebnis, setErgebnis] = useState<{
+ dokumentId: string
+ zusammenfassung: { summe: number; vorauszahlung: number; differenz: number }
+ } | null>(null)
+
+ const submit = async () => {
+ if (!einheit || sende) return
+ setSende(true)
+ setFehler(null)
+ setErgebnis(null)
+ const r = await erzeugeAbrechnung(einheit, Number(jahr))
+ setSende(false)
+ if (r.fehler) return setFehler(r.fehler)
+ if (r.dokument && r.zusammenfassung) {
+ setErgebnis({ dokumentId: r.dokument.id, zusammenfassung: r.zusammenfassung })
+ }
+ }
+
+ return (
+
+ )
+}
+
/** Eingegangene SparkReceipt-Belege (nur Verwaltung). */
function BelegeListe() {
const { belege, setBelegZugeordnet, analysiereBeleg, units } = useStore()
@@ -414,7 +523,12 @@ export default function Kosten() {
: 'Umlegbare und nicht umlegbare Kosten nach Haus und Wohnung – Grundlage für die Nebenkostenabrechnung'}
- {role === 'admin' && tab === 'kosten' && }
+ {role === 'admin' && tab === 'kosten' && (
+
+ )}
{role === 'admin' && (
diff --git a/src/store/store.tsx b/src/store/store.tsx
index 172a043..cee8759 100644
--- a/src/store/store.tsx
+++ b/src/store/store.tsx
@@ -174,6 +174,12 @@ interface Store extends Bootstrap {
deleteKosten: (id: string) => Promise
// ── Belege (SparkReceipt-Import; Verwaltung ordnet zu) ──
setBelegZugeordnet: (id: string, zugeordnet: boolean) => Promise
+ // ── Nebenkostenabrechnung als PDF (v1.23) ─────────────────
+ erzeugeAbrechnung: (unitId: string, jahr: number) => Promise<{
+ fehler: string | null
+ dokument?: DocumentItem
+ zusammenfassung?: { summe: number; vorauszahlung: number; differenz: number }
+ }>
/** Beleg erneut per OCR analysieren und Zuordnung aktualisieren. */
analysiereBeleg: (id: string) => Promise
}
@@ -509,6 +515,23 @@ export function StoreProvider({ children }: { children: ReactNode }) {
return fehlerNachricht(e)
}
},
+ // ── Nebenkostenabrechnung (v1.23) ─────────────────────────
+ async erzeugeAbrechnung(unitId, jahr) {
+ try {
+ const r = await apiPost<{ dokument: DocumentItem; zusammenfassung: { summe: number; vorauszahlung: number; differenz: number } }>(
+ '/api/abrechnungen', { unitId, jahr },
+ )
+ setDaten((d) => ({
+ ...d,
+ documents: d.documents.some((x) => x.id === r.dokument.id)
+ ? d.documents.map((x) => (x.id === r.dokument.id ? r.dokument : x))
+ : [...d.documents, r.dokument],
+ }))
+ return { fehler: null, dokument: r.dokument, zusammenfassung: r.zusammenfassung }
+ } catch (e) {
+ return { fehler: fehlerNachricht(e) }
+ }
+ },
}), [daten, bereit, me, role, meineEinheit, ladeAlles])
return {children}