diff --git a/Makefile b/Makefile
index 7130961..281632d 100644
--- a/Makefile
+++ b/Makefile
@@ -65,7 +65,7 @@ deploy: check-ssh
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
@echo "→ .env auf DS aktualisieren..."
@if [ -f .env ]; then \
- grep -E "BREVO_KEY" .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
+ cat .env | ssh $(DS_HOST) "cat > $(DS_PATH)/.env"; \
fi
@echo "→ PocketBase Hooks synchronisieren..."
@if ls $(HOOKS_SRC)/*.pb.js 2>/dev/null | grep -q .; then \
diff --git a/app/package-lock.json b/app/package-lock.json
index 7d728af..a298716 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -8,17 +8,21 @@
"name": "vereinshaus",
"version": "0.1.0",
"dependencies": {
- "pocketbase": "^0.26.9"
+ "pocketbase": "^0.26.9",
+ "web-push": "^3.6.7"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
+ "@types/web-push": "^3.6.4",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7",
- "vite-plugin-pwa": "^1.3.0"
+ "vite-plugin-pwa": "^1.3.0",
+ "workbox-core": "^7.4.1",
+ "workbox-precaching": "^7.4.1"
}
},
"node_modules/@apideck/better-ajv-errors": {
@@ -2639,6 +2643,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "25.9.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2653,6 +2667,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/web-push": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz",
+ "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -2666,6 +2690,15 @@
"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/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -2732,6 +2765,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -2850,6 +2895,12 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
@@ -2897,6 +2948,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -3139,7 +3196,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -3231,6 +3287,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -3879,6 +3944,28 @@
"node": ">= 0.4"
}
},
+ "node_modules/http_ece": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
+ "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
@@ -3886,6 +3973,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "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",
@@ -4421,6 +4514,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -4753,6 +4867,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "license": "ISC"
+ },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -4769,6 +4889,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
@@ -4803,7 +4932,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -5311,6 +5439,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "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/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -5346,6 +5494,12 @@
"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",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -6022,6 +6176,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
@@ -6260,6 +6421,25 @@
}
}
},
+ "node_modules/web-push": {
+ "version": "3.6.7",
+ "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
+ "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "asn1.js": "^5.3.0",
+ "http_ece": "1.2.0",
+ "https-proxy-agent": "^7.0.0",
+ "jws": "^4.0.0",
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "web-push": "src/cli.js"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
diff --git a/app/package.json b/app/package.json
index e0fc5a5..95ba95e 100644
--- a/app/package.json
+++ b/app/package.json
@@ -15,13 +15,17 @@
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
+ "@types/web-push": "^3.6.4",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7",
- "vite-plugin-pwa": "^1.3.0"
+ "vite-plugin-pwa": "^1.3.0",
+ "workbox-core": "^7.4.1",
+ "workbox-precaching": "^7.4.1"
},
"dependencies": {
- "pocketbase": "^0.26.9"
+ "pocketbase": "^0.26.9",
+ "web-push": "^3.6.7"
}
}
diff --git a/app/src/lib/components/Icon.svelte b/app/src/lib/components/Icon.svelte
new file mode 100644
index 0000000..3af0fc2
--- /dev/null
+++ b/app/src/lib/components/Icon.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/app/src/lib/icons.ts b/app/src/lib/icons.ts
new file mode 100644
index 0000000..ccac259
--- /dev/null
+++ b/app/src/lib/icons.ts
@@ -0,0 +1,15 @@
+import house from './icons/house.svg?raw';
+import users from './icons/users.svg?raw';
+import calendar from './icons/calendar.svg?raw';
+import currencyEur from './icons/currency-eur.svg?raw';
+import envelope from './icons/envelope.svg?raw';
+
+export const icons = {
+ house,
+ users,
+ calendar,
+ 'currency-eur': currencyEur,
+ envelope,
+} as const;
+
+export type IconName = keyof typeof icons;
diff --git a/app/src/lib/icons/calendar.svg b/app/src/lib/icons/calendar.svg
new file mode 100644
index 0000000..3b91cc4
--- /dev/null
+++ b/app/src/lib/icons/calendar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/icons/currency-eur.svg b/app/src/lib/icons/currency-eur.svg
new file mode 100644
index 0000000..c23e136
--- /dev/null
+++ b/app/src/lib/icons/currency-eur.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/icons/envelope.svg b/app/src/lib/icons/envelope.svg
new file mode 100644
index 0000000..f07a98b
--- /dev/null
+++ b/app/src/lib/icons/envelope.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/icons/house.svg b/app/src/lib/icons/house.svg
new file mode 100644
index 0000000..6346350
--- /dev/null
+++ b/app/src/lib/icons/house.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/icons/users.svg b/app/src/lib/icons/users.svg
new file mode 100644
index 0000000..7f7b6ca
--- /dev/null
+++ b/app/src/lib/icons/users.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/sepa.ts b/app/src/lib/sepa.ts
new file mode 100644
index 0000000..a49ff21
--- /dev/null
+++ b/app/src/lib/sepa.ts
@@ -0,0 +1,109 @@
+export interface SepaKopf {
+ glaeubigerid: string;
+ vereinIban: string;
+ vereinBic: string;
+ vereinName: string;
+ einzugsdatum: string; // YYYY-MM-DD
+}
+
+export interface SepaPosition {
+ endToEndId: string;
+ betrag: number;
+ mandatsreferenz: string;
+ mandatsdatum: string; // YYYY-MM-DD
+ debitorName: string;
+ debitorIban: string;
+ debitorBic: string;
+ verwendungszweck: string;
+}
+
+function x(str: string): string {
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function iban(raw: string): string {
+ return raw.replace(/\s/g, '').toUpperCase();
+}
+
+export function generatePain008(kopf: SepaKopf, positionen: SepaPosition[]): string {
+ if (positionen.length === 0) throw new Error('Keine Positionen für den SEPA-Export.');
+
+ const now = new Date().toISOString().slice(0, 19);
+ const msgId = `VH-${Date.now()}`;
+ const ctrlSum = positionen.reduce((s, p) => s + p.betrag, 0).toFixed(2);
+ const nbOfTxs = positionen.length;
+
+ const txXml = positionen.map((p) => `
+
SEPA-Beitragseinzug — in Entwicklung
+{#if sepaFehlt && !loading} +Laden…
+{:else if beitraege.length === 0} +Noch keine Beitragsarten — lege die erste an!
+{:else} +