Staging: docker-compose.staging.yml, Makefile-Targets, Seed-Script (18 Mitglieder, Termine, Orte, Beitragsarten)

This commit is contained in:
rene 2026-05-20 20:46:46 +02:00
parent 81f34905cf
commit a4436d70c2
3 changed files with 379 additions and 0 deletions

View file

@ -11,6 +11,13 @@ CONTAINER_PB := vereinshaus-pocketbase
CONTAINER_APP := vereinshaus-app CONTAINER_APP := vereinshaus-app
DOCKER := sudo /usr/local/bin/docker DOCKER := sudo /usr/local/bin/docker
STAGING_PATH := /volume1/docker/vereinshaus-staging
CONTAINER_PB_STAGING := vereinshaus-staging-pocketbase
CONTAINER_APP_STAGING:= vereinshaus-staging-app
STAGING_PB_URL := http://localhost:8091
STAGING_MIGRATIONS := $(STAGING_PATH)/pocketbase/migrations
STAGING_HOOKS := $(STAGING_PATH)/pocketbase/data/pb_hooks
TAR_EXCLUDE := --exclude='.git' \ TAR_EXCLUDE := --exclude='.git' \
--exclude='./app/node_modules' \ --exclude='./app/node_modules' \
--exclude='./app/.svelte-kit' \ --exclude='./app/.svelte-kit' \
@ -133,3 +140,65 @@ shell-pb: check-ssh
pb-admin: pb-admin:
@echo " PocketBase Admin: https://api.vereins.haus/_/" @echo " PocketBase Admin: https://api.vereins.haus/_/"
# ==============================================================
# STAGING
# ==============================================================
.PHONY: staging-deploy staging-seed staging-logs staging-status staging-stop
staging-deploy: check-ssh
@echo "→ Sync zu DS (Staging)..."
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(STAGING_PATH)/"
@echo "→ .env auf DS (Staging)..."
@if [ -f .env ]; then \
cat .env | ssh $(DS_HOST) "cat > $(STAGING_PATH)/.env"; \
fi
@echo "→ Hooks synchronisieren (Staging)..."
@ssh $(DS_HOST) "mkdir -p $(STAGING_HOOKS)"
@if ls $(HOOKS_SRC)/*.pb.js 2>/dev/null | grep -q .; then \
for f in $(HOOKS_SRC)/*.pb.js; do \
cat "$$f" | ssh $(DS_HOST) "cat > $(STAGING_HOOKS)/$$(basename $$f)"; \
done; \
fi
@echo "→ Migrations synchronisieren (Staging, nur neue)..."
@ssh $(DS_HOST) "mkdir -p $(STAGING_MIGRATIONS)"
@if ls $(MIGRATIONS_SRC)/*.js 2>/dev/null | grep -q .; then \
for f in $(MIGRATIONS_SRC)/*.js; do \
fname=$$(basename "$$f"); \
if ! ssh $(DS_HOST) "test -f $(STAGING_MIGRATIONS)/$$fname" 2>/dev/null; then \
cat "$$f" | ssh $(DS_HOST) "cat > $(STAGING_MIGRATIONS)/$$fname"; \
echo "$$fname"; \
fi; \
done; \
fi
@echo "→ Docker rebuild + restart (Staging)..."
@ssh $(DS_HOST) " \
cd $(STAGING_PATH) && \
$(DOCKER) compose -f docker-compose.staging.yml down && \
$(DOCKER) compose -f docker-compose.staging.yml build app-staging && \
$(DOCKER) compose -f docker-compose.staging.yml up -d"
@echo " ✓ Staging bereit."
@echo " App: https://staging.vereins.haus"
@echo " PocketBase: https://api-staging.vereins.haus/_/"
staging-seed:
@echo "→ Testdaten in Staging einfügen..."
@echo " Voraussetzung: PB_EMAIL + PB_PASSWORD in .env gesetzt (Staging-Superuser)"
@if [ -f .env ]; then \
export $$(grep -v '^#' .env | xargs) && \
PB_URL=https://api-staging.vereins.haus node scripts/seed.js; \
else \
PB_URL=https://api-staging.vereins.haus node scripts/seed.js; \
fi
staging-logs: check-ssh
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_APP_STAGING) --tail=50"
staging-status: check-ssh
@ssh $(DS_HOST) "$(DOCKER) ps \
--filter name=vereinshaus-staging \
--format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
staging-stop: check-ssh
@ssh $(DS_HOST) "cd $(STAGING_PATH) && $(DOCKER) compose -f docker-compose.staging.yml down"
@echo " ✓ Staging gestoppt."

View file

@ -0,0 +1,46 @@
version: "3.8"
services:
pocketbase-staging:
image: ghcr.io/muchobien/pocketbase:latest
container_name: vereinshaus-staging-pocketbase
restart: unless-stopped
command: ["--migrationsDir=/pb_data/migrations"]
volumes:
- /volume1/docker/vereinshaus-staging/pocketbase/data:/pb_data
- /volume1/docker/vereinshaus-staging/pocketbase/storage:/pb_public
- /volume1/docker/vereinshaus-staging/pocketbase/data/pb_hooks:/pb_hooks
- /volume1/docker/vereinshaus-staging/pocketbase/migrations:/pb_data/migrations
environment:
- TZ=Europe/Berlin
- BREVO_KEY=${BREVO_KEY}
- BREVO_SENDER=${BREVO_SENDER:-noreply@vereins.haus}
networks:
- default
- npm_bridge
app-staging:
build:
context: ./app
dockerfile: Dockerfile
args:
VITE_PB_URL: https://api-staging.vereins.haus
image: vereinshaus-staging-app
container_name: vereinshaus-staging-app
restart: unless-stopped
environment:
- TZ=Europe/Berlin
- HOST=0.0.0.0
- PORT=3000
- PUBLIC_VAPID_KEY=${PUBLIC_VAPID_KEY}
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:info@vereins.haus}
- PB_URL=http://pocketbase-staging:8090
networks:
- default
- npm_bridge
networks:
npm_bridge:
external: true
name: nginx-proxy-manager_bridge_net

264
scripts/seed.js Normal file
View file

@ -0,0 +1,264 @@
#!/usr/bin/env node
// Testdaten für vereins.haus Staging
// Aufruf: PB_URL=http://localhost:8090 PB_EMAIL=admin@test.de PB_PASSWORD=Test123456! node scripts/seed.js
const PB_URL = process.env.PB_URL || 'http://localhost:8090';
const PB_EMAIL = process.env.PB_EMAIL || '';
const PB_PWD = process.env.PB_PASSWORD || '';
if (!PB_EMAIL || !PB_PWD) {
console.error('Fehler: PB_EMAIL und PB_PASSWORD setzen.');
process.exit(1);
}
let token = '';
async function pb(method, path, body) {
const res = await fetch(`${PB_URL}/api/${path}`, {
method,
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: token } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} /${path}${res.status}: ${JSON.stringify(json)}`);
return json;
}
async function find(collection, filter) {
const r = await pb('GET', `collections/${collection}/records?filter=${encodeURIComponent(filter)}&perPage=1`);
return r.items?.[0] ?? null;
}
async function create(collection, data) {
return pb('POST', `collections/${collection}/records`, data);
}
async function update(collection, id, data) {
return pb('PATCH', `collections/${collection}/records/${id}`, data);
}
// ── 1. Admin-Login ──────────────────────────────────────────────────────────
console.log('→ Admin-Login…');
const auth = await pb('POST', 'collections/_superusers/auth-with-password', {
identity: PB_EMAIL, password: PB_PWD,
});
token = auth.token;
console.log(' ✓ Eingeloggt als', PB_EMAIL);
// ── 2. Verein ────────────────────────────────────────────────────────────────
console.log('→ Verein anlegen…');
let verein = await find('vereine', 'name = "TSV Musterstadt 1983 e.V."');
if (!verein) {
verein = await create('vereine', {
name: 'TSV Musterstadt 1983 e.V.',
adresse: 'Vereinsstraße 12',
plz: '80333',
ort: 'Musterstadt',
bundesland: 'BY',
plan: 'starter',
dosb_mitglied: true,
email: 'info@tsv-musterstadt.de',
telefon: '089 123456',
website: 'https://tsv-musterstadt.de',
glaeubigerid: 'DE98ZZZ09999999999',
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX',
});
}
console.log(' ✓ Verein:', verein.name, `(${verein.id})`);
// ── 3. Admin-User ────────────────────────────────────────────────────────────
console.log('→ Admin-User…');
let adminUser = await find('users', 'email = "vorstand@tsv-musterstadt.de"');
if (!adminUser) {
adminUser = await create('users', {
email: 'vorstand@tsv-musterstadt.de',
password: 'Test123456!', passwordConfirm: 'Test123456!',
name: 'Max Mustermann', verein_id: verein.id, rolle: null, // null = admin
emailVisibility: true,
});
}
console.log(' ✓ Admin:', adminUser.email);
// ── 4. Gruppen ───────────────────────────────────────────────────────────────
console.log('→ Gruppen…');
const gruppenDef = ['Vorstand', 'Aktive Mitglieder', 'Jugend U15', 'Senioren'];
const gruppen = {};
for (const name of gruppenDef) {
let g = await find('gruppen', `name = "${name}" && verein_id = "${verein.id}"`);
if (!g) g = await create('gruppen', { verein_id: verein.id, name });
gruppen[name] = g.id;
}
console.log(' ✓', Object.keys(gruppen).join(', '));
// ── 5. Mitglieder ────────────────────────────────────────────────────────────
console.log('→ Mitglieder (18)…');
const mitgliederDef = [
// Vorstand
{ vorname: 'Max', nachname: 'Mustermann', email: 'max.mustermann@example.de', telefon: '0170 1111111', geburtsdatum: '1975-03-15', eintrittsdatum: '2000-01-01', strasse: 'Hauptstraße 1', plz: '80333', ort: 'Musterstadt', iban: 'DE89370400440532013000', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Vorstand', mandatsreferenz: 'MANDAT-001', mandatsdatum: '2020-01-10' },
{ vorname: 'Sabine', nachname: 'Richter', email: 'sabine.richter@example.de', telefon: '0171 2222222', geburtsdatum: '1980-07-22', eintrittsdatum: '2005-03-01', strasse: 'Birkenweg 5', plz: '80334', ort: 'Musterstadt', iban: 'DE27100777770209299700', bic: 'SSKMDEMMXXX', status: 'aktiv', gruppe: 'Vorstand', mandatsreferenz: 'MANDAT-002', mandatsdatum: '2020-01-10' },
// Aktive mit IBAN
{ vorname: 'Anna', nachname: 'Schmidt', email: 'anna.schmidt@example.de', telefon: '0172 3333333', geburtsdatum: '1990-11-05', eintrittsdatum: '2015-09-01', strasse: 'Rosenstraße 8', plz: '80333', ort: 'Musterstadt', iban: 'DE61500105179767440929', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-003', mandatsdatum: '2021-04-15' },
{ vorname: 'Peter', nachname: 'Wagner', email: 'peter.wagner@example.de', telefon: '0173 4444444', geburtsdatum: '1985-02-28', eintrittsdatum: '2010-01-15', strasse: 'Gartenweg 12', plz: '80334', ort: 'Musterstadt', iban: 'DE24500105171911148770', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-004', mandatsdatum: '2021-04-15' },
{ vorname: 'Thomas', nachname: 'Fischer', email: 'thomas.fischer@example.de', telefon: '0174 5555555', geburtsdatum: '1978-06-10', eintrittsdatum: '2008-04-01', strasse: 'Lindenallee 3', plz: '80335', ort: 'Musterstadt', iban: 'DE12500105176228935005', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-005', mandatsdatum: '2021-04-15' },
{ vorname: 'Claudia', nachname: 'König', email: 'claudia.koenig@example.de', telefon: '0175 6666666', geburtsdatum: '1992-09-17', eintrittsdatum: '2018-06-01', strasse: 'Feldweg 7', plz: '80333', ort: 'Musterstadt', iban: 'DE67200501001234567890', bic: 'HASPDEHHXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-006', mandatsdatum: '2022-01-20' },
{ vorname: 'Michael', nachname: 'Koch', email: 'michael.koch@example.de', telefon: '0176 7777777', geburtsdatum: '1983-04-03', eintrittsdatum: '2012-02-01', strasse: 'Bergstraße 22', plz: '80336', ort: 'Musterstadt', iban: 'DE86200400600526015800', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-007', mandatsdatum: '2021-04-15' },
{ vorname: 'Lisa', nachname: 'Zimmermann', email: 'lisa.zimm@example.de', telefon: '0177 8888888', geburtsdatum: '1995-12-25', eintrittsdatum: '2020-10-01', strasse: 'Blumenstraße 4', plz: '80333', ort: 'Musterstadt', iban: 'DE21700519950021267002', bic: 'BYLADEM1AUG', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-008', mandatsdatum: '2022-03-05' },
{ vorname: 'Petra', nachname: 'Schreiber', email: 'petra.schreiber@example.de', telefon: '0178 9999999', geburtsdatum: '1988-08-14', eintrittsdatum: '2014-07-01', strasse: 'Weinbergweg 9', plz: '80334', ort: 'Musterstadt', iban: 'DE36200400600532013004', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: 'MANDAT-009', mandatsdatum: '2021-04-15' },
// Aktive ohne IBAN
{ vorname: 'Maria', nachname: 'Becker', email: 'maria.becker@example.de', telefon: '0179 1010101', geburtsdatum: '1987-01-30', eintrittsdatum: '2016-03-01', strasse: 'Kirchgasse 6', plz: '80335', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
{ vorname: 'Julia', nachname: 'Müller', email: 'julia.mueller@example.de', telefon: '0151 1111222', geburtsdatum: '1993-05-19', eintrittsdatum: '2019-01-01', strasse: 'Parkstraße 11', plz: '80333', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
{ vorname: 'Markus', nachname: 'Schäfer', email: 'markus.schaefer@example.de', telefon: '0152 3333444', geburtsdatum: '1991-07-07', eintrittsdatum: '2021-05-01', strasse: 'Schulweg 2', plz: '80336', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null },
// Jugend
{ vorname: 'Lena', nachname: 'Bauer', email: null, telefon: '0160 1234567', geburtsdatum: '2012-03-22', eintrittsdatum: '2023-09-01', strasse: 'Schulstraße 14', plz: '80333', ort: 'Musterstadt', iban: 'DE47200400600128491600', bic: 'COBADEFFXXX', status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: 'MANDAT-013', mandatsdatum: '2023-09-01' },
{ vorname: 'Kevin', nachname: 'Hoffmann', email: null, telefon: '0160 7654321', geburtsdatum: '2013-11-08', eintrittsdatum: '2023-09-01', strasse: 'Waldweg 3', plz: '80334', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: null, mandatsdatum: null },
{ vorname: 'Emma', nachname: 'Klein', email: null, telefon: '0160 1122334', geburtsdatum: '2011-06-15', eintrittsdatum: '2022-09-01', strasse: 'Seeweg 7', plz: '80335', ort: 'Musterstadt', iban: null, bic: null, status: 'aktiv', gruppe: 'Jugend U15', mandatsreferenz: null, mandatsdatum: null },
// Senioren
{ vorname: 'Gertrude', nachname: 'Neumann', email: 'g.neumann@example.de', telefon: '089 9876543', geburtsdatum: '1948-04-12', eintrittsdatum: '1990-01-01', strasse: 'Ahornweg 1', plz: '80333', ort: 'Musterstadt', iban: 'DE43500105176118506698', bic: 'BELADEBEXXX', status: 'aktiv', gruppe: 'Senioren', mandatsreferenz: 'MANDAT-016', mandatsdatum: '2019-06-01' },
// Passiv / Ausgetreten
{ vorname: 'Hans', nachname: 'Schneider', email: 'hans.schneider@example.de', telefon: '0173 0000001', geburtsdatum: '1965-10-20', eintrittsdatum: '1995-04-01', strasse: 'Bergblick 5', plz: '80334', ort: 'Musterstadt', iban: null, bic: null, status: 'passiv', gruppe: 'Senioren', mandatsreferenz: null, mandatsdatum: null },
{ vorname: 'Horst', nachname: 'Braun', email: 'horst.braun@example.de', telefon: null, geburtsdatum: '1960-02-14', eintrittsdatum: '1988-01-01', strasse: 'Alte Gasse 3', plz: '80336', ort: 'Musterstadt', iban: null, bic: null, status: 'ausgetreten', gruppe: 'Aktive Mitglieder', mandatsreferenz: null, mandatsdatum: null, austrittsdatum: '2024-12-31' },
];
let mitgliederIds = [];
for (const m of mitgliederDef) {
let rec = await find('mitglieder', `vorname = "${m.vorname}" && nachname = "${m.nachname}" && verein_id = "${verein.id}"`);
if (!rec) {
rec = await create('mitglieder', {
verein_id: verein.id,
vorname: m.vorname,
nachname: m.nachname,
email: m.email || null,
telefon: m.telefon || null,
geburtsdatum: m.geburtsdatum,
eintrittsdatum: m.eintrittsdatum,
austrittsdatum: m.austrittsdatum || null,
strasse: m.strasse,
plz: m.plz,
ort: m.ort,
iban: m.iban || null,
bic: m.bic || null,
mandatsreferenz:m.mandatsreferenz || null,
mandatsdatum: m.mandatsdatum || null,
status: m.status,
gruppe_ids: [gruppen[m.gruppe]].filter(Boolean),
});
}
mitgliederIds.push(rec.id);
}
console.log(`${mitgliederIds.length} Mitglieder`);
// ── 6. Veranstaltungsorte ────────────────────────────────────────────────────
console.log('→ Veranstaltungsorte…');
const orteDef = [
{ name: 'Turnhalle Grundschule Muster', adresse: 'Schulstraße 1, 80333 Musterstadt', typ: 'halle', aktiv: true },
{ name: 'Vereinsheim TSV', adresse: 'Vereinsstraße 12, 80333 Musterstadt', typ: 'gebaeude', aktiv: true },
{ name: 'Sportplatz West', adresse: 'Weststraße 99, 80335 Musterstadt', typ: 'platz', aktiv: true },
];
const orte = {};
for (const o of orteDef) {
let rec = await find('veranstaltungsorte', `name = "${o.name}" && verein_id = "${verein.id}"`);
if (!rec) rec = await create('veranstaltungsorte', { verein_id: verein.id, ...o });
orte[o.name] = rec.id;
}
// Ausfall: Turnhalle gesperrt wegen Schulveranstaltung
const turnhalleId = orte['Turnhalle Grundschule Muster'];
const existingAusfall = await find('ort_ausfaelle', `ort_id = "${turnhalleId}"`);
if (!existingAusfall) {
const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7);
const nextWeek2 = new Date(); nextWeek2.setDate(nextWeek2.getDate() + 9);
await create('ort_ausfaelle', {
ort_id: turnhalleId,
von: nextWeek.toISOString().slice(0,10),
bis: nextWeek2.toISOString().slice(0,10),
grund: 'Schulveranstaltung Halle nicht verfügbar',
});
}
console.log(' ✓ Orte:', Object.keys(orte).join(', '));
// ── 7. Beitragsarten ─────────────────────────────────────────────────────────
console.log('→ Beitragsarten…');
const beitraegeDef = [
{ name: 'Jahresbeitrag Erwachsene', betrag: 48, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder ab 18 Jahren' },
{ name: 'Jahresbeitrag Jugend', betrag: 24, rhythmus: 'jaehrlich', beschreibung: 'Für Mitglieder unter 18 Jahren' },
{ name: 'Aufnahmegebühr', betrag: 20, rhythmus: 'einmalig', beschreibung: 'Einmalig bei Vereinseintritt' },
{ name: 'Monatsbeitrag Fitness', betrag: 15, rhythmus: 'monatlich', beschreibung: 'Für die Fitnessgruppe (optional)' },
];
for (const b of beitraegeDef) {
const ex = await find('beitraege', `name = "${b.name}" && verein_id = "${verein.id}"`);
if (!ex) await create('beitraege', { verein_id: verein.id, ...b });
}
console.log(' ✓', beitraegeDef.map(b => b.name).join(', '));
// ── 8. Termine ───────────────────────────────────────────────────────────────
console.log('→ Termine…');
const d = (offsetDays, h = 0, m = 0) => {
const dt = new Date();
dt.setDate(dt.getDate() + offsetDays);
dt.setHours(h, m, 0, 0);
return dt.toISOString();
};
const termineDef = [
// Vergangen
{ titel: 'Jahreshauptversammlung 2026', beginn: d(-60, 19, 0), ende: d(-60, 21, 0), ort_id: orte['Vereinsheim TSV'], gruppe_ids: Object.values(gruppen), beschreibung: 'Jahresabrechnung, Vorstandswahl, Planung Sommerfest' },
{ titel: 'Trainingssession', beginn: d(-14, 18, 0), ende: d(-14, 20, 0), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Aktive Mitglieder']] },
{ titel: 'Jugendtraining', beginn: d(-7, 16, 0), ende: d(-7, 17, 30), ort_id: orte['Turnhalle Grundschule Muster'], gruppe_ids: [gruppen['Jugend U15']] },
// Upcoming
{ titel: 'Vorstandssitzung', beginn: d(3, 19, 30), ende: d(3, 21, 0), ort_id: orte['Vereinsheim TSV'], gruppe_ids: [gruppen['Vorstand']], beschreibung: 'Vorbereitung Sommerfest, Kassenstand Q1' },
{ titel: 'Sommerfest TSV', beginn: d(32, 14, 0), ende: d(32, 22, 0), ort_id: orte['Sportplatz West'], gruppe_ids: Object.values(gruppen), beschreibung: 'Für Mitglieder und Familien Grillen, Spiele, Live-Musik' },
{ titel: 'Auswärtsspiel Musterliga', beginn: d(18, 11, 0), ende: d(18, 13, 0), ort_id: null, gruppe_ids: [gruppen['Aktive Mitglieder']], ort: 'FC Gegner Stadionstraße 1', beschreibung: 'Hinfahrt 10:00 Uhr am Vereinsheim' },
];
// Wöchentliches Training als Serie
const serie_id = 'seed-serie-dienstag-2026';
const trainingsTermine = [];
for (let i = 1; i <= 8; i++) {
// Nächster Dienstag + i Wochen
const dt = new Date();
const daysToTue = (2 - dt.getDay() + 7) % 7 || 7;
dt.setDate(dt.getDate() + daysToTue + (i - 1) * 7);
dt.setHours(18, 30, 0, 0);
trainingsTermine.push({
titel: 'Dienstags-Training',
beginn: dt.toISOString(),
ende: new Date(dt.getTime() + 90 * 60 * 1000).toISOString(),
ort_id: orte['Turnhalle Grundschule Muster'],
gruppe_ids:[gruppen['Aktive Mitglieder']],
rrule: 'FREQ=WEEKLY;BYDAY=TU',
serie_id,
});
}
const allTermine = [...termineDef, ...trainingsTermine];
let termineAngelegt = 0;
for (const t of allTermine) {
const ex = await find('termine', `titel = "${t.titel}" && verein_id = "${verein.id}" && beginn = "${t.beginn}"`);
if (!ex) {
await create('termine', { verein_id: verein.id, verfuegbarkeit: 'offen', ...t });
termineAngelegt++;
}
}
console.log(`${termineAngelegt} Termine (inkl. ${trainingsTermine.length} in Dienstags-Serie)`);
// ── 9. Nachricht ─────────────────────────────────────────────────────────────
console.log('→ Beispiel-Nachricht…');
const exMsg = await find('nachrichten', `verein_id = "${verein.id}"`);
if (!exMsg) {
await create('nachrichten', {
verein_id: verein.id,
autor_id: adminUser.id,
betreff: 'Willkommen bei vereins.haus!',
text: 'Liebe Mitglieder,\n\nwir haben unsere Vereinsverwaltung auf vereins.haus umgestellt. Hier findet ihr alle Termine, Nachrichten und Informationen rund um unseren Verein.\n\nBei Fragen wendet euch an den Vorstand.\n\nEuer Vorstand\nTSV Musterstadt 1983 e.V.',
gruppe_ids: Object.values(gruppen),
gesendet_am: new Date().toISOString(),
});
}
// ── Zusammenfassung ──────────────────────────────────────────────────────────
console.log('\n✓ Seed abgeschlossen!\n');
console.log(' Verein: ', verein.name);
console.log(' Login: vorstand@tsv-musterstadt.de / Test123456!');
console.log(' Mitglieder:', mitgliederIds.length);
console.log(' PocketBase:', PB_URL);