Feature: SEPA-Export, Push-Notifications, Onboarding + vollständige UI
- Phosphor Icons (Icon.svelte, svg-Registry) - Schema-Abgleich: alle Felder zwischen PB-Migrations und types.ts konsistent - Stripe entfernt, SEPA pain.008 XML-Export implementiert (sepa.ts) - Beiträge: vollständiges CRUD + SEPA-Einzug-Sheet mit Vorschau - Termine: vollständiges CRUD (upcoming/vergangen, datetime-local) - Mitglieder: Formulare um alle Felder erweitert (Adresse, SEPA-Mandat, Notizen) - Nachrichten: Brevo E-Mail via PocketBase-Hook, UI mit Gruppen-Filter - Push-Notifications: VAPID, Custom Service Worker (injectManifest), Subscribe/Send API-Routen, automatische Subscription nach Login - Onboarding: 3-Schritt-Flow für neue Vereine, Guard im App-Layout - Makefile: .env wird vollständig zur DS übertragen
This commit is contained in:
parent
c2c4dfd518
commit
77c6f513b5
32 changed files with 3012 additions and 399 deletions
86
pocketbase/pb_hooks/nachrichten.pb.js
Normal file
86
pocketbase/pb_hooks/nachrichten.pb.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
onRecordAfterCreateSuccess(function(e) {
|
||||
if (!e.record) return;
|
||||
|
||||
var key = $os.getenv("BREVO_KEY");
|
||||
if (!key) {
|
||||
console.log("[nachrichten] BREVO_KEY nicht gesetzt – E-Mail übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vereinId = e.record.getString("verein_id");
|
||||
var betreff = e.record.getString("betreff");
|
||||
var text = e.record.getString("text");
|
||||
var gruppeIds = e.record.getStringSlice("gruppe_ids");
|
||||
|
||||
// Vereinsname für Absender
|
||||
var vereinName = "Ihr Verein";
|
||||
try {
|
||||
var verein = $app.findRecordById("vereine", vereinId);
|
||||
vereinName = verein.getString("name");
|
||||
} catch(err) {}
|
||||
|
||||
// Mitglieder-Filter
|
||||
var filter = 'verein_id = "' + vereinId + '" && status = "aktiv" && email != ""';
|
||||
if (gruppeIds && gruppeIds.length > 0) {
|
||||
var gruppenParts = [];
|
||||
for (var i = 0; i < gruppeIds.length; i++) {
|
||||
gruppenParts.push('gruppe_ids ~ "' + gruppeIds[i] + '"');
|
||||
}
|
||||
filter += " && (" + gruppenParts.join(" || ") + ")";
|
||||
}
|
||||
|
||||
var mitglieder;
|
||||
try {
|
||||
mitglieder = $app.findRecordsByFilter("mitglieder", filter, "nachname", 500, 0);
|
||||
} catch(err) {
|
||||
console.error("[nachrichten] Mitglieder laden fehlgeschlagen: " + String(err));
|
||||
return;
|
||||
}
|
||||
|
||||
var empfaenger = [];
|
||||
for (var j = 0; j < mitglieder.length; j++) {
|
||||
var m = mitglieder[j];
|
||||
var email = m.getString("email");
|
||||
if (email) {
|
||||
empfaenger.push({
|
||||
email: email,
|
||||
name: m.getString("vorname") + " " + m.getString("nachname")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (empfaenger.length === 0) {
|
||||
console.log("[nachrichten] Keine Empfänger mit E-Mail gefunden.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sender = $os.getenv("BREVO_SENDER") || "noreply@vereins.haus";
|
||||
var htmlContent = text
|
||||
? text.replace(/\n/g, "<br>")
|
||||
: "<p>(Kein Inhalt)</p>";
|
||||
|
||||
// In 50er-Batches senden (Brevo-Limit)
|
||||
var BATCH = 50;
|
||||
for (var b = 0; b < empfaenger.length; b += BATCH) {
|
||||
var batch = empfaenger.slice(b, b + BATCH);
|
||||
var body = JSON.stringify({
|
||||
sender: { name: vereinName, email: sender },
|
||||
to: [batch[0]],
|
||||
bcc: batch.slice(1),
|
||||
subject: betreff,
|
||||
htmlContent: htmlContent
|
||||
});
|
||||
|
||||
try {
|
||||
$http.send({
|
||||
url: "https://api.brevo.com/v3/smtp/email",
|
||||
method: "POST",
|
||||
headers: { "api-key": key, "Content-Type": "application/json" },
|
||||
body: body
|
||||
});
|
||||
console.log("[nachrichten] Batch " + (b / BATCH + 1) + " gesendet (" + batch.length + " Empfänger).");
|
||||
} catch(err) {
|
||||
console.error("[nachrichten] Brevo Fehler Batch " + (b / BATCH + 1) + ": " + String(err));
|
||||
}
|
||||
}
|
||||
}, "nachrichten");
|
||||
194
pocketbase/pb_migrations/1779230000_align_schema.js
Normal file
194
pocketbase/pb_migrations/1779230000_align_schema.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
|
||||
// vereine: +adresse, +dosb_mitglied; -stripe_customer_id
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411")
|
||||
c.fields.removeById("text1888339527") // stripe_customer_id
|
||||
c.fields.addAt(2, new Field({
|
||||
"type": "text", "id": "text2001000001", "name": "adresse",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "bool", "id": "bool2001000002", "name": "dosb_mitglied",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// gruppen: +beschreibung
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3099069179")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000003", "name": "beschreibung",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// mitglieder: +telefon, geburtsdatum, eintrittsdatum, austrittsdatum, strasse, plz, ort, bic, notizen
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2707111162")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000010", "name": "telefon",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "date", "id": "date2001000011", "name": "geburtsdatum",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"min": "", "max": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "date", "id": "date2001000012", "name": "eintrittsdatum",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"min": "", "max": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "date", "id": "date2001000013", "name": "austrittsdatum",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"min": "", "max": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000014", "name": "strasse",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000015", "name": "plz",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000016", "name": "ort",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000017", "name": "bic",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000018", "name": "notizen",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// beitraege: +beschreibung; rhythmus +halbjaehrlich (einmalig bleibt)
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3218207135")
|
||||
const rhythmus = c.fields.getById("select917011370")
|
||||
rhythmus.values = ["monatlich", "quartalsweise", "halbjaehrlich", "jaehrlich", "einmalig"]
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000020", "name": "beschreibung",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// einzuege: -stripe_payment_intent_id; status bezahlt→eingezogen
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_659326735")
|
||||
c.fields.removeById("text4235393406") // stripe_payment_intent_id
|
||||
const status = c.fields.getById("select2063623452")
|
||||
status.values = ["ausstehend", "eingezogen", "fehlgeschlagen", "storniert"]
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// termine: +beschreibung
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2279568741")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000030", "name": "beschreibung",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// nachrichten: +autor_id (relation zu users)
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_1415911511")
|
||||
c.fields.addAt(2, new Field({
|
||||
"type": "relation", "id": "relation2001000040", "name": "autor_id",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"cascadeDelete": false, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
}, (app) => {
|
||||
|
||||
// vereine rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411")
|
||||
c.fields.removeById("text2001000001")
|
||||
c.fields.removeById("bool2001000002")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text1888339527", "name": "stripe_customer_id",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// gruppen rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3099069179")
|
||||
c.fields.removeById("text2001000003")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// mitglieder rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2707111162")
|
||||
for (const id of ["text2001000010","date2001000011","date2001000012","date2001000013",
|
||||
"text2001000014","text2001000015","text2001000016","text2001000017","text2001000018"]) {
|
||||
c.fields.removeById(id)
|
||||
}
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// beitraege rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3218207135")
|
||||
const rhythmus = c.fields.getById("select917011370")
|
||||
rhythmus.values = ["monatlich", "quartalsweise", "jaehrlich", "einmalig"]
|
||||
c.fields.removeById("text2001000020")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// einzuege rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_659326735")
|
||||
const status = c.fields.getById("select2063623452")
|
||||
status.values = ["ausstehend", "bezahlt", "fehlgeschlagen", "storniert"]
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text4235393406", "name": "stripe_payment_intent_id",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// termine rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2279568741")
|
||||
c.fields.removeById("text2001000030")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// nachrichten rollback
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_1415911511")
|
||||
c.fields.removeById("relation2001000040")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
})
|
||||
58
pocketbase/pb_migrations/1779230100_sepa_fields.js
Normal file
58
pocketbase/pb_migrations/1779230100_sepa_fields.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
|
||||
// vereine: +glaeubigerid, +iban, +bic (Vereinskonto für SEPA-Einzug)
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000050", "name": "glaeubigerid",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000051", "name": "iban",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000052", "name": "bic",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
// mitglieder: +mandatsreferenz, +mandatsdatum (SEPA-Mandat des Mitglieds)
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2707111162")
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "text", "id": "text2001000053", "name": "mandatsreferenz",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
|
||||
}))
|
||||
c.fields.addAt(99, new Field({
|
||||
"type": "date", "id": "date2001000054", "name": "mandatsdatum",
|
||||
"help": "", "hidden": false, "presentable": false, "required": false, "system": false,
|
||||
"min": "", "max": ""
|
||||
}))
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
}, (app) => {
|
||||
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411")
|
||||
c.fields.removeById("text2001000050")
|
||||
c.fields.removeById("text2001000051")
|
||||
c.fields.removeById("text2001000052")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
{
|
||||
const c = app.findCollectionByNameOrId("pbc_2707111162")
|
||||
c.fields.removeById("text2001000053")
|
||||
c.fields.removeById("date2001000054")
|
||||
app.save(c)
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
// Erlaubt allen Usern desselben Vereins, die Subscriptions ihrer Vereinsmitglieder zu lesen
|
||||
// (notwendig damit die /api/push/senden Route alle Geräte des Vereins erreicht)
|
||||
migrate((app) => {
|
||||
const c = app.findCollectionByNameOrId("pbc_1438754935") // push_subscriptions
|
||||
c.listRule = '@request.auth.verein_id = user_id.verein_id'
|
||||
c.viewRule = '@request.auth.id = user_id'
|
||||
app.save(c)
|
||||
}, (app) => {
|
||||
const c = app.findCollectionByNameOrId("pbc_1438754935")
|
||||
c.listRule = '@request.auth.id = user_id'
|
||||
c.viewRule = '@request.auth.id = user_id'
|
||||
app.save(c)
|
||||
})
|
||||
11
pocketbase/pb_migrations/1779230300_vereine_create_rule.js
Normal file
11
pocketbase/pb_migrations/1779230300_vereine_create_rule.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
// Erlaubt eingeloggten Nutzern, einen Verein anzulegen (Onboarding)
|
||||
migrate((app) => {
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411") // vereine
|
||||
c.createRule = "@request.auth.id != ''"
|
||||
app.save(c)
|
||||
}, (app) => {
|
||||
const c = app.findCollectionByNameOrId("pbc_3589557411")
|
||||
c.createRule = null
|
||||
app.save(c)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue