1. ClubService.cs:134 - MonthlyMembershipFee in UpdateAsync hinzugefügt
2. Settings.razor:358-364 - Laden der Kassenbuch-Einstellungen beim Init
3. Settings.razor:665-689 - SaveCashBookSettings() Methode implementiert
Der Kassenbuch-Tab in den Vereins-Einstellungen funktioniert jetzt:
- Zeigt aktuellen Kontostand
- Erlaubt Ändern des monatlichen Mitgliedsbeitrags
- Speichert Änderungen via Fluxor-Action
- Quick-Links zu Kassenbuch, Kategorien, Berichten
- ICashBookService.CreatePenaltyEntriesForDayAsync
- Groups PersonExpenses by person, sums amounts
- Marks PersonExpenses as Done
- DayService calls after SaveChanges
- UserRole.Treasurer = "Kassenwart"
- IdentityRoleSeeder: seed Kassenwart role
- ClubRoleRequirement: rank Admin=4, Kassenwart=3
- ClubTreasurer policy in DI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- BookingCategories table with unique name per club
- CashBookEntries table with FK to Category, Day, Person
- Club: InitialBalance, MonthlyMembershipFee columns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- BookingCategoryConfiguration with unique index
- CashBookEntryConfiguration with FK relationships
- ClubConfiguration: InitialBalance, MonthlyMembershipFee
- AppDbContext: BookingCategories, CashBookEntries DbSets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Neue Datei erstellt:
- src/Koogle.Web/Components/Game/PlayerSelectorDialog.razor - Dialog zur manuellen Spielerauswahl
Geändert:
- src/Koogle.Web/Components/Pages/Days/DayDetails.razor:863-891 - ShowPlayerSelector implementiert
Funktionsweise:
1. Bei ParticipantsMode.FreeToChoose zeigt GameInputPanel.razor:23-32 den Button "Spieler wechseln"
2. Der neue Dialog listet alle Spieler aus Participants.PlayerIds
3. Bei DeathBox werden ausgeschiedene Spieler standardmäßig ausgeblendet (Toggle verfügbar)
4. Bei Spielerauswahl wird SetCurrentPlayerAction dispatcht
Features:
- Aktueller Spieler ist markiert (grün)
- Ausgeschiedene Spieler sind durchgestrichen mit "Ausgeschieden"-Badge
- Optional können ausgeschiedene Spieler eingeblendet werden (falls nötig)
- Funktioniert mit allen Game-Typen (DeathBox, Shit, Training)
Problem: RefreshSignInAsync in UserService.SwitchClubAsync (Zeile 719) setzte zwar den neuen Cookie, aber HttpClient.PostAsJsonAsync empfing diesen Cookie nur intern - der Browser bekam ihn nie.
Lösung: Form-POST statt HttpClient-API-Call, wie bei Login/Logout.
Änderungen:
1. ClubSwitcher.razor - Komplett überarbeitet:
- HttpClient durch natives <form method="post"> ersetzt
- AntiForgery-Token manuell gesetzt (wie LogoutButton)
- Kein JavaScript/Client-Code mehr nötig
2. AuthController.cs (Zeile 146):
- [FromBody] → [FromForm]
- [ValidateAntiForgeryToken] aktiviert
Ablauf jetzt:
1. User klickt auf Club im Dropdown
2. Form-POST an /auth/switch-club
3. Controller ruft SwitchClubAsync → DB-Update + RefreshSignInAsync
4. LocalRedirect("/dashboard") → Browser erhält neuen Cookie direkt
5. Claims sind beim Reload korrekt
Neue Datei: src/Koogle.Web/Components/Pages/Settings.razor
- Route /settings mit ClubAdmin-Policy
- Zeigt Name, LoginName, Kostenberechnung
- "Bearbeiten" Button öffnet bestehenden ClubEditDialog
- Kein Löschen, keine Neuanlage
Geändert: src/Koogle.Web/Components/Layout/NavMenu.razor
- Neuer Link "Vereins-Einstellungen" in "Stammdaten"-Gruppe (Icon: Tune)
1. Neue Methode CreateAbsentMemberExpensesAsync() (Zeile 322-421)
- Prüft Club.ExpenseCalculation (None → skip)
- Berechnet Average/Maximum aller Tages-Expenses
- Findet Absent-Expense via ExpenseTriggerType.Absent
- Erstellt PersonExpense für alle abwesenden Members
2. Aufruf in AdvanceStatusAsync() (Zeile 312-316)
- Wird aufgerufen wenn Status → Closed wechselt
- Vor SaveChangesAsync() → eine Transaktion
1. IMediaStorageService (src/Koogle.Domain/Interfaces/):
- Neue Methoden GetMediaBasePath() und GetFilePath() hinzugefügt
2. MediaStorageService (src/Koogle.Infrastructure/Services/):
- Erkennt Production+Linux → verwendet /home/data/club-media
- Development → verwendet wwwroot/club-media
3. Program.cs (src/Koogle.Web/):
- StaticFileOptions für /club-media aus /home/data/club-media in Production
4. DemoSeeder (src/Koogle.Infrastructure/Data/):
- Alle Methoden nutzen jetzt IMediaStorageService statt hardkodierte Pfade
5. ClubGifService (src/Koogle.Application/Services/):
- Alle Path.Combine("wwwroot", ...) durch _mediaStorage.GetFilePath() ersetzt
Das Verhalten:
- Windows/Development: weiterhin wwwroot/club-media
- Linux/Production (Azure): /home/data/club-media (persistent und beschreibbar)