diff --git a/backend/controllers/memberController.js b/backend/controllers/memberController.js
index 5ca5ed74..24f66ac4 100755
--- a/backend/controllers/memberController.js
+++ b/backend/controllers/memberController.js
@@ -2,6 +2,8 @@ import MemberService from "../services/memberService.js";
import MemberTransferService from "../services/memberTransferService.js";
import clickTtPlayerRegistrationService from "../services/clickTtPlayerRegistrationService.js";
import { emitMemberChanged } from '../services/socketService.js';
+import Member from '../models/Member.js';
+import UserClub from '../models/UserClub.js';
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
@@ -351,6 +353,28 @@ const transferMembers = async (req, res) => {
}
};
+const linkMemberUser = async (req, res) => {
+ try {
+ const clubId = Number(req.params.clubId);
+ const memberId = Number(req.params.memberId);
+ const userId = Number(req.body?.userId);
+ if (!clubId || !memberId || !userId) return res.status(400).json({ error: 'Mitglied, Verein und Benutzer sind erforderlich.' });
+ const [member, clubUser, existing] = await Promise.all([
+ Member.findOne({ where: { id: memberId, clubId } }),
+ UserClub.findOne({ where: { clubId, userId, approved: true } }),
+ Member.findOne({ where: { userId } }),
+ ]);
+ if (!member || !clubUser) return res.status(404).json({ error: 'Mitglied oder freigegebener Vereinsbenutzer wurde nicht gefunden.' });
+ if (existing && Number(existing.id) !== memberId) return res.status(409).json({ error: 'Dieses Benutzerkonto ist bereits einem Mitglied zugeordnet.' });
+ await member.update({ userId });
+ emitMemberChanged(clubId);
+ res.json({ memberId, userId });
+ } catch (error) {
+ console.error('[linkMemberUser] - Error:', error);
+ res.status(500).json({ error: 'Mitglied konnte nicht verknüpft werden.' });
+ }
+};
+
export {
getClubMembers,
getWaitingApprovals,
@@ -372,5 +396,6 @@ export {
requestClickTtPlayerRegistration,
deleteMemberImage,
setPrimaryMemberImage,
- generateMemberGallery
+ generateMemberGallery,
+ linkMemberUser
};
diff --git a/backend/migrations/20260722_add_member_user_link.sql b/backend/migrations/20260722_add_member_user_link.sql
new file mode 100644
index 00000000..d814ab34
--- /dev/null
+++ b/backend/migrations/20260722_add_member_user_link.sql
@@ -0,0 +1,4 @@
+ALTER TABLE member
+ ADD COLUMN user_id INT NULL,
+ ADD UNIQUE KEY member_user_id_unique (user_id),
+ ADD CONSTRAINT member_user_id_fk FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL;
diff --git a/backend/models/Member.js b/backend/models/Member.js
index 60af45fa..9d47bde4 100755
--- a/backend/models/Member.js
+++ b/backend/models/Member.js
@@ -123,6 +123,13 @@ const Member = sequelize.define('Member', {
type: DataTypes.INTEGER,
allowNull: false,
},
+ userId: {
+ type: DataTypes.INTEGER,
+ allowNull: true,
+ unique: true,
+ field: 'user_id',
+ comment: 'Linked login account for personal member features',
+ },
membershipStatus: {
type: DataTypes.STRING,
allowNull: false,
diff --git a/backend/models/index.js b/backend/models/index.js
index 476afaf7..e362fea3 100755
--- a/backend/models/index.js
+++ b/backend/models/index.js
@@ -223,6 +223,9 @@ User.hasMany(UserClub, { foreignKey: 'userId' });
UserClub.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(UserClub, { foreignKey: 'clubId' });
+User.hasOne(Member, { foreignKey: 'userId', as: 'memberProfile' });
+Member.belongsTo(User, { foreignKey: 'userId', as: 'user' });
+
Group.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDateGroup' });
DiaryDate.hasMany(Group, { foreignKey: 'diaryDateId', as: 'groupsDiaryDate' });
diff --git a/backend/routes/memberRoutes.js b/backend/routes/memberRoutes.js
index 338992e1..efcdf903 100755
--- a/backend/routes/memberRoutes.js
+++ b/backend/routes/memberRoutes.js
@@ -19,7 +19,8 @@ import {
requestClickTtPlayerRegistration,
deleteMemberImage,
setPrimaryMemberImage,
- generateMemberGallery
+ generateMemberGallery,
+ linkMemberUser
} from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
@@ -39,6 +40,7 @@ router.post('/image/:clubId/:memberId/:imageId/primary', authenticate, authorize
router.get('/get/:id/:showAll', authenticate, authorize('members', 'read'), getClubMembers);
router.get('/gallery/:clubId', authenticate, authorize('members', 'read'), generateMemberGallery);
router.post('/set/:id', authenticate, authorize('members', 'write'), setClubMembers);
+router.put('/link-user/:clubId/:memberId', authenticate, authorize('members', 'write'), linkMemberUser);
router.get('/sepa/:clubId/:memberId', authenticate, authorize('members', 'read'), getMemberSepaMandate);
router.put('/sepa/:clubId/:memberId', authenticate, authorize('members', 'write'), saveMemberSepaMandate);
router.get('/play-interest/:clubId', authenticate, authorize('members', 'read'), getMemberPlayInterests);
diff --git a/docs/collective-orders-plan.md b/docs/collective-orders-plan.md
new file mode 100644
index 00000000..788befc6
--- /dev/null
+++ b/docs/collective-orders-plan.md
@@ -0,0 +1,146 @@
+# Sammelbestellungen: gemeinsames Konzept für Club, Trainer und Trainingstagebuch
+
+Stand: 2026-07-22
+
+## Ziel
+
+Eine Sammelbestellung bündelt Wünsche von Mitgliedern, Trainingsteilnehmern und
+Vereinsverantwortlichen. Club- und Trainerrollen steuern Bestellung, Preise,
+Rückfragen, Zahlung und Ausgabe; alle berechtigten Nutzer können Wünsche
+eintragen und ihren eigenen Stand nachvollziehen.
+
+## Rollen und Rechte
+
+| Aktion | Club | Trainer | Mitglied / Trainingstagebuch-Nutzer |
+| --- | --- | --- | --- |
+| Sammelbestellung anlegen | Ja | Ja | Nein |
+| Eigene Artikelwünsche eintragen | Ja | Ja | Ja |
+| Eigene Wünsche ändern oder stornieren, solange offen | Ja | Ja | Ja |
+| Preise, Bestellung, Ausgabe und Zahlungsstatus bearbeiten | Ja | Ja | Nein |
+| Rückfrage zu einem Artikel stellen | Ja | Ja | Nein |
+| Auf Rückfrage antworten | Ja | Ja | Ja, nur zum eigenen Artikel |
+| Preis bestätigen | Ja | Ja | Ja, nur zum eigenen Artikel |
+| Gesamtrechnung und Bestellstatus bearbeiten | Ja | Ja | Nein |
+
+Der Ersteller einer Sammelbestellung erhält zusätzlich dieselben Steuerrechte
+wie Club/Trainer für genau diese Bestellung, auch wenn seine allgemeine Rolle
+später geändert wird.
+
+## Datenmodell
+
+### Sammelbestellung
+
+- ID, `club_id`, Titel, Beschreibung, Lieferant, Bestellfrist
+- Ersteller (`created_by_user_id`), optional verantwortlicher Trainer
+- Bestellstatus: `open`, `ordered`, `arrived`
+- Zahlungsstatus: `unpaid`, `partially_paid`, `paid`
+- Gesamtrechnungsbetrag in Cent, Währung, Rechnungsreferenz
+- Zeitstempel für Bestellung, Eingang und Abschluss
+
+### Artikelwunsch
+
+- ID, Sammelbestellung, anfragender Nutzer und optional zugeordnetes Mitglied
+- Artikelname, Variante/Größe, Menge, Freitextwunsch
+- Einzelpreis in Cent, Währung
+- Artikelstatus: `entered`, `question`, `ordered`, `cancelled`, `arrived`, `issued`
+- Zahlungsstatus: `unpaid`, `paid`
+- Preisbestätigung durch den Anfragenden (`price_confirmed_at`)
+- Rückfrage: Text, gestellt von, gestellt am, Antwort, beantwortet am
+- Ausgabe: ausgegeben von, ausgegeben am
+
+## Verbindliche Statuslogik
+
+### Einzelartikel
+
+1. `entered`: Wunsch wurde eingetragen.
+2. `question`: Verantwortliche stellen eine Rückfrage; der Nutzer kann antworten.
+3. `ordered`: Artikel ist beim Lieferanten bestellt.
+4. `cancelled`: Artikel wird nicht weitergeführt.
+5. `arrived`: Artikel ist eingetroffen.
+6. `issued`: Artikel wurde an den Nutzer ausgegeben.
+
+Der Preis darf von Club/Trainer jederzeit bis zur Ausgabe gepflegt werden. Ist
+ein Einzelpreis vorhanden oder geändert, kann der Anfragende ihn bestätigen.
+Die Preisbestätigung ist für die Bestellung optional konfigurierbar, sollte aber
+vor `ordered` sichtbar als offen markiert sein.
+
+Ein Artikel gilt nur dann als **abgeschlossen**, wenn sein Artikelstatus
+`issued` und sein Zahlungsstatus `paid` ist. Stornierte Artikel gelten als
+abgeschlossen, jedoch separat als storniert.
+
+### Gesamte Sammelbestellung
+
+- `open`: Wünsche werden gesammelt und geklärt.
+- `ordered`: Bestellung wurde ausgelöst.
+- `arrived`: Lieferung ist eingetroffen.
+
+Der Zahlungsstatus bleibt unabhängig: `unpaid`, `partially_paid`, `paid`.
+Der Gesamtbetrag der Rechnung wird manuell gepflegt und steht neben der Summe
+aller Einzelpreise, damit Abweichungen sichtbar sind. Eine Bestellung ist erst
+abgeschlossen, wenn sie `arrived` ist und alle nicht stornierten Artikel
+ausgegeben und bezahlt sind.
+
+## Ansichten
+
+### Für alle Nutzer
+
+- Liste offener und eigener Sammelbestellungen.
+- Wunschformular mit Artikel, Variante, Menge und Hinweis.
+- Eigene Artikel mit Preis, Preisbestätigung, Rückfrage und Status-Timeline.
+- Keine Sicht auf Wünsche, Preise oder Zahlungen anderer Nutzer.
+
+### Club- und Traineransicht
+
+- Neue Sammelbestellung anlegen und Verantwortliche festlegen.
+- Artikeltabelle mit Filtern: Status, Rückfrage offen, Preis unbestätigt,
+ unbezahlt, eingetroffen, auszugeben.
+- Preis, Artikelstatus, Rückfrage und Zahlungsstatus inline pflegen.
+- Gesamtstatus, Gesamtrechnung und Gesamtzahlungsstatus pflegen.
+- Übersichten für fehlende Preisbestätigungen, offene Rückfragen und noch
+ auszugebende Artikel.
+
+### Trainingstagebuch
+
+- Kontextlink aus einer Trainingseinheit auf aktive Sammelbestellungen.
+- Teilnehmer können dort ihren Wunsch eintragen oder den eigenen Status prüfen.
+- Keine Finanz- oder Verwaltungsaktionen für normale Teilnehmer.
+
+## Umsetzungstodos
+
+### Phase 1: Grundlage und Rechte
+
+- [ ] Datenmodell, Migrationen und Indizes für Bestellung und Artikelwunsch anlegen.
+- [ ] Berechtigung `collective_orders` mit read/write und Ersteller-Ausnahme definieren.
+- [ ] Sichere Zuordnung User ↔ Mitglied für eigene Wünsche verbindlich klären.
+- [ ] REST-Endpunkte für Liste, Detail, Bestellung und eigene Artikelwünsche erstellen.
+- [ ] Statusübergänge serverseitig validieren.
+
+### Phase 2: Club- und Trainer-Arbeitsbereich
+
+- [ ] Bestellliste und Detailansicht implementieren.
+- [ ] Bestellung anlegen, Gesamtbetrag und Gesamtstatus pflegen.
+- [ ] Artikelstatus, Einzelpreis und Zahlungsstatus pflegen.
+- [ ] Rückfrage-/Antwort-Workflow und Preisbestätigung umsetzen.
+- [ ] Abschlusslogik und Arbeitsfilter umsetzen.
+
+### Phase 3: Mitglieder- und Trainingstagebuch-Sicht
+
+- [ ] Eigene Wünsche eintragen, ändern und stornieren.
+- [ ] Eigene Rückfragen beantworten und Preise bestätigen.
+- [ ] Persönliche Statusanzeige und verständliche leere Zustände bauen.
+- [ ] Kontextlink aus Trainingstagebuch ergänzen.
+
+### Phase 4: Abgleich und Qualität
+
+- [ ] Abweichung zwischen Gesamtrechnung und Summe der Artikelpreise sichtbar machen.
+- [ ] Export für Lieferant, Ausgabe und offene Zahlungen ergänzen.
+- [ ] Rechte-, Status- und Mobil-Smoketests mit Club, Trainer und Mitglied durchführen.
+- [ ] Historie und Archivverknüpfung für abgeschlossene Bestellungen ergänzen.
+
+## Abnahmekriterien
+
+- [ ] Mitglieder können ausschließlich eigene Wünsche und Rückfragen sehen.
+- [ ] Club/Trainer können jeden Artikel vollständig steuern.
+- [ ] Kein Artikel wird als abgeschlossen markiert, bevor er ausgegeben und bezahlt ist.
+- [ ] Gesamtbetrag und Artikelpreissumme sind transparent vergleichbar.
+- [ ] Offene Rückfragen, unbestätigte Preise, fehlende Zahlungen und Ausgaben sind mit einem Filter erreichbar.
diff --git a/docs/manual_sql_migrations.md b/docs/manual_sql_migrations.md
index db766b67..600f03aa 100755
--- a/docs/manual_sql_migrations.md
+++ b/docs/manual_sql_migrations.md
@@ -18,6 +18,21 @@ Ergaenzend:
## 2026-07-22
+### `member.user_id`
+
+Sichere optionale Zuordnung eines Login-Kontos zu einem Vereinsmitglied für
+persönliche Mitgliederfunktionen:
+
+```sql
+ALTER TABLE member
+ ADD COLUMN user_id INT NULL,
+ ADD UNIQUE KEY member_user_id_unique (user_id),
+ ADD CONSTRAINT member_user_id_fk FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL;
+```
+
+Bestehende Mitglieder bleiben zunächst unverknüpft. Die Zuordnung erfolgt
+bewusst über die Verwaltungs-API und nicht über einen automatischen E-Mail-Abgleich.
+
### `club_communication_threads.reply_to`
Optionale Antwortadresse je Kommunikationsvorgang:
diff --git a/docs/simple-user-plan.md b/docs/simple-user-plan.md
new file mode 100644
index 00000000..cd53795c
--- /dev/null
+++ b/docs/simple-user-plan.md
@@ -0,0 +1,83 @@
+# Einfache Nutzer: Produkt- und Umsetzungsplan
+
+Stand: 2026-07-22
+
+## Zielgruppe und Zielbild
+
+Einfache Nutzer sind Vereinsmitglieder oder Helfer ohne Verwaltungsauftrag. Sie
+sollen persönliche Informationen finden und Rückmeldungen geben können, ohne
+Finanzen, Rollenverwaltung, Archiv oder interne Vorgänge zu sehen.
+
+## Rechteprinzip
+
+- Kein Zugriff auf Finanzen, Rollen, Benutzerverwaltung, Archiv oder Historie.
+- Nur eigene bzw. explizit freigegebene Vereinsdaten sehen.
+- Eigene Angaben und Rückmeldungen bearbeiten; vereinsweite Änderungen bleiben
+ bei Verwaltungsrollen.
+
+## Umsetzung in sinnvoller Reihenfolge
+
+### Phase 1: Zugang und persönlicher Einstieg
+
+- [ ] Rollenprofil `member` und Navigation gegen unzulässige Ziele auditieren.
+- [ ] Persönliches Dashboard für Termine, Training, Rückmeldungen und Vereinsnachrichten umsetzen.
+- [ ] Leere Zustände für fehlende Termine, Nachrichten und Rückmeldungen formulieren.
+- [ ] Persönliche Startseite als Standardziel nach Login festlegen.
+
+Abnahme:
+
+- [ ] Ein Nutzer ohne Verwaltungsrechte sieht nur seinen persönlichen Bereich.
+- [ ] Kein sichtbarer Link führt zu einer anschließenden Berechtigungsfehlermeldung.
+
+### Phase 2: Eigene Daten und Rückmeldungen
+
+- [ ] Ansicht „Mein Profil“ für freigegebene Stammdaten bereitstellen.
+- [ ] Änderungsanfrage für Kontaktdaten statt direkter Änderung geschützter Felder umsetzen.
+- [ ] Zu- und Absagen für relevante Trainings oder Veranstaltungen ermöglichen.
+- [ ] Mitgliedschafts- und SEPA-Hinweise verständlich, aber ohne sensible Zahlungsdaten anzeigen.
+- [ ] Rückmeldungen für berechtigte Verantwortliche sichtbar machen.
+
+Abnahme:
+
+- [ ] Kontaktdaten können sicher geändert bzw. zur Prüfung eingereicht werden.
+- [ ] Zu- und Absagen sind an der jeweiligen Veranstaltung nachvollziehbar.
+
+### Phase 3: Kommunikation und Informationen
+
+- [ ] Persönlichen Nachrichteneingang bereitstellen.
+- [ ] Vereinsankündigungen nach Datum sortiert anzeigen.
+- [ ] Gelesen-/ungelesen-Status und einen klaren Rückfrageweg ergänzen.
+- [ ] Datenschutz- und Sichtbarkeitsregeln für Nachrichten und Kontaktinformationen prüfen.
+
+Abnahme:
+
+- [ ] Neue Vereinsinformationen sind ohne Verwaltungsmenüs auffindbar.
+- [ ] Persönliche Nachrichten sind ausschließlich für den Empfänger sichtbar.
+
+### Phase 4: Mobil, Qualität und Einführung
+
+- [ ] Mobilansicht und langsame Verbindungen prüfen.
+- [ ] Berechtigungstest mit Mitglied, Trainer und Vorstand durchführen.
+- [ ] Onboarding für ersten Login, Profilprüfung und Rückmeldungen erstellen.
+- [ ] Pilot mit wenigen echten Mitgliedern durchführen und Feedback auswerten.
+
+Abnahme:
+
+- [ ] Die Testrollen sehen jeweils nur die vorgesehenen Daten und Aktionen.
+- [ ] Der persönliche Hauptablauf funktioniert auf Mobilgeräten ohne Hilfestellung.
+
+## Nicht Teil dieses Plans
+
+- Vereinsverwaltung, Buchhaltung, Rollenpflege und Archivarbeit.
+- Erweiterungen des Player-Produkts `mein-tt.de`; diese bleiben in der eigenen
+ [mein-tt.de-Roadmap](./mein-tt-product-plan.md).
+
+## Erste umsetzbare Tickets
+
+1. [ ] Bestehende `member`-Berechtigungen und Navigation auditieren.
+2. [ ] Datenvertrag und Wireframe für das persönliche Dashboard festlegen.
+3. [ ] Dashboard mit Terminen und Nachrichten implementieren.
+4. [ ] Profil- und Kontaktdaten-Änderungsanfrage implementieren.
+5. [ ] Trainings- und Veranstaltungsrückmeldungen ergänzen.
+6. [ ] Nachrichteneingang und Benachrichtigungsstatus ergänzen.
+7. [ ] Rollen- und Mobile-Smoke-Test mit Pilotgruppe durchführen.
diff --git a/docs/tt-club.de.conf b/docs/tt-club.de.conf
deleted file mode 100644
index 7c092af3..00000000
--- a/docs/tt-club.de.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-# HTTP: www.tt-club.de und tt-club.de -> HTTPS
-
- ServerName www.tt-club.de
- Redirect permanent / https://tt-club.de/
-
-
- ServerName tt-club.de
- Redirect permanent / https://tt-club.de/
-
diff --git a/docs/tt-verein.de-bootstrap.conf b/docs/tt-verein.de-bootstrap.conf
new file mode 100644
index 00000000..ede96ecf
--- /dev/null
+++ b/docs/tt-verein.de-bootstrap.conf
@@ -0,0 +1,16 @@
+# Temporärer HTTP-VHost für die erstmalige Let's-Encrypt-Ausstellung.
+# Diese Datei vor dem Zertifikat als tt-verein.de.conf aktivieren.
+
+ ServerName tt-verein.de
+ ServerAlias www.tt-verein.de
+
+ DocumentRoot /var/www/tt-tagebuch.de
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+ ErrorLog ${APACHE_LOG_DIR}/tt-verein.de_error.log
+ CustomLog ${APACHE_LOG_DIR}/tt-verein.de_access.log combined
+
diff --git a/docs/tt-club.de-le-ssl.conf b/docs/tt-verein.de-le-ssl.conf
similarity index 55%
rename from docs/tt-club.de-le-ssl.conf
rename to docs/tt-verein.de-le-ssl.conf
index 4e14f7c8..ba62018f 100644
--- a/docs/tt-club.de-le-ssl.conf
+++ b/docs/tt-verein.de-le-ssl.conf
@@ -1,18 +1,18 @@
+# tt-verein.de nutzt dieselbe ausgerollte Anwendung wie tt-tagebuch.de.
- ServerName tt-club.de
- ServerAlias www.tt-club.de
- # tt-club.de nutzt dieselbe ausgerollte Anwendung wie tt-tagebuch.de.
+ ServerName tt-verein.de
+ ServerAlias www.tt-verein.de
DocumentRoot /var/www/tt-tagebuch.de
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
- ErrorLog ${APACHE_LOG_DIR}/tt-club.de_error.log
- CustomLog ${APACHE_LOG_DIR}/tt-club.de_access.log combined
+ ErrorLog ${APACHE_LOG_DIR}/tt-verein.de_error.log
+ CustomLog ${APACHE_LOG_DIR}/tt-verein.de_access.log combined
SSLEngine on
- SSLCertificateFile /etc/letsencrypt/live/tt-club.de/fullchain.pem
- SSLCertificateKeyFile /etc/letsencrypt/live/tt-club.de/privkey.pem
+ SSLCertificateFile /etc/letsencrypt/live/tt-verein.de/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/tt-verein.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyRequests Off
ProxyPass /api http://localhost:3050/api
diff --git a/docs/tt-verein.de.conf b/docs/tt-verein.de.conf
new file mode 100644
index 00000000..56af6974
--- /dev/null
+++ b/docs/tt-verein.de.conf
@@ -0,0 +1,9 @@
+# HTTP: www.tt-verein.de und tt-verein.de -> HTTPS
+
+ ServerName www.tt-verein.de
+ Redirect permanent / https://tt-verein.de/
+
+
+ ServerName tt-verein.de
+ Redirect permanent / https://tt-verein.de/
+