From a4436d70c2f0b7c4afc65158fbd3a8fe211512eb Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 20 May 2026 20:46:46 +0200 Subject: [PATCH] Staging: docker-compose.staging.yml, Makefile-Targets, Seed-Script (18 Mitglieder, Termine, Orte, Beitragsarten) --- Makefile | 69 ++++++++++ docker-compose.staging.yml | 46 +++++++ scripts/seed.js | 264 +++++++++++++++++++++++++++++++++++++ 3 files changed, 379 insertions(+) create mode 100644 docker-compose.staging.yml create mode 100644 scripts/seed.js diff --git a/Makefile b/Makefile index 281632d..c543f39 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,13 @@ CONTAINER_PB := vereinshaus-pocketbase CONTAINER_APP := vereinshaus-app 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' \ --exclude='./app/node_modules' \ --exclude='./app/.svelte-kit' \ @@ -133,3 +140,65 @@ shell-pb: check-ssh pb-admin: @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." diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..9b1d51f --- /dev/null +++ b/docker-compose.staging.yml @@ -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 diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100644 index 0000000..7eea6f2 --- /dev/null +++ b/scripts/seed.js @@ -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);